From 7f98223273fa9096e0f0b453d1837b8ede31ebd0 Mon Sep 17 00:00:00 2001 From: Ahmed Alaa <46384841+BenAlaa@users.noreply.github.com> Date: Fri, 11 Sep 2026 13:48:24 +0300 Subject: [PATCH] docs: publish product-first guide Add the complete product guide and refreshed technical reference synchronized to the pinned T3 Code source revision, while excluding the internal transfer material.\n\nCo-authored-by: Codex --- BOOK_PLAN.md | 882 ++++-------------- CONTRIBUTING.md | 2 +- NOTICE.md | 4 +- README.md | 63 +- sources/excerpts.manifest.json | 23 +- sources/references.manifest.json | 49 +- sources/t3code.lock.json | 17 +- src/components/BookCover.astro | 15 +- src/components/CheckpointGraphLab.astro | 3 +- src/components/DecisionLedgerLab.astro | 352 ------- src/components/SynchronizedTraceLab.astro | 266 ------ src/content/book/00-cover.mdx | 26 +- src/content/book/01-how-to-read.mdx | 132 +-- src/content/book/02-contents.mdx | 174 ++-- src/content/book/03-architecture-map.mdx | 17 +- src/content/book/04-request-trace.mdx | 15 +- src/content/book/05-product-overview.mdx | 145 +++ src/content/book/06-install-onboarding.mdx | 196 ++++ src/content/book/07-environments-surfaces.mdx | 180 ++++ .../book/08-projects-threads-worktrees.mdx | 172 ++++ src/content/book/09-composer-context.mdx | 209 +++++ src/content/book/10-control-surface.mdx | 14 +- src/content/book/100-events-receipts.mdx | 14 +- .../book/11-providers-models-permissions.mdx | 250 +++++ .../book/110-projections-read-models.mdx | 26 +- src/content/book/12-workbench.mdx | 136 +++ src/content/book/120-post-commit-reactors.mdx | 14 +- src/content/book/13-source-control.mdx | 141 +++ src/content/book/130-persistence-recovery.mdx | 37 +- src/content/book/14-remote-background.mdx | 191 ++++ .../book/140-provider-adapter-contract.mdx | 53 +- src/content/book/15-mobile.mdx | 144 +++ .../book/150-provider-instances-routing.mdx | 18 +- src/content/book/16-devices.mdx | 140 +++ .../book/160-codex-app-server-json-rpc.mdx | 12 +- .../book/17-personalize-usage-updates.mdx | 187 ++++ src/content/book/170-claude-agent-sdk.mdx | 16 +- .../book/18-recipes-troubleshooting.mdx | 143 +++ src/content/book/180-acp-cursor-grok.mdx | 14 +- src/content/book/185-antigravity-provider.mdx | 119 +++ .../book/190-opencode-normalization.mdx | 58 +- src/content/book/20-domain-vocabulary.mdx | 10 +- src/content/book/200-usage-accounting.mdx | 99 +- src/content/book/210-project-discovery.mdx | 37 +- src/content/book/220-worktree-topology.mdx | 12 +- src/content/book/230-turn-lifecycle.mdx | 14 +- src/content/book/240-permissions-input.mdx | 19 +- src/content/book/250-work-items.mdx | 22 +- src/content/book/260-context-memory.mdx | 22 +- src/content/book/270-checkpoints-revert.mdx | 16 +- src/content/book/280-workbench-services.mdx | 43 +- .../book/285-device-host-architecture.mdx | 152 +++ .../book/290-shared-client-runtime.mdx | 32 +- src/content/book/30-repository-atlas.mdx | 12 +- src/content/book/300-web-runtime.mdx | 14 +- src/content/book/310-web-product-surfaces.mdx | 14 +- src/content/book/320-desktop-electron.mdx | 12 +- src/content/book/330-mobile-client.mdx | 68 +- src/content/book/340-access-transports.mdx | 45 +- src/content/book/350-t3-connect.mdx | 40 +- .../book/360-reconnect-environments.mdx | 32 +- .../book/370-distribution-artifacts.mdx | 12 +- .../380-release-updates-observability.mdx | 64 +- src/content/book/390-six-complete-traces.mdx | 355 ------- src/content/book/40-runtime-topologies.mdx | 10 +- .../400-decisions-limitations-roadmap.mdx | 413 -------- src/content/book/50-cli-bootstrap.mdx | 16 +- src/content/book/60-server-composition.mdx | 34 +- src/content/book/70-rpc-snapshots-resume.mdx | 27 +- src/content/book/80-auth-pairing.mdx | 28 +- src/content/book/90-commands-invariants.mdx | 20 +- src/generated/excerpts.json | 622 ++++++------ src/styles/components.css | 2 + tests/part-05-work-lifecycle.test.mjs | 40 +- tests/part-06-client-architectures.test.mjs | 29 +- tests/part-07-reach-ship.test.mjs | 41 +- tests/part-08-synthesis.test.mjs | 135 +-- tests/provider-harness-labs.test.mjs | 5 +- 78 files changed, 4023 insertions(+), 3214 deletions(-) delete mode 100644 src/components/DecisionLedgerLab.astro delete mode 100644 src/components/SynchronizedTraceLab.astro create mode 100644 src/content/book/05-product-overview.mdx create mode 100644 src/content/book/06-install-onboarding.mdx create mode 100644 src/content/book/07-environments-surfaces.mdx create mode 100644 src/content/book/08-projects-threads-worktrees.mdx create mode 100644 src/content/book/09-composer-context.mdx create mode 100644 src/content/book/11-providers-models-permissions.mdx create mode 100644 src/content/book/12-workbench.mdx create mode 100644 src/content/book/13-source-control.mdx create mode 100644 src/content/book/14-remote-background.mdx create mode 100644 src/content/book/15-mobile.mdx create mode 100644 src/content/book/16-devices.mdx create mode 100644 src/content/book/17-personalize-usage-updates.mdx create mode 100644 src/content/book/18-recipes-troubleshooting.mdx create mode 100644 src/content/book/185-antigravity-provider.mdx create mode 100644 src/content/book/285-device-host-architecture.mdx delete mode 100644 src/content/book/390-six-complete-traces.mdx delete mode 100644 src/content/book/400-decisions-limitations-roadmap.mdx diff --git a/BOOK_PLAN.md b/BOOK_PLAN.md index d69e67c..60dc697 100644 --- a/BOOK_PLAN.md +++ b/BOOK_PLAN.md @@ -1,718 +1,196 @@ # T3 Code Decoded — editorial and implementation plan -This plan is pinned to T3 Code commit -`fa219001dc2f14cfd9c7774c2c03c153359144be` (2026-08-23). It is both an -editorial contract and a build checklist: every chapter must explain one coherent -slice of the system, show the relevant runtime boundaries, and let the reader jump -to the exact source revision that supports the explanation. +This file is the editorial contract for the product guide and technical field guide. +The immutable upstream revision, capture date, and inventory live in +`sources/t3code.lock.json`. -## 1. Outcome +## Outcome -The finished book should let a technical reader answer five questions without -reading the monorepo in repository order: +The book serves two readers without splitting into separate sites. -1. What does T3 Code own, and what remains owned by Codex, Claude, Cursor, Grok, - OpenCode, Git, the shell, or the operating system? -2. How does one user intent travel from a client, through authorization and the - durable domain kernel, into a provider process, then return as ordered UI state? -3. How do local web, hosted web, desktop, and mobile share semantics while keeping - different process, storage, rendering, and native-integration designs? -4. How do pairing, remote access, updates, packaging, telemetry, and release - channels work beyond the happy path? -5. Which behaviors are guaranteed, best effort, latent, transitional, or explicit - future work, and where do the important failure and retention seams remain? +The product reader should be able to install T3 Code, choose a client and connection +path, configure a provider, organize parallel work, give an agent useful context, +supervise it locally or remotely, inspect files and terminals, use previews and +devices, review changes, understand usage, update safely, and recover from common +failures. -The book is explanatory, not a replacement for the upstream user guide. It covers -implementation, invariants, failure handling, and design consequences. It does not -invent a product roadmap or present repository clues as commitments. +The technical reader should be able to trace those behaviors through typed contracts, +the environment server, durable orchestration, provider adapters, work services, +client convergence, remote access, native integrations, packaging, and operations. -## 2. Source and claim contract +## Source and claim contract -### 2.1 Four claim classes +Every non-trivial technical claim uses one of four labels: -Every non-trivial claim belongs to exactly one class: - -| Label | Meaning | Required evidence | +| Label | Meaning | Evidence | | --- | --- | --- | -| **Verified behavior** | Executable behavior at the pinned revision | Code, test, schema, migration, or workflow | -| **Documented intent** | A maintainer's explanation or operating rule | Pinned internal/user/operations document | -| **Inference** | A design consequence derived from several sources | All inputs cited; inference named explicitly | -| **Future / proposed** | An explicit unshipped idea or limitation | Pinned future-work text or issue; never phrased as shipped | - -Release notes and the public docs may establish product wording, but code wins when -describing current mechanics. A discrepancy is shown, not silently reconciled. - -### 2.2 Source lock - -- Canonical checkout: the path in `T3CODE_SOURCE_DIR`, or the sibling `../t3code` -- Full source SHA: stored in `sources/t3code.lock.json` -- Exact excerpts: declared in `sources/excerpts.manifest.json` -- Generated excerpt text, line numbers, checksums, and permalinks: - `src/generated/excerpts.json` -- Refresh: `npm run source:sync` -- Drift check: `npm run source:check` - -No prose author copies a source block by hand. The synchronizer extracts it from the -pinned checkout; the validator rejects unknown IDs, changed checksums, invalid -ranges, and a checkout whose SHA no longer matches the lock. - -### 2.3 Status vocabulary - -- `draft`: narrative or visual is incomplete. -- `source-checked`: material claims have a pinned source trail and exact excerpts - have passed validation. -- `verified`: source checks, production build, interaction checks, links, and an - editorial review all pass. - -### 2.4 Roadmap vocabulary - -The book keeps these categories separate: - -- **Shipped at the source lock** — present in executable code. -- **Latent capability** — builder or contract supports it, but current distribution - or UI does not ship it. -- **Transition / compatibility path** — old and new behavior coexist. -- **Explicit future work** — upstream docs say it is planned or desired. -- **Community idea** — discussed externally but not committed by maintainers. - -There is no milestone-derived roadmap in the pinned repository. The known explicit -future items in `docs/internals/remote.md` are handled as future work, while the -implemented OAuth callback path is called out as a stale-document discrepancy. - -## 3. Reader routes +| Verified behavior | Executable behavior in the source lock | Code, test, schema, migration, or workflow | +| Documented intent | A maintainer's explanation or operating rule | User, internal, or operations documentation | +| Inference | A design consequence derived from cited inputs | Every input cited; interpretation named | +| Future / proposed | Explicitly unshipped or recommended work | Clearly separated from supported behavior | -The canonical route follows runtime causality: +Exact excerpts are generated from the locked Git object. Authors edit ranges in +`sources/excerpts.manifest.json`, run `npm run source:sync`, and commit the generated +checksums in `src/generated/excerpts.json`. Broader evidence lives in +`sources/references.manifest.json`. Source validation rejects a checkout mismatch, +missing object, invalid range, stale generated excerpt, or unknown citation id. -```text -ownership → vocabulary → boot/auth → command/event kernel → provider boundary -→ work lifecycle → client projection → remote path → distribution/operations -``` +Product chapters use plain language, task paths, decision guidance, and immutable +links to the applicable user documentation. They do not expose contributor tooling +or implementation detail unless it changes a user decision. -Alternative routes are surfaced in the reading guide: - -- **System designer:** 1, 4, 7, 9–15, 20, 26, 29, 34, 36, 40. -- **Provider integrator:** 1, 7, 9–20, 23–27, 39. -- **Client engineer:** 2, 7–8, 11, 23–24, 29–36, 39. -- **Release/operator:** 4–8, 13, 34–40. -- **Architecture auditor:** 1, 3, 9–15, 20, 23–26, 29, 36, 39–40. - -The front matter includes a complete spatial architecture map and a step-through -request trace, so readers have both a map and a causal story before Chapter 1. - -## 4. Chapter specification - -Each chapter entry below specifies the question it must settle, its primary source -anchors, and the visual or interactive artifact that makes the mechanism testable. -All paths are relative to the pinned T3 Code checkout. +## Reading order ### Start here -#### Cover - -- **Purpose:** establish the edition, source revision, and independent, public, - source-locked nature. -- **Artifact:** responsive use of the supplied cover, with an accessible text - alternative and no derivative asset generation. - -#### How to read a changing system - -- **Question:** how can a reader distinguish a fact, an interpretation, and a plan? -- **Sources:** source-lock and generated-excerpt pipeline in this repository. -- **Visual:** evidence ladder and freshness model. - -#### Contents and learning route - -- **Question:** why is the book ordered by causality instead of folders? -- **Artifact:** part roadmap plus role-specific routes. - -#### The complete system map - -- **Question:** where do clients, trust, domain logic, work services, adapters, and - provider processes live? -- **Sources:** `docs/internals/overview.md`, `apps/server/src/server.ts`, - `packages/contracts/src/rpc.ts`, `packages/client-runtime/src/connection`. -- **Visual:** interactive ownership bands and intent/state flow. - -#### One request, every boundary - -- **Question:** what happens between pressing Send and seeing a settled answer? -- **Sources:** orchestration contracts, normalizer, engine, reactors, provider - service, projectors, client thread reducer, checkpoint settlement. -- **Artifacts:** an interactive phase lab and a full sequence diagram. - -### Part I — Boundaries and vocabulary - -#### 1. Control surface, not agent brain - -- **Settles:** T3 Code owns orchestration, durable product state, work surfaces, and - transport; each provider still owns its native reasoning/context engine. -- **Sources:** `docs/internals/overview.md:5-28`, `ProviderAdapter.ts`, provider - drivers, orchestration contracts. -- **Visual:** responsibility matrix for T3, provider, OS, Git, and client. - -#### 2. Environment, project, thread, turn, session - -- **Settles:** the vocabulary and cardinalities that later chapters assume. -- **Sources:** `packages/contracts/src/environment.ts`, `project.ts`, - `orchestration.ts`, server projection schemas. -- **Visual/lab:** clickable entity relationship map; lifecycle vocabulary quiz. - -#### 3. Repository and dependency atlas - -- **Settles:** what each app/package/infra/native directory builds and which edges - are runtime, build-time, protocol, generated, or deployment-only. -- **Sources:** root/package manifests, workspace graph, Vite/Electron/Expo/Astro - configurations, native crates and packages. -- **Visual:** filterable monorepo graph and a reproducible production/test inventory by area. - -#### 4. Runtime topologies and technology placement - -- **Settles:** the five delivery shapes: local CLI web, hosted web, Electron, - React Native, and marketing; why Effect, SQLite, React, Expo, Astro, Electron, - native Ghostty, and typed contracts appear where they do. Unrecorded rationale is - labeled as inference; code proves placement and consequences, not author intent. -- **Sources:** each app entrypoint/config, server composition, runtime manifests. -- **Visual:** topology switcher showing process and trust boundaries per surface. - -### Part II — Boot and connect - -#### 5. The `npx t3` bootstrap path - -- **Settles:** CLI argument/config resolution, server startup, address selection, - browser/local client behavior, and bundled web assets. -- **Sources:** `apps/server/src/bin.ts`, `apps/server/src/cli/config.ts`, - `apps/server/src/cli/server.ts`, `apps/server/src/config.ts`, - `apps/server/vite.config.ts`, and `apps/server/package.json`. -- **Visual:** CLI bootstrap swimlane; package-content exploder. - -#### 6. Server composition and execution boundary - -- **Settles:** how Effect layers assemble HTTP, RPC, auth, orchestration, - providers, VCS, terminal, files, assets, usage, relay, and telemetry services. -- **Sources:** `apps/server/src/server.ts`, runtime-startup files, layer constructors. -- **Visual:** layer-construction DAG with startup and shutdown ownership. - -#### 7. Effect RPC, subscriptions, and wire contracts - -- **Settles:** request/response versus subscription semantics, schemas, - serialization, errors, capabilities, snapshots, and cursors. -- **Sources:** `packages/contracts/src/rpc.ts`, domain contract files, - server WebSocket router, `packages/client-runtime/src/rpc`. -- **Lab:** inspect a command, acknowledgement, snapshot, and event frame. - -#### 8. Pairing, scopes, credentials, and WebSocket upgrade - -- **Settles:** environment auth policy, browser/bearer/DPoP credentials, pairing - grants, session/ticket lifetimes, per-method scopes, secret handling, and the - authenticated WebSocket upgrade. Target resolution and reconnect stay in Chapter 29. -- **Sources:** `docs/internals/environment-auth.md`, server auth/session/pairing/ - secret-store layers, HTTP credential exchange, and RPC scope enforcement. -- **Lab:** credential, scope, TTL, one-use, persistence, and replay-resistance ladder. - -### Part III — Transactional domain core and post-commit delivery - -#### 9. Commands and invariants - -- **Settles:** external/internal commands, normalization, authorization, the - decider's persistence- and provider-I/O-free boundary despite clock/UUID Effect - dependencies, aggregate validation, the atomic event batch for an existing turn, - and multi-step bootstrap with compensating cleanup outside that transaction. -- **Sources:** orchestration contracts, `Normalizer.ts`, deciders, aggregate tests. -- **Lab:** choose valid/invalid command transitions and inspect emitted events. - -#### 10. Events, receipts, and idempotency - -- **Settles:** event envelope, command receipt, retry behavior, event publication, - and why acknowledgement does not mean provider work is complete. -- **Sources:** `OrchestrationEngine.ts`, persistence schemas, command receipt tests. -- **Visual:** transaction boundary and duplicate-command timeline. - -#### 11. Projection tables and read models - -- **Settles:** how events update shell/thread/activity/plan/checkpoint/runtime views, - then become HTTP snapshots and live subscriptions. -- **Sources:** projector registry/implementations, read stores, snapshot handlers. -- **Lab:** fold the same event stream into several projections. - -#### 12. Post-commit reactors: serialized handling without durable delivery - -- **Settles:** intent events trigger side effects only after commit, internal results - re-enter the command queue, workers serialize handling, scopes interrupt work on - shutdown, and the hot stream does not durably replay pending side effects. -- **Sources:** reactor layers, drainable worker utilities, engine dispatch tests. -- **Visual:** commit/publish/react/ingest timing diagram with crash points; contrast - the non-durable server bridge with mobile's durable client outbox. - -#### 13. SQLite, files, settings, secrets, migrations, and recovery - -- **Settles:** SQLite schema/WAL/migrations, settings JSON, secret files, - attachments/logs/terminal history, hidden Git refs, tombstones/retention, and - startup reconstruction. Provider adoption semantics wait for Chapter 15. -- **Sources:** persistence package, migrations, recovery services and tests. -- **Lab:** crash at selected phases and show the recoverable state. - -### Part IV — Five harnesses, one product model - -#### 14. The `ProviderAdapter` contract - -- **Settles:** discovery, sessions, turns, steer/interrupt, approval/input, modes, - canonical event streams, errors, and capabilities. -- **Sources:** `ProviderAdapter.ts`, provider contracts, adapter conformance tests. -- **Visual:** normalized boundary with required and optional capability lanes. - -#### 15. Drivers, instances, registries, and multi-instance routing - -- **Settles:** provider identity versus binary/driver/instance/native account/session, - discovery and health, registry lookup, managed processes, and recovery. -- **Includes:** legacy provider settings merged into explicit opaque instance configs - (explicit wins), secret-backed/redacted environment values, live child-scope - replacement, unavailable shadow entries, and the plaintext OpenCode password exception. -- **Sources:** provider registry/instance layers, driver discovery, ProviderService. -- **Lab:** route several threads across installed providers and accounts. - -#### 16. Codex through app-server JSON-RPC - -- **Settles:** app-server process lifecycle, initialize/account/model/config, - thread resume, turn calls, approvals, streaming notifications, and normalization. -- **Sources:** Codex driver and `packages/effect-codex-app-server`. -- **Visual:** native JSON-RPC to canonical event mapping. - -#### 17. Claude through the Agent SDK - -- **Settles:** SDK session lifecycle, permissions, models/modes, skills/commands, - resume metadata, tool events, and result handling. -- **Sources:** Claude driver, Claude skills scanner, adapter tests. -- **Visual:** Claude SDK event-to-product mapping and skill precedence. - -#### 18. Cursor and Grok through ACP - -- **Settles:** shared ACP client/transport, provider-specific launch/configuration, - session calls, capabilities, request forwarding, and notification mapping. -- **Sources:** Cursor/Grok drivers and `packages/effect-acp`. -- **Visual:** shared ACP spine with provider-specific forks. - -#### 19. OpenCode and the normalization matrix - -- **Settles:** OpenCode transport/session path and a five-provider comparison of - resume, models, plans, approvals, input, commands, skills, subagents, usage, and - failure semantics. -- **Sources:** OpenCode driver plus all adapter conformance fixtures. -- **Lab:** capability matrix that explains UI enablement and fallback. - -#### 20. Two usage systems: live context telemetry and transcript accounting - -- **Settles two separate pipelines:** (1) live per-thread token/context-window - telemetry normalized from provider runtime events; and (2) the historical Usage - page, which scans provider-owned transcript files independently of T3's - orchestration projections. The chapter never treats one as the source of the other. -- **Provider coverage:** the historical scanner supports Codex and Claude at this - revision; Cursor, Grok, and OpenCode contribute no Usage-page transcript source. - Live context telemetry is likewise emitted only by the Codex and Claude adapters - at this revision; the generic runtime event contract can represent it for a future - adapter, but Cursor, Grok, and OpenCode do not currently emit it. The five-adapter - matrix from Chapter 19 makes that implementation gap explicit. -- **Scanner pipeline:** provider home resolution → transcript discovery with mtime - window slack → provider-specific parse → within-file and cross-file de-duplication - → canonical timestamped records → `(day, hour?, provider, model)` aggregate - buckets → session counts and source diagnostics → persisted - `(path, size, mtime, provider)` scan cache. The contract can represent partial and - failed scans, while the current implementation predominantly reports `ok` or - `missing` and leaves malformed-record accounting at zero. -- **Accounting rules:** IANA-time-zone day buckets and an exact rolling 24-hour mode; - cached input and cache creation remain disjoint from uncached input; reasoning is a - subset of output and is never added twice; cost is provider-reported, LiteLLM - model-priced, or explicitly unpriced; cache savings are estimated alongside cost; - API-equivalent cost is not subscription billing. -- **Cross-environment composition:** raw transcripts never cross the wire. Each - environment returns typed aggregate buckets and a physical-source fingerprint. - Web and mobile use the shared merge to claim duplicate transcript directories once, - exclude incompatible contract versions, count distinct sessions without summing - per-bucket duplicates, and expose partial/failed coverage honestly. -- **Sources:** `packages/contracts/src/usage.ts`, `apps/server/src/usage/UsageService.ts`, - `usageTranscriptReader.ts`, `usageTranscripts.ts`, `usageAggregation.ts`, - `usagePricing.ts`, `usageScanCache.ts`, `packages/shared/src/usageMerge.ts`, the - usage RPC, web/mobile usage state and presentation, and their tests. -- **Third usage path:** Codex and Claude can also report per-task/subagent usage for - the Agents surface. It is neither the context meter nor historical cost input and - is deferred to Chapter 25 with provider tasks. -- **Visual:** two-lane topology separating live thread telemetry from transcript - accounting all the way through their distinct product presentations; there is no - joining or summation arrow. -- **Lab:** usage-accounting workbench. Choose provider records, duplicates, session - boundaries, cache categories, model-rate availability, time zone, and two - environments that may share a source fingerprint; step through parse, normalize, - deduplicate, bucket, price, summarize, and merge while every total shows its formula. - -### Part V — The work lifecycle - -#### 21. Project discovery and `t3.json` - -- **Settles:** roots, discovery, project identity, configuration precedence, - explicit registration entry points, checked-in-script import boundaries, setup - commands, environment labels, and project projection. -- **Sources:** project contracts/services, config loader, discovery tests and docs. -- **Visual:** filesystem decision lab, checked-in-action import flow, and project - identity projection. - -#### 22. Current checkout versus isolated worktrees - -- **Settles:** workspace kinds, branch/base selection, creation/setup/cleanup, - Git invariants, normal thread-deletion versus optional worktree cleanup, and - failure recovery. -- **Sources:** workspace/worktree services, VCS contracts, orchestration decider. -- **Lab:** choose a task topology and inspect checkout/branch consequences. - -#### 23. Start, stream, steer, interrupt, settle - -- **Settles:** complete thread/turn state machine and provider runtime states, - including buffering, steering, interruption, retries, compaction, and completion. -- **Sources:** orchestration contract/decider/reactors, ProviderService, reducers. -- **Lab:** event timeline with illustrative teaching controls at each state; it is - explicitly not presented as the server's complete legality matrix. - -#### 24. Permission modes, approvals, and structured input - -- **Settles:** approval-required, auto-accept-edits, auto, full-access; how provider - prompts become canonical pending requests and how clients resolve them. -- **Sources:** runtime mode contracts, provider adapters, approval/input reactors, - web/mobile components. -- **Visual:** security-responsibility matrix and multi-provider mapping. - -#### 25. Threads, provider tasks, plans, skills, and subagents - -- **Settles:** threads as durable work items; provider-emitted tasks/subagents as - normalized durable activity; ephemeral liveness and live plan progress; lifecycle/ - pin/snooze overlays; per-task token/tool/duration rollups currently normalized by - Codex and Claude; and why this is not a durable cross-provider scheduler. Task - usage is not fed into either live context telemetry or Chapter 20's historical - transcript accounting. -- **Sources:** orchestration/settings contracts, provider skills, work-log logic. -- **Lab:** quiet work-log projector for web, Agents surface, and mobile. - -#### 26. Who owns context, compaction, and memory - -- **Settles:** provider-owned prompt context and compaction versus T3-owned thread - history, resume cursor, and context-window telemetry, with client drafts/outbox - deferred to Chapter 33. It explicitly documents that no universal T3 long-term - memory subsystem exists at this revision and cross-links the separate historical - accounting pipeline in Chapter 20 instead of conflating usage with memory. -- **Sources:** provider sessions, runtime context-window events, thread state/reducer, - provider compaction paths, and client cache ownership. -- **Lab:** ownership and restart ledger covering provider context, T3 projections, - live telemetry, client cache, drafts, and provider-session recovery. - -#### 27. Hidden-ref checkpoints, diffs, and revert - -- **Settles:** before/after turn checkpoints, hidden Git refs, changed-file summary, - turn/branch/working-tree diffs, destructive restore semantics, `ready`/`missing`/ - `error`, provider-specific rollback, and partial failure. Client history-epoch - implementation is deferred to Chapter 29. -- **Sources:** checkpoint service/reactor/contracts, diff state, web/mobile review. -- **Lab:** Git graph before a turn, after a turn, and after revert. - -#### 28. Terminals, files, previews, MCP, VCS, and pull requests - -- **Settles:** the workbench services surrounding chat, their authorization and - streaming models, signed assets, desktop-only preview, and surface parity. -- **Sources:** terminal/files/assets/MCP/VCS/PR contracts and services; Ghostty - implementations; desktop preview APIs. -- **Visual:** capability matrix plus terminal and signed-asset data paths. - -### Part VI — Client architectures: shared semantics, platform edges - -#### 29. The shared client runtime - -- **Settles:** Primary/Bearer/Relay/SSH target taxonomy, connection registry/resolver/ - supervisor/one-attempt session, Effect Atom registry, - shell/thread factories, snapshot/cursor algorithms, cache ownership, and retry. -- **Sources:** `packages/client-runtime/README.md` and `src/connection`, `src/rpc`, - `src/state`. -- **Lab:** snapshot, duplicate, gap, reconnect, revert, and older-page races. - -#### 30. Web routes, state, and rendering performance - -- **Settles:** hosted/local/Electron runtime choice, router history, providers, - atoms, virtualization, row-local updates, trace export, and browser terminal. -- **Sources:** `apps/web/src/main.tsx`, `AppRoot.tsx`, route tree, atom registry, - `MessagesTimeline.tsx`, `ChatView.tsx`. -- **Visual:** React composition and hot-path rendering diagram. - -#### 31. Composer, work log, review, and sidebar lifecycle - -- **Settles:** message composition/attachments/commands/skills, optimistic states, - quiet timeline and Agents surface, review modes, thread ordering and pinning. -- **Sources:** web composer, session logic, diff panel, thread sort/sidebar. -- **Lab:** same canonical thread projected into focused product surfaces. - -#### 32. Desktop: Electron, IPC, server ownership, browser, and SSH - -- **Settles:** main/preload/renderer boundary, the host-local primary, Windows - WSL-only versus dual-instance modes, backend-pool ownership, fd 3/4/5 bootstrap - and telemetry, exposure, the separate SSH gateway, preview webviews, menus, and - updates. -- **Sources:** desktop main/app/preload/backend/preview/SSH/update modules. -- **Visual/lab:** mutually exclusive primary choices, optional WSL secondary, - separate SSH authority, and the boot/readiness/shutdown lifecycle. - -#### 33. Mobile: persistence, outbox, sharing, and native systems - -- **Settles:** Expo app composition, native navigation/feed choices, durable drafts - and intent outbox, share reservation, native terminal/diff/keyboards/widgets, - iOS notification capability, and OTA coordination. -- **Sources:** app config, connection runtime, outbox, ThreadFeed, native modules, - awareness, updates. -- **Lab:** offline command-outbox state machine and guarded foreground/background - OTA update handoff. - -### Part VII — Reach and ship - -#### 34. Primary, paired bearer, Tailscale endpoints, and SSH access - -- **Settles:** launch transport versus access transport, bind/exposure policy, - one-time pairing, bearer registration, Tailscale endpoint provisioning, and - desktop SSH gateway behavior. -- **Sources:** remote/environment-auth docs, Tailscale/SSH packages, resolver, - desktop exposure and gateway code. -- **Visual:** primary/direct, paired bearer over LAN or Tailscale, and desktop-managed - SSH sequences. Tailscale is an endpoint provider, not a connection target kind. - -#### 35. T3 Connect: OAuth, DPoP, relay, and tunnel - -- **Settles:** Clerk session, device key, DPoP, environment registration, relay - broker, tunnel provisioning, OAuth callback path, and what traffic does *not* - traverse the relay. -- **Sources:** `docs/internals/t3-connect.md`, relay infra, hosted routes, remote auth. -- **Visual/lab:** credential ladder and launch/data-plane toggle. - -#### 36. Reconnect, multi-environment state, notifications, and version skew - -- **Settles:** environment catalog, generation leases, shell/thread reconciliation, - background demand, multi-environment merge, awareness relay/APNs, and capability/ - exact-version recovery. -- **Sources:** client supervisor/state, background contracts/policy, relay awareness, - client version skew and self-update state. -- **Labs:** connection supervisor and notification throttling/fallback. - -#### 37. Distribution artifacts: CLI, hosted app, desktop, mobile, marketing, and AUR - -- **Settles:** npm CLI plus copied web build, hosted static web, Electron artifact - matrix/signing/native staging, mobile stores, marketing, AUR, and the difference - between builder support and artifacts actually shipped. -- **Sources:** build configs/scripts, package manifests, release workflow, marketing - download resolver, AUR scripts. -- **Visual:** artifact factory from source tree to installable products. - -#### 38. Release graph, three update systems, and observability/privacy - -- **Settles in three explicit acts:** (1) release DAG, two version domains, and the - npm-before-clients invariant; (2) Electron updater, managed-server exact-version - preflight, and EAS fingerprint/OTA as three independent state machines; (3) - PostHog product analytics, local/OTLP tracing, browser trace ingestion, and local - desktop resource telemetry with precise identities, destinations, and controls. -- **Sources:** release/mobile workflows, updater state machines, self-update, - analytics/observability/resource telemetry. -- **Labs:** release-channel resolver, OTA eligibility grid, update handshake. - -### Part VIII — Synthesis - -#### 39. Six complete traces - -- **Traces:** local first turn; relay-connected mobile turn; approval round-trip; - offline mobile task drain; checkpoint diff/revert; stable release and exact-version - update. -- **Artifact:** synchronized swimlanes whose steps link back to the owning chapters - and exact sources. - -#### 40. Decisions, trade-offs, limitations, and an honest roadmap - -- **Settles:** why server authority, the transactional event core, hot post-commit - reactors, snapshot + cursor, adapters, one reconnect owner, durable mobile intent, - exact-version updates, and scope-driven background work are valuable—and what - complexity each choice creates. -- **Includes:** verified discrepancies, platform asymmetries, latent artifact targets, - transitional plan UI, no universal memory layer, and explicitly documented future - remote work. It also covers tombstone deletion/selected cleanup, retained event/ - binding/worktree/checkpoint state, attachment cleanup outside the projection - transaction, the replay-marker retention inference, and the server reactor crash window. -- **Visual:** decision ledger with pressure, choice, benefit, cost, alternative, and - reversal trigger. - -## 5. Visual system - -Visuals use a consistent grammar rather than decorative diagrams: - -- navy = client/product surface; -- blue = typed transport or contract; -- amber = durable intent/state; -- green = post-commit side effect or external execution; -- violet = provider-native protocol; -- red = failure, trust, or destructive boundary; -- dashed edge = asynchronous, retryable, or eventual; -- solid edge = synchronous call or transactional relation. - -Every figure has a title, numbered caption, text equivalent, source trail, keyboard -operation where interactive, and a static print state. Mermaid is reserved for -sequences/flows whose source is clearer as text. Bespoke Astro components handle -state machines, comparisons, timelines, and simulations. - -## 6. Interaction inventory - -The final book contains, at minimum: - -1. ownership layer map; -2. end-to-end request trace; -3. entity/cardinality explorer; -4. monorepo graph filters; -5. runtime topology switcher; -6. connection supervisor state machine; -7. RPC frame inspector; -8. command/decider lab; -9. projection fold lab; -10. crash/recovery lab; -11. provider capability matrix; -12. work lifecycle controller; -13. quiet work-log projector; -14. usage accounting and cross-environment de-duplication workbench; -15. context/compaction/memory ownership ledger; -16. checkpoint Git graph; -17. cursor/reconnect race lab; -18. mobile outbox lab; -19. notification delivery lab; -20. artifact/release explorer; -21. OTA/update eligibility lab; -22. synchronized six-trace ownership and failure-boundary stepper; -23. decision ledger; -24. background demand/power-policy lab; -25. three-updater failure comparison; -26. telemetry identity/destination/privacy flow. - -An interaction is included only when changing an input reveals a state transition, -invariant, or trade-off that static prose would obscure. - -## 7. Book engine - -The old `codex-decoded` engine supplied the useful visual precedent: source cards, -architecture diagrams, and linear chapter navigation. This edition replaces its -eager Vite/hash/HTML-string architecture with: - -- Astro static routes and typed MDX content; -- one content collection as the navigation/search/metadata authority; -- build-time excerpt extraction and source-lock validation; -- Pagefind full-text search with development metadata fallback; -- lazy Mermaid and no framework runtime for ordinary pages; -- accessible sidebar, keyboard search, theme, heading navigation, previous/next, - reduced motion, and print styles; -- responsive source cards with real line numbers, checksums, copy, and immutable - GitHub permalinks; -- `BASE_PATH` support for repository-scoped GitHub Pages without changing links. - -The target is a content-first static site: JavaScript is paid only for search, -diagrams, and genuine simulations. - -## 8. Authoring waves and review gates - -### Wave A — mental model and kernel - -- Front matter and Chapters 1–15. -- Gate: one request can be traced from RPC to committed event, reactor, provider, - runtime ingestion, projection, and client cursor with no unexplained jump. - -### Wave B — providers, usage, and work lifecycle - -- Chapters 16–28. -- Gate: every provider is compared against the actual adapter contract; context, - memory, task, plan, live token telemetry, historical usage accounting, and - checkpoint ownership are not conflated. - -### Wave C — clients and remote access - -- Chapters 29–36. -- Gate: every surface difference is explicit; shared runtime algorithms and - presentation-specific algorithms are both explained. - -### Wave D — distribution and synthesis - -- Chapters 37–40 and six end-to-end traces. -- Gate: current stable, nightly, builder-only, mobile, and managed-server paths are - distinct; roadmap statements are evidence-classified. - -### Review loop for every wave - -1. Source audit against the pinned checkout. -2. Claim/excerpt/source-trail validation. -3. Cross-chapter vocabulary and forward-reference review. -4. Diagram and simulator invariant review. -5. Production build, search index, internal links, and responsive static checks. -6. Editorial pass for causal order, repetition, and unstated assumptions. -7. Local commit. Merge authored waves through pull requests once the public remote - and branch protection are active. - -## 9. Automated quality gates - -Required local commands: - -```sh -npm run source:check -npm test -npm run build -``` - -The validation suite will grow to enforce: - -- unique slugs and chapter order; -- a complete 1–40 chapter table of contents; -- valid excerpt IDs and source-lock SHA; -- no source-checked chapter without a source trail; -- no broken internal route/heading/source permalink; -- alt text and accessible names for visual/interactive components; -- all simulations usable by keyboard and meaningful in print; -- Pagefind indexing every non-cover chapter; -- bounded client bundles, with Mermaid and labs split by route; -- zero external analytics or network dependency in the local book. - -## 10. Definition of done - -The project is complete when all 40 chapters and front matter are present, all are -`source-checked` or `verified`, every planned flow has either a figure or lab, exact -source references resolve at the pinned revision, the six synthesis traces agree -with their detailed chapters, the static build and validation suite pass, and a -fresh reader can progress from ownership to deployment without requiring knowledge -that appears later in the book. - -## 11. Public repository and GitHub Pages - -The publication target is a public GitHub repository named `t3code-decoded` with -GitHub Pages serving the validated static build. It is owned by the personal -`BenAlaa` account, not an EasyGenerator organization. - -- Default branch: `main` (no parallel `master` branch). -- Pages source: GitHub Actions artifact from `npm ci`, source validation, tests, - and `npm run build`. -- Pull requests: required before any authored book commit reaches `main`; the only - bootstrap exception is GitHub's generated placeholder commit used to create the - base branch before protection is enabled. -- Reviews: at least one approving review; stale approvals dismissed when new - commits are pushed; latest-push approval and conversation resolution required. - The owner account `BenAlaa` receives **pull-request-only** bypass so a solo-owned - PR can merge without self-approval while still leaving a PR and bypass audit - trail. It receives no routine direct-push exemption. -- Checks: `Validate and build` and `Conventional changes` must pass; the latter - enforces the PR title/body and every fine-grained commit subject. Force pushes - and branch deletion are disabled, and history stays linear through rebase merges. -- Community files: detailed `README.md`, `CONTRIBUTING.md`, code of conduct, - security policy, issue forms, pull-request template, and `CODEOWNERS`. -- Licensing/attribution: distinguish original book prose/site code from short - MIT-licensed T3 Code excerpts, and state clearly that this is an independent, - unofficial study guide. - -The public repository and Pages pipeline are active. The complete source-validated -edition is published from protected `main`. New work remains on local or topic -branches until it is pushed and opened as a pull request, then merges only after -the required checks and review policy are satisfied. No authored project work is -pushed directly to `main`; every change keeps its pull-request audit trail. - -Commits inside a part remain fine-grained: shared engine capability, individual -chapter or tightly coupled chapter pair, source manifest change, lab/figure, and -review correction are separate when they can be understood and reverted alone. -Pull-request bodies use the repository template and explain outcome, non-goals, -evidence, interactions, validation, and the review's riskiest assumptions. +- Cover and source synchronization +- Reading routes and evidence labels +- Complete contents + +### Part I — Product guide + +1. T3 Code as a product +2. Install and reach a useful first thread +3. Environments, clients, and connection paths +4. Organize projects, threads, and worktrees +5. Compose tasks with the right context +6. Choose providers, models, accounts, and permissions +7. The workbench: files, terminals, browser preview, and SnapShots +8. Source control: checkpoints, pull requests, reviews, and stacks +9. Remote environments and unattended work +10. Mobile: supervise agents from anywhere +11. Device lab: simulators, emulators, and agent-driven testing +12. Personalize, measure usage, update, and protect privacy +13. Complete recipes and troubleshooting + +Each product chapter must answer four questions: what the capability does, how to +start, why it helps, and what is easy to misunderstand. The complete guide covers +web, desktop, iOS, Android, local and remote environments, six providers, parallel +work, rich context, review, recovery, and device verification. + +### Part II — Architecture orientation and boundaries + +14. The complete system map +15. One request, every boundary +16. Control surface, not agent brain +17. Environment, project, thread, turn, and session +18. Repository and dependency atlas +19. Runtime topologies and technology placement + +This part establishes ownership before implementation detail. It includes provider +control services, agent-session import, device hosts and sessions, review graphs, and +all client surfaces. + +### Part III — Boot and connect + +20. The `npx t3` bootstrap path +21. Server composition, activation, and readiness +22. HTTP, WebSocket RPC, snapshots, and resume +23. Pairing, credentials, TTLs, and scopes + +This part explains configuration, process ownership, Effect layers, typed methods, +subscriptions, cursors, version negotiation, pairing, bearer and DPoP credentials, +and method authorization. + +### Part IV — Transactional domain core + +24. Commands, invariants, and the boundary of atomicity +25. Events, receipts, idempotency, and the post-commit gap +26. Projection tables and read models +27. Post-commit reactors and the delivery gap +28. Persistence, reconstruction, and crash recovery + +This part keeps the SQLite transaction boundary precise. It distinguishes accepted +intent from completed provider work, synchronous projections from hot reactors, and +durable records from files, Git refs, settings, secrets, or live processes. + +### Part V — Six providers, one product model + +29. The `ProviderAdapter` contract +30. Drivers, instances, registries, and multi-instance routing +31. Codex through app-server JSON-RPC +32. Claude through the Agent SDK +33. ACP transport, Cursor, and Grok +33A. Antigravity through ACP, managed auth, and account catalogs +34. OpenCode ownership, recovery, and six-provider normalization +35. Usage accounting and provider limits without a false ledger + +Provider chapters preserve native differences in authentication, models, sessions, +steering, approvals, input, tasks, skills, attachments, compaction, usage, and rewind. +The usage chapter keeps live context, transcript history, custom prices, and pooled +subscription windows separate. + +### Part VI — Work lifecycle and integrated tools + +36. Project discovery, onboarding import, and `t3.json` +37. Current checkout versus isolated worktrees +38. Start, stream, steer, interrupt, settle +39. Permission modes, approvals, and structured input +40. Threads, provider tasks, plans, skills, and subagents +41. Context is provider-owned; history is T3-owned +42. Hidden-ref checkpoints, diffs, and revert +43. Terminals, files, previews, MCP, VCS, and review graphs +43A. Device hosts, targets, sessions, and agent control + +This part follows a work item through discovery, workspace selection, provider work, +interaction, context, checkpointing, workbench tools, several linked reviews, GitHub +stacks, local or SSH device hosts, and agent device tools. + +### Part VII — Client architectures + +44. Shared runtime: connections, state, and convergence +45. One React renderer, three runtime edges +46. One thread, many deliberate projections +47. Electron desktop: one renderer, explicit native authority +48. Mobile: adaptive workspaces, persistence, and native systems + +The client chapters explain shared settings, environment selection, load balancing, +snapshot and stream convergence, browser and desktop edges, adaptive mobile layouts, +offline drafts and uploads, native review and terminal surfaces, media, notifications, +voice input, and OTA compatibility. + +### Part VIII — Reach and ship + +49. Reachability is a route; authority is a separate proof +50. T3 Connect: OAuth, DPoP, relay, and tunnel +51. Reconnect, environments, notifications, and version skew +52. Distribution: artifacts, channels, and what actually ships +53. Release, update, and observability: three safety boundaries + +This part separates endpoint reachability from authorization, covers direct, Tailscale, +T3 Connect, and SSH paths, then traces reconnection, packaging, stores, hosted surfaces, +nightlies, reversible server updates, analytics, tracing, and resource diagnostics. + +## Visual and interaction contract + +Every chapter must contain or point to a diagram, interactive lab, comparison, state +machine, or decision table that clarifies the mechanism. Motion is reader-triggered, +bounded, and disabled or simplified under `prefers-reduced-motion`. Every visual has +a text equivalent, works with keyboard input when interactive, remains meaningful in +print, and does not fetch data at runtime. + +The site keeps one readable measure, persistent chapter navigation, full-text search, +light/dark/system themes, responsive tables, linkable headings, and previous/next +navigation. Product diagrams show tasks and choices; technical diagrams show ownership, +durability, data flow, and failure boundaries. + +## Validation contract + +Completion requires: + +- unique slugs and display order; +- complete product and technical contents; +- valid source lock, excerpt ids, checksums, references, and immutable links; +- no draft chapters; +- accessible names and text equivalents for visuals; +- keyboard-safe interactions and reduced-motion behavior; +- all source, content, test, build, Pagefind, and built-site checks passing; +- no external runtime dependency for local reading; and +- a clean reading path from product task to implementation boundary. + +## Publication + +The repository publishes the validated static Astro build through GitHub Pages. The +public site follows protected `main`; local work can remain ahead until it is reviewed. +Commits remain focused and conventional, and pull requests explain the problem, +resulting behavior, evidence, interaction changes, and validation. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bcbc5c4..9396ba0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -161,7 +161,7 @@ For commands, side effects, persistence, transport, updates, and cleanup, answer Quote only the minimum source required to explain the design. Do not copy upstream documentation or substantial source files into the book. Preserve attribution and immutable links. Never include credentials, local paths containing personal data, -private downstream implementation details, or unpublished repository content. +private Easy Code implementation details, or unpublished repository content. ## Diagrams and interactive labs diff --git a/NOTICE.md b/NOTICE.md index 5fdcba0..94a5830 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -7,8 +7,8 @@ architecture. The upstream project is available at: https://github.com/pingdotgg/t3code -The source snapshot used by this edition is commit -`fa219001dc2f14cfd9c7774c2c03c153359144be`. T3 Code is licensed under the MIT +The referenced T3 Code source is commit +`859304b7808ab9a4be87b1bddcd07c6485bf9c4f`. T3 Code is licensed under the MIT License. The following upstream notice and permission terms remain applicable to every included excerpt: diff --git a/README.md b/README.md index 7fab792..6dfaaf1 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ # T3 Code Decoded - **A source-grounded, interactive guide to the architecture and implementation of T3 Code.** + **A complete product guide and source-grounded interactive guide to T3 Code's architecture and implementation.** [Read the book](#read-the-book) · [Explore the plan](./BOOK_PLAN.md) · [Contribute](./CONTRIBUTING.md) · [Source policy](#source-grounding) @@ -18,25 +18,31 @@ Created and maintained by [Ahmed Alaa (`@BenAlaa`)](https://github.com/BenAlaa). > [!NOTE] > The public Pages site is deployed from protected `main`. It reflects the latest -> merged revision; topic branches remain local or unmerged until their reviewed -> pull requests are ready. +> merged milestone; stacked local authoring branches can be ahead while later parts +> wait for their own reviewed pull requests. > No authored work is pushed directly to `main`. ## Read the book [Read the published book](https://benalaa.github.io/t3code-decoded/). When reviewing -an unmerged change, run that branch locally with the instructions below—the -published site intentionally tracks protected `main`, not unpublished work. +an unmerged milestone, run that branch locally with the instructions below—the +published site intentionally tracks protected `main`, not local stacked work. -The current edition is pinned to -[`pingdotgg/t3code@fa219001d`](https://github.com/pingdotgg/t3code/tree/fa219001dc2f14cfd9c7774c2c03c153359144be). +The book is synchronized with +[`pingdotgg/t3code@859304b78`](https://github.com/pingdotgg/t3code/tree/859304b7808ab9a4be87b1bddcd07c6485bf9c4f), +captured on 11 September 2026. Every exact excerpt in the book is generated from that revision, checksum-verified, and linked back to immutable GitHub source lines. ## What this book explains -T3 Code is not the reasoning engine inside Codex, Claude, Cursor, Grok, or -OpenCode. It is the server-authoritative control plane around them: it normalizes +The product guide explains installation, onboarding, environments, projects, +threads, worktrees, composing with rich context, providers, permissions, files, +terminals, previews, SnapShots, source control, remote access, mobile, device +testing, usage, updates, privacy, complete workflows, and troubleshooting. + +The technical guide explains that T3 Code is not the reasoning engine inside Codex, +Claude, Cursor, Grok, OpenCode, or Antigravity. It is the server-authoritative control plane around them: it normalizes intent, durably records product state, starts and supervises provider runtimes, projects ordered state to several clients, and surrounds the conversation with worktrees, Git checkpoints, terminals, files, previews, remote access, usage, and @@ -48,30 +54,29 @@ Web · Desktop · Mobile ▼ T3 server: command → event → projection → reactor │ │ - │ └─ files · Git · terminal · tunnel + │ └─ files · Git · terminal · devices · tunnel ▼ ProviderAdapter │ native protocol ▼ -Codex · Claude · Cursor · Grok · OpenCode +Codex · Claude · Cursor · Grok · OpenCode · Antigravity ``` -The book follows that causal path instead of mirroring repository folders. Its -eight parts cover: - -1. ownership boundaries, vocabulary, repository topology, and runtime shapes; -2. CLI/server boot, Effect RPC, pairing, authorization, subscriptions, and resume; -3. commands, events, receipts, projections, reactors, SQLite, and recovery; -4. the provider adapter contract, all five harness integrations, and both usage - systems; -5. projects, worktrees, turns, permissions, plans, tasks, context, memory, - checkpoints, terminals, files, VCS, MCP, preview, and pull requests; -6. the shared client runtime plus web, Electron, and React Native clients; -7. direct/relay/Tailscale/SSH access, reconnection, packaging, releases, updates, and telemetry; -8. complete end-to-end traces, architectural trade-offs, limitations, and - evidence-bounded roadmap analysis. - -See [BOOK_PLAN.md](./BOOK_PLAN.md) for the complete 40-chapter specification, +The book starts with product tasks, then follows that causal path instead of +mirroring repository folders. Its eight parts cover: + +1. the complete product guide and task recipes; +2. architecture orientation, ownership boundaries, vocabulary, repository topology, and runtime shapes; +3. CLI/server boot, Effect RPC, pairing, authorization, subscriptions, and resume; +4. commands, events, receipts, projections, reactors, SQLite, and recovery; +5. the provider adapter contract, all six provider integrations, historical usage, + live context, and subscription limits; +6. projects, worktrees, turns, permissions, plans, tasks, context, memory, + checkpoints, terminals, files, VCS, MCP, previews, pull requests, and devices; +7. the shared client runtime plus web, Electron, and React Native clients; +8. direct/relay/Tailscale/SSH access, reconnection, packaging, releases, updates, and telemetry. + +See [BOOK_PLAN.md](./BOOK_PLAN.md) for the complete 53-chapter specification, figure/lab inventory, review gates, and definition of done. ## Why another set of docs? @@ -82,7 +87,7 @@ answers a different class of questions: - Where is the transaction boundary? - What does a command receipt actually prove? - Which state survives a server restart? -- How do five provider protocols become one product vocabulary? +- How do six provider integrations become one product vocabulary? - Why does reconnect logic live above a one-attempt RPC session? - What is shared across clients, and what deliberately differs? - Which remote component allocates credentials, and where does application traffic @@ -192,7 +197,7 @@ checkout: ```sh git clone https://github.com/pingdotgg/t3code.git ../t3code -git -C ../t3code checkout fa219001dc2f14cfd9c7774c2c03c153359144be +git -C ../t3code checkout 859304b7808ab9a4be87b1bddcd07c6485bf9c4f npm run source:check ``` diff --git a/sources/excerpts.manifest.json b/sources/excerpts.manifest.json index f39f958..e4e938c 100644 --- a/sources/excerpts.manifest.json +++ b/sources/excerpts.manifest.json @@ -1,3 +1,4 @@ + [ { "id": "architecture-boundary", @@ -26,24 +27,24 @@ { "id": "provider-adapter-core", "path": "apps/server/src/provider/Services/ProviderAdapter.ts", - "start": 47, - "end": 71, + "start": 67, + "end": 94, "language": "typescript", "label": "Provider adapter lifecycle contract" }, { "id": "provider-adapter-interactions", "path": "apps/server/src/provider/Services/ProviderAdapter.ts", - "start": 73, - "end": 94, + "start": 97, + "end": 117, "language": "typescript", "label": "Provider approval and input contract" }, { "id": "provider-adapter-stream", "path": "apps/server/src/provider/Services/ProviderAdapter.ts", - "start": 126, - "end": 134, + "start": 149, + "end": 157, "language": "typescript", "label": "Canonical provider event stream" }, @@ -395,7 +396,7 @@ "id": "usage-web-context-meter", "path": "apps/web/src/lib/contextWindow.ts", "start": 50, - "end": 96, + "end": 90, "language": "typescript", "label": "Latest valid context-window snapshot derivation" }, @@ -1258,8 +1259,8 @@ { "id": "resource-telemetry-demand-history", "path": "docs/internals/resource-telemetry.md", - "start": 129, - "end": 170, + "start": 9, + "end": 50, "language": "markdown", "label": "Bounded native history and diagnostics-driven telemetry streaming" }, @@ -1290,8 +1291,8 @@ { "id": "connect-environment-link-handshake", "path": "apps/web/src/cloud/linkEnvironment.ts", - "start": 402, - "end": 493, + "start": 256, + "end": 347, "language": "typescript", "label": "Client-side relay challenge, environment proof, link, and runtime setup" }, diff --git a/sources/references.manifest.json b/sources/references.manifest.json index c68320b..503199b 100644 --- a/sources/references.manifest.json +++ b/sources/references.manifest.json @@ -1,3 +1,4 @@ + [ { "id": "rpc-group", @@ -662,10 +663,8 @@ { "id": "stale-startup-overview", "path": "docs/internals/overview.md", - "start": 124, - "end": 130, "kind": "file", - "label": "stale documented startup sequence" + "label": "current server boundaries and startup constraints" }, { "id": "orchestration-snapshot-contracts", @@ -1022,10 +1021,8 @@ { "id": "auth-doc-ticket-overclaim", "path": "docs/internals/environment-auth.md", - "start": 95, - "end": 111, "kind": "file", - "label": "documentation claim that WebSocket tickets carry scopes" + "label": "current environment authentication contract" }, { "id": "decider-effect-dependencies", @@ -1634,8 +1631,8 @@ { "id": "provider-adapter-registry-live", "path": "apps/server/src/provider/Layers/ProviderAdapterRegistry.ts", - "start": 36, - "end": 100, + "start": 30, + "end": 85, "kind": "file", "label": "live adapter facade excludes unavailable instances" }, @@ -1778,8 +1775,8 @@ { "id": "codex-app-server-child-process", "path": "packages/effect-codex-app-server/src/client.ts", - "start": 212, - "end": 268, + "start": 213, + "end": 264, "kind": "file", "label": "Codex app-server child process and stdio client construction" }, @@ -2166,8 +2163,8 @@ { "id": "claude-provider-skills-commands", "path": "apps/server/src/provider/Layers/ClaudeProvider.ts", - "start": 920, - "end": 962, + "start": 526, + "end": 588, "kind": "file", "label": "Claude snapshot combines discovered skills and initialization commands" }, @@ -2334,16 +2331,16 @@ { "id": "turn-lifecycle-settlement-guards", "path": "packages/client-runtime/src/state/threadSettled.ts", - "start": 286, - "end": 312, + "start": 14, + "end": 45, "kind": "file", - "label": "Thread settlement eligibility guards" + "label": "queued turn-start guard used by thread lifecycle decisions" }, { "id": "turn-lifecycle-waiting-ui-test", "path": "apps/web/src/components/chat/ComposerPrimaryActions.test.tsx", - "start": 214, - "end": 219, + "start": 95, + "end": 109, "kind": "file", "label": "Stop action retained while provider waits for input" }, @@ -2957,7 +2954,7 @@ }, { "id": "workbench-terminal-mobile", - "path": "apps/mobile/src/features/terminal/ThreadTerminalPanel.tsx", + "path": "apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx", "start": 31, "end": 210, "kind": "file", @@ -3463,7 +3460,7 @@ "id": "client-connection-resolver", "path": "packages/client-runtime/src/connection/resolver.ts", "start": 51, - "end": 280, + "end": 252, "kind": "file", "label": "Target-specific preparation into a prepared connection" }, @@ -3951,7 +3948,7 @@ "id": "release-server-update-architecture", "path": "docs/internals/server-updates.md", "start": 1, - "end": 99, + "end": 58, "kind": "file", "label": "Stable launcher, reversible trial, database snapshot, and client correlation" }, @@ -3991,7 +3988,7 @@ "id": "telemetry-browser-client-tracing", "path": "apps/web/src/observability/clientTracing.ts", "start": 16, - "end": 133, + "end": 125, "kind": "file", "label": "Web tracer delegates OTLP batches through the selected primary environment" }, @@ -4007,7 +4004,7 @@ "id": "telemetry-resource-architecture", "path": "docs/internals/resource-telemetry.md", "start": 1, - "end": 352, + "end": 51, "kind": "file", "label": "Native resource telemetry topology, demand, retention, and packaging" }, @@ -4043,7 +4040,7 @@ "id": "access-connection-resolver", "path": "packages/client-runtime/src/connection/resolver.ts", "start": 51, - "end": 277, + "end": 252, "kind": "file", "label": "Route-specific target resolution into a common prepared connection" }, @@ -4146,10 +4143,8 @@ { "id": "remote-explicit-future-work", "path": "docs/internals/remote.md", - "start": 223, - "end": 229, "kind": "file", - "label": "Explicitly unbuilt remote endpoint-provider, callback-broker, and multi-environment work" + "label": "Current remote endpoint, SSH, Tailscale, and T3 Connect architecture" }, { "id": "plan-ui-transition", @@ -4163,7 +4158,7 @@ "id": "plan-ui-capability-filter", "path": "apps/web/src/providerModels.ts", "start": 80, - "end": 112, + "end": 108, "kind": "file", "label": "Legacy plan-mode capability filtering before model dispatch" }, diff --git a/sources/t3code.lock.json b/sources/t3code.lock.json index 58584c2..af8b2b5 100644 --- a/sources/t3code.lock.json +++ b/sources/t3code.lock.json @@ -1,14 +1,15 @@ + { "repository": "https://github.com/pingdotgg/t3code", - "commit": "fa219001dc2f14cfd9c7774c2c03c153359144be", - "shortCommit": "fa219001d", + "commit": "859304b7808ab9a4be87b1bddcd07c6485bf9c4f", + "shortCommit": "859304b78", "branch": "main", - "capturedAt": "2026-08-24T00:00:00+03:00", + "capturedAt": "2026-09-11T00:00:00+03:00", "sourceDirHint": "../t3code", "inventoryRulesVersion": 1, - "productionFiles": 1844, - "productionLines": 533213, - "testFiles": 881, - "testLines": 247766, - "notes": "Recomputed from pinned Git objects by scripts/inventory-source.mjs. Counts include tracked implementation/build source extensions, exclude assets and .repos, and classify test/spec/fixture paths separately; generated protocol source remains included." + "productionFiles": 2288, + "productionLines": 669059, + "testFiles": 1170, + "testLines": 404139, + "notes": "Recomputed from pinned Git objects by scripts/inventory-source.mjs. Counts include tracked implementation and build-source extensions, exclude assets and .repos, and classify tests, specs, and fixtures separately." } diff --git a/src/components/BookCover.astro b/src/components/BookCover.astro index 70c2e1e..95a9f08 100644 --- a/src/components/BookCover.astro +++ b/src/components/BookCover.astro @@ -1,3 +1,4 @@ + --- import { Image } from "astro:assets"; import cover from "../../cover.png"; @@ -8,20 +9,20 @@ const base = import.meta.env.BASE_URL.endsWith("/") ? import.meta.env.BASE_URL :
- T3 Code Decoded book cover: an octopus connecting Codex, Claude Code, Cursor, Grok, OpenCode, and tools + T3 Code Decoded book cover with an octopus linking coding-agent tools
- Source-level field guide -

Follow the control plane all the way down.

-

From a tap on a phone to a provider subprocess, persisted event, hidden Git checkpoint, and streamed UI update—every layer is explained against one pinned source revision.

+ Product guide + source-level field guide +

Use the whole product. Then follow it all the way down.

+

Start with installation, daily workflows, remote work, reviews, mobile, and device testing. Continue from one user action into provider processes, durable events, checkpoints, clients, and recovery.

pingdotgg/t3code · commit {lock.shortCommit} Local/hosted web · Electron · iOS/Android - Codex · Claude · Cursor · Grok · OpenCode + Codex · Claude · Cursor · Grok · OpenCode · Antigravity
diff --git a/src/components/CheckpointGraphLab.astro b/src/components/CheckpointGraphLab.astro index 24cf096..37583e0 100644 --- a/src/components/CheckpointGraphLab.astro +++ b/src/components/CheckpointGraphLab.astro @@ -1,3 +1,4 @@ + --- interface Props { id: string; } const { id } = Astro.props; @@ -127,7 +128,7 @@ const payload = JSON.stringify(states).replaceAll("<", "\\u003c"); .checkpoint-graph-lab { margin:2rem 0; overflow:hidden; border:1px solid var(--line-strong); border-radius:14px; background:var(--paper-raised); font-family:var(--font-ui); } .checkpoint-graph-lab header { padding:1rem 1.2rem; border-bottom:1px solid var(--line); } .checkpoint-graph-lab header span,.detail-eyebrow { color:var(--signal-deep); font:750 .65rem/1 var(--font-ui); letter-spacing:.1em; text-transform:uppercase; } .checkpoint-graph-lab h3 { margin:.35rem 0; } .checkpoint-graph-lab header p { margin:0; color:var(--ink-soft); font-size:.82rem; } .checkpoint-controls { display:flex; flex-wrap:wrap; gap:.45rem; padding:1rem 1.2rem .7rem; } .checkpoint-controls button { min-height:2.35rem; border:1px solid var(--line); border-radius:999px; background:var(--paper); color:var(--ink-soft); padding:.35rem .65rem; font:700 .72rem/1.2 var(--font-ui); cursor:pointer; } .checkpoint-controls button[aria-pressed="true"] { border-color:var(--signal); background:var(--signal-soft); color:var(--signal-deep); } .checkpoint-controls button:focus-visible { outline:3px solid color-mix(in srgb,var(--signal) 45%,transparent); outline-offset:2px; } - .checkpoint-board { display:grid; grid-template-columns:minmax(17rem,.9fr) minmax(17rem,1.1fr); gap:1rem; padding:.4rem 1.2rem 1.2rem; } .git-graph { position:relative; min-height:15.5rem; overflow:hidden; border:1px solid var(--line); border-radius:10px; background:var(--paper-muted); padding:1rem; } .branch-label { color:var(--ink-faint); font:700 .67rem/1 var(--font-ui); letter-spacing:.07em; text-transform:uppercase; } .branch-line { position:absolute; top:5rem; left:2.45rem; right:54%; height:2px; background:var(--ink-faint); opacity:.6; } .commit-node,.checkpoint-node { position:absolute; z-index:1; display:grid; place-items:center; width:3.25rem; height:3.25rem; border:2px solid var(--line-strong); border-radius:50%; background:var(--paper); color:var(--ink-soft); text-align:center; box-shadow:0 1px 0 color-mix(in srgb,var(--ink) 10%,transparent); } .commit-node b,.checkpoint-node b { font-size:.86rem; } .commit-node span,.checkpoint-node span,.workspace-node span { position:absolute; top:3.65rem; width:6.8rem; color:var(--ink-soft); font:650 .66rem/1.25 var(--font-ui); } .baseline { top:3.35rem; left:1.4rem; } .commit-node span { top:-1.65rem; } .workspace-node { position:absolute; top:3.55rem; right:1.4rem; z-index:1; display:grid; place-items:center; width:3rem; height:2.75rem; border:2px dashed var(--line-strong); border-radius:.45rem; background:var(--paper); color:var(--ink-soft); text-align:center; } .workspace-node b { font-size:1.1rem; } .workspace-node span { top:-2.15rem; } .after { opacity:.43; } .after span { right:-1.8rem; } .checkpoint-rail { position:absolute; top:8.9rem; left:2.45rem; right:2.45rem; height:2px; border-top:2px dashed var(--line-strong); } .checkpoint-node { top:7.3rem; border-color:var(--signal); background:var(--signal-soft); color:var(--signal-deep); } .zero { left:1.4rem; } .one { right:1.4rem; opacity:.43; } .one span { right:-1.8rem; } .graph-key { position:absolute; right:1rem; bottom:.7rem; left:1rem; margin:0; color:var(--ink-faint); font-size:.67rem; line-height:1.35; } .graph-key span { color:var(--signal); } + .checkpoint-board { display:grid; grid-template-columns:minmax(17rem,.9fr) minmax(17rem,1.1fr); gap:1rem; padding:.4rem 1.2rem 1.2rem; } .git-graph { position:relative; min-height:15.5rem; overflow:hidden; border:1px solid var(--line); border-radius:10px; background:var(--paper-muted); padding:1rem; } .branch-label { color:var(--ink-faint); font:700 .67rem/1 var(--font-ui); letter-spacing:.07em; text-transform:uppercase; } .branch-line { position:absolute; top:5rem; left:2.45rem; right:54%; height:2px; background:var(--ink-faint); opacity:.6; } .commit-node,.checkpoint-node { position:absolute; z-index:1; display:grid; place-items:center; width:3.25rem; height:3.25rem; border:2px solid var(--line-strong); border-radius:50%; background:var(--paper); color:var(--ink-soft); text-align:center; box-shadow:0 1px 0 color-mix(in srgb,var(--ink) 10%,transparent); } .commit-node b,.checkpoint-node b { font-size:.86rem; } .commit-node span,.checkpoint-node span,.workspace-node span { position:absolute; top:3.65rem; width:6.8rem; color:var(--ink-soft); font:650 .66rem/1.25 var(--font-ui); } .baseline { top:3.35rem; left:1.4rem; } .workspace-node { position:absolute; top:3.55rem; right:1.4rem; z-index:1; display:grid; place-items:center; width:3rem; height:2.75rem; border:2px dashed var(--line-strong); border-radius:.45rem; background:var(--paper); color:var(--ink-soft); text-align:center; } .workspace-node b { font-size:1.1rem; } .after { opacity:.43; } .after span { right:-1.8rem; } .checkpoint-rail { position:absolute; top:8.9rem; left:2.45rem; right:2.45rem; height:2px; border-top:2px dashed var(--line-strong); } .checkpoint-node { top:7.3rem; border-color:var(--signal); background:var(--signal-soft); color:var(--signal-deep); } .zero { left:1.4rem; } .one { right:1.4rem; opacity:.43; } .one span { right:-1.8rem; } .graph-key { position:absolute; right:1rem; bottom:.7rem; left:1rem; margin:0; color:var(--ink-faint); font-size:.67rem; line-height:1.35; } .graph-key span { color:var(--signal); } .checkpoint-graph-lab[data-checkpoint-current="after"] [data-graph-node="after"], .checkpoint-graph-lab[data-checkpoint-current="diff"] [data-graph-node="after"], .checkpoint-graph-lab[data-checkpoint-current="diff"] [data-graph-node="one"] { opacity:1; } .checkpoint-graph-lab[data-checkpoint-current="after"] [data-graph-node="one"] { opacity:1; } .checkpoint-graph-lab[data-checkpoint-current="revert"] [data-graph-node="after"], .checkpoint-graph-lab[data-checkpoint-current="revert"] [data-graph-node="one"] { opacity:.14; text-decoration:line-through; } .checkpoint-graph-lab[data-checkpoint-current="failure"] [data-graph-node="after"] { opacity:.14; text-decoration:line-through; } .checkpoint-graph-lab[data-checkpoint-current="failure"] [data-graph-node="one"] { border-color:var(--orange); background:color-mix(in srgb,var(--orange) 14%,var(--paper)); color:color-mix(in srgb,var(--orange) 72%,var(--ink)); opacity:1; } .checkpoint-detail { border-left:4px solid var(--signal); background:var(--paper-muted); padding:.95rem; } .checkpoint-detail h4 { margin:.3rem 0 .45rem; } .checkpoint-detail > p { margin:0; color:var(--ink-soft); font-size:.82rem; line-height:1.55; } .checkpoint-detail dl { margin:1rem 0 0; display:grid; gap:.75rem; } .checkpoint-detail dl div { border-top:1px solid var(--line); padding-top:.55rem; } .checkpoint-detail dt { color:var(--ink-faint); font:750 .63rem/1 var(--font-ui); letter-spacing:.07em; text-transform:uppercase; } .checkpoint-detail dd { margin:.35rem 0 0; color:var(--ink-soft); font-size:.76rem; line-height:1.45; } .checkpoint-detail ul { margin:0; padding-left:1rem; } .checkpoint-detail code { font-size:.68rem; overflow-wrap:anywhere; } .checkpoint-graph-lab:not([data-checkpoint-graph-ready="true"]) .checkpoint-controls,.checkpoint-graph-lab:not([data-checkpoint-graph-ready="true"]) .checkpoint-board { display:none; } .checkpoint-graph-lab[data-checkpoint-graph-ready="true"] .checkpoint-fallback { display:none; } .checkpoint-fallback { margin:0 1.2rem 1.2rem; } .checkpoint-fallback li { margin:.85rem 0; } .checkpoint-fallback p { margin:.25rem 0; color:var(--ink-soft); font-size:.76rem; line-height:1.45; } .checkpoint-noscript { margin:0 1.2rem 1rem; color:var(--ink-faint); } diff --git a/src/components/DecisionLedgerLab.astro b/src/components/DecisionLedgerLab.astro deleted file mode 100644 index d5d88a9..0000000 --- a/src/components/DecisionLedgerLab.astro +++ /dev/null @@ -1,352 +0,0 @@ ---- -interface Props { - id?: string; -} - -const { id = "architecture-decision-ledger" } = Astro.props; -if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(id)) { - throw new Error("DecisionLedgerLab requires a stable kebab-case id."); -} - -const decisions = [ - { - category: "domain", - classification: "Shipped", - title: "Server authority", - pressure: "Remote clients must control one workspace without becoming competing owners of provider processes, Git, terminals, or files.", - choice: "One environment server owns product authority; clients use authenticated RPC and projections.", - benefit: "Authorization, orchestration, workspace effects, and durable product history meet at one address.", - cost: "Availability and recovery depend on that environment; clients reconcile rather than write authoritatively.", - alternative: "Client-owned or peer-to-peer workspaces with a separate conflict and credential model.", - trigger: "Concurrent offline editing or multi-writer workspace authority becomes a core requirement.", - }, - { - category: "domain", - classification: "Shipped", - title: "Transactional event core", - pressure: "A command needs a durable acceptance boundary and a retry answer before external work completes.", - choice: "Decide, append events, fold projections, and write a receipt in one SQLite transaction.", - benefit: "Accepted intent and its receipt survive together; failed transactions leave no acceptance result.", - cost: "Files, provider calls, and later delivery remain outside the transaction; projections require maintenance.", - alternative: "Direct CRUD where replayable history and independently shaped read models are not valuable.", - trigger: "Immutable command history and projection diversity stop paying for their operational complexity.", - }, - { - category: "delivery", - classification: "Shipped", - title: "Hot post-commit reactors", - pressure: "Do not call a harness while the domain transaction could still roll back.", - choice: "Publish committed events to scoped, hot reactor workers; keep failures isolated from acceptance.", - benefit: "Rollback cannot have caused provider work, and one consumer failure does not undo durable history.", - cost: "A crash after commit can lose pending observation; sends and runtime ingestion are not a durable outbox.", - alternative: "Durable outbox with attempts, idempotency keys, and a policy for ambiguous external acceptance.", - trigger: "An accepted turn must eventually imply an attempted provider send across process loss.", - }, - { - category: "delivery", - classification: "Shipped", - title: "Snapshot + cursor", - pressure: "Clients need rebuildable read state and bounded resume without calling every page one global snapshot.", - choice: "Each projection advances its own cursor; composed snapshots use a safe watermark and subscriptions repair gaps.", - benefit: "Read models evolve independently and can rebuild from committed events.", - cost: "Several watermarks, order-sensitive projectors, replay limits, and client race guards must remain intelligible.", - alternative: "One authoritative document per thread with fewer projections and less independent fan-out.", - trigger: "Projection lag, cross-view joins, or replay operations exceed the current cursor model's operating envelope.", - }, - { - category: "integration", - classification: "Shipped", - title: "Provider adapters", - pressure: "Harnesses disagree about sessions, approvals, streams, context, and process ownership.", - choice: "A narrow adapter contract emits canonical runtime facts while ProviderService owns product policy and routing.", - benefit: "One product model preserves native provenance without inventing false provider parity.", - cost: "Adapter maintenance and capability differences remain explicit; no universal conformance proof exists.", - alternative: "A stricter generic protocol that may discard native features or push product policy into every driver.", - trigger: "Important native lifecycles require pervasive escape hatches that the canonical contract cannot express.", - }, - { - category: "connectivity", - classification: "Shipped", - title: "One reconnect owner", - pressure: "Several views and devices can observe an environment without independent retry loops or merged authorities.", - choice: "An environment-scoped supervisor owns generations and one active lease; caches synchronize separately.", - benefit: "Transport lifecycle has one owner while shell and thread views keep precise refresh boundaries.", - cost: "Leases, generations, and surface-specific reconciliation add client-runtime complexity.", - alternative: "A global connection manager that would need another way to preserve environment ownership.", - trigger: "The product adds a real cross-environment write model, not just combined presentation.", - }, - { - category: "connectivity", - classification: "Shipped", - title: "Durable mobile intent outbox", - pressure: "A user can send while the phone loses connectivity or foreground time.", - choice: "Optimistically enqueue, persist durably, then confirm and deliver through a serialized mobile outbox.", - benefit: "User intent can survive locally before the environment can accept it.", - cost: "Backoff, existence guards, confirmation, and delivery choices become a separate lifecycle.", - alternative: "Always-online direct send, sacrificing the local recovery contract.", - trigger: "Intent becomes multi-device collaborative work needing server-issued identities or all clients need the same queue.", - }, - { - category: "operations", - classification: "Shipped", - title: "Exact-version updates", - pressure: "A visible client must not request a server runtime that is unavailable or incompatible.", - choice: "Publish the exact CLI first; stage and preflight the exact server runtime before launcher activation.", - benefit: "An update target is reproducible and the managed-server trial has a defined rollback boundary.", - cost: "Release order, launcher compatibility, and platform-specific update state machines remain necessary.", - alternative: "Floating channel updates paired with a stronger compatibility negotiation protocol.", - trigger: "Compatibility proof becomes sufficient without requiring equal client and server versions.", - }, - { - category: "connectivity", - classification: "Shipped", - title: "Scope-driven background work", - pressure: "Mobile should remain useful in the background without keeping every environment permanently active.", - choice: "Reference-counted scopes retain declared per-environment background demand.", - benefit: "Continuation has an owner and can end when that owner releases its interest.", - cost: "Scope cleanup and platform scheduling constraints remain part of correctness; it is not a durable job queue.", - alternative: "An always-on global worker with higher resource use and leak risk.", - trigger: "The product needs OS-managed durable jobs with explicit completion receipts.", - }, -]; - -const payload = JSON.stringify(decisions).replaceAll("<", "\\u003c"); -const first = decisions[0]; ---- - -
-
- Interactive decision ledger -

Trace the trade-off, then test its reversal trigger

-

Filter the ledger, select a decision, and move through its six-step path. No animation advances on its own.

-
- -
-
- Category -
- - - - - - -
-
- -
- - -
-
-

{first.classification} {first.category}

-

{first.title}

-
-
    -
  1. 1Pressure

    {first.pressure}

  2. -
  3. 2Choice

    {first.choice}

  4. -
  5. 3Benefit

    {first.benefit}

  6. -
  7. 4Cost

    {first.cost}

  8. -
  9. 5Alternative

    {first.alternative}

  10. -
  11. 6Reversal trigger

    {first.trigger}

  12. -
-
- -

1 of 6 · Pressure

- -
-
-
-

- Server authority selected. Step 1 of 6: Pressure. -

-
- -
- Complete static ledger -
- - - - {decisions.map((decision) => ( - - - - - - - - - - ))} - -
DecisionPressureChoiceBenefitCostAlternativeReversal trigger
{decision.classification}{decision.title}{decision.pressure}{decision.choice}{decision.benefit}{decision.cost}{decision.alternative}{decision.trigger}
-
-
- - -
- - - - diff --git a/src/components/SynchronizedTraceLab.astro b/src/components/SynchronizedTraceLab.astro deleted file mode 100644 index fe7f04b..0000000 --- a/src/components/SynchronizedTraceLab.astro +++ /dev/null @@ -1,266 +0,0 @@ ---- -interface Props { id: string; } - -const { id } = Astro.props; -if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(id)) { - throw new Error("SynchronizedTraceLab requires a stable kebab-case id."); -} -const labId = `synchronized-trace-${id}`; ---- - -
-
- Interactive synthesis lab · Chapter 39 -

Move one trace through the owner that acts next

-

Select a complete trace, then advance deliberately. The marker crosses the owner that acts next; its final boundary says what does—and does not—converge.

-
- -
-
- - - - - - -
- -
- -

Step 1 of 6

- - -
- -
-

Trace 1 · Local first turn

-

Client sends one existing-thread command

-

The user action enters the environment through an authorized RPC method; no provider work has happened yet.

- - - -
-
Owner now
client and RPC entry policy
-
Handoff / evidence
typed turn command reaches the selected environment
-
Durable fact now
none yet
-
Boundary if it stops here
an RPC failure is not a provider failure and does not create an accepted turn
-
-
-

Local first turn. Step 1 of 6.

-
- - -
- Static six-trace ledger -
- - - - - - - - - - -
TraceOwner sequenceDurable evidenceConvergence or failure boundary
Local first turnclient → RPC/engine → SQLite → provider reactor/adapter → engine → client stateaccepted event, projections, and receipt commit togetherhot reactor delivery is not replayed after a crash; later projections only show committed facts
Relay-connected mobile turnphone → relay control plane → selected environment → relay bootstrap return → direct mobile session → environment statedevice-held DPoP-bound environment session material and environment projectionsrelay launch authorization is not thread synchronization; ordinary traffic bypasses the relay
Approval round-tripprovider → runtime ingestion → pending projection → client → response intent → provider reactorpending request and response-requested event are separate durable factsnative provider reply is a later hot handoff where a stale or unknown request can fail; runtime-mode labels do not equal provider semantics
Offline mobile drainmobile atom → atomic outbox file → live shell → environment receipt/result → phone cleanup → later projection reconciliationconfirmed phone file, then remote command receiptthere is no transaction shared by phone storage and server SQLite; cleanup follows the command result, while projections reconcile separately
Checkpoint diff / revertcompletion → Git hidden ref/diff → checkpoint projection → durable revert request → ordered saga → engine completionhidden ref precedes checkpoint metadata; the request and later completion are separate orchestration factsrestore can happen before provider rollback; later failure can leave a real partial state
Stable exact updaterelease automation → exact npm runtime → client exposure → connected-client request → server preflight → stable launcher trialexact package publication, then launcher pending/trial/commit statepublish does not replace a machine; launcher can roll back a failed trial before commit
-
-
-
- - - - diff --git a/src/content/book/00-cover.mdx b/src/content/book/00-cover.mdx index f432c60..b908268 100644 --- a/src/content/book/00-cover.mdx +++ b/src/content/book/00-cover.mdx @@ -6,14 +6,14 @@ part: Start here partOrder: 0 title: T3 Code Decoded shortTitle: Cover -summary: A source-grounded, end-to-end field guide to T3 Code's control plane, provider adapters, clients, remote access, and distribution system. +summary: A complete product guide and source-grounded technical field guide to T3 Code across web, desktop, mobile, six providers, remote environments, reviews, previews, and device testing. status: source-checked gates: [sources] objectives: [] -keywords: [T3 Code, architecture, agent harness, control plane] -sourceAreas: [apps/server, apps/web, apps/desktop, apps/mobile, packages/contracts] +keywords: [T3 Code, product guide, user documentation, architecture, agent harness, control plane] +sourceAreas: [docs/user, apps/server, apps/web, apps/desktop, apps/mobile, packages/contracts] visuals: [book cover] -updatedAt: 2026-08-24 +updatedAt: "2026-09-11" --- import BookCover from "../../components/BookCover.astro"; @@ -22,10 +22,21 @@ import Callout from "../../components/Callout.astro"; - This edition is locked to [`pingdotgg/t3code@fa219001d`](https://github.com/pingdotgg/t3code/tree/fa219001dc2f14cfd9c7774c2c03c153359144be). “Shipped,” “supported,” and “implemented” always mean that revision unless a dated release or roadmap note says otherwise. + The book is synchronized with [`pingdotgg/t3code@859304b78`](https://github.com/pingdotgg/t3code/tree/859304b7808ab9a4be87b1bddcd07c6485bf9c4f), captured on **11 September 2026**. -## What this book is trying to make obvious +## Two paths through T3 Code + +The product guide explains what T3 Code offers and how to use it: installation, +onboarding, projects, threads, worktrees, providers, permissions, files, terminals, +previews, SnapShots, source control, remote environments, mobile clients, usage, +updates, and simulator or emulator testing. + +The technical guide opens the implementation behind those workflows: server and +client ownership, durable orchestration, provider adapters, persistence, recovery, +connection routes, native integrations, packaging, and release operations. + +## What the technical path makes obvious T3 Code is easy to mistake for an agent. It is more useful—and more technically interesting—to see it as a **control surface and execution boundary around several @@ -35,4 +46,5 @@ operations, and gives clients shared semantics with deliberate platform-specific capabilities. Some delivery and side-effect machinery remains intentionally ephemeral; the book marks those seams instead of calling all server state durable. -That division of ownership is the spine of the book. Every later module connects back to it. +That division of ownership is the spine of the technical path. The product path gives +each boundary a concrete user purpose before the implementation chapters name it. diff --git a/src/content/book/01-how-to-read.mdx b/src/content/book/01-how-to-read.mdx index be1994b..1fcc7c9 100644 --- a/src/content/book/01-how-to-read.mdx +++ b/src/content/book/01-how-to-read.mdx @@ -4,67 +4,85 @@ order: 1 kind: front part: Start here partOrder: 0 -title: How to read a changing system +title: How to read this guide shortTitle: Reading guide -summary: The evidence rules, source labels, revision contract, and reading paths used throughout the book. +summary: Choose a product, workflow, or architecture route and understand the evidence labels used throughout the book. status: source-checked -gates: [sources] +gates: [sources, links, editorial] objectives: - - Distinguish verbatim source, abridged source, diagrams, and interpretation. - - Choose a linear, feature-first, or implementation-first reading path. - - Understand what the pinned revision does and does not promise. -keywords: [evidence, source references, revision, reading path] -sourceAreas: [sources/t3code.lock.json, sources/excerpts.manifest.json] -visuals: [evidence legend] -updatedAt: 2026-08-24 + - Choose the shortest reading path for your goal. + - Move from a product task to the implementation that supports it. + - Distinguish verified behavior, documented intent, inference, and proposals. +keywords: [product guide, evidence, source references, reading path] +sourceAreas: [sources/t3code.lock.json, sources/excerpts.manifest.json, docs/user] +visuals: [reading routes, evidence legend] +updatedAt: "2026-09-11" --- import Callout from "../../components/Callout.astro"; -import EvidenceClaim from "../../components/EvidenceClaim.astro"; -import SourceExcerpt from "../../components/SourceExcerpt.astro"; -import SourceSnapshot from "../../components/SourceSnapshot.astro"; - -This is a field guide to a moving codebase. It therefore treats provenance as part of the interface, not a footnote. - - - -## Four evidence labels - -1. **Verified behavior** is backed by executable code, a test, schema, migration, or workflow at the pinned revision. -2. **Documented intent** reports what a maintainer document says without silently upgrading prose into runtime behavior. -3. **Inference** names a design consequence derived from cited evidence. The sources prove the inputs; the book owns the interpretation. -4. **Future / proposed** is explicitly unshipped. A builder capability, community request, or future-work paragraph is never called a supported feature. - -Exact source cards are a separate presentation device. Their code is read directly -from the locked Git object—not the possibly dirty working tree—and shows real line -numbers, a visible checksum prefix, and an immutable GitHub permalink. - - - - - A stable source lock makes an explanation reproducible. It does not make it permanently current; later editions must deliberately refresh and review the evidence. - - -## Three reading paths - -### Linear: follow one task - -Start with **The request trace**, then read the numbered chapters in order. This follows a user action from client state through authenticated RPC, commands, events, provider I/O, checkpoints, and back to every UI. - -### Architecture-first: map then zoom - -Open **The complete system map**, choose a layer, and follow its cross-links. This is useful when you already know event sourcing, provider protocols, or Electron/React Native and want the unfamiliar seams. - -### Transfer-first: design another orchestrator - -Read the ownership model, adapter boundary, domain kernel, connection runtime, and final transfer guide. Those chapters separate reusable patterns from choices that only make sense for T3 Code's bring-your-own-subscription model. - - - The live repository moves quickly. A source link with `fa219001d` is intentionally historical and stable. A link to `main` may be newer, but it cannot prove what this edition read. The book prefers reproducibility over silently chasing HEAD. +import Mermaid from "../../components/Mermaid.astro"; + +The book has a product path and a technical path. Read them in order for the full +story, or enter through the task you need to complete. + + P{Need to use T3 Code?} + P -->|yes| PG[Product guide\nChapters 1–13] + PG --> R[Complete recipes\nand troubleshooting] + P -->|understand or extend it| M[System map\nand request trace] + M --> T[Technical guide\nChapters 16–53] + PG -. product question .-> T`} /> + +## Product-first route + +Read Chapters 1–13 if you want a complete operating guide. They move from first +installation to parallel tasks, rich prompts, provider choice, workbench tools, +source control, remote access, mobile supervision, device testing, personalization, +usage, updates, and troubleshooting. No architecture knowledge is assumed. + +## Workflow-first route + +Use Contents or search to jump directly to the relevant product chapter. The final +product chapter includes complete recipes for local feature work, parallel worktrees, +remote supervision, pull-request review, and device verification. Follow its links +backward when a recipe needs setup or a feature decision. + +## Architecture-first route + +Open **The complete system map**, then **One request, every boundary**. Continue with +Chapter 16 to follow execution ownership, boot, RPC, authorization, the durable +domain kernel, six providers, work lifecycle, clients, access routes, distribution, +and operations. + +## From product questions to technical chapters + +| Product question | Technical destination | +| --- | --- | +| Why can I close one client and continue elsewhere? | Turn lifecycle, projections, shared client runtime, and reconnect. | +| Why do providers expose different choices? | ProviderAdapter, provider instances, and the provider-specific chapters. | +| What exactly does revert restore? | Checkpoints and context ownership. | +| Why does remote setup belong to one machine? | Environment auth, access transports, and T3 Connect. | +| How can a simulator run on another host? | Device hosts, targets, sessions, and agent control. | +| Why can a thread hold several reviews? | Projections, workbench services, and source-control integration. | + +## Evidence labels + +1. **Verified behavior** is backed by executable code, a test, schema, migration, + or workflow. +2. **Documented intent** reports what a maintainer document says without turning + prose into runtime behavior. +3. **Inference** names a design consequence derived from cited evidence. The + sources prove the inputs; the book owns the interpretation. +4. **Future / proposed** is explicitly unshipped. A builder capability, community + request, or idea is never called a supported feature. + +Exact source cards are read from the locked Git object and show line numbers, +checksum prefixes, and immutable permalinks. When documentation and executable +behavior disagree, the book calls out the discrepancy and follows the executable +contract for statements about behavior. + + + **Draft** means the explanation still needs a source pass. **Source checked** means + claims and diagrams were reconciled to the locked checkout. **Verified** adds link, + interaction, build, and editorial review. - -## Status words - -- **Draft**: structured and cited, but still awaiting a second source pass. -- **Source checked**: claims and diagrams have been reconciled to the pinned checkout. -- **Verified**: source checked, link checked, and reviewed in the end-to-end narrative. diff --git a/src/content/book/02-contents.mdx b/src/content/book/02-contents.mdx index e56a7a8..f167875 100644 --- a/src/content/book/02-contents.mdx +++ b/src/content/book/02-contents.mdx @@ -4,94 +4,112 @@ order: 2 kind: front part: Start here partOrder: 0 -title: Contents and learning route +title: Contents and learning routes shortTitle: Contents -summary: "The complete ordered reading route: establish ownership, then trace the durable control plane, adapters, work lifecycle, clients, remote paths, shipping system, and architectural synthesis." +summary: "The complete product-first route through T3 Code, followed by its control plane, six providers, work lifecycle, clients, device hosts, remote paths, and shipping system." status: source-checked gates: [sources, links, editorial] objectives: - - See why the order follows runtime causality instead of repository folders. - - Understand the scope of every completed part and its visual explanations. -keywords: [contents, chapters, roadmap, learning path] -sourceAreas: [BOOK_PLAN.md] + - Choose a product, workflow, or implementation route. + - See every product and technical chapter in the guide. +keywords: [contents, product guide, chapters, learning path] +sourceAreas: [BOOK_PLAN.md, docs/user] visuals: [chapter roadmap] -updatedAt: 2026-08-24 +updatedAt: "2026-09-11" --- import Callout from "../../components/Callout.astro"; -The old book introduced several keystone concepts long after depending on them. This edition orders the material by **causal distance from one user request**. The repository atlas remains available as reference, but it does not dictate the story. +The product guide comes first. The technical guide then follows one request from +the client through authorization, durable state, provider execution, work services, +client convergence, remote access, and release operations. ## Start here -- **How to read a changing system** — evidence, revision, and reading paths. -- **The complete system map** — the same architecture in spatial form. -- **One request, every boundary** — a step-through trace before the zoomed chapters. - -## Part I · Boundaries and vocabulary - -1. Control surface, not agent brain -2. Environment · project · thread · turn · session -3. Repository and dependency atlas -4. Runtime topologies and technology choices - -## Part II · Boot and connect - -5. The `npx t3` bootstrap path -6. Server composition and execution boundary -7. Effect RPC, subscriptions, and wire contracts -8. Pairing, scopes, credentials, and WebSocket upgrade - -## Part III · The durable domain kernel - -9. Commands and invariants -10. Events, receipts, and idempotency -11. Projection tables and read models -12. Post-commit reactors: serialized handling without durable delivery -13. SQLite, files, settings, secrets, migrations, and recovery - -## Part IV · Five harnesses, one product model - -14. The `ProviderAdapter` contract -15. Drivers, instances, registries, and multi-instance routing -16. Codex through app-server JSON-RPC -17. Claude through the Agent SDK -18. Cursor and Grok through ACP -19. OpenCode and the normalization matrix -20. Two usage systems: live context telemetry and transcript accounting - -## Part V · The work lifecycle - -21. Project discovery and `t3.json` -22. Current checkout vs isolated worktrees -23. Start, stream, steer, interrupt, settle -24. Permission modes, approvals, and structured input -25. Threads, provider tasks, plans, skills, and subagents -26. Who owns context, compaction, and memory -27. Hidden-ref checkpoints, diffs, and revert -28. Terminals, files, previews, MCP, VCS, and pull requests - -## Part VI · Client architectures: shared semantics, platform edges - -29. The shared client runtime -30. Web routes, state, and rendering performance -31. Composer, work log, review, and sidebar lifecycle -32. Desktop: Electron, IPC, server ownership, browser, SSH -33. Mobile: React Native, persistence, outbox, sharing, notifications - -## Part VII · Reach and ship - -34. Primary, paired bearer, Tailscale endpoints, and SSH access -35. T3 Connect: OAuth, DPoP, relay, and tunnel -36. Reconnect, multi-environment state, notifications, version skew -37. Distribution artifacts: CLI, hosted app, desktop, mobile, marketing, and AUR -38. Release graph, three update systems, and observability/privacy - -## Part VIII · Synthesis - -39. Six complete traces: ownership, convergence, and failure boundaries -40. Decisions, trade-offs, limitations, and an honest roadmap - - - `BOOK_PLAN.md` is the authoring contract for this repository. Product roadmap claims inside the book use a stricter split: shipped code, explicit maintainer future work, and community ideas are three different categories. +- **Cover** — scope and source synchronization. +- **How to read this guide** — product, workflow, and architecture routes. +- **Contents and learning routes** — this complete map. + +## Part I · Product guide + +1. T3 Code as a product +2. Install and reach a useful first thread +3. Environments, clients, and connection paths +4. Organize projects, threads, and worktrees +5. Compose tasks with the right context +6. Choose providers, models, accounts, and permissions +7. The workbench: files, terminals, browser preview, and SnapShots +8. Source control: checkpoints, pull requests, reviews, and stacks +9. Remote environments and unattended work +10. Mobile: supervise agents from anywhere +11. Device lab: simulators, emulators, and agent-driven testing +12. Personalize, measure usage, update, and protect privacy +13. Complete recipes and troubleshooting + +## Part II · Architecture orientation and boundaries + +14. The complete system map +15. One request, every boundary +16. Control surface, not agent brain +17. Environment · project · thread · turn · session +18. Repository and dependency atlas +19. Runtime topologies and technology choices + +## Part III · Boot and connect + +20. The `npx t3` bootstrap path +21. Server composition, activation, and readiness +22. HTTP, WebSocket RPC, snapshots, and resume +23. Pairing, credentials, TTLs, and scopes + +## Part IV · The durable domain kernel + +24. Commands, invariants, and the boundary of atomicity +25. Events, receipts, idempotency, and the post-commit gap +26. Projection tables and read models +27. Post-commit reactors and the delivery gap +28. Persistence, reconstruction, and crash recovery + +## Part V · Six providers, one product model + +29. The `ProviderAdapter` contract +30. Drivers, instances, registries, and multi-instance routing +31. Codex through app-server JSON-RPC +32. Claude through the Agent SDK +33. ACP transport, Cursor, and Grok +33A. Antigravity through ACP, managed auth, and account catalogs +34. OpenCode ownership, recovery, and six-provider normalization +35. Usage accounting and provider limits without a false ledger + +## Part VI · The work lifecycle and integrated tools + +36. Project discovery, onboarding import, and `t3.json` +37. Current checkout versus isolated worktrees +38. Start, stream, steer, interrupt, settle +39. Permission modes, approvals, and structured input +40. Threads, provider tasks, plans, skills, and subagents +41. Context is provider-owned; history is T3-owned +42. Hidden-ref checkpoints, diffs, and revert +43. Terminals, files, previews, MCP, VCS, and review graphs +43A. Device hosts, targets, sessions, and agent control + +## Part VII · Client architectures: shared semantics, platform edges + +44. Shared runtime: connections, state, and convergence +45. One React renderer, three runtime edges +46. One thread, many deliberate projections +47. Electron desktop: one renderer, explicit native authority +48. Mobile: adaptive workspaces, persistence, and native systems + +## Part VIII · Reach and ship + +49. Reachability is a route; authority is a separate proof +50. T3 Connect: OAuth, DPoP, relay, and tunnel +51. Reconnect, environments, notifications, and version skew +52. Distribution: artifacts, channels, and what actually ships +53. Release, update, and observability: three safety boundaries + + + Press `Cmd/Ctrl+K` to search product tasks, concepts, source paths, provider names, + and implementation terms across the entire guide. diff --git a/src/content/book/03-architecture-map.mdx b/src/content/book/03-architecture-map.mdx index 0d7aee9..1787db3 100644 --- a/src/content/book/03-architecture-map.mdx +++ b/src/content/book/03-architecture-map.mdx @@ -1,9 +1,10 @@ --- slug: architecture-map -order: 3 -kind: front -part: Start here -partOrder: 0 +order: 140 +number: "14" +kind: chapter +part: Part II · Architecture orientation +partOrder: 2 title: The complete system map shortTitle: System map summary: A spatial map that separates client surfaces, transport, transactional state, hot delivery, side effects, provider routing, and external execution. @@ -15,7 +16,7 @@ objectives: keywords: [architecture map, components, packages, boundaries] sourceAreas: [apps/server, packages/contracts, packages/client-runtime, apps/web, apps/desktop, apps/mobile, apps/marketing, infra/relay] visuals: [interactive layer map, ownership table] -updatedAt: 2026-08-24 +updatedAt: "2026-09-11" --- import Callout from "../../components/Callout.astro"; @@ -30,7 +31,7 @@ behind that environment's server boundary. A client can supervise several such environments, while Electron and mobile also own device-local storage, navigation, notifications, preview, update, and operating-system integrations. -
+
+
|typed command| W[WebSocket RPC + scope] W --> Q[Serialized in-memory queue] @@ -59,7 +60,7 @@ notifications, preview, update, and operating-system integrations. B -.->|best effort; not replayed| R[Provider command reactor] R --> V[ProviderService + instance registry] V --> A[Provider adapter] - A --> P[Codex · Claude · Cursor · Grok · OpenCode] + A --> P[Codex · Claude · Cursor · Grok · OpenCode · Antigravity] P -->|native notifications| A A -->|canonical runtime events| I[Runtime ingestion] I -->|internal commands| Q diff --git a/src/content/book/04-request-trace.mdx b/src/content/book/04-request-trace.mdx index 5d71bd2..681906f 100644 --- a/src/content/book/04-request-trace.mdx +++ b/src/content/book/04-request-trace.mdx @@ -1,9 +1,10 @@ --- slug: request-trace -order: 4 -kind: front -part: Start here -partOrder: 0 +order: 150 +number: "15" +kind: chapter +part: Part II · Architecture orientation +partOrder: 2 title: One request, every boundary shortTitle: Request trace summary: Step through a user turn from a client command to an ordered SQL commit, provider-native execution, canonical event ingestion, checkpoint diff, and streamed UI state. @@ -16,7 +17,7 @@ objectives: keywords: [turn lifecycle, command, event, provider, checkpoint, stream] sourceAreas: [packages/contracts/src/orchestration.ts, apps/server/src/orchestration, apps/server/src/provider] visuals: [interactive request stepper, sequence diagram] -updatedAt: 2026-08-24 +updatedAt: "2026-09-11" --- import Callout from "../../components/Callout.astro"; @@ -78,12 +79,12 @@ existing thread—optional lifecycle resets, then message and turn-start—is de and committed atomically. - Attachment normalization persists decoded bytes before the engine transaction. A command can therefore fail after external filesystem work has begun; Chapter 9 models that branch explicitly. + Attachment normalization persists decoded bytes before the engine transaction. A command can therefore fail after external filesystem work has begun; Chapter 24 models that branch explicitly. ## The full causal loop -
+
D[Choose environment and project] + D --> T[Start or resume a thread] + T --> C[Compose task plus context] + C --> P[Choose provider, model, and permission mode] + P --> A[Agent works on environment machine] + A --> Q{Needs attention?} + Q -->|question or approval| U + Q -->|continues| A + A --> R[Inspect messages, tools, files, diff, terminal, or device] + R --> O{Outcome} + O -->|follow up| C + O -->|restore| T + O -->|review or PR| S[Source-control workflow] + O -->|done| X[Settle thread]`} /> + +The loop is intentionally durable. Closing a browser does not redefine the task; +the environment remains the authority for its threads. Mobile can queue a message +while offline and upload it after reconnecting. Supported provider sessions can +resume after a server restart when continuation is enabled. + +## The five objects you should know + +**A client** is the interface in front of you. The desktop and web clients expose +the fullest setup and review surfaces. Mobile is optimized for supervising work, +responding to questions and approvals, and sending follow-ups while away. + +**An environment** is one running T3 server plus its machine, filesystem, provider +credentials, projects, and state. “Local,” “office workstation,” and “cloud VM” +are common environments. A client may connect to several at once. + +**A project** points to a workspace directory inside one environment. Checkouts of +the same repository can be grouped in the UI, while each machine still owns its +own files and provider setup. + +**A thread** is a durable unit of work inside a project. It contains the +conversation, agent activity, selected provider settings, workspace choice, +checkpoints, and related review links. + +**A turn** begins when you send a message and includes the agent's response and +work. A thread normally contains many turns: initial task, correction, test run, +review response, and final cleanup. + + + When something is missing, ask two questions: “Which environment owns this + project?” and “Which thread owns this task?” Most setup and navigation mistakes + become obvious once both answers are explicit. + + +## Three good starting workflows + +### Focused local task + +Install the desktop app, add a repository, start a thread in the current checkout, +attach the relevant issue or screenshot, and use **Supervised** or **Auto** while +you learn the agent's behavior. Inspect the diff, then commit or continue from the +same thread. + +### Parallel feature work + +Create one **New worktree** thread per independent change. Start background tasks +with `Cmd/Ctrl+Enter`, then use the sidebar to move between them. Keep a task in +one thread so its branch, decisions, approvals, and pull request remain connected. + +### Remote supervision + +Run T3 Code on the machine that has the repositories and provider logins. Keep it +available with the desktop host or a background service, expose it through T3 +Connect or a private network, and connect from mobile. Notifications can take you +straight to a completed turn, failure, question, or approval request. + +## Product boundaries that affect daily use + +- Work executes on the environment machine. Install provider CLIs, Git hosting + tools, SDKs, and credentials there—not merely on the device displaying the UI. +- A phone does not become the host. It controls a server elsewhere. +- Provider capabilities differ. Plan modes, rewind support, questions, attachment + limits, and approval behavior are not identical across runtimes. +- T3 Code preserves product history and file diffs, but provider-native context + remains subject to that provider's continuation and compaction behavior. +- Estimated usage cost is an analytical view, not a subscription invoice. + +## Where the rest of this guide goes + +The next chapters turn this map into tasks: install and first run; choose clients +and environments; organize projects, threads, and worktrees; write context-rich +requests; choose providers, models, and permissions; then use terminals, previews, +devices, Git, remote access, recovery, and customization. The technical part of +the book later explains why these product behaviors are reliable. + +### Source trail + +Sources: +[product README](https://github.com/pingdotgg/t3code/blob/859304b7808ab9a4be87b1bddcd07c6485bf9c4f/README.md), +[installation](https://github.com/pingdotgg/t3code/blob/859304b7808ab9a4be87b1bddcd07c6485bf9c4f/docs/user/install.md), +[remote access](https://github.com/pingdotgg/t3code/blob/859304b7808ab9a4be87b1bddcd07c6485bf9c4f/docs/user/remote-access.md), and +[working with threads](https://github.com/pingdotgg/t3code/blob/859304b7808ab9a4be87b1bddcd07c6485bf9c4f/docs/user/thread-sidebar.md). diff --git a/src/content/book/06-install-onboarding.mdx b/src/content/book/06-install-onboarding.mdx new file mode 100644 index 0000000..6eff2d8 --- /dev/null +++ b/src/content/book/06-install-onboarding.mdx @@ -0,0 +1,196 @@ +--- +slug: install-onboarding +order: 20 +number: "2" +kind: chapter +part: Part I · Product guide +partOrder: 1 +title: Install and reach a useful first thread +shortTitle: Install and onboarding +summary: Choose a host, install the right T3 Code surface, connect a provider, import useful projects, and verify the first agent task. +status: source-checked +gates: [sources, links, interaction, editorial] +objectives: + - Select an installation path for local, WSL, SSH, and mobile use. + - Complete the welcome flow without confusing client and environment machines. + - Verify a provider and project with a small first task. +keywords: [installation, onboarding, welcome wizard, providers, import, updates] +sourceAreas: [docs/user/install.md, docs/user/welcome-wizard.md, docs/user/background-service.md, docs/user/updating.md] +visuals: [installation decision flow] +updatedAt: "2026-09-11" +--- + +import Callout from "../../components/Callout.astro"; +import Mermaid from "../../components/Mermaid.astro"; + +Your first decision is where agents will run. Choose the machine that already has +the repositories, build tools, and credentials you want the agent to use. That +machine becomes the environment host. You may operate it from the same desktop, +a browser, or a phone later. + +## Choose an installation path + + L{This desktop?} + L -->|yes| D[Install desktop app] + L -->|quick trial| N[Run npx t3 at latest] + L -->|no, SSH host| H[Add SSH environment from desktop] + L -->|no, persistent Linux or macOS host| B[Install user background service] + D --> W{Use WSL workspace?} + W -->|yes| WS[Select distro in Connections; install Node and providers inside WSL] + W -->|no| P[Configure provider on host] + N --> P + H --> P + B --> P + WS --> P + P --> O[Run welcome wizard and import projects] + O --> F[Send a small first task]`} /> + +| Path | Best for | Requirement or tradeoff | +|---|---|---| +| Desktop app | Everyday local use, native integrations, easiest host setup | Bundles the server runtime; provider tools still need setup where projects run. | +| `npx t3@latest` | Fast trial or foreground command-line host | Requires a supported Node.js release and stops with the terminal process. | +| Background service | Always-available Linux or macOS host | Runs as your user; Windows services are not supported. | +| Desktop-managed SSH | Projects and credentials on another machine | Remote host needs supported Node.js and provider tools visible to non-interactive SSH. | +| Mobile app | Supervision away from the host | Connects to another environment; it does not run agents locally. | + +Command-line and SSH environments require Node.js 22.16+ in the 22 line, 23.11+ +in the 23 line, or 24.10+. The desktop app includes its own server runtime. + +## Install the client or server + +For a no-install trial, run: + +```bash +npx t3@latest +``` + +It starts the server and opens its local web client. Use +`npx t3@latest --help` to see launch options. + +For desktop, install a release from GitHub or use the supported platform package: + +| Platform | Command | +|---|---| +| Windows | `winget install T3Tools.T3Code` | +| macOS | `brew install --cask t3-code` | +| Arch Linux stable | `yay -S t3code-bin` | +| Arch Linux nightly | `yay -S t3code-nightly-bin` | + +With desktop already running, `npx t3 app` opens a new thread for the current +directory. Pass a path to open another directory. This command talks to the local +desktop app; it is not a general command for a standalone or SSH server. + +### WSL + +In **Settings → Connections**, choose the WSL distribution that should own your +projects. Install Node.js and provider CLIs inside that distribution. Desktop +installs its matching T3 server there. The first start after an app update can take +longer while that runtime catches up. + +### Background host + +On Linux or macOS, install and inspect the user service with: + +```bash +npx t3@latest service install +npx t3@latest service status +``` + +Use `service update` to repair or update it and `service uninstall` to stop and +remove startup. Uninstalling leaves project, thread, and setting data intact. +Linux uses a systemd user service and normally needs lingering; macOS starts at +login and requires the machine to remain logged in and awake. + +## Connect at least one provider + +Open **Settings → Providers**, select the environment, enable a provider, and +complete its installation and login on that machine. + +| Provider | Host-side setup | +|---|---| +| Codex | Install Codex CLI; run `codex login`. | +| Claude | Install Claude Code; run `claude auth login`. | +| Cursor | Install Cursor CLI; run `agent login` (the executable is `cursor-agent`). | +| Grok Build | Install Grok Build CLI; run `grok login`. | +| OpenCode | Install OpenCode; run `opencode auth login`. | +| Antigravity | Install its managed runtime and sign in from provider settings. | + +If a CLI is not on the server process's `PATH`, set **Binary path**. This is common +with language version managers. Provider cards can detect available upgrades; +**Update now** appears only when T3 Code recognizes the installer that owns the +binary. Mark API keys and other private instance variables as **Sensitive**. After +you save them, Settings keeps the value available to the provider but no longer +shows the original text. + + + When you connect from mobile, a browser, or another desktop, install and + authenticate providers on the selected environment machine. Settings sends the + setup intent there; credentials do not migrate from the client device. + + +## Use the welcome wizard deliberately + +The welcome flow has three useful stages: + +1. **Connect computers.** Select the environments you want to set up. Add hosts + through T3 Connect or a direct pairing link if needed. Removing a computer from + this setup selection does not disconnect it. +2. **Check agents.** The wizard checks Codex and Claude on each selected computer + and can open a terminal with the relevant install or login command. Other + providers remain available in Settings. +3. **Import projects.** Select directories discovered from recent Codex and Claude + use. Git repositories appear first and matching remotes are grouped. + +The initial project selection favors Git repositories active in the last 30 days +with at least three conversations. Linked worktrees, Codex scratch directories +under `Documents/Codex`, and paths under `Downloads` are excluded. You can change +every checkbox or skip import entirely. + +Imported history is a practical bridge, not a byte-for-byte archive. T3 Code +imports recent visible conversation content, omits tool activity and attachments, +and keeps at most 200 messages per conversation. One import attempt reads up to +100 conversation files and 64 MiB per project, with at most 100,000 input records; +individual files larger than 16 MiB are skipped. Large histories may require +another pass. Completed conversations are not duplicated, and navigation pauses +while an import is running. + +The setup terminal uses the selected provider instance's home and environment. +Sensitive settings stay redacted in the interface and terminal metadata while the +process can use them. + + + **Still connecting** means the workspace could not yet be confirmed; choose + **Reload**. **Could not read settings** means storage is unavailable or unreadable; + restore access and choose **Retry**. T3 Code does not replace unreadable settings + with defaults. + + +## Verify the installation + +Create or import one small project, then start a thread with a bounded prompt such +as: “Read the repository README and tell me the test command. Do not change +files.” Choose **Supervised** for this check. A healthy setup proves four things: + +- the client reaches the intended environment; +- the environment can read the project directory; +- the provider is authenticated and can start a session; +- questions, responses, and permission requests return to the thread. + +After that, try a small edit and inspect the diff. Do not diagnose Git, SDK, and +provider failures at once; the read-only task separates basic connectivity from +toolchain setup. + +## Keep client and server versions aligned + +The app in front of you and the environment server can be on different versions. +When a mismatch appears, update the machine named in the notice after active work +finishes. The update choices and recovery behavior live in +[Personalize, measure usage, update, and protect privacy](../personalize-usage-updates/). + +### Source trail + +Sources: [install](https://github.com/pingdotgg/t3code/blob/859304b7808ab9a4be87b1bddcd07c6485bf9c4f/docs/user/install.md), +[welcome wizard](https://github.com/pingdotgg/t3code/blob/859304b7808ab9a4be87b1bddcd07c6485bf9c4f/docs/user/welcome-wizard.md), +[background service](https://github.com/pingdotgg/t3code/blob/859304b7808ab9a4be87b1bddcd07c6485bf9c4f/docs/user/background-service.md), and +[updating](https://github.com/pingdotgg/t3code/blob/859304b7808ab9a4be87b1bddcd07c6485bf9c4f/docs/user/updating.md). diff --git a/src/content/book/07-environments-surfaces.mdx b/src/content/book/07-environments-surfaces.mdx new file mode 100644 index 0000000..9ef5583 --- /dev/null +++ b/src/content/book/07-environments-surfaces.mdx @@ -0,0 +1,180 @@ +--- +slug: environments-surfaces +order: 30 +number: "3" +kind: chapter +part: Part I · Product guide +partOrder: 1 +title: Environments, clients, and connection paths +shortTitle: Environments and surfaces +summary: Decide where work executes, which client to use, and how to connect securely through local, direct, Tailscale, T3 Connect, or SSH paths. +status: source-checked +gates: [sources, links, interaction, editorial] +objectives: + - Distinguish the environment host from the client displaying it. + - Choose a connection route for local and remote work. + - Operate multiple machines without losing track of execution location. +keywords: [environment, web, desktop, mobile, T3 Connect, Tailscale, SSH, pairing] +sourceAreas: [AGENTS.md, docs/user/remote-access.md, docs/user/background-service.md, docs/user/mobile-notifications.md, docs/user/devices.md] +visuals: [multi-surface environment topology] +updatedAt: "2026-09-11" +--- + +import Callout from "../../components/Callout.astro"; +import Mermaid from "../../components/Mermaid.astro"; + +An environment is the machine-side boundary of T3 Code: one running server, its +filesystem, provider credentials, project records, threads, terminals, Git tools, +and device integrations. A client is a view and control surface for one or more +environments. + +This is why the same thread can appear on a laptop and phone without moving its +workspace. Both clients address the environment that already owns it. + + L + D --> T + D --> C + D --> S + W --> L + W --> T + W --> C + M --> L + M --> T + M --> C + L --> E[Environment server] + T --> E + C --> E + S --> E + E --> F[(workspace files and Git)] + E --> P[provider runtimes and credentials] + E --> H[terminals, browsers, and device hosts] + E --> R[(threads and settings)]`} /> + +## Choose the client for the moment + +| Surface | Strongest use | Product limits to remember | +|---|---|---| +| Desktop | Primary workstation, local hosting, SSH environments, SnapShots, native preview/browser integration | Native features and host setup are machine-specific. | +| Local web | Quick command-line launch and full browser-based workspace | The terminal process or background service must keep the server alive. | +| Hosted web at `app.t3.codes` | Reach HTTPS-exposed environments without installing a client | It connects directly to your endpoint; it cannot make an unreachable server reachable. | +| Mobile | Notifications, approvals, questions, follow-ups, diffs, and remote supervision | Provider setup and some authoring features remain on web/desktop. | + +Mobile stores drafts and queued messages locally, so a temporary disconnect does +not force you to rewrite a prompt. Upload and delivery resume when the environment +reconnects. This is valuable on unreliable networks, but a queued instruction has +not affected the workspace until it actually sends. + +## Choose a connection route + +### Same machine or private LAN + +The local desktop and `npx t3@latest` paths are simplest for one machine. For a +second device on a reachable private network, start the server on a reachable +address and create a one-time pairing link: + +```bash +npx t3 serve --host +npx t3 pair +``` + +Scan the QR code or paste the URL into **Add environment**. A `127.0.0.1` URL only +works on the host itself. Each new device needs a fresh pairing link, but an +already paired device reconnects without the original token. + +### Tailscale HTTPS + +If both devices share a tailnet, enable **Tailscale HTTPS** in desktop Connections +or run `npx t3 serve --tailscale-serve`. For an existing server, +`npx t3 pair --tailscale` creates a persistent Tailscale Serve mapping and a +pairing URL. This path is especially useful for the hosted web client because it +provides HTTPS. + +### T3 Connect + +T3 Connect links environments and clients signed into the same T3 account without +router forwarding. Enable it in desktop Connections or run: + +```bash +npx t3@latest connect +``` + +Signing in saves the connection identity; it does not keep the server running. +Use the desktop host, `npx t3 serve`, or a background service as well. T3 Connect +is required for background mobile push notifications; a direct or Tailscale link +alone does not deliver those pushes. + +### Desktop-managed SSH + +Use **Settings → Connections → Add environment → SSH** when a remote machine +already holds the repository and provider login. Desktop starts or reuses a T3 +server over SSH and manages the port forward. Node and providers must work in a +non-interactive shell. A version manager configured only in an interactive shell +is a common source of “binary not found” errors. + + + Treat pairing URLs and returned authorization codes like passwords. Do not put + them in screenshots, logs, issues, or documentation. Revoke unused links and + client sessions from the host's Connections settings. + + +## Work across several environments + +The same repository may exist on a laptop, workstation, and VM. T3 Code can group +those checkouts for presentation, but each one remains an independent workspace. +Provider logins, environment variables, settings, uncommitted files, running +processes, and available models can differ. + +New-thread **Auto balance** can select among grouped environments using current CPU +and memory signals. Give each machine a preference: **Prefer**, **Normal**, **Less +often**, or **Manual only**. The chosen environment becomes stable for that draft; +selecting a specific branch or worktree also fixes the choice. Existing threads +never migrate. + +Use manual selection when data locality matters—for example, a secret exists only +on one host, an emulator runs on a Mac, or a large build cache lives on a +workstation. Load balancing is a convenience for equivalent checkouts, not a file +or credential synchronization mechanism. + +## Remote devices and notifications + +The Device panel streams iOS Simulators and Android Emulators from the environment +or a configured SSH device host. Secure pages (HTTPS or localhost) support live +video. Plain HTTP remote access degrades iOS to still images and cannot show +Android video. The simulator's `localhost` is not automatically the environment's +development server; arrange Metro or other dev-server reachability separately. + +With T3 Connect and **Device Notifications** enabled, mobile alerts when work +finishes, fails, asks a question, or needs approval. iOS Live Activities and +Android ongoing activity can show progress. Normal alerts stay quiet while the app +is foregrounded. Viewing the thread on another device does not silence the phone. + +## Operate a host responsibly + +- Keep an always-on host awake and its service running. +- Install Git, providers, SDKs, and device tools on the environment or configured + device host. +- Update the server named in a version-mismatch notice. +- Use **Settings → Connections** or `t3 connect status` to inspect configuration; + use service status and logs to diagnose reachability. +- Revoke a client session when a device is lost or should no longer connect. +- Deregister unused T3 Connect environments; unlinking exposure and uninstalling a + background service are separate actions. + +### Source trail + +Sources: [remote access](https://github.com/pingdotgg/t3code/blob/859304b7808ab9a4be87b1bddcd07c6485bf9c4f/docs/user/remote-access.md), +[background service](https://github.com/pingdotgg/t3code/blob/859304b7808ab9a4be87b1bddcd07c6485bf9c4f/docs/user/background-service.md), +[mobile notifications](https://github.com/pingdotgg/t3code/blob/859304b7808ab9a4be87b1bddcd07c6485bf9c4f/docs/user/mobile-notifications.md), and +[devices](https://github.com/pingdotgg/t3code/blob/859304b7808ab9a4be87b1bddcd07c6485bf9c4f/docs/user/devices.md). diff --git a/src/content/book/08-projects-threads-worktrees.mdx b/src/content/book/08-projects-threads-worktrees.mdx new file mode 100644 index 0000000..4232aa2 --- /dev/null +++ b/src/content/book/08-projects-threads-worktrees.mdx @@ -0,0 +1,172 @@ +--- +slug: projects-threads-worktrees +order: 40 +number: "4" +kind: chapter +part: Part I · Product guide +partOrder: 1 +title: Organize projects, threads, and worktrees +shortTitle: Projects and threads +summary: Structure agent work so each task has a clear project, durable conversation, workspace, branch, review link, and lifecycle. +status: source-checked +gates: [sources, links, interaction, editorial] +objectives: + - Choose between the current checkout and a new worktree. + - Run parallel tasks without mixing branches or conversations. + - Use thread states and links to keep active work understandable. +keywords: [projects, threads, worktrees, branches, parallel work, settlement] +sourceAreas: [docs/user/thread-sidebar.md, docs/user/project-settings.md, docs/user/source-control.md, docs/user/welcome-wizard.md] +visuals: [work organization lifecycle] +updatedAt: "2026-09-11" +--- + +import Callout from "../../components/Callout.astro"; +import Mermaid from "../../components/Mermaid.astro"; + +A project answers “which repository or folder?” A thread answers “which task and +conversation?” A workspace answers “which working directory and branch?” Keep +those three choices aligned and T3 Code can support many simultaneous agents +without turning their output into an undifferentiated queue. + +## Pick the workspace before sending + +| Choice | Use it when | Main risk | +|---|---|---| +| Current checkout | The task is exclusive, read-only, or intentionally continues its current branch | Parallel agents can edit the same files and Git index. | +| New worktree | The task should have its own branch and working directory | More local checkouts consume disk and need normal branch cleanup later. | +| Existing worktree | A second thread should inspect or continue work already isolated there | Two writing agents can still conflict if they operate concurrently. | + +For most independent implementation tasks, **New worktree** is the safest default. +It isolates both the branch and filesystem changes. A new thread alone isolates the +conversation; it does not necessarily isolate files. + + W{Workspace choice} + W -->|current checkout| C[Use existing directory and branch] + W -->|new worktree| N[Create separate directory and branch] + W -->|existing worktree| E[Select that checkout] + C --> T[Start durable thread] + N --> T + E --> T + T --> A[Agent turns, approvals, questions, checkpoints] + A --> I[Inspect diff, tools, terminal, and delegated agents] + I --> F{More work?} + F -->|follow-up| A + F -->|review| R[Commit, push, or link pull requests] + R --> M{Work complete?} + M -->|no| A + M -->|yes| S[Settle thread] + S -->|resume later| U[Un-settle and continue] + U --> A`} /> + +## Start a thread with explicit intent + +On web and desktop, a new thread keeps the current project and carries the current +model and permission selections. Project defaults can override the remembered +model, while configured workspace defaults determine branch and checkout behavior. +Changing the project keeps the current environment when that project exists there; +otherwise T3 Code selects an environment that has it. + +Use **New thread in this worktree** from the branch toolbar when a new conversation +should share an existing isolated checkout. This is useful for a review pass after +the implementation agent finishes. Avoid concurrent edits unless you explicitly +want both threads writing the same files. + +To dispatch several tasks, press `Cmd+Enter` on macOS or `Ctrl+Enter` on Windows +and Linux. T3 Code starts the task and immediately gives you another draft. With +**New worktree**, every background submission gets its own worktree. + + + Two threads in the same checkout can overwrite each other's files, change the + same index, or move the branch underneath one another. Use worktrees for + simultaneous coding tasks; use extra threads in one checkout mainly for staged + handoffs or read-only analysis. + + +## Configure projects at the right scope + +Open **Settings → Projects** to set defaults for model, workspace behavior, +automatic pull, agent browser access, and project actions. + +- **All projects** defines values inherited by projects without overrides. +- Selecting one project creates or removes explicit overrides. +- **All machines** writes defaults to connected machines; offline machines keep + their previous values. +- `t3.json` workspace preferences beat machine defaults when the project has no + explicit workspace override. +- Shared project actions remain inherited until you edit a project's list; reset + it to inherit again. Editing the list creates an independent copy and preserves + existing actions. + +Project grouping is a client-level presentation choice across machines. Names, +icons, removal, and imported actions still apply to selected checkouts. When +several checkouts exist, use the checkout picker and inspect mixed-value markers +before changing settings. + +Choose an icon, emoji, or project image to make similar checkouts recognizable +across connected clients. **Automatic** returns to T3 Code's detection. Agent +browser-access changes take effect when a new agent session starts, so restart the +session before testing a changed policy. + +Enable **Automatically pull** only for a clean default-branch checkout with an +upstream. T3 Code fast-forwards; it skips a dirty checkout, untracked files, local +commits, another branch, or missing upstream. The skip protects local work—it is a +signal to reconcile the checkout yourself. + +## Treat the sidebar as a work queue + +Thread states express attention, not repository state: + +| State | Meaning | Use | +|---|---|---| +| Pinned | High-priority active work | Keep a task above the active list. | +| Active | Current work | Normal place for running and recently handled threads. | +| Snoozed | Intentionally hidden until a wake time | Park a dependency or scheduled follow-up. | +| Settled | Finished or no longer needing attention | Clear the active queue without deleting history. | + +Drag threads within and between sections on web/desktop; use **Arrange threads** on +mobile. Ordering persists across connected clients. New threads appear above +manually arranged active work. Activity does not continuously reorder the list. + +By default, environments can settle inactive threads after three days and settle +threads after linked pull requests merge. Live work, pending approval, unanswered +questions, and background work prevent automatic settlement. Tune these rules in +**Settings → General**. Un-settling restores a thread and temporarily protects it +until new activity resumes normal rules. + +Pinning does not prevent settlement, and settling clears the pin. Manually settling +an idle thread dismisses unanswered asynchronous questions without sending an +answer. An open pull request does not block inactivity settlement; a closed pull +request settles only idle work and does not settle a thread whose work resumed +after that review closed. If reordering is unavailable for one environment, update +that environment's server. + +## Keep review context with the task + +T3 Code detects a pull request for an unsettled thread's saved branch. You can also +link several pull requests—including reviews from another repository on the same +host—from the command palette, the Linked pull requests panel, or a PR context +menu. Creating a PR through thread Git actions links it automatically. + +This makes the thread the operational record for the change: prompt, agent work, +branch, diffs, review discussion, and completion state. For stacked GitHub work, +the Pull Requests surface shows layers and can merge or rebase a stack when the +environment supports it. Rebase-stack rewrites remote branch history and can +restart checks, so inspect its confirmed scope first. + +## Find and inspect work + +- Open the command palette with `Cmd/Ctrl+K` to search threads across connected + environments. Message search begins after two characters and includes user + prompts and final agent responses. +- Copy a thread reference when another task needs it. T3 Code prefers its pull + request URL when one is available. +- Expand tool calls to see full commands and results. +- Open **Agents** on web or desktop to follow delegated subagent activity. +- Drag files onto a thread row to open it and attach the files to its composer. + +### Source trail + +Sources: [working with threads](https://github.com/pingdotgg/t3code/blob/859304b7808ab9a4be87b1bddcd07c6485bf9c4f/docs/user/thread-sidebar.md), +[project settings](https://github.com/pingdotgg/t3code/blob/859304b7808ab9a4be87b1bddcd07c6485bf9c4f/docs/user/project-settings.md), and +[source control](https://github.com/pingdotgg/t3code/blob/859304b7808ab9a4be87b1bddcd07c6485bf9c4f/docs/user/source-control.md). diff --git a/src/content/book/09-composer-context.mdx b/src/content/book/09-composer-context.mdx new file mode 100644 index 0000000..bddec62 --- /dev/null +++ b/src/content/book/09-composer-context.mdx @@ -0,0 +1,209 @@ +--- +slug: composer-context +order: 50 +number: "5" +kind: chapter +part: Part I · Product guide +partOrder: 1 +title: Compose tasks with the right context +shortTitle: Composer and context +summary: Write actionable prompts and add files, citations, skills, commands, SnapShots, and follow-up answers without losing drafts or overloading a turn. +status: source-checked +gates: [sources, links, interaction, editorial] +objectives: + - Build a task message from goal, constraints, evidence, and finish condition. + - Choose the right context mechanism for each kind of information. + - Recover and reuse prompts safely across devices and environments. +keywords: [composer, context, attachments, citations, skills, commands, snapshots, prompts] +sourceAreas: [docs/user/composer.md, docs/user/question-attachments.md, docs/user/snap-shot.md, docs/user/keybindings.md] +visuals: [context selection flow] +updatedAt: "2026-09-11" +--- + +import Callout from "../../components/Callout.astro"; +import Mermaid from "../../components/Mermaid.astro"; + +The composer turns intent into an agent turn. A strong request says what outcome +you want, names the relevant boundary, supplies evidence the agent cannot infer, +and defines how to know the work is done. T3 Code then lets you attach different +kinds of context without flattening everything into pasted prose. + +## Build a useful task + +A practical message has four parts: + +1. **Outcome:** the behavior, answer, or artifact you want. +2. **Scope:** the project area and what is in or out. +3. **Evidence and constraints:** files, screenshots, quoted responses, issue text, + compatibility needs, or repository instructions. +4. **Finish condition:** tests, review evidence, file format, or an explicit report. + +For example: “Fix the empty search result state in the mobile project. Match the +attached design and preserve tablet layout. Run the focused component tests and +report the files changed.” The task is concrete without prescribing an internal +implementation before the agent inspects the code. + + F{A file or media asset?} + F -->|yes| A[Attach or paste it] + F -->|no| R{Exact part of an earlier response?} + R -->|yes| C[Cite selected text] + R -->|no| V{Another app window is the evidence?} + V -->|yes| S[Capture a SnapShot on desktop] + V -->|no| K{Reusable provider workflow?} + K -->|yes| SK[Select a skill with dollar] + K -->|no| M{Provider or T3 operation?} + M -->|yes| CMD[Select a slash command] + M -->|no| P[Write it directly in the prompt] + A --> Q[Review task, model, and permission mode] + C --> Q + S --> Q + SK --> Q + CMD --> Q + P --> Q + Q --> SEND[Send when uploads finish]`} /> + +## Attach files and media + +Each message accepts up to eight files. Images may be up to 10 MB and other files +up to 50 MB, subject to the selected environment's upload support and limit. +Uploads start immediately but the message cannot send until all finish. Retry or +remove a failed upload. + +Web and desktop accept dragged or pasted images. HEIC and HEIF photos become JPEG +in those clients and when selected from iOS; the size limit applies after +conversion. Mobile also accepts files through the system share sheet. + +Attachments upload to the environment machine. That matters for two reasons: + +- a remote provider receives environment-side paths, not paths on your phone; +- a stash containing uploaded files can only be restored in its original + environment. + +Mobile keeps local draft attachments and queued messages through disconnects and +app restarts, then resumes uploads. Signing out of T3 Connect keeps that work on +the device, but it is restored only after signing back into the same account. On +web and desktop, reloading during an upload means attaching that file again. + +### Preview and keep message files + +Select an image or video to preview it. On web and desktop, its context menu can +save the media or copy its path or URL; on mobile, touch and hold to save or share. +Playback support depends on the client, so save a video when its format cannot play +in place. A linked environment file remains the original file: moving or deleting +it can break the preview. + +Agent links can open files outside the workspace read-only. An outside-workspace +HTML file cannot load neighboring scripts, styles, or images. Web and desktop can +render HTML and PDF; switch HTML to source when markup matters, and a line link +opens source automatically. Rendered HTML cannot access the T3 Code session. +Mobile opens PDFs through the native iOS viewer or Android's system chooser. + +## Cite, recall, or stash instead of rewriting + +On web and desktop, select text inside one assistant response and choose **Cite in +composer**. Add a comment explaining what should change. The quote remains readable +even if its source later becomes unavailable; selecting it navigates back when the +source still exists. Mobile displays citations but does not create them. + +Press `ArrowUp` in an empty composer to recall sent prompt text in the current +thread, and `ArrowDown` to move forward. Recall restores only typed text—not +attachments, terminal context, or other extras. Once you edit recalled text it +becomes a normal draft. + +Use `Cmd+S` or `Ctrl+S` on web/desktop to stash the current prompt and attachments. +With an empty composer, the shortcut restores one stash or opens the stash menu. +Uploaded files remain available for 24 hours; after expiry, restore the text and +attach the missing file again. + + + Text can be reused broadly, but an uploaded file belongs to the environment that + received it and expires from a stash after 24 hours. Keep the source file if the + task may move machines or wait longer. + + +## Use commands and skills + +Type `/` to open commands and `$` to select a skill available for the chosen +environment and provider. Mobile exposes both on **New task** as well. + +Provider-native commands must begin the message. T3 Code commands such as +`/model` and `/plan`, plus skill mentions, can appear on any line. `/compact` +reduces context for a supported existing conversation; web and desktop also expose +this action from the context meter. + +Skills are best for repeatable procedures with their own instructions or tools. +Choose one when the task matches it; do not add skills merely because they are +available. Provider configuration controls which skills appear, and provider rules +can affect whether several named skills run in one message. Provider-specific skill +rules are collected in [Choose providers, models, accounts, and permissions](../providers-models-permissions/). + +## Dictate a draft on iPhone + +On a supported iPhone with iOS 26 or later, choose the microphone, record for up to +five minutes, and confirm to transcribe. The text is inserted at the selection you +had when recording began; review names, paths, and commands before sending. + +The first use may download Apple's speech model and needs a network connection. +Later transcription for that language runs on the device and can work offline. +Canceling, leaving the screen, or an audio interruption discards the recording but +keeps the existing draft. T3 Code deletes temporary audio after transcription or +cancellation; the submitted message contains the text. + +## Capture visual context with SnapShots + +The desktop app can capture another application window and attach the image to the +current draft. A SnapShot carries the app name and window title and can include +accessibility text, controls, and element positions. That richer context helps an +agent reason about an error dialog or target interface beyond raw pixels. + +Enable **Settings → SnapShots**, grant platform permissions, and choose a global +shortcut. The default on macOS and Windows is both Shift keys. Switch to the target +window and press the shortcut; T3 Code attaches the capture and returns to the +draft. Captures persist on disk until attached, so closing the app during handoff +does not necessarily lose them. + +Turn **Include app text** off when the screenshot alone is appropriate. Accessible +content varies by application, and T3 Code falls back to the image if an app is too +slow. SnapShots support macOS, Windows, and Wayland Linux; X11 is unsupported and +individual Wayland desktops have different setup requirements. + +## Answer questions without disturbing the main draft + +When an agent asks a question with custom input, its answer can include up to eight +attachments across the answer set. Each question keeps its own files, separate from +the normal composer draft. Files upload to the thread's environment and remain in +history after submission. Questions restricted to fixed choices do not accept +attachments. + +A failed response keeps its answer draft so you can retry. Reverting a thread +removes files attached to discarded answers; deleting the thread applies the same +cleanup as other message attachments. If attachment controls do not appear on a +custom-answer question, update the environment server. + +This is useful when the agent discovers missing evidence mid-turn: attach the +failing screenshot or sample document directly to the question rather than canceling +the task and rebuilding the main prompt. + +## Context hygiene for long-running work + +- Start a new thread for a separate task; context relevance usually matters more + than conversation length. +- Use a follow-up in the same thread when it depends on its decisions and changes. +- Cite the exact disputed sentence instead of pasting a whole response. +- Attach source evidence; summarize what the agent should learn from it. +- Compact a supported long conversation when the context meter or provider suggests + it, then restate any constraint that must survive summarization. +- Remember that links to environment files can break if those files move or are + deleted. Save durable artifacts inside the project when appropriate. + +Messages support up to 120,000 characters. An oversized draft stays in the composer +so you can split it. Several focused turns are usually easier to review than one +message that combines unrelated work. + +### Source trail + +Sources: [messages and context](https://github.com/pingdotgg/t3code/blob/859304b7808ab9a4be87b1bddcd07c6485bf9c4f/docs/user/composer.md), +[question attachments](https://github.com/pingdotgg/t3code/blob/859304b7808ab9a4be87b1bddcd07c6485bf9c4f/docs/user/question-attachments.md), +[SnapShots](https://github.com/pingdotgg/t3code/blob/859304b7808ab9a4be87b1bddcd07c6485bf9c4f/docs/user/snap-shot.md), and +[keybindings](https://github.com/pingdotgg/t3code/blob/859304b7808ab9a4be87b1bddcd07c6485bf9c4f/docs/user/keybindings.md). diff --git a/src/content/book/10-control-surface.mdx b/src/content/book/10-control-surface.mdx index 030f93e..4b2e1b3 100644 --- a/src/content/book/10-control-surface.mdx +++ b/src/content/book/10-control-surface.mdx @@ -1,10 +1,10 @@ --- slug: control-surface -order: 10 -number: "1" +order: 160 +number: "16" kind: chapter -part: Part I · Boundaries and vocabulary -partOrder: 1 +part: Part II · Boundaries and vocabulary +partOrder: 2 title: Control surface, not agent brain shortTitle: The ownership boundary summary: For each environment, T3 Code authoritatively coordinates repository and provider execution without replacing a provider's model loop, native context, or authentication. @@ -17,7 +17,7 @@ objectives: keywords: [control surface, harness, ownership, server, provider adapter] sourceAreas: [docs/internals/overview.md, apps/server/src/server.ts, apps/server/src/provider/Services/ProviderAdapter.ts] visuals: [ownership matrix, server capability assembly] -updatedAt: 2026-08-24 +updatedAt: "2026-09-11" --- import Callout from "../../components/Callout.astro"; @@ -61,9 +61,9 @@ binding and recovery state. ## A normalized, deliberately bounded seam - + - + The contract is not a promise of feature equality. Its lifecycle, interaction, history, rollback, feedback, and event-stream surface is deliberately bounded, but diff --git a/src/content/book/100-events-receipts.mdx b/src/content/book/100-events-receipts.mdx index 86fa0a1..2022825 100644 --- a/src/content/book/100-events-receipts.mdx +++ b/src/content/book/100-events-receipts.mdx @@ -1,10 +1,10 @@ --- slug: events-receipts -order: 100 -number: "10" +order: 250 +number: "25" kind: chapter -part: Part III · Transactional domain core and post-commit delivery -partOrder: 3 +part: Part IV · Transactional domain core and post-commit delivery +partOrder: 4 title: Events, receipts, idempotency, and the post-commit gap shortTitle: Events and receipts summary: Events record durable domain facts, command receipts bind an id to one aggregate but not its payload, and hot publication after commit leaves a crash window that an acknowledgement cannot close. @@ -18,7 +18,7 @@ objectives: keywords: [events, command receipts, idempotency, retry, sequence, publication, PubSub, crash window] sourceAreas: [packages/contracts/src/orchestration.ts, apps/server/src/persistence/Layers/OrchestrationEventStore.ts, apps/server/src/persistence/Services/OrchestrationCommandReceipts.ts, apps/server/src/orchestration/Layers/OrchestrationEngine.ts, apps/server/src/orchestration/Layers/ProviderCommandReactor.ts] visuals: [event envelope ledger, transaction boundary, duplicate-command matrix, crash-window explorer] -updatedAt: 2026-08-24 +updatedAt: "2026-09-11" --- import Callout from "../../components/Callout.astro"; @@ -75,7 +75,7 @@ The event store therefore has two orders with different visibility: - a per-stream `stream_version`, computed and stored internally. The per-stream version is not an expected-version token in the command protocol. -As Chapter 9 showed, one serialized command worker prevents concurrent decisions +As Chapter 24 showed, one serialized command worker prevents concurrent decisions inside a server process. ## A receipt is smaller than the command it deduplicates @@ -111,7 +111,7 @@ cannot catch changed intent within the same aggregate. -
+
T subgraph T[one SQL transaction] diff --git a/src/content/book/11-providers-models-permissions.mdx b/src/content/book/11-providers-models-permissions.mdx new file mode 100644 index 0000000..8066895 --- /dev/null +++ b/src/content/book/11-providers-models-permissions.mdx @@ -0,0 +1,250 @@ +--- +slug: providers-models-permissions +order: 60 +number: "6" +kind: chapter +part: Part I · Product guide +partOrder: 1 +title: Choose providers, models, accounts, and permissions +shortTitle: Providers and permissions +summary: Configure agent runtimes per environment, select models and account instances, and choose a permission mode that matches the task and provider. +status: source-checked +gates: [sources, links, interaction, editorial] +objectives: + - Choose a provider and model based on capability and environment setup. + - Configure separate accounts or endpoints without mixing provider state. + - Set a permission mode with a clear understanding of provider differences. +keywords: [providers, models, accounts, Codex, Claude, Cursor, Grok, OpenCode, Antigravity, permissions] +sourceAreas: [docs/user/install.md, docs/user/permission-modes.md, docs/user/providers-codex.md, docs/user/providers-claude.md, docs/user/providers-opencode.md, docs/user/providers-antigravity.md, docs/user/usage.md] +visuals: [turn configuration decision tree] +updatedAt: "2026-09-11" +--- + +import Callout from "../../components/Callout.astro"; +import Mermaid from "../../components/Mermaid.astro"; + +Every thread combines four decisions: the provider runtime, a configured provider +instance, a model and its options, and a permission mode. They are related but not +interchangeable. “Codex Work” and “Codex Personal” can be separate instances of +one provider; each can expose several models; the thread can run any of them under +Supervised, Auto-accept edits, Auto, or Full access. + + H{Which provider is installed and suited to task?} + H --> I[Choose configured instance and account] + I --> M[Choose catalog or custom model] + M --> O[Set supported options: reasoning, tier, variants] + O --> R{How much review does this task need?} + R -->|inspect every action| S[Supervised] + R -->|edits can flow| AE[Auto-accept edits] + R -->|provider can review routine actions| A[Auto] + R -->|trusted task and environment| F[Full access] + S --> T[Send turn] + AE --> T + A --> T + F --> T + T --> Q{Native provider request or question?} + Q -->|yes| U[Respond in conversation] + Q -->|no| X[Agent continues]`} /> + +## Start with environment reality + +Provider installation and authentication live on the environment machine. Enable +instances in **Settings → Providers** for that environment. If the executable is +not on the server's `PATH`, set its binary path. Environment variables on an +instance are appropriate for API keys, base URLs, and router configuration; launch +arguments are for CLI arguments. + +| Provider | Important product behavior | +|---|---| +| Codex | Supports shared-home account switching, asynchronous questions, app-access approvals, and `/feedback`. | +| Claude | Supports separate config-directory instances, configurable auto-compaction, skills, and router endpoints. | +| Cursor | Uses Cursor CLI and participates in automatic permission review. | +| Grok Build | Supports remembered matching approvals for a session. | +| OpenCode | Can run a local managed server or connect to an external one; Auto falls back to supervised behavior. | +| Antigravity | Uses an account-specific ACP runtime and T3-managed sign-in; lacks T3 Plan mode and thread rewind. | + +Choose for the task and the account you actually want charged. A familiar model +name does not imply equivalent tools or continuation semantics across providers. + +## Configure more than one account + +Provider instances let one environment expose separate accounts, endpoints, or +presets. Give instances names that communicate their boundary, such as **Claude +Work**, **Claude Router**, or **Codex Personal**. + +### Codex shared and separate homes + +Codex can switch accounts in an existing thread when instances share the same +`CODEX_HOME`. A shadow home supplies another account's login and model catalog +while shared sessions and configuration remain available. Keep the primary home +at `~/.codex`, sign the second account into a fresh directory, and configure both +instances with the same main home plus the second instance's shadow path. + +Use completely separate `CODEX_HOME` paths when sessions and configuration must +also be isolated. Those instances cannot continue one another's threads. Do not +populate a shadow home by copying the whole primary Codex directory; start fresh +and log in. The shadow account needs its own `auth.json`; configure Codex to store +credentials in a file if the login otherwise lives in an OS credential store. + +If an account is absent from the thread picker, compare the instances' main and +shadow paths, refresh provider status, and verify the account each reports. Two +instances unexpectedly showing the same account usually means the shadow directory +was copied instead of created fresh. + +Codex app tools can request access for one operation, the current session, or +permanently; read the named app and scope before approving. A usage-limit message +can name its window and reset time. Retry after reset, or follow the workspace-plan +instruction to add credits or raise its spend limit. `/feedback` deliberately +uploads the conversation and Codex logs to OpenAI and returns an ID for support. + +### Claude config directories + +Claude isolates accounts and presets with `CLAUDE_CONFIG_DIR`. Sign in using the +same directory that the provider instance will set. Changing `HOME` is not an +equivalent substitute because credentials can remain in system keychains or other +locations. Existing threads can switch only among compatible Claude instances +that use the same config directory. + +Claude instances can route through OpenRouter or another compatible endpoint using +environment variables. Give router presets separate config directories and follow +the router's current compatibility guidance. For OpenRouter, set +`ANTHROPIC_BASE_URL`, put the key in `ANTHROPIC_AUTH_TOKEN`, and set +`ANTHROPIC_API_KEY` to an explicitly empty value. Run `/logout` first if that config +directory has a cached Anthropic login. Any local router must be reachable from the +environment machine. + +Claude loads skills from the config directory and the project's `.claude/skills`, +with the config copy winning on duplicate names. A skill marked +`disable-model-invocation` can still be selected manually, but invoke these one per +message. When a subscription limit is reached, Claude can hold the turn until the +window reopens; stop it if you prefer to continue later. + +### OpenCode and Antigravity instances + +OpenCode can launch locally when **Server URL** is blank or connect to an existing +server. A configured password applies to the local server and T3 Code's connection; +without one, the local server can use `OPENCODE_SERVER_PASSWORD`. For an external +server, configure its URL and password explicitly—T3 Code does not forward that +local environment value. T3 Code requires OpenCode 1.14.19 or newer at this source +revision. After a dropped connection, send another prompt to reconnect the same +session. + +Antigravity instances each have their own Google sign-in; managed runtime files +are shared by the environment. Remote Google callbacks return to `127.0.0.1` on +the environment. Copy the complete returned URL into the setup form instead of +changing its hostname. The URL contains a temporary code and should be kept +private. + +Provider setup is available on web and desktop, not mobile. Besides a personal +Google account, an instance can use Gemini Enterprise, a Gemini API key, or Agent +Platform / Vertex AI credentials. The selected method and its fields control the +agent; ambient Gemini credential variables do not override them. The Antigravity +API-key field is stored as plain text in environment settings. Changing methods +stops that instance's sessions, so sign out before replacing an account. + +Managed installation supports Apple Silicon macOS, Linux x64/ARM64, and Windows +x64/ARM64 and can need several gigabytes. A manual install requires the ACP binary +and `localharness_external` helper at the same version in one directory. Intel Macs +can use a supported remote environment. + +## Choose a model and defaults + +The composer model picker selects the provider instance, model, and supported +options for the thread. T3 Code remembers the selection for new threads. A project +model default takes precedence; resetting the project setting returns to the +remembered choice. + +Web and desktop can add unlisted custom models under **Settings → Providers → +Models**. Only options implemented by that provider adapter have an effect. +Antigravity uses its account-provided catalog and does not accept custom models. +Leaving reasoning level or service tier unset delegates the choice to provider +configuration. + +Refresh provider status after changing credentials, native configuration, or the +available model catalog. Existing threads retain their selected model even if it +disappears; if the provider rejects it, pick an available model and retry. + +For OpenCode, web and desktop use **Refresh provider status** and mobile uses +**Refresh models**. Reconnecting also refreshes the catalog; periodic health checks +do not. Allow the local helper to sit idle for 30 seconds before refreshing native +configuration again, or restart or reload an external server when it owns the +cache. + +## Choose permission mode per thread + +| Mode | T3 Code intent | Good starting use | +|---|---|---| +| **Supervised** | Ask before commands and file changes, though some providers allow read-only actions | Unfamiliar repository, sensitive environment, or first run with a provider. | +| **Auto-accept edits** | Let edits proceed; other actions may still ask | Focused code changes where command execution deserves review. | +| **Auto** | Ask the provider's automatic reviewer to approve routine actions | Routine work with Codex, Claude, or Cursor. | +| **Full access** | Permit commands and edits without T3 approval prompts | Trusted, bounded work in an isolated workspace. | + +New threads default to **Full access** unless you change the mode before sending. +A thread created from another thread inherits its mode. Approval cards appear in +the conversation; approve or reject them there. Permission modes do not stop an +agent from asking ordinary task questions. + + + Providers enforce these choices differently. Auto behaves like Supervised for + OpenCode and Antigravity because they lack the same automatic reviewer. + Antigravity can still issue native approvals in Full access. Always judge the + actual request shown in the conversation. + + +OpenCode requires approval for `.env` and `.env.local` reads in restricted modes, +while `.env.example` is allowed. Its **Allow for workspace** decision can affect +matching requests in other sessions on the same workspace, especially when using a +shared external server. Grok's **Always allow this session** remembers a matching +command or tool input, not blanket authority for every future action. + +Denying one OpenCode action does not stop the whole turn. Antigravity fixed-choice +questions still require one offered answer even in Full access. + +## Know the provider-specific limits + +- Codex asynchronous questions can remain pending through reconnects; dismissing a + question closes it without sending an answer. +- Claude can auto-compact between 100,000 and 1,000,000 tokens, or compact on + demand with `/compact`. This changes timing, not the model context window. +- OpenCode configuration may remain cached while its local helper stays alive; + repeated refreshes can keep that helper alive. +- Antigravity uses its native `/plan`; T3 Code Plan mode is unavailable. It cannot + rewind provider conversation state, so edit-and-resubmit and thread revert are + unavailable even though T3 Code retains history and diffs. +- Antigravity's direct attachment limits can be lower than T3 Code's general upload + limits: 1 MiB per text file, 10 MiB per image, 20 MiB per audio clip, and 50 MiB + total per message. It accepts supported images, PDFs, text, and audio; a successful + T3 upload does not guarantee provider acceptance. +- Antigravity discovers project skills in `.gemini/skills`, `.agents/skills`, then + legacy `.agent/skills` precedence, and global skills under its Gemini config + directories. Its subagents appear as batches: individual children cannot be + opened or controlled, and an idle batch does not prove every child succeeded. + +Disabling an Antigravity instance stops sessions but keeps its login. Signing out +also removes that instance's saved login; removing the managed runtime keeps logins +and thread or workspace data. Before runtime removal, disable instances, cancel an +active installation, and clear paths that point into the managed runtime. + +Usage estimates, pooled reset windows, and custom pricing are explained once in +[Personalize, measure usage, update, and protect privacy](../personalize-usage-updates/). + +### A practical selection checklist + +Before a consequential turn, verify: + +1. the environment contains the intended checkout and credentials; +2. the provider instance names the right account or endpoint; +3. the selected model exists and supports the needed capability; +4. the permission mode matches both workspace isolation and task risk; +5. provider-specific limitations do not invalidate the workflow; +6. any usage or reset window is adequate for the task. + +### Source trail + +Sources: [permission modes](https://github.com/pingdotgg/t3code/blob/859304b7808ab9a4be87b1bddcd07c6485bf9c4f/docs/user/permission-modes.md), +[Codex](https://github.com/pingdotgg/t3code/blob/859304b7808ab9a4be87b1bddcd07c6485bf9c4f/docs/user/providers-codex.md), +[Claude](https://github.com/pingdotgg/t3code/blob/859304b7808ab9a4be87b1bddcd07c6485bf9c4f/docs/user/providers-claude.md), +[OpenCode](https://github.com/pingdotgg/t3code/blob/859304b7808ab9a4be87b1bddcd07c6485bf9c4f/docs/user/providers-opencode.md), +[Antigravity](https://github.com/pingdotgg/t3code/blob/859304b7808ab9a4be87b1bddcd07c6485bf9c4f/docs/user/providers-antigravity.md), and +[usage](https://github.com/pingdotgg/t3code/blob/859304b7808ab9a4be87b1bddcd07c6485bf9c4f/docs/user/usage.md). diff --git a/src/content/book/110-projections-read-models.mdx b/src/content/book/110-projections-read-models.mdx index 2aaea7c..b53f9eb 100644 --- a/src/content/book/110-projections-read-models.mdx +++ b/src/content/book/110-projections-read-models.mdx @@ -1,10 +1,10 @@ --- slug: projections-read-models -order: 110 -number: "11" +order: 260 +number: "26" kind: chapter -part: Part III · Transactional domain core and post-commit delivery -partOrder: 3 +part: Part IV · Transactional domain core and post-commit delivery +partOrder: 4 title: Projection tables and read models shortTitle: Projections summary: New events fold through ordered SQLite projectors before the enclosing command commits; bootstrap replays committed events from per-projector cursors, and queries expose deliberately different read-model shapes. @@ -17,7 +17,7 @@ objectives: keywords: [projections, SQLite, read model, cursor, snapshot, replay, checkpoint] sourceAreas: [apps/server/src/orchestration/Layers/ProjectionPipeline.ts, apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts, apps/server/src/persistence/Layers/ProjectionState.ts, apps/server/src/ws.ts] visuals: [ordered projection fold, cursor watermark lab, read-model comparison] -updatedAt: 2026-08-24 +updatedAt: "2026-09-11" --- import Callout from "../../components/Callout.astro"; @@ -59,7 +59,7 @@ approval/input counts and whether a plan is actionable from current dependent ro thread/shell fold last, after the tables it reads to derive shell summary fields. -
+
P[projects] P --> M[messages] @@ -204,6 +204,20 @@ replace the global cursor. the filesystem into a transactional resource. +## Pull requests, ordering, and project presentation + +The projection schema keeps several user-facing facts independently queryable: +manual active-thread order, unsettled timestamps, project auto-pull policy, project +icons, branch-associated reviews, and the many-to-many link between threads and pull +requests. This is why a review can remain attached to a thread even when it is not the +branch’s primary review, and why a stack can be rendered as related layers rather than +flattened into one URL. + +The migration sequence makes the ownership clear: migrations 42 and 48 introduce +singular and branch-aware review state; migration 50 replaces that shape with plural +thread pull requests. The projector, snapshot query, and clients then agree on the +same graph. Browse the current [migration directory](https://github.com/pingdotgg/t3code/tree/859304b7808ab9a4be87b1bddcd07c6485bf9c4f/apps/server/src/persistence/Migrations). + ## Source trail F[Files and diffs] + F --> T[Terminal command] + T --> B[Browser preview] + B --> O[Observe behavior] + O -->|page issue| A[Annotate or describe] + O -->|external app issue| S[SnapShot] + A --> P + S --> P + T -->|failure output| P`} /> + +## Choose the surface that answers the question + +| Question | Open | Why it helps | Watch for | +| --- | --- | --- | --- | +| “What is in this file?” | **Files** | Browse the project tree and inspect source or media without leaving the thread. | A rendered preview can hide exact source; use the source view when syntax matters. | +| “Does this command pass?” | **Terminal** | Run a shell on the environment that owns the project. | A remote project's terminal runs remotely, not on the device showing the UI. | +| “Does the page behave correctly?” | **Preview/Browser** | Exercise the app beside the conversation and give the agent concrete UI feedback. | A server must listen on a reachable port; signed-in state belongs to the selected browser profile. | +| “What is wrong in this other app?” | **SnapShot** | Capture the active desktop window and attach its visual and, when available, accessibility context. | Review sensitive content before sending; SnapShots are desktop-only and opt-in. | + +### Files: inspect before you redirect the agent + +Open the thread's right panel and choose **Files**. Navigate the workspace tree, +select a file, and use the appropriate source, image, video, Markdown, or web +preview. This is ideal for answering a narrow question—whether the agent edited +the right component, whether a generated asset looks correct, or where a setting +lives—without asking it to spend another turn rediscovering the file. + +Files are read from the selected environment. If you are controlling a server over +SSH, Tailscale, or T3 Connect, the phone or laptop is only the viewer; the path and +contents remain on the host. Treat the panel as an inspection surface, and use the +conversation to request changes so the thread preserves the reason for the edit. + +### Terminal: run on the project's machine + +Open the terminal drawer from the thread or command palette, create a terminal, +and run the smallest command that answers your question. Multiple terminals are +useful when one process must keep running while another runs tests or Git commands. +The terminal is a real shell owned by the environment, so its installed tools, +credentials, filesystem, and network are those of the host. + +Terminal history survives client reconnection, within server limits: up to **5,000 +lines and 8 MiB per terminal**. The server removes the oldest output when either +limit is crossed, and a client may display less than the server retained. Save +important results in the project or summarize them in the thread; scrollback is a +working buffer rather than permanent documentation. See the immutable +[terminal history guide](https://github.com/pingdotgg/t3code/blob/859304b7808ab9a4be87b1bddcd07c6485bf9c4f/docs/user/terminal.md). + + + When a failure is already visible in a terminal, attach or reference that terminal + context instead of pasting a huge log. Include the command, the first meaningful + error, and what you expected. Long-running noise makes diagnosis harder. + + +### Browser preview: close the implementation loop + +Start the application in a terminal, then open its discovered port in the preview. +Use the preview to navigate, refresh, inspect the current page, and report behavior +in the same thread that owns the code. If annotation tools are available, point to +the affected region and describe the intended result. The useful prompt is concrete: +“This menu clips at this width; keep it inside the viewport,” rather than “fix UI.” + +Desktop preview profiles can keep separate browsing identities. To reuse an +existing signed-in session, open **Settings → Integrations → Browser profiles → +Add profile**, select **Import from**, close the source browser, and complete any OS +keyring prompt. Import is a **one-time cookie copy**: future sign-ins diverge, some +sites require authentication again, and partitioned cookies are skipped. Safari +import on macOS needs temporary Full Disk Access; Windows supports Firefox and +Helium profiles with standard profile encryption. The exact platform limits are in +the [browser import guide](https://github.com/pingdotgg/t3code/blob/859304b7808ab9a4be87b1bddcd07c6485bf9c4f/docs/user/browser-import.md). + +## SnapShots: bring another window into the draft + +A SnapShot captures the active window, records its app name and title, and attaches +it to the current draft. With **Include app text**, it can also carry accessibility +data—visible text, controls, and positions—so an agent can reason beyond pixels. +If an app responds slowly or exposes little accessibility data, capture continues +with the image. + +1. In the desktop app, open **Settings → SnapShots** and enable the feature. +2. Allow capture and choose a shortcut. macOS asks for Screen Recording and asks + for Accessibility only when app text is enabled. Windows needs no setup. +3. Switch to the window, press the shortcut, then review the attachment in the + draft before sending. If no thread is open, T3 Code creates a draft in the + current project. +4. Tune sound, flash, and the fly-to-draft animation independently. Reduced-motion + OS settings disable the animation. + +Pending captures are saved until the draft attachment is recorded, so closing the +app mid-capture does not normally lose them. **Finish later** disables capture but +keeps installed setup pieces. Turning SnapShots off releases the shortcut. + +On Linux, SnapShots require Wayland. GNOME uses a bundled extension, KDE Plasma 6 +uses a helper, and Hyprland or Niri asks you to review the exact compositor config +change before saving. Other Wayland desktops can fall back to a manual window +picker; X11 is unsupported. Follow the desktop-specific recovery steps in the +[SnapShots guide](https://github.com/pingdotgg/t3code/blob/859304b7808ab9a4be87b1bddcd07c6485bf9c4f/docs/user/snap-shot.md). + +## A practical workbench pass + +For a UI bug, ask the agent to identify the likely files, inspect the resulting diff, +start the app in one terminal, and run a focused check in another. Open the preview, +reproduce the issue, and attach an annotation or a SnapShot when the missing context +lives outside the preview. Before accepting the work, refresh the preview, inspect +the final diff, and keep the command result in the thread. That path makes your +acceptance based on visible evidence rather than a confident status message. diff --git a/src/content/book/120-post-commit-reactors.mdx b/src/content/book/120-post-commit-reactors.mdx index bc3fbd5..efeda70 100644 --- a/src/content/book/120-post-commit-reactors.mdx +++ b/src/content/book/120-post-commit-reactors.mdx @@ -1,10 +1,10 @@ --- slug: post-commit-reactors -order: 120 -number: "12" +order: 270 +number: "27" kind: chapter -part: Part III · Transactional domain core and post-commit delivery -partOrder: 3 +part: Part IV · Transactional domain core and post-commit delivery +partOrder: 4 title: Post-commit reactors and the delivery gap shortTitle: Post-commit reactors summary: Durable intent crosses a non-durable hot-stream seam into independent reactor workers, whose serialized handlers can still fork overlapping provider work and whose progress is not reconstructed after a crash. @@ -18,7 +18,7 @@ objectives: keywords: [reactors, PubSub, side effects, delivery gap, DrainableWorker, provider ingestion, crash recovery] sourceAreas: [apps/server/src/orchestration/Layers, apps/server/src/provider/Layers/ProviderService.ts, apps/server/src/serverRuntimeStartup.ts, packages/shared/src/DrainableWorker.ts] visuals: [commit publication sequence, crash cursor, reactor topology, worker ownership matrix] -updatedAt: 2026-08-24 +updatedAt: "2026-09-11" --- import Callout from "../../components/Callout.astro"; @@ -43,7 +43,7 @@ database can say “accepted” while every side-effect subscriber sees nothing. -
+
D[decide] D --> TX[(event + projections + receipt)] @@ -70,7 +70,7 @@ command execution; it does not repair the post-commit side-effect bridge. subscriptions are fresh streams created from process-local PubSubs. The provider reactor explicitly says pending hot-stream work cannot resume. No reactor progress row, outbox append beside the command transaction, or startup event-store replay - appears in these paths at the pinned revision. + appears in these paths. diff --git a/src/content/book/13-source-control.mdx b/src/content/book/13-source-control.mdx new file mode 100644 index 0000000..a8308d6 --- /dev/null +++ b/src/content/book/13-source-control.mdx @@ -0,0 +1,141 @@ +--- +slug: source-control +order: 80 +number: "8" +kind: chapter +part: "Part I · Product guide" +partOrder: 1 +title: "Source control: checkpoints, pull requests, reviews, and stacks" +shortTitle: Source control +summary: "Turn agent work into reviewable Git history, inspect per-turn checkpoints, collaborate through hosted pull requests, and manage dependent GitHub stacks." +status: source-checked +gates: [sources, links, interaction, editorial] +objectives: + - Distinguish T3 checkpoints from commits and hosted pull requests. + - Configure a hosting account on the environment that owns the repository. + - Create, link, review, merge, and stack pull requests with the right safeguards. +keywords: [Git, checkpoints, pull requests, code review, GitHub stacks, GitLab, Bitbucket, Azure DevOps] +sourceAreas: [docs/user/source-control.md, apps/server/src/checkpointing, apps/server/src/vcs, apps/web/src/components/GitActionsControl.tsx, apps/web/src/components/pullRequest] +visuals: [source-control promotion path, hosting capability matrix] +updatedAt: "2026-09-11" +--- + +import Callout from "../../components/Callout.astro"; +import Mermaid from "../../components/Mermaid.astro"; + +T3 Code gives agent work three useful levels of history. A **checkpoint** captures +the workspace around a turn. A **commit** creates normal local Git history. A +**pull request** publishes a branch for checks and human review. Promote work from +one level to the next only when it has earned it. + + C[Hidden checkpoint] + C --> D[Inspect turn or branch diff] + D -->|wrong direction| R[Revert to checkpoint] + R --> T + D -->|verified| L[Local commit] + L --> P[Push and create PR] + P --> V[Checks and review] + V -->|changes requested| T + V -->|approved| M[Merge] + P -. optional .-> K[Link PR to thread] + K -. keeps context .-> V`} /> + +## Checkpoints are the thread's safety net + +When a turn changes files, T3 Code records checkpoint metadata and can show a +turn-scoped diff in the conversation or Changes panel. Use it to answer “what did +that instruction change?” without mixing the result with every uncommitted edit on +the branch. If a turn went in the wrong direction, revert from its checkpoint UI. + +A checkpoint is not a substitute for a commit. It is an internal restoration point, +and reverting can affect workspace files and provider conversation state. Before a +revert, inspect later turns and preserve anything you still need. If the workspace +contains valuable manual edits, commit or copy them first. After reverting, verify +the working tree and tell the agent what should happen next. + +## Connect the host where Git actually runs + +Install Git and authenticate the hosting provider **on the environment server**. +For a remote project, signing in on your viewing laptop does not authenticate the +remote host. Then open **Settings → Source Control** and choose **Rescan**. + +| Host | Required setup | Product scope and limitations | +| --- | --- | --- | +| GitHub | GitHub CLI 2.81.0+ and `gh auth login` | Clone, publish, PRs, review, auto-merge, fork-workflow approval, revert PRs, and stacks. | +| GitLab | GitLab CLI and `glab auth login` | Merge requests, review, and auto-merge. | +| Bitbucket | Access token, or email plus API token, in server environment | Restart after environment changes; declined PRs cannot be reopened. | +| Azure DevOps | Azure CLI, DevOps extension, and `az login` | Use the host website for diff viewing or comment changes. | + +API access and Git remote authentication are related but separate. A connected +provider account can load PR metadata while `git push` still fails because the +repository remote uses different SSH or HTTPS credentials. + +## Clone, publish, and create a pull request + +Open the command palette with `Cmd/Ctrl+K`, choose **Add Project**, select a host or +paste a Git URL, and choose the destination directory. To publish an existing local +repository, use **Publish Repository**. This creates the hosted repository, adds +`origin`, and pushes commits; if the repository has no commits, create one before +the first push. + +From a thread's Git actions: + +1. Inspect the branch or turn diff. Exclude secrets, generated debris, and unrelated + edits. +2. Run the relevant checks and create a focused local commit. +3. Push the branch and create the pull request. T3 Code can draft commit subjects, + PR titles, and descriptions from the change. +4. Set the writing model and style in **Settings → Source Control**. **Repository + conventions** uses project instructions and recent commit subjects. +5. Read the generated text before publishing. It should state the user-visible + problem, resulting behavior, and validation rather than narrate the conversation. + + + Creating a remote repository, pushing a branch, posting a review, and merging all + change shared state. Inspect the exact repository, branch, diff, and audience + before you perform each action. + + +## Review without losing the implementation context + +Open **Pull requests** to inspect changes and comments, request reviewers, check out +a branch, or merge. GitLab names them merge requests. GitHub, GitLab, and Azure +DevOps can enable auto-merge while checks are pending. Auto-merge still obeys branch +rules; use it when the change is final and only external gates remain. + +A thread may link several PRs, even review work from another repository on the same +host. Use **Link pull request** from the palette or Linked pull requests panel, link +from a detected badge, or choose **Link to thread** from a review. PRs created by the +thread are linked automatically. Linking adds context; it does not copy or merge the +changes. + +The linked panel groups stacks and shows current review state. Unlinking a layer +keeps it out of later synchronization. With **Auto-settle merged threads**, a thread +can settle after every linked review reaches a terminal state; an open or unsynced +link keeps it active. Mobile can open linked reviews and their stacks, while linking +and unlinking are done on web or desktop. + +## GitHub stacks: treat dependencies as dependencies + +A stack is an ordered set of PRs where a higher layer depends on work below it. +Open a stack badge to navigate the layers. + +**Merge stack** submits the selected PR and every unmerged layer below it together, +respecting branch rules and merge queues. Read the confirmation because its scope is +larger than one visible PR. After a merge, GitHub rebases remaining layers. + +**Rebase stack** updates remote branches bottom to top without changing your local +checkout. It can rewrite remote history and restart checks. The operation is not +atomic: if layer three fails, layers one and two may already be updated. Resolve the +failed layer before retrying, and expect a manual conflict after amending a lower +layer even when the files look independent. + +## Recovery checklist + +If source control appears disconnected, authenticate on the environment host and +rescan. For GitHub verification errors, confirm CLI 2.81.0 or newer. If metadata +loads but pushing fails, inspect the remote URL and its credentials. If a review +cannot load, use the host website while checking network access, permissions, and +rate limits. The complete, revision-pinned behavior is in the +[source-control user guide](https://github.com/pingdotgg/t3code/blob/859304b7808ab9a4be87b1bddcd07c6485bf9c4f/docs/user/source-control.md). diff --git a/src/content/book/130-persistence-recovery.mdx b/src/content/book/130-persistence-recovery.mdx index ae66ca5..670d9b7 100644 --- a/src/content/book/130-persistence-recovery.mdx +++ b/src/content/book/130-persistence-recovery.mdx @@ -1,10 +1,10 @@ --- slug: persistence-recovery -order: 130 -number: "13" +order: 280 +number: "28" kind: chapter -part: Part III · Transactional domain core and post-commit delivery -partOrder: 3 +part: Part IV · Transactional domain core and post-commit delivery +partOrder: 4 title: Persistence, reconstruction, and crash recovery shortTitle: Persistence and recovery summary: SQLite is the durable domain core, but settings, secrets, attachments, terminal history, logs, and hidden Git refs cross separate filesystems and recovery protocols with sharply different guarantees. @@ -18,7 +18,7 @@ objectives: keywords: [SQLite, WAL, migrations, recovery, settings, secrets, attachments, terminal history, Git refs, tombstones] sourceAreas: [apps/server/src/persistence, apps/server/src/orchestration/Layers/ProjectionPipeline.ts, apps/server/src/serviceLauncher.ts, apps/server/src/serverSettings.ts, apps/server/src/terminal, apps/server/src/vcs] visuals: [startup reconstruction DAG, projector replay counter, recovery matrix, retention taxonomy] -updatedAt: 2026-08-24 +updatedAt: "2026-09-11" --- import Callout from "../../components/Callout.astro"; @@ -62,8 +62,8 @@ busy timeout waits for locks instead of immediately returning `SQLITE_BUSY`. ## Migrations are a numbered program -`Migrations.ts` imports every migration and constructs a statically ordered map. At -this pinned revision the manifest contains 41 migrations. Startup invokes the +`Migrations.ts` imports every migration and constructs a statically ordered map. The +current manifest reaches migration 050. Startup invokes the migration runner after the SQLite pragmas; it does not discover filenames and sort them at runtime. @@ -91,8 +91,8 @@ The first migrations establish the architectural core: legacy evidence, not proof that checkpoint blobs currently drive recovery. -Selected migration tests exercise upgrades from older shapes. There is no injected -process-crash test halfway through the full 41-step startup runner. +Selected migration tests exercise upgrades from earlier shapes. There is no injected +process-crash test halfway through the full migration program. ## Projectors reconstruct from cursors—with a 1,000-event ceiling @@ -107,13 +107,13 @@ pages internally. Bootstrap calls it without supplying a larger or unlimited value. A projector that is 1,001 events behind applies only the first 1,000 during that startup and the server proceeds with the projector still behind. -
+
|yes| R[restore state.sqlite + WAL + SHM] U -->|no| O[open SQLite] R --> O O --> P[busy timeout + FK + WAL] - P --> M[run 41 migrations] + P --> M[run 50 migrations] M --> C[read each projector cursor] C -->|default total limit 1000| F[fold persisted events] F --> Q[load optimized command model] @@ -150,6 +150,12 @@ handoff delay, the launcher stops that child, stages and syncs copies of `state.sqlite`, `state.sqlite-wal`, and `state.sqlite-shm`, and then starts the trial. It does **not** write the restore marker before the trial. +Only the stable launcher writes durable service state. It records pending state +before acknowledging the request, and its same-directory state replacement syncs +both the file and directory. The trial must finish migrations, acquire startup +dependencies, bind HTTP, and park its long-running roots at the activation gate +before reporting `prepared`; a listening socket alone is insufficient. + The marker belongs to the rollback path: immediately before copying backup files over the live triplet, the launcher creates and syncs a marker inside the backup. On a later boot, pending service state without that marker resumes the trial; a @@ -236,7 +242,7 @@ garbage collector was found at this revision. ## Terminal history restores text, not a process -Terminal history is capped at 5,000 lines by default and persisted through a keyed +Terminal history is capped at 5,000 lines and 8 MiB of UTF-8 text and persisted through a keyed coalescing worker after a 40 ms debounce. Output updates memory, schedules the history write, and then reaches live subscribers. The worker directly overwrites the history file; it does not use the settings atomic-rename helper. @@ -248,12 +254,17 @@ Because the worker directly overwrites the target instead of using an atomic ren an abruptly interrupted write also carries a partial-file risk; the exact filesystem outcome is an inference, not a behavior asserted by the normal write path. -On first open after restart, the manager reads and sanitizes saved text and then +On first open after restart, the manager reads only the bounded tail, skips an +incomplete UTF-8 prefix, applies the line limit, sanitizes saved text, and then starts a new PTY. Saved text does not encode the old shell process, working job, input buffer, or terminal emulator state. Tests cover caps, sanitization, deletion, inactive-history eviction, and legacy filename migration—not crash injection or scope-finalizer flush. +The client render buffer has its own 512 KiB limit. Retained device-query traffic is +removed, and the web renderer detaches its PTY writer during replay, so a historical +terminal query cannot produce a fresh reply in the replacement shell. + ## Provider logs are diagnostic, not recovery truth Provider runtime NDJSON logging declares itself best-effort. It rotates at 10 MiB diff --git a/src/content/book/14-remote-background.mdx b/src/content/book/14-remote-background.mdx new file mode 100644 index 0000000..945e41f --- /dev/null +++ b/src/content/book/14-remote-background.mdx @@ -0,0 +1,191 @@ +--- +slug: remote-background +order: 90 +number: "9" +kind: chapter +part: "Part I · Product guide" +partOrder: 1 +title: "Remote environments and unattended work" +shortTitle: Remote and background +summary: "Connect securely through T3 Connect, direct pairing, Tailscale, or desktop-managed SSH, then keep Linux and macOS hosts available with the background service." +status: source-checked +gates: [sources, links, interaction, editorial] +objectives: + - Choose a remote connection mode from reachability, setup, and hosting constraints. + - Pair and revoke devices without confusing a pairing link with permanent reachability. + - Install, update, and troubleshoot the per-user background service. +keywords: [remote access, T3 Connect, pairing, Tailscale, SSH, background service, load balancing] +sourceAreas: [docs/user/remote-access.md, docs/user/background-service.md, apps/server/src/cloud, apps/server/src/serviceLauncher.ts, apps/desktop/src] +visuals: [remote-mode decision tree, reachability path] +updatedAt: "2026-09-11" +--- + +import Callout from "../../components/Callout.astro"; +import Mermaid from "../../components/Mermaid.astro"; + +A remote T3 Code client controls the environment on another machine. The project, +Git checkout, provider credentials, agent process, and terminals stay on that host. +The host must remain running, awake where required, and reachable through the mode +you choose. + + B{Want managed internet setup?} + B -->|yes| C[T3 Connect] + B -->|no| D{Both devices share a LAN or private network?} + D -->|plain LAN is enough| E[Direct pairing] + D -->|tailnet available| F[Tailscale HTTPS] + D -->|desktop can SSH to host| G[Desktop-managed SSH] + C --> H[Pair client and connect directly to environment] + E --> H + F --> H + G --> H + H --> I{Must survive logout or closed terminal?} + I -->|Linux or macOS| J[Install user background service] + I -->|foreground is acceptable| K[Keep t3 serve session open]`} /> + +## Compare the four paths + +| Mode | Best fit | Host setup | Main limitation | +| --- | --- | --- | --- | +| **T3 Connect** | Reach your environments across networks with account-based discovery. | Sign in and enable Connect, or run `npx t3@latest connect`. | A saved login alone does not expose a stopped server; push notifications use this path. | +| **Direct pairing** | LAN or an existing private network can route directly to the host. | Enable Network access or run `npx t3 serve --host `. | `127.0.0.1` is reachable only on the host; hosted web needs HTTPS. | +| **Tailscale HTTPS** | Both devices share a tailnet and you want a private HTTPS endpoint. | Enable Tailscale HTTPS or use `--tailscale-serve`. | Requires Tailscale and a free Serve port/mapping. | +| **Desktop-managed SSH** | Your desktop can SSH to a development machine. | Add an SSH environment; remote Node and provider setup must work non-interactively. | Managed by desktop; password prompts and broken non-interactive version-manager setup block launch. | + +## T3 Connect + +On a desktop host, open **Settings → Connections**, sign in, and enable **T3 +Connect**. On a command-line host run: + +```sh +npx t3@latest connect +``` + +Follow the browser sign-in flow; over SSH, paste the returned authorization code. +Accept the offered background service when you want unattended access. If you +decline it, start `npx t3 serve` yourself and keep that process alive. On the other +device, sign in to the same account and choose the environment. + +Connect renews credentials without deliberately dropping a healthy conversation. +A failed renewal can fail that request while the existing connection remains alive. +If an environment is offline, `t3 connect status` checks saved authorization and +link configuration, but it is not a live reachability test; follow with `t3 service +status` and inspect the reported log. + +## Direct pairing and Tailscale + +For LAN access, enable **Settings → Connections → Network access** on the desktop +host; changing it restarts the app. For a CLI server, bind to an address the other +device can actually route to: + +```sh +npx t3 serve --host 192.168.1.20 +``` + +Generate a fresh one-time pairing URL with `npx t3 pair`, then scan its QR code or +paste it into **Add environment**. Settings are under **Connections** on web and +desktop and **Environments** on mobile. Pairing grants that client a renewable +session; the original link is not needed for reconnecting. Create a fresh link for +each device and treat it like a password. + +The link and the paired session have different lifetimes. A link created in +**Settings → Connections** can be copied only by the client that created it, while +that Connections page remains open. Leaving or reloading the page discards the +displayed one-time link; create another instead of trying to recover it. This does +not disconnect devices that already paired. To remove an already paired device, +revoke its session on the host. + +For Tailscale, join both devices to the same tailnet and enable **Tailscale HTTPS**, +or run `npx t3 serve --tailscale-serve`. For an already running server, `npx t3 pair +--tailscale` creates a persistent Serve mapping. Choose another port with +`--tailscale-serve-port` if 443 is occupied. Remove the default mapping with: + +```sh +tailscale serve --https=443 off +``` + +[app.t3.codes](https://app.t3.codes) requires an HTTPS server endpoint. A hosted +pairing link provides credentials; it cannot make a private HTTP endpoint reachable +or add TLS to it. + +## Desktop-managed SSH + +Choose **Settings → Connections → Add environment → SSH** and enter an SSH alias or +`user@host`. T3 Code starts or reuses the remote server and maintains the forwarding +path. The remote host needs a compatible Node.js and provider credentials. When the +launch cannot find Node, test the same non-interactive shell T3 uses: + +```sh +ssh user@example.com 'sh -lc "command -v node && node --version"' +``` + +Configure the version manager's default for non-interactive shells if this differs +from your login terminal. Removing the connection stops a server T3 Code launched, +but leaves a server that was already running. After an app update, retry a failed +SSH launch once before rebuilding the connection. + +## Balance new threads across environments + +On web or desktop, enable **Settings → Connections → Load balancing**. Grouped +projects can then place new drafts on an eligible machine according to **Prefer**, +**Normal**, **Less often**, or **Manual only**. These are preferences, not reserved +percentages. Resource checks happen while choosing a new draft; once chosen, its +environment stays stable. Selecting a branch, worktree, or explicit machine also +pins it. Existing threads never migrate, and mobile selects manually. + +## Run the host in the background + +Linux and macOS can install a per-user service: + +| Task | Command | +| --- | --- | +| Install and start | `npx t3@latest service install` | +| Inspect state and log path | `npx t3@latest service status` | +| Update or repair | `npx t3@latest service update` | +| Stop and remove startup entry | `npx t3@latest service uninstall` | + +Uninstalling leaves projects, threads, and settings intact. Update uses the CLI +version you invoke; use an exact version to pin, or a matching channel such as +`@nightly`. An older CLI refuses to replace a newer service unless you explicitly +allow a downgrade. Updates restart the server, so finish active turns and terminal +commands first. + +Linux requires systemd user services and lingering to survive logout. If status +reports `linger-disabled`, an administrator can run `sudo loginctl enable-linger +"$(id -un)"`; run T3 itself as your normal user. macOS starts at login and stops at +logout, so keep the Mac logged in and awake. Installing it over SSH while nobody is +logged in at the Mac can leave the service installed but unable to start until the +next graphical login. Windows background services are not supported. + + + Signing out or deregistering T3 Connect changes cloud access. It does not stop or + uninstall the local background service. Likewise, installing a service does not + sign the environment into T3 Connect. + + +## Revoke and recover + +Use **Settings → Connections** on the host to revoke an unused link or a device's +existing session. Deregister an environment from the account's **T3 Connect** page +to remove cloud access and free its slot. CLI hosts can use `t3 connect unlink` to +disable exposure while keeping login, or `t3 connect logout` to clear login too. + +For clock/proof errors, correct time on both devices, update, and restart the host. +For link limits, deregister an unused environment. For 403 errors, inspect relay, +proxy, and firewall policy; retain any trace or Cloudflare Ray ID. For transient +408, 429, or 5xx failures, check reachability and allow startup retry. + +For service failures, start with `t3 service status` on the host and use the log +path it prints. On Linux, `linger-disabled` points to logout persistence; +`user-manager-unavailable` points to the systemd user session; and +`service-disabled` or `service-stopped` calls for the reported log and +`systemctl --user status t3code.service`. Use the repair command T3 prints. On +macOS, check **System Settings → General → Login Items** when startup disappears. +If background agent work cannot read Desktop, Documents, or Downloads, grant Full +Disk Access to the Node executable named in the service's launch-agent plist. + +An open connection can remain listed after its access credential expires. That is +not evidence that a revoked device can reconnect: use the host's session list and a +fresh connection attempt when verifying revocation. The complete tables live in the +[remote-access guide](https://github.com/pingdotgg/t3code/blob/859304b7808ab9a4be87b1bddcd07c6485bf9c4f/docs/user/remote-access.md) +and [background-service guide](https://github.com/pingdotgg/t3code/blob/859304b7808ab9a4be87b1bddcd07c6485bf9c4f/docs/user/background-service.md). diff --git a/src/content/book/140-provider-adapter-contract.mdx b/src/content/book/140-provider-adapter-contract.mdx index 39e0dcb..1c145e4 100644 --- a/src/content/book/140-provider-adapter-contract.mdx +++ b/src/content/book/140-provider-adapter-contract.mdx @@ -1,10 +1,10 @@ --- slug: provider-adapter-contract -order: 140 -number: "14" +order: 290 +number: "29" kind: chapter -part: Part IV · Five harnesses, one product model -partOrder: 4 +part: Part V · Six providers, one product model +partOrder: 5 title: The ProviderAdapter contract shortTitle: ProviderAdapter contract summary: ProviderAdapter is the narrow provider-native command and event boundary; ProviderService supplies instance routing and recovery, while orchestration remains the durable product model. @@ -18,7 +18,7 @@ objectives: keywords: [ProviderAdapter, ProviderService, provider runtime, canonical events, sessions, turns, capabilities, errors] sourceAreas: [apps/server/src/provider/Services/ProviderAdapter.ts, apps/server/src/provider/Layers/ProviderService.ts, apps/server/src/provider/Errors.ts, packages/contracts/src/provider.ts, packages/contracts/src/providerRuntime.ts, apps/server/src/orchestration/Layers/ProviderCommandReactor.ts, apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts] visuals: [adapter boundary map, operation ledger, canonical event funnel, round-trip swimlane, interactive contract tracer] -updatedAt: 2026-08-24 +updatedAt: "2026-09-11" --- import AdapterContractLab from "../../components/AdapterContractLab.astro"; @@ -48,9 +48,9 @@ event in the canonical union. ## The contract is deliberately smaller than the product - + - + The complete shape has fourteen required members and one optional operation. Reading them by responsibility is more useful than reading them in file order. @@ -79,7 +79,7 @@ activity, plan changes, requests, usage, completion, and failures arrive later o There is no steer member in the SPI. A provider can interpret sendTurn while a turn is already active as steering—OpenCode has a concrete tested implementation—but the generic contract alone promises only a - send request. Chapters 16–19 document each adapter's behavior separately. + send request. Chapters 31–35 document each adapter's behavior separately. ## A session uses T3 identity and opaque native continuation @@ -104,29 +104,28 @@ the envelope while the owning adapter remains responsible for decoding it. it. -## Capability negotiation is only one field today +## Capability negotiation stays deliberately small -The declared capability object contains one property: -`sessionModelSwitch: "in-session" | "unsupported"`. It tells the provider reactor -whether a model change may stay within an existing native session or requires a -restart. +The declared capability object records whether model changes may stay in-session, +whether a resumed turn can continue without a synthetic prompt, and whether native +conversation history supports rollback. Manual compaction is an optional adapter +operation with either a native start function or a slash command. -That is the entire SPI capability surface at this revision. Approvals, structured -input, plans, tasks, skills, usage telemetry, rollback, steering, and native modes -must not be inferred from this one object. They are concrete adapter behaviors, +Approvals, structured input, plans, tasks, skills, usage limits, attachments, +steering, and native modes must not be inferred from those few fields. They are concrete adapter behaviors, sometimes expressed through typed failure, sometimes absent, and sometimes implemented through provider-specific extensions. - - Codex, Claude, Cursor, Grok, and OpenCode all advertise - sessionModelSwitch: "in-session" at the pinned commit. The - unsupported branch is real reactor logic and part of the contract, - but it is not a current five-provider difference. + + The shared contract gives orchestration explicit switches for operations that can + corrupt provider continuity when guessed. Antigravity, for example, declares that + conversation rollback is unavailable; the UI and revert path must preserve that + distinction. ## Canonical events are a grammar, not a checklist - + Every runtime event shares an id, driver kind, T3 thread, timestamp, optional instance/turn/item/request ids, provider references, and optional raw provenance. @@ -136,10 +135,10 @@ configuration, file, warning, and error events. That breadth provides a stable **target vocabulary** for adapters and consumers. It is not proof of feature parity. For example, the schema can represent token -usage for any provider, while Chapter 20 verifies that only Codex and Claude emit -that event at this revision. +usage for any provider, while Chapter 35 separates live context snapshots, +historical transcript accounting, and provider-reported subscription limits. -
+
|hot domain event| R[ProviderCommandReactor] R --> S[ProviderService] @@ -218,9 +217,9 @@ events in order. Every concrete adapter has behavior tests for its own transport normalization. There is no repository-wide executable conformance suite that feeds the same full -feature script to all five adapters and proves semantic equivalence. That absence +feature script to all six adapters and proves semantic equivalence. That absence is appropriate to surface: one interface gives the product a stable integration -point, while Chapters 16–19 retain provider differences instead of hiding them. +point, while Chapters 31–35 retain provider differences instead of hiding them. A meta-harness benefits from a small command SPI and a rich canonical event diff --git a/src/content/book/15-mobile.mdx b/src/content/book/15-mobile.mdx new file mode 100644 index 0000000..e2f7815 --- /dev/null +++ b/src/content/book/15-mobile.mdx @@ -0,0 +1,144 @@ +--- +slug: mobile +order: 100 +number: "10" +kind: chapter +part: "Part I · Product guide" +partOrder: 1 +title: "Mobile: supervise agents from anywhere" +shortTitle: Mobile +summary: "Navigate projects and threads, send rich prompts, inspect files and reviews, and use push and live activity updates without keeping the app connected." +status: source-checked +gates: [sources, links, interaction, editorial] +objectives: + - Connect the mobile app and navigate efficiently across environments, projects, and threads. + - Compose with photos, videos, files, paste, and voice while preserving drafts through interruptions. + - Configure notifications and understand what requires T3 Connect. +keywords: [mobile, iOS, Android, navigation, notifications, live activities, attachments, voice] +sourceAreas: [docs/user/mobile-notifications.md, apps/mobile/src/Stack.tsx, apps/mobile/src/features/home, apps/mobile/src/features/files, apps/mobile/src/features/review, apps/mobile/src/state/thread-outbox-model.ts] +visuals: [mobile supervision loop, capability table] +updatedAt: "2026-09-11" +--- + +import Callout from "../../components/Callout.astro"; +import Mermaid from "../../components/Mermaid.astro"; + +The mobile app is a remote control for real development environments. It is suited +to checking progress, answering an approval, adding context, reviewing a diff, or +starting the next task while away from the host. Agent execution and project files +remain on the connected environment. + + T[Open the thread] + T --> R{What is needed?} + R -->|approval or answer| Q[Respond to request] + R -->|inspect| F[Files or review diff] + R -->|redirect work| C[Compose text, voice, or media] + Q --> A[Agent continues on host] + F --> C + C --> A + A --> N`} /> + +## Connect and orient yourself + +Use **Settings → Environments** to add a direct pairing URL or manage known +environments. For T3 Connect, sign in with the same account as the host and select +the linked environment. A connection indicator tells you whether the selected host +is reachable; switching environments changes the machine whose projects and threads +you are viewing. + +The Home screen groups work by project and thread and can filter or search the list. +Open a thread to see its durable conversation, running state, requests, linked pull +requests, and composer. On wider phones and tablets, adaptive navigation can keep a +sidebar and detail visible together; on compact layouts, destinations open as a +stack and Back returns to the previous list. Hardware keyboard commands are +available on supported devices. + +Use swipe and row actions deliberately: archive or settle work when it is finished, +and use the archived view when you need it again. A thread remains tied to the +environment where it started; mobile does not move it to another machine. + +## Do real review work from a small screen + +From a thread, open **Files** to browse its workspace tree and preview source, +Markdown, images, video, and supported web content. Open the Git overview or a +linked review to inspect changed files, review state, comments, and stack context. +Keep the question narrow on a phone: verify one behavior or file, then send a clear +follow-up. A desktop remains better for a broad, multi-file audit. + +Mobile shows provider permission and input requests in the timeline. Read the exact +tool, path, or question before responding. Approval lets the environment act; the +phone itself does not perform the command. + +## Compose with the context you already have + +The composer supports text plus photos, videos, images pasted from the clipboard, +and files when the connected server advertises attachment uploads. Use the +attachment menu to choose the camera, photo library, or file picker, review the +tiles, and remove anything irrelevant before sending. Large or unsupported items +can be rejected, and a message has an attachment-count limit shown by the app. + +On iOS, voice input transcribes speech on the device before placing the text in the +draft. It is useful for a quick correction but still deserves an edit for filenames, +commands, and acceptance criteria. Drafts are local to the mobile app and survive +normal navigation. If a send fails or the connection disappears, T3 Code can retain +queued intent and retry once the environment has a live synchronized connection. +Check the final thread state rather than repeatedly tapping Send; duplicate +instructions can make an agent redo work. + + + State the target, observed behavior, desired behavior, and how to verify it. Attach + one relevant image or file. “The checkout button overlaps the total on this + screenshot at 390 px; fix it and verify the mobile breakpoint” is enough to drive + a focused turn. + + +## Notifications, ongoing activity, and Live Activities + +Sign in to T3 Connect, link the environment, and enable **Device Notifications** in +Settings. Alerts cover completion, failure, approvals, and input requests; tapping +one opens its thread. The environment must have agent-activity publishing enabled. + +Enable **Ongoing Agent Activity** on Android or **Live Activity Updates** on iOS to +follow active work outside the app. Finished results can remain visible for up to +15 minutes. Dismissing Android's activity card does not disable future alerts; +change the setting to stop future cards. + +When several threads change together, T3 Code can combine them into one attention +alert or one finished-work alert. The summary lists thread titles, prioritizes work +that needs input or failed, and opens the highest-priority thread when tapped. An +individual alert continues to open its own thread. This keeps a burst of agent +updates actionable without requiring one notification per thread. + +Ordinary alerts remain quiet while the mobile app is foregrounded, while ongoing +activity continues to update. Watching the thread on another device does not silence +the phone. The mobile app does not have to keep a live environment connection for +push delivery because the notification path uses T3 Connect. + +Reopening the app can silently reconcile the current aggregate after a cold start or +time away. Replaying the same state does not create another alert or extend a +finished card's deadline, and old completions are not presented as fresh alerts. +After tapping any notification, treat the synchronized thread as current; the +notification is a route and attention signal, not a copy of authoritative thread +state. + +| Situation | Expected behavior | +| --- | --- | +| Direct or Tailscale connection only | Interactive control works while reachable; background push does not. | +| Android 7+ with Google Play services | Ordinary notifications are supported. | +| Android 16+ | The system may promote ongoing activity to a Live Update, depending on settings and device support. | +| iOS with Live Activity Updates | Current agent state can appear as a Live Activity. | +| Android app force-stopped in system Settings | Push delivery pauses until the app is opened again. | + +OS notification permissions and Android channels remain under system Settings. +Battery-saving modes can delay cleanup, especially on Android 7. See the pinned +[mobile-notifications guide](https://github.com/pingdotgg/t3code/blob/859304b7808ab9a4be87b1bddcd07c6485bf9c4f/docs/user/mobile-notifications.md). + +## A sensible mobile routine + +Use notifications as an invitation to inspect, not as proof that the result is +correct. Open the target thread, read the last agent message and tool state, inspect +the relevant diff or file, then answer the request or send one bounded follow-up. +For a visual issue, attach a photo or video and include a reproducible condition. +When the host is offline, fix host reachability first; changing screens or +reinstalling the app cannot start a powered-off environment. diff --git a/src/content/book/150-provider-instances-routing.mdx b/src/content/book/150-provider-instances-routing.mdx index e4d3303..b520e34 100644 --- a/src/content/book/150-provider-instances-routing.mdx +++ b/src/content/book/150-provider-instances-routing.mdx @@ -1,10 +1,10 @@ --- slug: provider-instances-routing -order: 150 -number: "15" +order: 300 +number: "30" kind: chapter -part: Part IV · Five harnesses, one product model -partOrder: 4 +part: Part V · Six providers, one product model +partOrder: 5 title: Drivers, instances, registries, and multi-instance routing shortTitle: Provider instances and routing summary: T3 Code separates an open driver kind from configured instance identity, discovery snapshots, durable thread bindings, native sessions, and continuation compatibility so several accounts of one harness can coexist. @@ -18,7 +18,7 @@ objectives: keywords: [ProviderDriver, ProviderInstance, registry, multi-instance, routing, settings, secrets, resume, recovery] sourceAreas: [packages/contracts/src/providerInstance.ts, packages/contracts/src/settings.ts, apps/server/src/provider/ProviderDriver.ts, apps/server/src/provider/Layers/ProviderInstanceRegistryHydration.ts, apps/server/src/provider/Layers/ProviderInstanceRegistryLive.ts, apps/server/src/provider/Layers/ProviderAdapterRegistry.ts, apps/server/src/provider/Layers/ProviderRegistry.ts, apps/server/src/provider/Layers/ProviderService.ts, apps/server/src/provider/Layers/ProviderSessionDirectory.ts] visuals: [provider identity ladder, settings reconciliation timeline, routing and recovery decision tree, interactive fleet router] -updatedAt: 2026-08-24 +updatedAt: "2026-09-11" --- import Callout from "../../components/Callout.astro"; @@ -65,8 +65,8 @@ adapter state.
-`ProviderDriverKind` is intentionally not a closed five-literal union. The shipped -build registers Codex, Claude, Cursor, Grok, and OpenCode, but settings and persisted +`ProviderDriverKind` is intentionally not a closed six-literal union. The shipped +build registers Codex, Claude, Cursor, Grok, Antigravity, and OpenCode, but settings and persisted state can outlive a build, move between forks, or refer to a driver that is absent after rollback. Schema decoding accepts any valid slug; the runtime makes absence visible. @@ -127,7 +127,7 @@ Enabled state has its own precedence. Explicit `false` in the instance envelope decoded driver config always disables. Otherwise the envelope value wins, then the driver config value, then the default is enabled. -
+
H[explicit map + missing legacy defaults] H --> Q{unchanged live identity?} @@ -238,7 +238,7 @@ For an operation such as send, interrupt, approval, structured input, or rollbac Starting a new session also stops stale live sessions for the same T3 thread on other current instances. -
+
B{persisted binding?} B -->|no| X[validation failure] diff --git a/src/content/book/16-devices.mdx b/src/content/book/16-devices.mdx new file mode 100644 index 0000000..a29e0c1 --- /dev/null +++ b/src/content/book/16-devices.mdx @@ -0,0 +1,140 @@ +--- +slug: devices +order: 110 +number: "11" +kind: chapter +part: "Part I · Product guide" +partOrder: 1 +title: "Device lab: simulators, emulators, and agent-driven testing" +shortTitle: Device lab +summary: "Watch and control iOS Simulators and Android Emulators beside a thread, configure test conditions, and let agents use the same devices for verification." +status: source-checked +gates: [sources, links, interaction, editorial] +objectives: + - Prepare a local or SSH device host and open multiple simulator sessions. + - Use the Tools drawer to test appearance, accessibility, permissions, location, and network conditions. + - Control agent access and diagnose remote streaming or development-server reachability. +keywords: [devices, iOS Simulator, Android Emulator, device hub, agent-device, accessibility, SSH device host] +sourceAreas: [docs/user/devices.md, apps/server/src/device, apps/web/src/components/device] +visuals: [device ownership topology, verification matrix] +updatedAt: "2026-09-11" +--- + +import Callout from "../../components/Callout.astro"; +import Mermaid from "../../components/Mermaid.astro"; + +The Device panel places a live iOS Simulator or Android Emulator beside a project +thread. You and the agent can observe the same device, but through different +controls: you interact with the video surface and Tools drawer; an authorized agent +uses `device_*` tools through the managed `agent-device` command line. + +|touch, type, toolbar| P[Device panel] + A[Coding agent] -->|device tools when enabled| H[Device hub] + P -->|stream and control| H + H --> D[iOS Simulator or Android Emulator] + D -->|runs on| M[Environment machine or SSH device host] + D -->|app traffic| S[Metro or development server] + R[Remote client] -->|HTTPS or localhost preferred| P`} /> + +## Prepare the host + +Open the right panel in a project thread and choose **Device**. First use walks +through starting the device hub, checking platform support, and deciding whether +agents may control devices. Merely opening the panel downloads or starts nothing. + +iOS requires macOS and Xcode. Android requires SDK Platform-Tools, Android Emulator, +the latest Command-line Tools, and a virtual device created in Android Studio's +Device Manager. T3 detects standard SDK paths; set `ANDROID_HOME` for a custom +location. After installing a missing dependency, restart the environment server and +refresh the device list. + +The simulator belongs to the environment host, not necessarily the computer showing +the client. This makes a powerful setup possible: T3 Code can run on one server and +use a Mac or Android workstation added as an SSH device host. + +## Open and operate devices + +Choose a running device, or select **Start** beside a stopped one. Every device gets +its own right-panel tab. Use **+ → Device** for another, and rename a tab from its +context menu. Only the visible tab streams video; hidden tabs and their devices keep +running. + +Click or drag to touch, type while the screen is focused, and use the toolbar for +Android Home, Back, and Recents, iOS rotation, and power. Closing a tab stops +watching but leaves the simulator running; use power when you mean to stop it. +Closed tabs remain closed after reload and can be reopened from **+ → Device**. + +Turning off **Settings → Integrations → Devices** stops T3 helper processes. It does +not power off simulators or emulators. + +## Turn the Tools drawer into a test matrix + +Open **Tools** for the active device. The drawer reads values back after each change, +so a displayed state is the device's reported state rather than an optimistic toggle. +It shows only controls the selected platform and device support. A missing iOS-only +or Android-only control is therefore a capability boundary, not a separate setting +you need to enable. + +| Test dimension | Useful check | +| --- | --- | +| Light/dark mode and text size | Contrast, truncation, dynamic type, and responsive layout. | +| Accessibility | Element frames and platform accessibility settings expose missing labels or poor hit targets. | +| Location | Verify empty, nearby, distant, or permission-denied location behavior. | +| App permissions | Exercise first-run, granted, denied, and revoked states. | +| iOS-specific tools | Liquid Glass, color filters, VoiceOver, and a test push notification. | +| Android-specific tools | Orientation and network toggling for rotation and offline recovery. | + +Change one variable at a time, reproduce the behavior, and tell the agent the exact +state. A useful acceptance pass covers the default state, one adverse state, and a +recovery back to normal. You and the agent are changing the same simulator: a theme, +permission, location, orientation, or network change is immediately part of the +other participant's test conditions. State the condition in the thread before +asking the agent to verify it. + +## Give agents access deliberately + +Enable **Agent device access** in **Settings → Integrations → Devices** to install +and expose the device CLI to newly started agent sessions. Restart an existing agent +session after enabling it so the session receives the needed environment. The first +iOS tap may build a small test runner and take a few minutes once per server. + +When an agent opens a device, its panel opens for web and desktop clients on that +thread; mobile shows device activity in the timeline. Disable agent access to hide +device tools from agents started afterward. Your own panel remains available. + + + Opening the Device panel does not authorize the coding agent. Enable agent access + only when automated taps, screenshots, or inspection are useful, and disable it + when the task should stay away from simulators. + + +## Remote streams and SSH device hosts + +Device streams travel through the environment server and work over LAN, Tailscale, +and T3 Connect. Live video requires HTTPS or localhost. Over plain remote HTTP, iOS +falls back to slower still images and Android cannot display video. + +To add another machine, choose an environment under **Settings → Integrations → +Devices → Device hosts**, then enter an SSH alias or `user@host`, optional identity +file, and port. The environment server resolves the SSH config and keys; password +prompts are unsupported. **Test connection** checks SSH, Node, npm, and platform +tools without installing. First listing installs pinned device tools. Node 22+ and +npm must work in a non-interactive SSH shell. + +Removing a host closes its sessions and stops reachable T3 helpers, but simulators +keep running. Connections can recover after interruption. + +T3 handles discovery, streaming, and control. You still own app builds, +installation, and access to Metro or another development server. A simulator on a +different machine cannot reach the environment's `localhost`; bind the dev server +to a reachable address or establish the required forwarding. The authoritative +task guide is the [Devices documentation](https://github.com/pingdotgg/t3code/blob/859304b7808ab9a4be87b1bddcd07c6485bf9c4f/docs/user/devices.md). + +## A repeatable verification session + +Ask the agent to boot the target device and install or open the app. Watch its +reproduction, then test the same path yourself. Switch appearance, text size, and a +relevant permission or network condition. Have the agent capture evidence and run +the smallest automated check. Finish by restoring the device state, inspecting the +code diff, and writing the verified device/OS conditions into the thread or PR. diff --git a/src/content/book/160-codex-app-server-json-rpc.mdx b/src/content/book/160-codex-app-server-json-rpc.mdx index 8d00e36..18fc8d2 100644 --- a/src/content/book/160-codex-app-server-json-rpc.mdx +++ b/src/content/book/160-codex-app-server-json-rpc.mdx @@ -1,10 +1,10 @@ --- slug: codex-app-server-json-rpc -order: 160 -number: "16" +order: 310 +number: "31" kind: chapter -part: Part IV · Five harnesses, one product model -partOrder: 4 +part: Part V · Six providers, one product model +partOrder: 5 title: Codex through app-server JSON-RPC shortTitle: Codex app-server summary: Codex runs as a local app-server child speaking typed JSON-RPC over stdio; T3 translates its volatile native requests and notifications into canonical runtime events without making provider delivery durable. @@ -14,7 +14,7 @@ objectives: [Trace the Codex process and JSON-RPC lifecycle, Distinguish native keywords: [Codex, app-server, JSON-RPC, stdio, resume, approvals, plans, token usage] sourceAreas: [packages/effect-codex-app-server/src, apps/server/src/provider/Layers/CodexAdapter.ts] visuals: [Codex JSON-RPC normalization flow, native-to-canonical boundary ledger] -updatedAt: 2026-08-24 +updatedAt: "2026-09-11" --- import Callout from "../../components/Callout.astro"; @@ -40,7 +40,7 @@ incoming server requests to handlers, and fails pending work when the stream end Spawn, process-exit, protocol, transport, and native request failures stay typed until the adapter maps them into T3’s shared provider-error taxonomy. -
+
R[provider command reactor] R --> A[Codex adapter] diff --git a/src/content/book/17-personalize-usage-updates.mdx b/src/content/book/17-personalize-usage-updates.mdx new file mode 100644 index 0000000..257f518 --- /dev/null +++ b/src/content/book/17-personalize-usage-updates.mdx @@ -0,0 +1,187 @@ +--- +slug: personalize-usage-updates +order: 120 +number: "12" +kind: chapter +part: "Part I · Product guide" +partOrder: 1 +title: "Personalize, measure usage, update, and protect privacy" +shortTitle: Personalize and operate +summary: "Tune appearance, motion, and keybindings; understand usage estimates and pooled limits; update clients and servers safely; and control product telemetry." +status: source-checked +gates: [sources, links, interaction, editorial] +objectives: + - Personalize each client without confusing device-local and environment-shared settings. + - Read estimated costs and provider limits accurately across environments. + - Update a client/server pair with minimal disruption and make an informed telemetry choice. +keywords: [appearance, themes, motion, keybindings, usage, limits, updates, telemetry, privacy] +sourceAreas: [docs/user/appearance.md, docs/user/keybindings.md, docs/user/keyboard-focus.md, docs/user/usage.md, docs/user/updating.md, docs/user/telemetry.md] +visuals: [settings ownership map, update decision path] +updatedAt: "2026-09-11" +--- + +import Callout from "../../components/Callout.astro"; +import Mermaid from "../../components/Mermaid.astro"; + +T3 Code spans clients and environments, so settings do not all live in one place. +Appearance is usually device-local, keybindings belong to an environment, model +price overrides are shared by clients of their target environments, and updates may +need action on a different machine from the one in your hands. + + D[Device or browser] + U --> E[Environment server] + U --> O[Operating system] + D --> A[Theme, mobile typography, panel motion] + E --> K[Keybindings and project scripts] + E --> P[Price overrides and telemetry setting] + O --> R[Reduced motion, notification permissions, app permissions] + C[Connected client version] --> V[Server version compatibility] + V --> E`} /> + +## Make the interface comfortable for long sessions + +Open **Settings → Appearance** to choose a theme and system, light, or dark mode. +You can select separate theme variants for light and dark. Preferences are stored +per device or browser. Mobile has its own theme plus text, code, and terminal +preferences; Android 12+ can use wallpaper-driven **Material You**, and Material You +Layout changes spacing and shape separately from color. + +Panels open immediately by default. Raise **Panel animations** from 0 up to 400 ms +to add motion. The OS reduced-motion setting overrides animation, and switching +threads restores that thread's panel state without replaying transitions. + +Web and desktop can create a palette or import T3 Code and VS Code themes. Export +JSON to share it. An environment can publish themes and set a default with `t3 theme +set `; connected clients apply that command once and can choose another theme +later. `t3 theme clear` removes the default without changing current clients. Keep a +published theme filename stable, write updates atomically, and avoid reserved IDs. +Environment-published themes come only from the server serving the web app or the +desktop app's main local environment; app.t3.codes and additional connections do +not supply them. **Duplicate** makes an editable independent copy, and a custom +theme with the same ID wins. If a selected published theme disappears, the client +returns to the standard theme. +See the [appearance guide](https://github.com/pingdotgg/t3code/blob/859304b7808ab9a4be87b1bddcd07c6485bf9c4f/docs/user/appearance.md). + +## Build a keyboard layer around your habits + +Use **Settings → Keybindings** on web and desktop to inspect command IDs, defaults, +and conflicts. The environment stores rules in `~/.t3/userdata/keybindings.json`: + +```json +[ + { "key": "mod+g", "command": "terminal.toggle" }, + { "key": "mod+shift+g", "command": "terminal.new", "when": "terminalFocus" } +] +``` + +`mod` means Command on macOS and Control elsewhere. Conditions include +`terminalFocus`, `terminalOpen`, `previewFocus`, `previewOpen`, and +`modelPickerOpen`, with `!`, `&&`, `||`, and parentheses. The **last matching rule +wins**, so put a specific rule after a general one. Preserve `!terminalFocus` on +global bindings that should not steal shell input. Project scripts use +`script.{id}.run`. + +Assign `thread.stop` yourself if you want a dedicated interrupt key. In desktop, +`mod+w` closes the focused terminal or right-panel tab before the window; in a web +browser it closes the browser tab, so choose another binding. The palette retains +focus while open and returns it to the composer when closed. Full syntax and quit +behavior are in the [keybinding guide](https://github.com/pingdotgg/t3code/blob/859304b7808ab9a4be87b1bddcd07c6485bf9c4f/docs/user/keybindings.md). + +T3 Code creates the keybinding file with defaults and adds later defaults without +overwriting your custom commands. Invalid rules are ignored; an unreadable JSON +document falls back to defaults. `chat.new` can ask for a project, while +`chat.newLocal` uses the current one. While the palette or model picker is open, +number shortcuts select its entries, and model shortcuts also work in Settings. A +terminal becoming ready does not steal focus from a composer you returned to. + +Desktop quit uses **Hold** by default: hold `Cmd/Ctrl+Q` for 1.2 seconds or press it +twice within 500 ms. If keyboard repeat is disabled, use two presses. Settings can +change this to one **Direct** press or **Double press** only; the app menu quits +immediately. + +## Read Usage as evidence, not an invoice + +**Usage** combines available Codex, Claude Code, and Grok Build session history +across selected environments. It reports tokens, cache savings, model breakdowns, +and estimated API-equivalent cost. The estimate is not a subscription bill, and +totals can omit history the server does not have. Filter environments to locate a +machine or refresh when recent sessions or model prices are missing. Grok turns +without a saved completed-turn record are absent from totals. + +From the environment dropdown, **Model prices** lets you override exact model IDs +and USD rates per million tokens. Optional cache rates fall back to input price; `0` +means free. Target one or several environments. Each destination reports success; +offline destinations are not queued after the dialog closes, so reconnect and use +**Retry failed saves**. In a mixed multi-environment table, untouched cells keep +each environment's existing rate. + +**Usage → Limits** pools provider-reported subscription accounts, deduplicates the +same account across environments and hubs, and shows windows, resets, and reset +credits where available. Account columns stay aligned across windows; a gap means +that account does not report the window, and a hatched segment shows what the next +reset restores. Codex accounts with banked reset credits expose **Use reset** from +their account details. API +key and proxied accounts may expose no subscription limit. `/usage-limits` opens the +current cached snapshot above the composer; it does not run the agent or refresh. +For pooled CLIProxyAPI accounts, add a hub under **Settings → Providers → Usage +providers**. No plugin is required. This connection reports usage but does not +route provider requests. + +## Update the machine named in the notice + +Client and server can be on different versions and machines. When a notice appears, +identify the environment named by it and finish active agents and terminal commands: +server updates restart the connection. Projects, threads, settings, and files remain. + + H[Identify named host] + H --> W{How does its server run?} + W -->|managed service| S[Use Update server or matching service update] + W -->|desktop hosted| D[Update desktop app on host] + W -->|foreground CLI| C[Copy exact-version command and preserve serve options] + S --> R[Restart and reconnect] + D --> R + C --> R + R -->|failure| X[Retry once, check correct host, launch exact version] + R -->|success| V[Verify thread and terminal state]`} /> + +Enable **Settings → General → Continue threads after restarts** when you want +supported active threads to resume after an update, crash, or reboot. Apply it to +all connected environments after older servers update. It does not enable startup, +terminals can still be interrupted, and sessions without provider resume state need +a new message. + +Keep the client open while a remote update installs and reconnects. **Update +server** can update a supported background service; for a desktop-hosted server it +also closes and relaunches the desktop app on that host. A failed service update can +roll back to its previous version. If recovery still fails, retry once, confirm you +updated the named host, then relaunch a command-line server at the exact client +version. Older servers may need one local update before remote update, rollback, or +shared continuation settings are available. + +For a service, use `npx t3@ service update`; `@latest` helps only +when the client is on latest. For a foreground host, relaunch the copied exact +version and preserve `serve`, `--host`, or `--tailscale-serve`. Mobile store releases +install normally; background app updates preserve drafts and queued messages and +apply when you leave, or ask after a long foreground session. Follow the pinned +[updating guide](https://github.com/pingdotgg/t3code/blob/859304b7808ab9a4be87b1bddcd07c6485bf9c4f/docs/user/updating.md). + +## Privacy and product telemetry + +The server sends PostHog product events tied to a hashed account or installation ID. +Events can include provider, model, reasoning effort, permission mode, result, +duration, and main-agent token totals. They exclude prompts, responses, file +contents, authentication tokens, conversation IDs, raw provider events, and child +agent output; child token usage is excluded from totals. + +Disable collection by setting `T3CODE_TELEMETRY_ENABLED=false` in the **server's +environment before startup**. Restart the server so the value takes effect. This +stops product events from being recorded or sent. See the +[product-usage-data guide](https://github.com/pingdotgg/t3code/blob/859304b7808ab9a4be87b1bddcd07c6485bf9c4f/docs/user/telemetry.md). + + + When a preference appears not to stick, first ask whether it belongs to this + device, the selected environment, or the operating system. That single check + resolves most multi-environment settings confusion. + diff --git a/src/content/book/170-claude-agent-sdk.mdx b/src/content/book/170-claude-agent-sdk.mdx index 65ac233..2c84278 100644 --- a/src/content/book/170-claude-agent-sdk.mdx +++ b/src/content/book/170-claude-agent-sdk.mdx @@ -1,10 +1,10 @@ --- slug: claude-agent-sdk -order: 170 -number: "17" +order: 320 +number: "32" kind: chapter -part: Part IV · Five harnesses, one product model -partOrder: 4 +part: Part V · Six providers, one product model +partOrder: 5 title: Claude through the Agent SDK shortTitle: Claude Agent SDK summary: Claude uses an SDK query stream rather than app-server RPC; T3 holds live query context and deferred interactions while normalizing assistant, tool, task, plan, usage, and result observations into one product contract. @@ -14,7 +14,7 @@ objectives: [Follow Claude query configuration resume steering interruption and keywords: [Claude, Agent SDK, query stream, permissions, skills, commands, TodoWrite, tasks, resume, usage] sourceAreas: [apps/server/src/provider/Layers/ClaudeAdapter.ts, apps/server/src/provider/Drivers/ClaudeDriver.ts, apps/server/src/provider/Drivers/ClaudeSkills.ts] visuals: [Claude SDK message normalization, skill precedence, query lifecycle] -updatedAt: 2026-08-24 +updatedAt: "2026-09-11" --- import Callout from "../../components/Callout.astro"; @@ -49,7 +49,7 @@ accepting legacy `sessionId`), optional `resumeSessionAt`, and a turn count, the supplies those hints to a fresh query. T3 durable history remains an independent orchestration record. -
+
Q[Claude SDK query] Q -->|assistant · tool · TodoWrite · result| A[Claude adapter] @@ -116,11 +116,11 @@ every installed skill was discoverable. Claude result and selected message usage can update a per-thread token/context snapshot, including input/cache/output/reasoning/tool/duration fields where present. -This supports the live context meter. Chapter 20’s Usage page independently scans +This supports the live context meter. Chapter 35’s Usage page independently scans provider-owned transcripts, de-duplicates them, and prices where possible; it does not use this runtime event as its source of truth. -At the pinned revision only Codex and Claude emit this live telemetry. A generic +Only Codex and Claude emit this live telemetry. A generic runtime schema can represent future data, but that is not evidence Cursor, Grok, or OpenCode emit it now. diff --git a/src/content/book/18-recipes-troubleshooting.mdx b/src/content/book/18-recipes-troubleshooting.mdx new file mode 100644 index 0000000..6b6be46 --- /dev/null +++ b/src/content/book/18-recipes-troubleshooting.mdx @@ -0,0 +1,143 @@ +--- +slug: recipes-troubleshooting +order: 130 +number: "13" +kind: chapter +part: "Part I · Product guide" +partOrder: 1 +title: "Complete recipes and troubleshooting" +shortTitle: Recipes and troubleshooting +summary: "Combine projects, agents, workbench tools, source control, remote access, mobile, and devices into repeatable workflows, then diagnose failures from the owning layer." +status: source-checked +gates: [sources, links, interaction, editorial] +objectives: + - Run end-to-end feature, review, remote, and mobile verification workflows. + - Diagnose failures by separating client, connection, environment, provider, workspace, and host-service state. + - Preserve evidence and recover without duplicating work or losing useful changes. +keywords: [recipes, troubleshooting, workflow, verification, recovery, remote, source control] +sourceAreas: [docs/user, apps/web/src, apps/mobile/src, apps/server/src] +visuals: [layered troubleshooting flow, recipe checklists] +updatedAt: "2026-09-11" +--- + +import Callout from "../../components/Callout.astro"; +import Mermaid from "../../components/Mermaid.astro"; + +The most reliable way to use T3 Code is to keep intent, execution, evidence, and +review in the same thread. When something breaks, identify which owner failed +before changing settings: the viewing client, its connection, the environment +server, the provider, the workspace, or an external service. + + C{Client responsive?} + C -->|no| C1[Restart or update the client; preserve drafts] + C -->|yes| N{Environment connected?} + N -->|no| N1[Check host power, reachability, pairing, clock, and service status] + N -->|yes| S{Thread and snapshots synchronized?} + S -->|no| S1[Reconnect once; select correct environment and project] + S -->|yes| P{Provider can start or resume?} + P -->|no| P1[Check provider auth, model, permissions, and host logs] + P -->|yes| W{Workspace or test failed?} + W -->|yes| W1[Inspect checkpoint diff, terminal output, and device or preview] + W -->|no| E[Check external host API, Git credentials, browser profile, or device dependency] + W1 --> R[Send one bounded correction and verify] + E --> R`} /> + +## Recipe 1: take a feature from request to reviewed pull request + +1. Open or add the project on the environment that has the required tools and + credentials. Create a fresh thread and choose the provider, model, and permission + mode appropriate to the task. +2. Describe the problem, constraints, and acceptance checks. Attach the smallest + relevant file, image, or SnapShot. +3. Let the agent inspect and implement. Answer permission or input requests after + reading their exact scope. +4. Inspect per-turn checkpoints and the branch diff. Use Files for exact source, + Terminal for focused checks, Preview for browser behavior, or Device for mobile + behavior. +5. If the direction is wrong, preserve unrelated work and revert the appropriate + checkpoint. Send a concrete correction. +6. When behavior and checks pass, create a focused commit. Review the generated + message, push, and create a PR only when you intend to publish. +7. Link the PR to the thread, follow checks and review comments, and return to the + same thread for revisions. Merge or enable auto-merge after the final diff and + scope are correct. + +## Recipe 2: fix a visual bug with browser or SnapShot evidence + +Start the app in one terminal and keep a second terminal for tests. Open its port in +Preview, reproduce at the exact viewport and state, and annotate the affected +region. If the bug is in another desktop app, capture a SnapShot with app text when +useful. Ask for the expected visual behavior and a focused verification. Refresh +after the change, test a nearby breakpoint or theme, inspect the diff, and turn off +or revoke temporary capture permissions when you no longer want them. + +## Recipe 3: supervise a long task from mobile + +On the host, enable T3 Connect and a background service; verify `t3 service status` +before leaving. On mobile, sign in, enable Device Notifications and optional ongoing +activity, then open the environment and thread. When alerted, read the final message +and inspect the relevant file or review diff. Answer approvals carefully. Use voice, +photo, video, or file context for one clear follow-up. If the host goes offline, +restore host power, login/linger state, and service reachability before resending. + +## Recipe 4: verify a mobile UI across adverse states + +Open the Device panel, start the target simulator, and authorize agent device access +only if automation is needed. Establish a baseline, then change one Tools setting at +a time: dark mode, larger text, accessibility frames, denied permission, fake +location, rotation, or offline network. Capture the failing condition, ask the agent +to fix and verify it, and repeat the user path yourself. Restore state afterward and +record device, OS, orientation, and conditions with the result. + +## Recipe 5: work on a remote SSH machine + +Add the SSH environment from desktop and confirm non-interactive Node and provider +commands work. Open the remote project; remember that Files, Terminal, Git, and the +agent now operate on that host. If you need mobile access later, expose the server +through T3 Connect or a routable private path and keep it running with the supported +service. Never diagnose a remote missing binary by installing it on the viewing +laptop. + +## Symptom-to-owner table + +| Symptom | Likely owner | First useful evidence | Recovery | +| --- | --- | --- | --- | +| Environment is offline | Host/service/network | `t3 service status`, its log path, host clock and route | Start/repair service, wake host, fix route or pairing; restart after permanent Connect rejection. | +| T3 Connect login exists but host is absent | Exposure/server lifecycle | `t3 connect status` plus service status | Start `t3 serve` or install/start service; login alone is insufficient. | +| Hosted web cannot connect to LAN URL | TLS/reachability | Endpoint scheme and route from browser | Use HTTPS such as Tailscale Serve/T3 Connect, or a local browser that can open direct HTTP. | +| Provider does not start | Provider on environment | Provider login command, model availability, server log | Authenticate on host, rescan/restart, choose supported model/provider. | +| Push metadata loads but Git push fails | Git remote credentials | `git remote -v` and host-side SSH/HTTPS auth | Configure the remote's credentials; provider API login is separate. | +| Terminal history is missing | Scrollback bounds/client view | Whether output exceeded 5,000 lines or 8 MiB | Rerun focused command or use a saved log; do not rely on scrollback as archive. | +| Browser preview lacks login | Preview profile | Selected profile and import time | Import once with source browser closed or sign in directly; imports do not stay synchronized. | +| SnapShot shortcut does nothing | OS/compositor setup | SnapShots Settings status and desktop session | Check permission, shortcut collision, Wayland helper/extension, and compositor config. | +| Device video is blank remotely | Secure-context/platform path | URL scheme and device platform | Use HTTPS/localhost; plain HTTP gives iOS stills and no Android video. | +| Simulator cannot reach Metro | Network topology | Simulator host and Metro bind address | Bind or forward Metro to an address reachable from the device host. | +| Usage totals seem low | Available history/filter | Selected environments and scan state | Wait for scans, refresh, inspect model pricing; estimates remain incomplete if history is absent. | +| Update notice remains | Wrong host/version | Named environment and client version | Update the server machine with the exact client version and preserve startup options. | + +## Recover without compounding the failure + +Capture the current thread, environment, command, and first meaningful error. Avoid +repeated Send, repeated merge, or repeated update actions while state is unknown. +Inspect the workspace diff before reverting or restarting. A client reconnect does +not necessarily stop the agent; a server restart can interrupt it. After recovery, +wait for synchronized thread state and verify files and Git status before issuing a +new instruction. + + + When an agent, update, or remote action has an uncertain outcome, first re-read the + authoritative thread and workspace state. Repeating the action can create a second + turn, duplicate external work, or hide the original failure. + + +## Collect a useful bug report + +Record the client surface and version, environment/server version, connection mode, +host OS, provider, project state, exact action, and visible error. Include a trace ID +or Cloudflare Ray ID when T3 displays one. Redact pairing URLs, authorization codes, +tokens, prompts, private source, and unrelated terminal output. Prefer a minimal +reproduction and a small screenshot or video over a full session dump. + +The user-facing facts in these recipes are pinned to the reference repository's +[user documentation](https://github.com/pingdotgg/t3code/tree/859304b7808ab9a4be87b1bddcd07c6485bf9c4f/docs/user). diff --git a/src/content/book/180-acp-cursor-grok.mdx b/src/content/book/180-acp-cursor-grok.mdx index d009164..449ffab 100644 --- a/src/content/book/180-acp-cursor-grok.mdx +++ b/src/content/book/180-acp-cursor-grok.mdx @@ -1,10 +1,10 @@ --- slug: acp-cursor-grok -order: 180 -number: "18" +order: 330 +number: "33" kind: chapter -part: Part IV · Five harnesses, one product model -partOrder: 4 +part: Part V · Six providers, one product model +partOrder: 5 title: ACP transport, Cursor, and Grok shortTitle: ACP, Cursor, and Grok summary: A shared JSON-RPC runtime gives Cursor and Grok one transport skeleton, while their startup, mode/model, extension, interruption, and rollback semantics deliberately diverge. @@ -18,7 +18,7 @@ objectives: keywords: [ACP, JSON-RPC, Cursor, Grok, session resume, extension, approval, steering, interrupt, rollback] sourceAreas: [packages/effect-acp/src, apps/server/src/provider/acp, apps/server/src/provider/Layers/CursorAdapter.ts, apps/server/src/provider/Layers/GrokAdapter.ts] visuals: [ACP session startup sequence, semantic fork matrix, steering race explorer] -updatedAt: 2026-08-24 +updatedAt: "2026-09-11" --- import Callout from "../../components/Callout.astro"; @@ -54,7 +54,7 @@ normalized ACP observations. -
+
|stdio JSON-RPC| A[effect-acp client] A --> R[AcpSessionRuntime] @@ -152,7 +152,7 @@ The rollback row is intentionally asymmetric. Cursor’s implementation removes from its local `ctx.turns` array; source inspection does not find a Cursor ACP native revert call in that method. That changes the adapter’s read snapshot, not necessarily the provider’s remote history. Grok refuses the operation explicitly. Orchestration -revert still has its own durable events and projection cleanup (Chapter 13); neither +revert still has its own durable events and projection cleanup (Chapter 28); neither adapter row should be read as a transaction across that durable model and native provider state. diff --git a/src/content/book/185-antigravity-provider.mdx b/src/content/book/185-antigravity-provider.mdx new file mode 100644 index 0000000..1f93f31 --- /dev/null +++ b/src/content/book/185-antigravity-provider.mdx @@ -0,0 +1,119 @@ +--- +slug: antigravity-provider +order: 335 +number: "33A" +kind: chapter +part: Part V · Six providers, one product model +partOrder: 5 +title: Antigravity through ACP, managed auth, and account catalogs +shortTitle: Antigravity provider +summary: How T3 Code turns Antigravity's managed runtime, Google authentication, ACP extensions, attachments, skills, and subagent batches into the shared product model. +status: source-checked +gates: [sources, links, interaction, editorial] +objectives: + - Separate Antigravity installation, authentication, ACP transport, and session state. + - Trace an account sign-in and a turn through their distinct boundaries. + - Identify capability differences that the shared UI must preserve. +keywords: [Antigravity, ACP, Google auth, Gemini, provider adapter, attachments, subagents] +sourceAreas: [apps/server/src/provider/Layers/AntigravityAdapter.ts, apps/server/src/provider/acp, docs/user/providers-antigravity.md] +visuals: [Antigravity control and runtime lanes, capability table] +updatedAt: "2026-09-11" +--- + +import Callout from "../../components/Callout.astro"; +import Mermaid from "../../components/Mermaid.astro"; + +Antigravity is the sixth built-in provider. Calling it “another ACP adapter” misses +most of the design. ACP carries session traffic, but installation, Google account +authentication, callback recovery, model discovery, usage limits, attachments, +skills, and subagent presentation each cross a separate boundary. + +## Four boundaries, one provider card + + I[Managed runtime\ninstallation and version] + UI --> AU[Auth controller\nGoogle · ADC · API key] + AU --> CB[Browser callback\nor pasted code] + AU --> AC[Saved account access] + AC --> CAT[Account model catalog\nand usage limits] + UI --> T[Start thread] + T --> AD[AntigravityAdapter] + AD --> ACP[ACP session transport] + ACP --> AG[Antigravity runtime] + AG --> EV[Native events] + EV --> AD + AD --> OR[Canonical T3 events] + OR --> CL[Web · desktop · mobile]`} /> + +The driver constructs the configured instance and its presentation. The provider +snapshot reports installation, account status, models, commands, skills, and limits. +The adapter owns live sessions and translates native ACP events. Orchestration owns +the durable T3 thread. A healthy provider snapshot does not prove a particular +thread’s native session is still alive. + +## Authentication is not environment pairing + +Environment pairing answers “may this client control the T3 server?” Antigravity +authentication answers “which Google or API-backed account may this provider use?” +The web or desktop client asks the server’s provider-auth service to begin sign-in. +Google normally returns to a loopback callback. When the browser is on another +device, the user can paste the final callback URL or code so the environment that +owns the provider finishes the exchange. + +The supported sign-in shapes are Google account, Application Default Credentials, +Gemini API key, and Vertex AI configuration. Their credentials and available models +are different, so the provider snapshot must come from the configured environment +and account rather than a static global list. + +## Models and capabilities stay account-shaped + +Antigravity uses the account catalog and does not support T3 Code custom models. A +resumed thread keeps its selected model; if the account later loses access, the user +must choose an available model before continuing. Planning uses Antigravity’s native +`/plan` command rather than T3’s separate Plan mode. + +| Capability | Antigravity behavior | Product consequence | +| --- | --- | --- | +| Permissions | Native ACP permission requests are normalized | The UI presents shared choices without claiming identical provider policy. | +| Attachments | Images, PDFs, text, and supported audio with provider limits | T3 validates and translates before session input. | +| Skills | User skills are discovered from the Gemini home | Availability belongs to the selected provider instance. | +| Subagents | Child activity arrives in batches | The client shows the batch; it cannot open or steer every child independently. | +| Rewind | Provider conversation rewind is unavailable | A filesystem checkpoint must not pretend it also rolled back native context. | +| Models | Account-discovered catalog | Refresh after account or entitlement changes. | + + + A shared composer does not imply a least-common-denominator runtime. T3 Code + normalizes what can be represented honestly and keeps unsupported operations + unavailable. Antigravity’s missing rewind and custom-model paths are visible + capability decisions, not errors to hide behind another provider’s behavior. + + +## Session and event path + +Starting a thread resolves the exact provider instance, model, working directory, +mode, attachments, and runtime instructions. The adapter starts or resumes its ACP +session and consumes native updates. Text, tool calls, permissions, questions, +plans, usage, errors, and batched child activity become canonical provider events. +Those observations re-enter orchestration, update projections, and stream to every +client that follows the thread. + +Attachment preparation enforces Antigravity’s own sizes and supported formats. The +current user contract documents 1 MiB per text file, 10 MiB per image, 20 MiB per +audio clip, and 50 MiB total per message. Workspace visibility and provider input +support are separate checks: a file visible in T3 Code is not automatically valid +provider input. + +## Recovery model + +Google sign-in is persisted across server restarts. Provider status can be refreshed +from Settings or mobile thread settings to reload access, models, skills, and limits. +A live thread still depends on ACP continuation data and the exact configured +instance. Losing a native session, removing the instance, changing account access, +and reverting Git state are four different events and require different recovery. + +### Current source trail + +- [Antigravity adapter](https://github.com/pingdotgg/t3code/blob/859304b7808ab9a4be87b1bddcd07c6485bf9c4f/apps/server/src/provider/Layers/AntigravityAdapter.ts) +- [ACP support](https://github.com/pingdotgg/t3code/blob/859304b7808ab9a4be87b1bddcd07c6485bf9c4f/apps/server/src/provider/acp/AntigravityAcpSupport.ts) +- [Authentication support](https://github.com/pingdotgg/t3code/blob/859304b7808ab9a4be87b1bddcd07c6485bf9c4f/apps/server/src/provider/antigravityAuthSupport.ts) +- [Product guide](https://github.com/pingdotgg/t3code/blob/859304b7808ab9a4be87b1bddcd07c6485bf9c4f/docs/user/providers-antigravity.md) diff --git a/src/content/book/190-opencode-normalization.mdx b/src/content/book/190-opencode-normalization.mdx index 3b881db..3dd2926 100644 --- a/src/content/book/190-opencode-normalization.mdx +++ b/src/content/book/190-opencode-normalization.mdx @@ -1,11 +1,11 @@ --- slug: opencode-normalization -order: 190 -number: "19" +order: 340 +number: "34" kind: chapter -part: Part IV · Five harnesses, one product model -partOrder: 4 -title: OpenCode ownership, recovery, and five-provider normalization +part: Part V · Six providers, one product model +partOrder: 5 +title: OpenCode ownership, recovery, and six-provider normalization shortTitle: OpenCode and normalization summary: OpenCode is an HTTP SDK integration with scoped local-server ownership or external-server attachment; its session adoption and cwd-fork policy show why a common product model must preserve provider-specific recovery semantics. status: source-checked @@ -14,11 +14,11 @@ objectives: - Trace OpenCode’s local/external server ownership and session-scoped event pump. - Follow resume adoption, not-found handling, cwd equivalence, and history-preserving fork. - Separate native OpenCode methods from canonical runtime events and durable orchestration state. - - Read the five-provider matrix as an evidence ledger, not a feature-negotiation protocol. + - Read the six-provider matrix as an evidence ledger, not a feature-negotiation protocol. keywords: [OpenCode, SDK, external server, session adoption, fork, cwd, permissions, promptAsync, rollback, normalization, tasks, skills, usage] sourceAreas: [apps/server/src/provider/Layers, apps/server/src/provider/opencodeRuntime.ts, apps/server/src/provider/Services/ProviderAdapter.ts, apps/server/src/usage] visuals: [OpenCode session ownership lifecycle, resume decision tree, provider normalization matrix] -updatedAt: 2026-08-24 +updatedAt: "2026-09-11" --- import Callout from "../../components/Callout.astro"; @@ -48,7 +48,7 @@ the SDK event subscription, and exit-watch fibers. -
+
|yes| E[external server handle\nnot scope-owned] @@ -146,38 +146,38 @@ durable thread-revert path and any workspace/Git checkpoint restoration. reconciliation boundary rather than claiming a single atomic undo. -## Five providers, one comparison language +## Six providers, one comparison language -The book’s five harnesses are Codex, Claude, Cursor, Grok, and OpenCode. The matrix is +The six providers are Codex, Claude, Cursor, Grok, Antigravity, and OpenCode. The matrix is an evidence ledger with four cell types: **native mapping**, **adapter behavior or emulation**, **explicitly unsupported**, and **not evidenced in the inspected source**. It is not the `ProviderAdapter` SPI and not a runtime capability negotiation table. -The SPI’s declared capability object at this pinned revision contains only -`sessionModelSwitch`; these five adapters report it as in-session. +The SPI’s declared capability object contains only +`sessionModelSwitch`; the adapters report their concrete support through the shared boundary. -
+
- - + + - - - - - - - - - - - + + + + + + + + + + +
Source-grounded normalization categories across five providers
OperationCodexClaudeCursorGrokOpenCode
Source-grounded normalization categories across six providers
OperationCodexClaudeCursorGrokAntigravityOpenCode
transportnative app-server JSON-RPC mappingnative Agent SDK stream mappingnative ACP stdio mappingnative ACP stdio + XAI extension mappingnative SDK/HTTP event mapping
resumeadapter passes a Codex resume cursor to app-server runtimeadapter uses SDK resume metadataadapter passes native session id to ACP load/new flowadapter passes native session id to ACP load/new flowa recognized cursor re-adopts sessionId; confirmed miss starts fresh; absent/malformed/wrong-version cursor means no resume; cwd change forks
mid-turn sendcalls native turn/start; app-server may queue a new native turn id while interrupt still targets the current onequeues into the live SDK loop and reuses the active product turn idreuses active product turn while prompts are in flightreuses active turn with target-aware settlementcalls promptAsync and reuses active product turn
approval/inputnative JSON-RPC requests mapped to canonical request/input eventsSDK-side deferred interactions mapped to canonical request/input eventsACP permission + Cursor question extension mappingACP permission + XAI question extension mappingnative permission/question events mapped; replies call SDK endpoints
plans, tasks, subagentsnative plans plus multi-agent signals become plan and task.* eventsTodoWrite, coordinator, and member observations become plan and task.* eventsCursor plan/todo extensions become plan events; no task.* emission branch foundACP plan observations become plan events; no task.* emission branch foundplan mode selects a native agent and task-like tools become item activity; no plan or task.* emission branch found
commands and skills discoverysnapshot requests native skills and adds a feedback slash commandsnapshot combines initialization commands with discovered filesystem skillssnapshot exposes models/probe state; no skills or slash commands foundsnapshot exposes models/probe state; no skills or slash commands foundprovider inventory exposes skills; no slash commands found
live context telemetryemits canonical token-usage snapshotsemits canonical token-usage snapshots when normalization succeedsno canonical token-usage emission branch foundno canonical token-usage emission branch foundno canonical token-usage emission branch found
historical Usage sourceCodex JSONL session transcripts are scannedClaude JSONL project transcripts are scannednot scannednot scannednot scanned
rollbackThread behaviornative app-server rollback mappingadapter-local turn snapshot truncation plus resume-cursor refreshadapter-only local snapshot truncationexplicitly unsupportednative session.revert mapping
in-session model switchdeclared SPI capability: in-sessiondeclared SPI capability: in-sessiondeclared SPI capability: in-sessiondeclared SPI capability: in-sessiondeclared SPI capability: in-session
failure projectiontyped process/protocol/request failures plus runtime warning/error eventstyped SDK/request failures plus terminal result classificationtyped ACP failures plus provider-specific callback and cancel handlingtyped ACP failures plus late-event suppression around interruptiontyped SDK/HTTP failures; when probing a recognized cursor, only confirmed not-found permits fresh-session replacement
transportnative app-server JSON-RPC mappingnative Agent SDK stream mappingnative ACP stdio mappingnative ACP stdio + XAI extension mappingACP with Antigravity extensions and managed runtimenative SDK/HTTP event mapping
resumeadapter passes a Codex resume cursor to app-server runtimeadapter uses SDK resume metadataadapter passes native session id to ACP load/new flowadapter passes native session id to ACP load/new flowadapter resumes through its ACP session files and continuation identitya recognized cursor re-adopts sessionId; confirmed miss starts fresh; absent/malformed/wrong-version cursor means no resume; cwd change forks
mid-turn sendcalls native turn/start; app-server may queue a new native turn id while interrupt still targets the current onequeues into the live SDK loop and reuses the active product turn idreuses active product turn while prompts are in flightreuses active turn with target-aware settlementuses ACP prompt flow with provider-specific active-session statecalls promptAsync and reuses active product turn
approval/inputnative JSON-RPC requests mapped to canonical request/input eventsSDK-side deferred interactions mapped to canonical request/input eventsACP permission + Cursor question extension mappingACP permission + XAI question extension mappingACP permissions and Antigravity input extensions are normalizednative permission/question events mapped; replies call SDK endpoints
plans, tasks, subagentsnative plans plus multi-agent signals become plan and task.* eventsTodoWrite, coordinator, and member observations become plan and task.* eventsCursor plan/todo extensions become plan events; no task.* emission branch foundACP plan observations become plan events; no task.* emission branch foundnative /plan; subagent activity is grouped into batchesplan mode selects a native agent and task-like tools become item activity; no plan or task.* emission branch found
commands and skills discoverysnapshot requests native skills and adds a feedback slash commandsnapshot combines initialization commands with discovered filesystem skillssnapshot exposes models/probe statesnapshot includes provider skills and commands where discovereddiscovers user skills from the Gemini homeprovider inventory exposes skills; no slash commands found
live context telemetryemits canonical token-usage snapshotsemits canonical token-usage snapshots when normalization succeedsprovider-specific supportprovider-specific supportreports account limit windows through provider statusprovider-specific support
historical Usage sourceCodex JSONL session transcripts are scannedClaude JSONL project transcripts are scannednot scannednot scannedsubscription limits are account-reported rather than transcript-priced historynot scanned
rollbackThread behaviornative app-server rollback mappingadapter-local turn snapshot truncation plus resume-cursor refreshadapter-only local snapshot truncationexplicitly unsupportedexplicitly unavailablenative session.revert mapping
in-session model switchdeclared SPI capabilitydeclared SPI capabilitydeclared SPI capabilitydeclared SPI capabilityaccount catalog and ACP capability decidedeclared SPI capability
failure projectiontyped process/protocol/request failures plus runtime warning/error eventstyped SDK/request failures plus terminal result classificationtyped ACP failures plus provider-specific callback and cancel handlingtyped ACP failures plus late-event suppression around interruptionmanaged runtime, authentication, account, and ACP failures stay distincttyped SDK/HTTP failures; when probing a recognized cursor, only confirmed not-found permits fresh-session replacement
-An absence cell means no implementation branch was found at this pinned revision; -it does not claim the upstream product can never expose that feature. Chapters 16, -17, and 20 provide the longer Codex, Claude, and usage trails. The matrix keeps +An absence cell means no implementation branch was found; +it does not claim the upstream product can never expose that feature. Chapters 31, +32, 33A, and 35 provide the longer Codex, Claude, Antigravity, and usage trails. The matrix keeps provider discovery (skills/commands), live adapter normalization (plans/tasks and context telemetry), and the independent transcript scanner in separate rows so one surface cannot masquerade as another. diff --git a/src/content/book/20-domain-vocabulary.mdx b/src/content/book/20-domain-vocabulary.mdx index 2864676..b8be368 100644 --- a/src/content/book/20-domain-vocabulary.mdx +++ b/src/content/book/20-domain-vocabulary.mdx @@ -1,10 +1,10 @@ --- slug: domain-vocabulary -order: 20 -number: "2" +order: 170 +number: "17" kind: chapter -part: Part I · Boundaries and vocabulary -partOrder: 1 +part: Part II · Boundaries and vocabulary +partOrder: 2 title: Environment, project, thread, turn, and session shortTitle: Domain vocabulary summary: The entities share a UI, but they have different identity, cardinality, ownership, and restart behavior. @@ -17,7 +17,7 @@ objectives: keywords: [environment, project, thread, turn, session, entity model] sourceAreas: [packages/contracts/src/environment.ts, packages/contracts/src/orchestration.ts, packages/contracts/src/providerRuntime.ts] visuals: [entity relationship table, lifecycle scenario lab] -updatedAt: 2026-08-24 +updatedAt: "2026-09-11" --- import Callout from "../../components/Callout.astro"; diff --git a/src/content/book/200-usage-accounting.mdx b/src/content/book/200-usage-accounting.mdx index 4ec50bd..59ae440 100644 --- a/src/content/book/200-usage-accounting.mdx +++ b/src/content/book/200-usage-accounting.mdx @@ -1,24 +1,25 @@ --- slug: usage-accounting -order: 200 -number: "20" +order: 350 +number: "35" kind: chapter -part: Part IV · Five harnesses, one product model -partOrder: 4 +part: Part V · Six providers, one product model +partOrder: 5 title: Usage accounting without a false ledger shortTitle: Usage accounting -summary: "The live context meter and historical transcript usage view are deliberately different measurements: one projects the latest usable context snapshot for a thread, while the other scans provider-owned files into a deduplicated, priced historical estimate." +summary: "Live thread context, historical transcript accounting, and provider-reported subscription limits answer different questions and keep separate evidence, merge, pricing, and refresh rules." status: source-checked gates: [sources, interaction] objectives: - Trace the live context-window snapshot from provider adapter to the web composer. - Trace historical Claude and Codex transcript records through parsing, deduplication, bucketing, pricing, and environment merge. + - Explain Grok transcript accounting, custom prices, pooled subscription windows, hubs, and reset credits. - Interpret coverage, source fingerprints, cache state, and cost provenance without claiming a billing settlement. - Keep task and subagent accounting as a distinct later concern. keywords: [usage, context window, transcripts, pricing, deduplication, cache, RPC, provenance, environments] sourceAreas: [packages/contracts/src/providerRuntime.ts, packages/contracts/src/usage.ts, apps/server/src/provider/Layers, apps/server/src/orchestration/Layers, apps/server/src/usage, packages/shared/src/usageMerge.ts, apps/web/src/components/usage, apps/mobile/src/features/usage] visuals: [two-lane usage pipeline, usage accounting lab, source-fingerprint merge] -updatedAt: 2026-08-24 +updatedAt: "2026-09-11" --- import Callout from "../../components/Callout.astro"; @@ -29,19 +30,22 @@ import SourceExcerpt from "../../components/SourceExcerpt.astro"; import SourceList from "../../components/SourceList.astro"; import UsageAccountingLab from "../../components/UsageAccountingLab.astro"; -“Usage” names two different questions in T3 Code. +“Usage” names three different questions in T3 Code. The composer asks: **how full is this thread's context window at the latest valid -provider update?** The Usage screens ask: **what token-shaped records can this -environment read from selected provider transcript directories over a historical -time range?** They share words such as model, tokens, and session, but they do not -share a source of truth, an update cadence, or a settlement guarantee. - - +provider update?** The Usage history asks: **what token-shaped records can this +environment read from provider transcript directories over a historical time +range?** The Limits view asks: **how much provider-reported subscription capacity +remains across the selected accounts and environments?** They share words such as +model, tokens, account, and session, but they do not share a source of truth, an +update cadence, or a settlement guarantee. + + A current context snapshot is not added to the historical Usage total. Conversely, a historical transcript scan cannot certify that the currently displayed context meter is complete, current, or billed. The first is a live operational aid; the - second is an evidence-limited accounting estimate. + second is an evidence-limited accounting estimate. A subscription-limit bar is a + provider-reported rolling window; it is neither of those token totals. ## The live lane projects a current context snapshot @@ -55,7 +59,7 @@ identity. -At this pinned revision, source inspection finds canonical token-usage emission in +Source inspection finds canonical token-usage emission in two adapters:
@@ -88,7 +92,7 @@ files. - + The meter can be absent because no valid activity has reached it, because an @@ -99,16 +103,17 @@ files. metadata—not a cross-thread, cross-day, or cost total. -## The historical lane scans files, not orchestration state +## The historical lane scans three provider homes, not orchestration state -The version-4 usage contract intentionally reads selected CLI homes: Claude JSONL +The version-5 usage contract reads selected CLI homes: Claude JSONL under a resolved Claude home (preferring `.claude/projects`, with a `projects` -fallback) and Codex JSONL under the resolved Codex shared-home `sessions` tree. The +fallback), Codex JSONL under the resolved Codex shared-home `sessions` tree, and +Grok `updates.jsonl` files under its resolved sessions home. The server exposes preaggregated buckets, never raw transcript records. Consequently it can include provider turns made outside T3 Code, but it cannot account for a -provider whose history is not in those two scanned layouts. +provider whose history is not in those three scanned layouts. - + @@ -123,7 +128,9 @@ Parser rules are provider-specific. Claude accepts assistant records with a usag object and deduplicates repeated content-block accounting by message/request key. Codex follows turn-context model state and `last_token_usage` deltas; it subtracts cached tokens from the reported input count and applies a one-second fork-copy -suppression heuristic. Codex records carry no global duplicate key: consecutive +suppression heuristic. Grok reads completed-turn updates, can split model-level +usage, converts provider cost ticks, and uses session, prompt, and model identity +for deduplication when available. Codex records carry no global duplicate key: consecutive delta suppression and fork-copy suppression are parser-local. That one-second threshold is implementation behavior, not a portable provider promise. @@ -172,11 +179,51 @@ one session repeatedly across day/model buckets. passes answered summaries to the shared versioned merge. +## Custom prices are environment settings + +Automatic model pricing is a convenience, not an authority. Web and desktop let a +user set exact per-million input, output, cache-read, and cache-write rates for a +model on one or several environments. Blank cache rates inherit the input rate; +zero means free. Saving is fan-out work: each destination reports success, and a +failed destination can be retried without rewriting the environments that already +accepted the change. Closing the dialog does not create an offline write queue. + +The historical scan applies these overrides before automatic pricing. A client that +selects several environments can therefore see a mixed value when their local price +settings differ. This is an intentional consequence of environment ownership. + +## Subscription limits are provider and account state + +Provider snapshots can carry normalized rolling windows with stable ids, a kind, +label, used percentage, optional reset time, and optional duration. Sparse updates +merge by window id so one rate-limit event does not erase unrelated windows. An +unavailable reason distinguishes an account that cannot report limits from a probe +that failed while a prior good snapshot may still be useful. + +The Limits view pools equivalent accounts across selected environments and optional +CLIProxyAPI hubs. It presents **remaining** capacity, keeps the same account segment +in the same visual column across windows, and explains the next reset. Codex accounts +may also report banked reset credits; redemption pins the request to the displayed +credit id so a retry cannot silently consume a different credit. + +`/usage-limits` reads the same snapshot inside a thread. It does not run an agent or +refresh provider state. API-key accounts and some proxy configurations may never +report subscription windows, which is different from a temporary probe failure. + + L[Latest thread context\noperational meter] + B[Claude · Codex · Grok\ntranscript files] --> H[Historical tokens and\nAPI-equivalent estimate] + C[Provider account probes\nand optional hubs] --> Q[Remaining subscription\nlimit windows] + P[Environment model-price overrides] --> H + R[Sparse rate-limit updates] --> Q + L ~~~ H + H ~~~ Q`} /> + -
+
B{usedTokens > 0?} @@ -185,7 +232,7 @@ one session repeatedly across day/model buckets. D --> E[composer context meter] end subgraph H[Historical transcript accounting · selected files] - F[Claude / Codex JSONL homes] --> G[parse + normalize] + F[Claude / Codex / Grok JSONL homes] --> G[parse + normalize] G --> I[within-file and cross-file dedupe] I --> J[time bounds + day/hour buckets] J --> K[reported cost or rate-table estimate] @@ -196,7 +243,7 @@ one session repeatedly across day/model buckets. A ~~~ F`} />
-There is an important diagnostic limitation at this pinned revision. The contract +There is an important diagnostic limitation. The contract permits `ok`, `missing`, `partial`, and `failed` source states, but the service path observed here emits `missing` when the directory is absent—or when the existence check itself fails—and otherwise reports `ok`. Directory listing errors are swallowed @@ -244,7 +291,7 @@ live `88,000 / 200,000` context snapshot remains deliberately outside that total Runtime task/subagent work has its own lifecycle and optional usage-shaped data. This chapter intentionally does not roll it into thread context telemetry or - provider-transcript accounting. The planned Chapter 25 will define its identity, + provider-transcript accounting. Chapter 40 defines its identity, fan-out, retries, and attribution rules before a task-level total is presented. diff --git a/src/content/book/210-project-discovery.mdx b/src/content/book/210-project-discovery.mdx index ad6073f..f9b7e81 100644 --- a/src/content/book/210-project-discovery.mdx +++ b/src/content/book/210-project-discovery.mdx @@ -1,13 +1,13 @@ --- slug: project-discovery -order: 210 -number: "21" +order: 360 +number: "36" kind: chapter -part: Part V · The work lifecycle -partOrder: 5 -title: Project discovery and t3.json +part: Part VI · The work lifecycle +partOrder: 6 +title: Project discovery, onboarding import, and t3.json shortTitle: Project discovery -summary: "A T3 project is an environment-local durable record for one normalized workspace root; checked-in t3.json and Git-remote identity enrich its behavior and presentation without replacing that record." +summary: "A T3 project is an environment-local durable record for one normalized workspace root; onboarding can import existing agent sessions, while checked-in t3.json and Git-remote identity enrich behavior and presentation without replacing that record." status: source-checked gates: [sources, interaction] objectives: @@ -18,7 +18,7 @@ objectives: keywords: [projects, workspace root, discovery, t3.json, repository identity, environments, configuration, setup scripts, projection] sourceAreas: [apps/server/src/workspace/WorkspacePaths.ts, apps/server/src/project, packages/contracts/src/t3ProjectFile.ts, packages/shared/src/threadEnvMode.ts, packages/client-runtime/src/state/projectGrouping.ts] visuals: [filesystem discovery decision explainer, project identity projection] -updatedAt: 2026-08-24 +updatedAt: "2026-09-11" --- import Callout from "../../components/Callout.astro"; @@ -29,8 +29,8 @@ import ProjectDiscoveryLab from "../../components/ProjectDiscoveryLab.astro"; import SourceExcerpt from "../../components/SourceExcerpt.astro"; import SourceList from "../../components/SourceList.astro"; -“Project” is deliberately not a synonym for “Git repository.” At this pinned -revision, it starts as an environment-local durable record with a normalized +“Project” is deliberately not a synonym for “Git repository.” It starts as an +environment-local durable record with a normalized directory as its `workspaceRoot`. Git can later provide an identity for that directory; clients can then choose to group equivalent identities across environments. Neither upgrade makes paths, directories, and project records the @@ -140,7 +140,7 @@ entries merely because a checkout contains `t3.json`. -
+
L[best-effort file loader] L --> I[deduplicated import suggestions] @@ -174,7 +174,7 @@ not promise that a remote URL is permanently frozen into project history. -
+
R{validated directory?} R -->|yes| C[project.create] @@ -244,6 +244,21 @@ coexist—but none automatically proves the next one. establish such a recursive importer. +## Importing native agent history + +The welcome flow can scan Codex and Claude homes for work the user already did, +group candidates by repository identity, and import selected conversations into T3 +projects. Git repositories sort ahead of ordinary folders, and recent, repeated work +can be preselected without importing every scratch session. + +The scanner reads provider-native metadata. The importer creates canonical T3 +messages and activities with a stable source identity so retries do not duplicate the +same history. Imported history is durable and searchable; resumability remains a +provider decision because display records alone cannot recreate a missing native +session. The boundaries live in +[AgentSessionScanner.ts](https://github.com/pingdotgg/t3code/blob/859304b7808ab9a4be87b1bddcd07c6485bf9c4f/apps/server/src/project/AgentSessionScanner.ts) and +[AgentSessionImporter.ts](https://github.com/pingdotgg/t3code/blob/859304b7808ab9a4be87b1bddcd07c6485bf9c4f/apps/server/src/project/AgentSessionImporter.ts). + -
+
L[local request: no new worktree] L --> R[stored thread path, else project root] diff --git a/src/content/book/230-turn-lifecycle.mdx b/src/content/book/230-turn-lifecycle.mdx index dba889e..c2c9755 100644 --- a/src/content/book/230-turn-lifecycle.mdx +++ b/src/content/book/230-turn-lifecycle.mdx @@ -1,10 +1,10 @@ --- slug: turn-lifecycle -order: 230 -number: "23" +order: 380 +number: "38" kind: chapter -part: Part V · The work lifecycle -partOrder: 5 +part: Part VI · The work lifecycle +partOrder: 6 title: Start, stream, steer, interrupt, settle shortTitle: Turn lifecycle summary: A T3 turn begins as a durably accepted command and later crosses hot reactor and provider-runtime boundaries; session state, output, checkpoints, and client liveness each have narrower guarantees. @@ -18,7 +18,7 @@ objectives: keywords: [thread, turn, lifecycle, receipt, reactor, provider runtime, buffering, interrupt, checkpoint, settlement, liveness] sourceAreas: [packages/contracts/src/orchestration.ts, packages/contracts/src/providerRuntime.ts, apps/server/src/orchestration/decider.ts, apps/server/src/orchestration/Layers/ProviderCommandReactor.ts, apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts, apps/server/src/orchestration/Layers/CheckpointReactor.ts, apps/server/src/provider/Layers/ProviderService.ts, packages/client-runtime/src/state/threads.ts] visuals: [turn lifecycle swimlane, illustrative-control state-machine lab] -updatedAt: 2026-08-24 +updatedAt: "2026-09-11" --- import Callout from "../../components/Callout.astro"; @@ -137,7 +137,7 @@ The shared client reducer makes the same distinction: a completed assistant message does not settle its turn while that turn remains the session's active running turn; leaving `running` is the turn-end signal for the client model. -
+
E[(message + start intent + receipt)] E -. hot domain event .-> R[command reactor] @@ -217,7 +217,7 @@ provenance; it must not be presented as a new T3 turn, completion, checkpoint, or a T3-owned summary artifact. - At this pinned revision, the runtime vocabulary can represent context + The runtime vocabulary can represent context compaction and ingestion can preserve its observation as activity. The lifecycle folding inspected here does not establish a generic “compaction completed the turn” rule. Any future product behavior that does so should be explicitly sourced diff --git a/src/content/book/240-permissions-input.mdx b/src/content/book/240-permissions-input.mdx index 59b5dd2..1a483dc 100644 --- a/src/content/book/240-permissions-input.mdx +++ b/src/content/book/240-permissions-input.mdx @@ -1,10 +1,10 @@ --- slug: permissions-and-input -order: 240 -number: "24" +order: 390 +number: "39" kind: chapter -part: Part V · The work lifecycle -partOrder: 5 +part: Part VI · The work lifecycle +partOrder: 6 title: Permission modes, approvals, and structured input shortTitle: Permissions and input summary: T3 Code persists a four-value runtime-mode choice on a thread, but each provider maps that choice into its own controls; live approval and structured-input requests travel through distinct canonical flows. @@ -17,7 +17,7 @@ objectives: keywords: [runtime mode, permission, approval, structured input, reactor, pending state, provider adapter, security] sourceAreas: [packages/contracts/src/orchestration.ts, packages/contracts/src/provider.ts, apps/server/src/provider, apps/server/src/orchestration/Layers, apps/web/src/session-logic.ts, apps/mobile/src/state/use-selected-thread-requests.ts] visuals: [permission mapping matrix, interactive request router] -updatedAt: 2026-08-24 +updatedAt: "2026-09-11" --- import Callout from "../../components/Callout.astro"; @@ -51,7 +51,7 @@ policy, not an independent per-turn override hidden in the start command. | `auto` | `on-request`, `workspace-write`, `auto_review` reviewer | `auto` | prefers an implement-mode alias when offered; permission callbacks still wait | no ACP session-mode mapping; a raised permission callback waits for an explicit response | broad operations ask; `question` is allowed | | `full-access` | `never`, `danger-full-access`, user reviewer | `bypassPermissions` plus `allowDangerouslySkipPermissions: true` | prefers an implement-mode alias and auto-selects an offered allow option; otherwise waits on the callback | auto-selects an offered allow-always or allow-once option; otherwise waits on the callback | allows all permission rules | -The table records executed mappings at this pinned revision. It does **not** turn +The table records the executed mappings. It does **not** turn the blank Claude map entry, Cursor mode aliases, Grok callback policy, or OpenCode's shared non-full-access ruleset into claims of equivalent safety. Codex, for example, also selects a native sandbox and reviewer for every mode. Claude intercepts @@ -96,7 +96,8 @@ map. - OpenCode maps `permission.asked` and `question.asked` separately, then calls `permission.reply` or `question.reply`. -All five adapters implement both response operations in the current source. That +All six adapters implement the shared response boundary, with provider-specific +support for originating each request shape. That does not promise every provider originates both request kinds in every configuration. The adapter capability surfaces are the positive evidence here; they are not a guarantee that a native runtime will open a given request type during every session. @@ -141,7 +142,7 @@ runtime ingestion and become durable activities. Acceptance of the client command is not a synchronous certificate that a provider completed the action, and the hot provider bridge is not a replayable outbox. -
+
I[runtime ingestion] I --> A[(request activity)] @@ -170,7 +171,7 @@ addressed to a thread; each client chooses its controls. | Person operating the client | the authorization decision | that a broad mode replaces reviewing a specific request | Permission capability metadata could make these differences easier to negotiate in -a UI, but none exists at this pinned revision. That is a **future design direction**, +a UI, but no central inbox is implemented. That is a **future design direction**, not current behavior. -Codex and Claude have concrete task normalization paths in the pinned source. +Codex and Claude have concrete task normalization paths. Codex turns collaboration-agent notifications into task lifecycle/progress events. Claude maps SDK task, workflow, and member progress messages into task events, including typed usage when present. This audit does not establish equivalent task-rollup coverage for Cursor, Grok, or OpenCode; absence here is an evidence boundary, not a claim they can never support it. -
+
A[provider adapter] A -. canonical task event .-> I[runtime ingestion] @@ -137,11 +137,11 @@ For Codex and Claude task data, the normalizer can roll up per-task `totalTokens input/cache/output/reasoning fields, `toolUses`, and `durationMs` when typed usage supplies them. These are task-local, provider-observed rollups. They are not invoice records. **Inference from the separate ingestion paths:** they do not feed either -Chapter 20 lane; neither the live thread-context meter nor the transcript-scan +Chapter 35 lane; neither the live thread-context meter nor the transcript-scan historical usage total consumes this task rollup. - - Chapter 20’s live lane selects one context-window snapshot for a thread; its + + Chapter 35’s live lane selects one context-window snapshot for a thread; its historical lane scans selected Codex and Claude transcript homes. Task usage is activity payload for an Agents/work-log projection. No common accumulator joins those three measurements in the pinned implementation. @@ -151,7 +151,7 @@ historical usage total consumes this task rollup. The inspected task path terminates in activity payloads and the Agents fold. The live context path admits thread.token-usage.updated, while historical accounting reads provider transcript sources. No inspected accumulator joins the - task rollup into either Chapter 20 total; that negative conclusion is bounded to + task rollup into either Chapter 35 total; that negative conclusion is bounded to these concrete paths. diff --git a/src/content/book/260-context-memory.mdx b/src/content/book/260-context-memory.mdx index 82fc46a..436277b 100644 --- a/src/content/book/260-context-memory.mdx +++ b/src/content/book/260-context-memory.mdx @@ -1,10 +1,10 @@ --- slug: context-compaction-memory -order: 260 -number: "26" +order: 410 +number: "41" kind: chapter -part: Part V · The work lifecycle -partOrder: 5 +part: Part VI · The work lifecycle +partOrder: 6 title: Context is provider-owned; history is T3-owned shortTitle: Context and memory summary: "T3 Code retains a product-visible thread history and an opaque provider continuation cursor, while each provider owns the prompt context it may resume or compact; the pinned system has no universal T3 long-term-memory subsystem." @@ -18,7 +18,7 @@ objectives: keywords: [context, compaction, memory, resume cursor, session recovery, thread history, telemetry, restart] sourceAreas: [packages/contracts/src/provider.ts, packages/contracts/src/providerRuntime.ts, apps/server/src/persistence/ProviderSessionRuntime.ts, apps/server/src/provider/Layers, apps/server/src/orchestration/Layers, packages/client-runtime/src/state] visuals: [context ownership map, restart ledger] -updatedAt: 2026-08-24 +updatedAt: "2026-09-11" --- import Callout from "../../components/Callout.astro"; @@ -107,7 +107,7 @@ These are adapter-specific policies—not an inter-provider migration protocol. Codex thread id is not meaningful to Claude; an OpenCode session id is not an executable representation of a Codex prompt. -
+
For the separate historical question—tokens/cost-shaped records from provider -transcript files—read [Chapter 20, “Usage accounting without a false ledger”](../usage-accounting/). +transcript files—read [Chapter 35, “Usage accounting without a false ledger”](../usage-accounting/). That scan is neither the source of the live context meter nor a T3 memory layer. ## 5. “Long-term memory” is absent as a universal product subsystem @@ -223,7 +223,7 @@ event-subscription `afterSequence` from the cached shell sequence. That helps a screen catch up; it does not act as provider context or server authority. Mobile additionally owns environment-scoped local drafts and a persisted command outbox. Those are client resilience/delivery mechanisms and receive their detailed -treatment in Chapter 33 rather than being rebranded as memory here. +treatment in Chapter 48 rather than being rebranded as memory here. The distinction is particularly important after a disconnected mobile action: delivery retry can re-attempt a client command, while provider session recovery @@ -231,7 +231,7 @@ still depends on the server's binding and the provider's ability to use its curs Neither mechanism means a client has captured a complete native model context. - Chapter 33 will cover client caches, optimistic state, drafts, offline queues, + Chapter 48 covers client caches, optimistic state, drafts, offline queues, and reconnect reconciliation by surface. This chapter only places them outside the provider-context boundary so a reader does not mistake local UI state for T3-owned long-term memory. @@ -254,7 +254,7 @@ motion; keyboard tab navigation and a complete static/print ledger are included. 3. Represent compaction as provider-originated provenance unless the product owns the summarization algorithm and its exact replay/retention semantics. 4. Name context telemetry, task usage, and transcript accounting as distinct data - products. Chapter 20's historical ledger must not silently become a memory + products. Chapter 35's historical ledger must not silently become a memory system. 5. If a universal memory feature is proposed, design its user control, source provenance, deletion/retention, isolation, provider injection, and restart diff --git a/src/content/book/270-checkpoints-revert.mdx b/src/content/book/270-checkpoints-revert.mdx index fcde684..0f0cb43 100644 --- a/src/content/book/270-checkpoints-revert.mdx +++ b/src/content/book/270-checkpoints-revert.mdx @@ -1,10 +1,10 @@ --- slug: checkpoints-revert -order: 270 -number: "27" +order: 420 +number: "42" kind: chapter -part: Part V · The work lifecycle -partOrder: 5 +part: Part VI · The work lifecycle +partOrder: 6 title: Hidden-ref checkpoints, diffs, and revert shortTitle: Checkpoints and revert summary: Each completed turn can leave a Git tree snapshot under a hidden ref. Review reads those snapshots; revert restores workspace content first, then attempts provider-history rollback before it can complete the durable history rewrite. @@ -18,7 +18,7 @@ objectives: keywords: [checkpoint, git, hidden ref, diff, restore, revert, rollback, worktree, projection] sourceAreas: [apps/server/src/checkpointing, apps/server/src/orchestration/Layers/CheckpointReactor.ts, apps/server/src/vcs/GitVcsDriver.ts, apps/server/src/provider/Layers, apps/web/src/components/DiffPanel.tsx] visuals: [checkpoint timeline, checkpoint graph lab, revert saga] -updatedAt: 2026-08-24 +updatedAt: "2026-09-11" --- import Callout from "../../components/Callout.astro"; @@ -99,7 +99,7 @@ side-by-side review modes, but they answer different questions. | Working tree | `HEAD` → current tracked worktree, plus patches for untracked files | live filesystem | yes | | Branch changes | merge-base(selected base, `HEAD`) → `HEAD` via `git diff ...HEAD` | live repository refs | yes | -
+
Z[hidden baseline ref · turn 0] Z --> P[provider changes workspace] @@ -161,7 +161,7 @@ the implementation is not uniform. Codex delegates to its live app-server runtim Claude and Cursor truncate their adapter-maintained turn arrays (Claude also updates its resume cursor). OpenCode reads native session messages, finds the earlier assistant-message boundary, then calls `session.revert`. Grok deliberately returns -an unsupported provider-side rollback error at this pinned revision. +an unsupported provider-side rollback error. That table is a capability map for this implementation, not a promise that the upstream provider has erased files, billed tokens, cached context, tool side effects, @@ -179,7 +179,7 @@ general remote-history deletion guarantee. This chapter stops at server-side completion and derived projection pruning. How web and mobile invalidate, race, refetch, and reconcile a reverted snapshot is a - client history-epoch concern deferred to Chapter 29. + client history-epoch concern deferred to Chapter 44. Do not infer that a fresh server event alone settles every local cache race. diff --git a/src/content/book/280-workbench-services.mdx b/src/content/book/280-workbench-services.mdx index f7d0c64..0797f6d 100644 --- a/src/content/book/280-workbench-services.mdx +++ b/src/content/book/280-workbench-services.mdx @@ -1,11 +1,11 @@ --- slug: workbench-services -order: 280 -number: "28" +order: 430 +number: "43" kind: chapter -part: Part V · The work lifecycle -partOrder: 5 -title: Terminals, files, previews, MCP, VCS, and pull requests +part: Part VI · The work lifecycle +partOrder: 6 +title: Terminals, files, previews, MCP, VCS, and review graphs shortTitle: Workbench services summary: "T3 Code's chat surrounds itself with server-owned workbench services. Their transports differ deliberately: terminal and preview state stream, assets use expiring signed HTTP paths, pull-request diffs use authenticated HTTP slices, and desktop owns the only in-app browser host." status: source-checked @@ -18,7 +18,7 @@ objectives: keywords: [terminal, PTY, assets, signed URL, preview, browser automation, MCP, VCS, worktree, pull request, authorization, streaming] sourceAreas: [apps/server/src/terminal, apps/server/src/assets, apps/server/src/preview, apps/server/src/mcp, apps/server/src/vcs, apps/server/src/pullRequest, packages/contracts/src] visuals: [interactive workbench capability matrix, service data paths, surface parity ledger] -updatedAt: 2026-08-24 +updatedAt: "2026-09-11" --- import Callout from "../../components/Callout.astro"; @@ -61,7 +61,7 @@ result through surface-specific code. | VCS / worktrees | server driver and filesystem | RPC snapshots/commands | “A client branch selector creates a checkout locally.” | | Pull requests | server host-provider service | RPC controls; HTTP diff slices | “A rendered action proves the user may write.” | -
+
|authenticated RPC| S[Environment server services] S -->|snapshot + live events| T[Server PTY] @@ -187,14 +187,15 @@ desktop host is connected. T3 can attach its own `t3-code` MCP server to a provider session when the agent browser-access setting permits it. This is not the same as discovering or managing -every external MCP server configured by Codex, Claude, Cursor, Grok, or OpenCode. +every external MCP server configured by Codex, Claude, Cursor, Grok, Antigravity, +or OpenCode. It is a narrowly scoped T3 endpoint for preview operations. At provider-session preparation, the server either revokes/clears prior MCP state when access is disabled or issues a fresh random bearer credential. Its scope is bound to the environment, thread, provider instance, and a registry-generated MCP -session id—the latter is not an adapter-native Codex, Claude, Cursor, Grok, or -OpenCode session identity. The registry stores a hash, not the raw token, and only +session id—the latter is not an adapter-native Codex, Claude, Cursor, Grok, +Antigravity, or OpenCode session identity. The registry stores a hash, not the raw token, and only advertises the `preview` capability. The `/mcp` route sits outside ordinary environment auth, so it requires that provider-scoped bearer credential. Registry liveness is refreshed by both MCP traffic and active provider turns; stop/revoke @@ -203,7 +204,8 @@ liveness window. -All five adapter paths can consume the T3-owned MCP configuration, but their native +The six adapter paths can consume the T3-owned MCP configuration according to their +declared support, but their native injection mechanisms differ: Codex uses app-server arguments plus a bearer-token environment variable; Claude supplies an HTTP MCP server; Cursor and Grok pass ACP MCP server definitions; OpenCode registers the remote server only when T3 owns the @@ -246,9 +248,9 @@ access merely by rendering a branch, diff, or worktree picker. Two earlier boundaries matter here: -- [Chapter 22](../worktree-topology/) traces the start-turn worktree +- [Chapter 37](../worktree-topology/) traces the start-turn worktree saga, including its server filesystem effects and failure seam. -- [Chapter 27](../checkpoints-revert/) traces hidden checkpoint refs, +- [Chapter 42](../checkpoints-revert/) traces hidden checkpoint refs, comparison modes, and the destructive restore/revert path. The workbench is where those results become visible: a terminal's cwd can be the @@ -296,6 +298,21 @@ future mobile release. route selects only environments that advertised pull-request capability. +## 8. A thread can own a review graph + +Source control and review state no longer collapse to one pull-request link. A thread +can link several reviews, including another repository on the same host. The server +refreshes open links and periodically revisits closed ones so reopen state can return; +terminal merged state stays terminal. GitHub stacks add layer discovery, navigation, +sync, guarded rebase, and merge operations that verify expected heads before changing +remote state. + +This graph remains separate from Git checkpoints. A checkpoint identifies workspace +state for diff and restore. A linked review identifies hosted collaboration state. +Creating a pull request may connect both workflows, but no transaction spans the Git +working tree and the hosting provider. The user-facing rules are documented in +[source-control.md](https://github.com/pingdotgg/t3code/blob/859304b7808ab9a4be87b1bddcd07c6485bf9c4f/docs/user/source-control.md). + ## What a surface can honestly claim | Surface | Strongly evidenced workbench roles | Important non-equivalence | diff --git a/src/content/book/285-device-host-architecture.mdx b/src/content/book/285-device-host-architecture.mdx new file mode 100644 index 0000000..ef94578 --- /dev/null +++ b/src/content/book/285-device-host-architecture.mdx @@ -0,0 +1,152 @@ +--- +slug: device-host-architecture +order: 435 +number: "43A" +kind: chapter +part: Part VI · The work lifecycle +partOrder: 6 +title: Device hosts, targets, sessions, and agent control +shortTitle: Device architecture +summary: The runtime architecture behind local and SSH simulator hosts, host-scoped targets, concurrent thread sessions, proxied video, user controls, and optional agent tools. +status: source-checked +gates: [sources, links, interaction, editorial] +objectives: + - Distinguish a device host, target, hub process, and thread session. + - Trace discovery, video, control, and agent-tool traffic through the server. + - Reason about concurrency, disconnects, and security boundaries. +keywords: [device host, simulator, emulator, SSH, video proxy, MCP, agent-device] +sourceAreas: [packages/contracts/src/device.ts, apps/server/src/device, apps/server/src/mcp/toolkits/device] +visuals: [host-scoped device topology, recovery state machine] +updatedAt: "2026-09-11" +--- + +import Callout from "../../components/Callout.astro"; +import Mermaid from "../../components/Mermaid.astro"; + +The device system is a peer to provider orchestration, not a special kind of chat +message. It discovers simulator targets, owns helper processes, proxies video and +controls, associates open sessions with threads, and optionally exposes the same +targets to an agent through a managed tool surface. + +## The identity ladder + + LH[Local device host] + E --> SH[SSH device host] + LH --> H1[Device hub process] + SH --> H2[Remote hub process] + H1 --> I[iOS target] + H1 --> A[Android target] + H2 --> RI[Remote iOS target] + H2 --> RA[Remote Android target] + I --> S1[Thread device session] + A --> S2[Thread device session] + RI --> S3[Thread device session] + U[Web or desktop panel] -->|open · input · tools| E + G[Agent MCP / agent-device] -->|authorized commands| E + E -->|proxied video and state| U`} /> + +A **device host** names the machine and connection method that can run platform +tools. A **target** is one simulator or emulator discovered on that host. A +**session** is T3’s thread-associated observation/control relationship with that +target. Target IDs are meaningful only with their host; concurrent sessions must +therefore route with both identities rather than assuming one global simulator. + +## Discovery and host ownership + +`LocalDeviceHost` launches helpers on the environment machine. `SshDeviceHost` +resolves SSH configuration, checks the remote toolchain, installs pinned helper +packages, owns remote processes, and reports stop or transport failures without +silently losing ownership. The host abstraction lets DeviceService discover targets +and manage sessions without moving SSH complexity into clients. + +The current `device.ts` contract still contains an outdated comment suggesting SSH +hosts are future work. Executable `SshDeviceHost` code, settings schemas, tests, and +the shipped user guide establish current behavior. The executable service and user +contract are authoritative for the host model described here. + +The streaming hub and the agent driver have separate consent and lifetime. T3 +installs pinned `expo-device-hub` only after device viewing is enabled, while +`agent-device` remains absent and stopped until agent access is granted. The hub is +a supervised child because its iOS path loads private native frameworks; a helper +crash must remain outside the environment server process. SSH hosts forward both +helper endpoints to server loopback. + +## One server mediates three traffic classes + +1. **Inventory and lifecycle** requests list hosts and targets, start a stopped + target, open or close a session, and select its thread association. +2. **Observation and control** streams carry frames, screen size, focus, taps, + swipes, keys, platform buttons, logs, and tool state. +3. **Agent operations** arrive through the device MCP toolkit or managed + `agent-device` command and pass the server’s agent-access policy before reaching + a host. + +Only the visible client tab requests live video, which reduces background work while +the simulator itself keeps running. Remote video travels through the environment +server. Browsers require HTTPS or localhost for WebCodecs; plain HTTP falls back to +an iOS MJPEG stream and cannot display Android video. + +The two platforms do not share a wire format. iOS sends AVCC envelopes over HTTP +and accepts input on a binary WebSocket. Android multiplexes SEMU-framed H.264 and +JSON gestures on one WebSocket. The viewer probes decoder support because the iOS +encoder's H.264 High 5.1 output is not accepted by every hardware decoder. + +## Concurrency and recovery + + HostConfigured + HostConfigured --> Available: test and discover succeed + HostConfigured --> Unavailable: SSH or toolchain fails + Available --> SessionOpen: user or agent opens target + SessionOpen --> Streaming: visible client subscribes + Streaming --> SessionOpen: tab hidden + SessionOpen --> Recovering: host connection drops + Recovering --> SessionOpen: same host and target return + Recovering --> Unavailable: identity or helper cannot recover + SessionOpen --> Available: close observation session + Available --> [*]: remove host configuration`} /> + +Several targets and thread sessions can coexist. A host disconnect does not transfer +ownership to another machine with a similar simulator name. Recovery must re-establish +the configured host, helper, and target identity. Removing a host closes its T3 +sessions and attempts to stop reachable helpers, but it does not power off the +underlying simulators. + +## Security and product boundaries + +The helper hub is never a public environment endpoint. Its upstream implementations +contain unauthenticated execution or action routes, so they bind to loopback behind +`DeviceHubProxy`. The proxy authenticates each request, allowlists only required +stream, configuration, screenshot, and read routes, strips short-lived WebSocket +tickets before forwarding, and prevents compression from buffering an endless +MJPEG body. Device settings bypass that hub: typed `device.action` RPC handlers +execute specific platform commands through the selected `DeviceHost`. + +Device settings are environment-scoped because SSH keys, aliases, platform tools, +and simulator processes live on particular machines. Agent access is an explicit +integration setting and affects newly started provider sessions; opening the user +panel does not automatically grant agent tools. The simulator’s app network is also +outside T3’s control: a remote simulator cannot reach a development server bound only +to the environment’s loopback address. + +Provider process environments are fixed when their sessions spawn. Enabling agent +device access starts the helper for eligible sessions, but an already running agent +must restart before the managed CLI and its environment become available. Detailed +driving instructions arrive from `device_open`, keeping them aligned with the pinned +CLI without loading them into every conversation. + + + “No device” can mean the host is unreachable, the platform toolchain cannot list + targets, or the selected target exists but its video transport is unavailable. + Diagnose in that order: host, inventory, then session/stream. Recreating a thread + cannot repair an SSH key or an Android SDK installation. + + +### Current source trail + +- [Device contract](https://github.com/pingdotgg/t3code/blob/859304b7808ab9a4be87b1bddcd07c6485bf9c4f/packages/contracts/src/device.ts) +- [Device service](https://github.com/pingdotgg/t3code/blob/859304b7808ab9a4be87b1bddcd07c6485bf9c4f/apps/server/src/device/DeviceService.ts) +- [SSH host implementation](https://github.com/pingdotgg/t3code/blob/859304b7808ab9a4be87b1bddcd07c6485bf9c4f/apps/server/src/device/SshDeviceHost.ts) +- [Agent device toolkit](https://github.com/pingdotgg/t3code/tree/859304b7808ab9a4be87b1bddcd07c6485bf9c4f/apps/server/src/mcp/toolkits/device) diff --git a/src/content/book/290-shared-client-runtime.mdx b/src/content/book/290-shared-client-runtime.mdx index 717e907..0a39713 100644 --- a/src/content/book/290-shared-client-runtime.mdx +++ b/src/content/book/290-shared-client-runtime.mdx @@ -1,10 +1,10 @@ --- slug: shared-client-runtime -order: 290 -number: "29" +order: 440 +number: "44" kind: chapter -part: "Part VI · Client architectures: shared semantics, platform edges" -partOrder: 6 +part: "Part VII · Client architectures: shared semantics, platform edges" +partOrder: 7 title: "Shared runtime: connections, state, and convergence" shortTitle: Shared client runtime summary: "packages/client-runtime makes web and mobile agree about environment selection, one-attempt RPC sessions, reconnect supervision, and snapshot-plus-cursor convergence. Its most consequential work is rejecting stale history rather than merely rendering new data." @@ -18,7 +18,7 @@ objectives: keywords: [client-runtime, environment, websocket, RPC, Effect Atom, snapshot, cursor, reconnect, retry, pagination, revert, cache] sourceAreas: [packages/client-runtime/src/connection, packages/client-runtime/src/rpc, packages/client-runtime/src/state, packages/contracts/src/orchestration.ts, apps/server/src/ws.ts] visuals: [shared-runtime convergence map, client convergence lab] -updatedAt: 2026-08-24 +updatedAt: "2026-09-11" --- import Callout from "../../components/Callout.astro"; @@ -104,7 +104,7 @@ The seemingly strict “no retry” setting is a division of responsibility. The supervisor owns intent (`connect`/`disconnect`), network status, session generation, last failure, and the visible phases `available`, `offline`, `connecting`, `backoff`, `connected`, and `blocked`. It reports preparation, opening, and -synchronization separately. Transient failures retry on the pinned 3/4/8/16-second +synchronization separately. Transient failures retry on a 3/4/8/16-second ladder; blocked errors wait for an external signal rather than spinning. A stable connection for at least 30 seconds resets retry state. A manual retry resets it; a foreground probe can replace a stale mobile lease and make the immediate first @@ -165,7 +165,7 @@ emits the buffered tail. A completion marker, when the server advertises support means the buffered work before that marker has been delivered; it is not a claim that future domain events have stopped. -
+
R[Resolver] R --> P[Prepared connection] @@ -265,6 +265,24 @@ lets web and mobile differ at the platform edge while sharing the same answer to the expensive correctness questions: what may be cached, what may be resumed, and what must be thrown away. +## Shared settings and environment selection + +Multi-environment clients reconcile settings that should follow the user separately +from settings that name machine-local paths, credentials, hosts, or binaries. Shared +patches carry version information and expose mismatches rather than overwriting an +environment that cannot interpret them. Project defaults can therefore converge while +worktree roots and provider executables remain local. + +When several connected environments can start the same project, the runtime may use +weighted load balancing for a new thread. That decision selects an authority before +work begins; it does not merge repositories or provider sessions after the fact. + +Attachments, media references, terminal output, device state, linked pull requests, +pending requests, and usage limits also live in shared runtime modules so web and +mobile apply the same identity and merge rules. See +[sharedSettings.ts](https://github.com/pingdotgg/t3code/blob/859304b7808ab9a4be87b1bddcd07c6485bf9c4f/packages/client-runtime/src/state/sharedSettings.ts) and +[load-balancing.ts](https://github.com/pingdotgg/t3code/blob/859304b7808ab9a4be87b1bddcd07c6485bf9c4f/packages/client-runtime/src/load-balancing.ts). + +
S[apps/server] C --> R[packages/client-runtime] diff --git a/src/content/book/300-web-runtime.mdx b/src/content/book/300-web-runtime.mdx index 9a5d8b6..ec124e9 100644 --- a/src/content/book/300-web-runtime.mdx +++ b/src/content/book/300-web-runtime.mdx @@ -1,10 +1,10 @@ --- slug: web-runtime -order: 300 -number: "30" +order: 450 +number: "45" kind: chapter -part: "Part VI · Client architectures: shared semantics, platform edges" -partOrder: 6 +part: "Part VII · Client architectures: shared semantics, platform edges" +partOrder: 7 title: One React renderer, three runtime edges shortTitle: Web runtime summary: "The web client keeps one route tree and environment-oriented state model across hosted, locally served, and Electron renderer deployments. Platform differences enter at history, authentication, native-host, and transport boundaries—not in a forked product model." @@ -18,7 +18,7 @@ objectives: keywords: [web, Electron, history, router, environment, atoms, virtualization, tracing, terminal, performance] sourceAreas: [apps/web/src/main.tsx, apps/web/src/AppRoot.tsx, apps/web/src/router.ts, apps/web/src/connection, apps/web/src/components/chat, apps/web/src/components/ThreadTerminalDrawer.tsx] visuals: [web runtime architecture and hot path] -updatedAt: 2026-08-24 +updatedAt: "2026-09-11" --- import Callout from "../../components/Callout.astro"; @@ -118,7 +118,7 @@ reconnecting, and a client can hold several environment presentations at once. The route tells the UI *which* scoped thread to request; the registry and environment atoms answer *through which prepared connection* it can be reached. -
+
BH[browser history] E[Electron file shell] --> HH[hash history] @@ -183,7 +183,7 @@ addressed by environment, thread, terminal id, cwd, optional worktree, and runti environment. User input becomes a typed write command; resize becomes a typed resize command; the renderer hydrates the terminal surface from the attached session's buffered text and status. The client owns the screen component and local -focus/selection behavior. The environment server owns the PTY, as Chapter 28 +focus/selection behavior. The environment server owns the PTY, as Chapter 43 establishes. diff --git a/src/content/book/310-web-product-surfaces.mdx b/src/content/book/310-web-product-surfaces.mdx index a9bda49..466fa90 100644 --- a/src/content/book/310-web-product-surfaces.mdx +++ b/src/content/book/310-web-product-surfaces.mdx @@ -1,10 +1,10 @@ --- slug: web-product-surfaces -order: 310 -number: "31" +order: 460 +number: "46" kind: chapter -part: "Part VI · Client architectures: shared semantics, platform edges" -partOrder: 6 +part: "Part VII · Client architectures: shared semantics, platform edges" +partOrder: 7 title: One thread, many deliberate projections shortTitle: Web product surfaces summary: "The composer, timeline, Agents panel, review panel, and sidebar do not own competing thread records. They select and project one canonical environment-scoped thread through different interaction contracts, including optimistic local affordances that must yield to canonical state." @@ -18,7 +18,7 @@ objectives: keywords: [composer, attachment, command, skill, optimistic UI, Agents, work log, review, sidebar, pinning, projection] sourceAreas: [apps/web/src/components/chat, apps/web/src/components/AgentsPanel.tsx, apps/web/src/components/DiffPanel.tsx, apps/web/src/components/Sidebar.tsx, packages/client-runtime/src/state] visuals: [interactive thread projection lab] -updatedAt: 2026-08-24 +updatedAt: "2026-09-11" --- import Callout from "../../components/Callout.astro"; @@ -58,7 +58,7 @@ model and modes on the thread. The server's normalizer then owns timestamp, workspace, and attachment normalization before dispatch. A successful dispatch result names the accepted command/sequence boundary; it is stronger than a local button state, but later provider work still crosses the post-commit reactor seam -described in Chapters 10–12 and 23. +described in Chapters 25–27 and 38. The web persists chosen mode/model before sending a turn; normalization and @@ -173,7 +173,7 @@ the displayed pin, task, review, and local-view fields to see why a quiet timeli and a detailed Agents panel can honestly look different without holding different canonical threads. -
+
diff --git a/src/content/book/320-desktop-electron.mdx b/src/content/book/320-desktop-electron.mdx index d40bc15..4048968 100644 --- a/src/content/book/320-desktop-electron.mdx +++ b/src/content/book/320-desktop-electron.mdx @@ -1,10 +1,10 @@ --- slug: desktop-electron -order: 320 -number: "32" +order: 470 +number: "47" kind: chapter -part: "Part VI · Client architectures: shared semantics, platform edges" -partOrder: 6 +part: "Part VII · Client architectures: shared semantics, platform edges" +partOrder: 7 title: "Electron desktop: one renderer, explicit native authority" shortTitle: Electron desktop summary: "The desktop reuses the web renderer but not browser authority: Electron main assembles effects, supervises local backend children, exposes a narrow preload bridge, and separately brokers WSL, SSH, previews, menus, updates, telemetry, and shutdown." @@ -18,7 +18,7 @@ objectives: keywords: [electron, desktop, preload, backend pool, WSL, SSH, telemetry, preview, shutdown] sourceAreas: [apps/desktop/src/main.ts, apps/desktop/src/app, apps/desktop/src/backend, apps/desktop/src/wsl, apps/desktop/src/ssh, apps/desktop/src/preload.ts, apps/desktop/src/preview] visuals: [desktop process topology, interactive boot readiness shutdown lab] -updatedAt: 2026-08-24 +updatedAt: "2026-09-11" --- import Callout from "../../components/Callout.astro"; @@ -94,7 +94,7 @@ so a slow `wsl.exe` cold start does not block the primary readiness path. -
+
|curated preload IPC| M[Electron main] M -->|protocol + window + menu + updates| R diff --git a/src/content/book/330-mobile-client.mdx b/src/content/book/330-mobile-client.mdx index f219337..770a7e9 100644 --- a/src/content/book/330-mobile-client.mdx +++ b/src/content/book/330-mobile-client.mdx @@ -1,13 +1,13 @@ --- slug: mobile-client-continuity -order: 330 -number: "33" +order: 480 +number: "48" kind: chapter -part: "Part VI · Client architectures: shared semantics, platform edges" -partOrder: 6 -title: "Mobile: persistence, outbox, sharing, and native systems" +part: "Part VII · Client architectures: shared semantics, platform edges" +partOrder: 7 +title: "Mobile: adaptive workspaces, persistence, and native systems" shortTitle: Mobile continuity -summary: "The Expo client reuses T3 Code's connection and projection semantics but owns a deliberately mobile durability layer: cached snapshots in SQLite, credentials in secure storage, drafts and commands in atomic files, a transactional share inbox, native rendering bridges, and an OTA restart gate that yields to unsaved work." +summary: "The Expo client reuses T3 Code's connection and projection semantics while adding adaptive navigation, native review and terminal surfaces, voice input, cached snapshots in SQLite, secure credentials, durable drafts and commands, transactional sharing, notifications, and a guarded OTA restart path." status: source-checked gates: [sources, interaction] objectives: @@ -18,7 +18,7 @@ objectives: keywords: [mobile, Expo, React Native, connection runtime, SQLite, SecureStore, drafts, outbox, sharing, native terminal, native diff, Live Activity, OTA] sourceAreas: [apps/mobile/app.config.ts, apps/mobile/src/App.tsx, apps/mobile/src/Stack.tsx, apps/mobile/src/connection, apps/mobile/src/persistence, apps/mobile/src/state, apps/mobile/src/features/sharing, apps/mobile/src/features/updates, apps/mobile/modules] visuals: [mobile ownership graph, offline and OTA continuity simulator, native capability matrix] -updatedAt: 2026-08-24 +updatedAt: "2026-09-11" --- import Callout from "../../components/Callout.astro"; @@ -60,7 +60,7 @@ and share-extension lifecycle URLs: the latter is deliberately ignored because t -The connection runtime is not a mobile rewrite of Chapter 29. It merges the shared +The connection runtime is not a mobile rewrite of Chapter 44. It merges the shared `Connection.layer` and shared shell/thread snapshot loaders with mobile platform services: @@ -78,10 +78,10 @@ services: The platform supplies no SSH gateway. Its `SshEnvironmentGateway` operations fail with an explicit “desktop only” blocked error. Mobile can connect through supported direct/bearer or relay targets, but it does not provision a remote CLI over SSH and -does not spawn a local provider. Chapter 34 returns to the access transports; -Chapter 35 follows relay authentication and tunneling. +does not spawn a local provider. Chapter 49 returns to the access transports; +Chapter 50 follows relay authentication and tunneling. -
+
-## 8. The design lesson is selective durability +## 8. Adaptive navigation and native work surfaces + +The phone layout prioritizes one route at a time; the tablet layout can keep a thread +beside files, review, terminal, or other detail surfaces. Those panes are navigation +state, not duplicated thread authority. Native diff highlighting, file-tree search, +image/video/PDF/web previews, and the Ghostty-backed terminal let mobile inspect the +same work with platform-specific rendering. + +New-task drafts can coexist per project and remain local until submitted. Attachments +have durable local copies and join the outbox only after their upload prerequisites +are satisfied. iOS voice input performs on-device transcription before the text enters +the composer under the revision guard above. Android ongoing notifications and iOS Live Activities expose work state, +while opening the app still reconciles the authoritative environment snapshot. + +Appearance follows platform capabilities: Android can use Material You and wallpaper +colors, while both platforms share semantic theme choices and readable terminal +palettes. These native features are organized under the current +[mobile feature tree](https://github.com/pingdotgg/t3code/tree/859304b7808ab9a4be87b1bddcd07c6485bf9c4f/apps/mobile/src/features). + +## 9. The design lesson is selective durability T3 Code does not place every mobile value in one “offline database.” It gives each kind of state the smallest recovery contract it needs: diff --git a/src/content/book/340-access-transports.mdx b/src/content/book/340-access-transports.mdx index 0cca9e2..6d328ca 100644 --- a/src/content/book/340-access-transports.mdx +++ b/src/content/book/340-access-transports.mdx @@ -1,10 +1,10 @@ --- slug: access-transports -order: 340 -number: "34" +order: 490 +number: "49" kind: chapter -part: "Part VII · Reach and ship" -partOrder: 7 +part: "Part VIII · Reach and ship" +partOrder: 8 title: "Reachability is a route; authority is a separate proof" shortTitle: Access transports summary: "A T3 Code environment can be host-local, paired through an endpoint, provisioned through a Tailnet, or reached through the desktop SSH gateway. These paths differ in how they launch and expose a server, but every usable route still has to establish an endpoint, enroll a client, and preserve the server's authority boundary." @@ -18,7 +18,7 @@ objectives: keywords: [remote access, primary, bearer, pairing, Tailscale, Tailnet, SSH, endpoint, exposure, trust boundary] sourceAreas: [apps/server/src/auth, packages/client-runtime/src/connection, apps/desktop/src/backend, apps/desktop/src/ssh, packages/tailscale/src, packages/ssh/src] visuals: [access transport route map, interactive access transport lab] -updatedAt: 2026-08-24 +updatedAt: "2026-09-11" --- import AccessTransportLab from "../../components/AccessTransportLab.astro"; @@ -40,8 +40,8 @@ arrow in the picture. This chapter uses two narrower terms: Neither word is authorization. A route merely lets a client reach an environment that can evaluate its credential and scopes. The durable server, filesystem, provider processes, and work state stay on the environment side of that boundary. -For credential exchange, ticketing, and per-method scopes, see [Chapter 8](../auth-pairing/); for the shared resolver, supervisor, and RPC -session after a route is prepared, see [Chapter 29](../shared-client-runtime/). +For credential exchange, ticketing, and per-method scopes, see [Chapter 23](../auth-pairing/); for the shared resolver, supervisor, and RPC +session after a route is prepared, see [Chapter 44](../shared-client-runtime/). Tailscale can make one or more endpoints available—a Tailnet IP or a MagicDNS @@ -59,6 +59,13 @@ id and obtain the associated profile through the catalog; Relay has its own managed-bootstrap path. This is a model of *how a client prepares a connection*, not a taxonomy of every network that may carry the bytes. +The environment identity remains stable across route changes. Its initialization +publishes a complete identifier atomically and retains a recovery candidate when it +repairs an empty identity file, so concurrent initializers converge on the same +winner. Advertised endpoints are only reachability hints: the connecting device +must prove one works, and endpoint selection must not substitute host loopback when +the requested shareable route is unavailable. + That distinction resolves a common false equivalence. A desktop app launching its own host-local backend has a launch path, but a paired phone or browser never needs to replay that launch. It needs an advertised endpoint and an enrollment credential. @@ -73,7 +80,7 @@ itself. advertises endpoints rather than representing a client target. -
+
|launch| E[Environment server\nfiles · providers · durable state] P -->|loopback HTTP / WS| E @@ -99,7 +106,7 @@ The important restraint is what this does **not** imply. A broad bind is a large set of machines able to attempt a connection; it is not a grant of orchestration, terminal, or review authority. Likewise, a loopback bind is a route constraint, not a substitute for the desktop main/preload boundary described in -[Chapter 32](../desktop-electron/). Bind deliberately, then make the client prove +[Chapter 47](../desktop-electron/). Bind deliberately, then make the client prove its entitlement at the environment. @@ -128,9 +135,16 @@ random URL plus a token pasted into unrelated settings. Later edits refuse to continue if the saved target, profile, or credential no longer forms that bearer shape. +The hosted web app remains a direct client, with its connection catalog in browser +storage. A hosted pairing URL puts the environment address in its query and the +pairing secret in its fragment, exchanges that secret with the environment, then +removes it from browser history. The hosted origin never receives the fragment. +Serving the UI over HTTPS also cannot make a plain-HTTP LAN environment reachable +from that browser context. + This chapter stops at the trust-boundary consequence. It does not restate the token exchange, browser-session, DPoP, WebSocket-ticket, or RPC-scope machinery from -Chapter 8. The operational point here is simpler: **an endpoint makes enrollment +Chapter 23. The operational point here is simpler: **an endpoint makes enrollment possible; enrollment makes a saved access route usable; neither transfers environment ownership to the client.** @@ -161,7 +175,7 @@ discovery an invisible background prerequisite for ordinary local use. Once a user selects a reachable Tailnet endpoint and pairs, the client has not become a “Tailscale connection.” It has a Bearer target whose profile points at a Tailnet address. That is why the resolver, retry policy, cache ownership, and -session readiness remain the shared-runtime concerns of Chapter 29, regardless of +session readiness remain the shared-runtime concerns of Chapter 44, regardless of whether the bytes cross Wi-Fi, a Tailnet, or another HTTPS route. @@ -189,9 +203,14 @@ refreshes the SSH bootstrap path before remote authorization continues. The consequence is deliberately asymmetric: the desktop owns the SSH process and tunnel lifetime; the renderer sees a ready environment route; the remote machine still owns the server process, projects, files, git, terminals, and provider -sessions. That complements Chapter 32's process map rather than turning SSH into +sessions. That complements Chapter 47's process map rather than turning SSH into another desktop-local backend pool member. +Cleanup follows process ownership. Disconnecting stops a remote server only when +the SSH launcher created and owns it; a server discovered already running survives +the client tunnel. Reconnection restores the forward before the shared runtime opens +its HTTP/WebSocket session. + The desktop service delegates to an SSH environment manager. The manager reserves a local port, forwards it to remote loopback, waits for HTTP readiness, and @@ -218,7 +237,7 @@ The resulting checklist is short: 4. Does the saved target describe the route honestly—Primary, Bearer, Relay, or SSH—without mistaking Tailscale or an SSH forward for a new authority model? -If all four answers are sound, Chapter 29 can supervise a normal prepared +If all four answers are sound, Chapter 44 can supervise a normal prepared connection. If any answer is missing, a live socket would only conceal a reachability or trust-boundary defect until the next device, network, or restart. diff --git a/src/content/book/350-t3-connect.mdx b/src/content/book/350-t3-connect.mdx index 27be0df..ea1c01f 100644 --- a/src/content/book/350-t3-connect.mdx +++ b/src/content/book/350-t3-connect.mdx @@ -1,10 +1,10 @@ --- slug: t3-connect -order: 350 -number: "35" +order: 500 +number: "50" kind: chapter -part: "Part VII · Reach and ship" -partOrder: 7 +part: "Part VIII · Reach and ship" +partOrder: 8 title: "T3 Connect: OAuth, DPoP, relay, and tunnel" shortTitle: T3 Connect summary: "T3 Connect uses a Clerk account credential and a device-held DPoP key to authorize relay control-plane operations, links a local environment through a signed proof, provisions a Cloudflare tunnel, then gives the client a direct DPoP-bound connection to that environment. The relay authorizes, provisions, and brokers setup; normal T3 traffic does not transit it." @@ -18,7 +18,7 @@ objectives: keywords: [T3 Connect, Clerk, OAuth, PKCE, DPoP, relay, Cloudflare Tunnel, cloudflared, environment registration, WebSocket] sourceAreas: [docs/internals/t3-connect.md, packages/shared/src/connectAuth.ts, apps/server/src/cloud/CliTokenManager.ts, apps/web/src/cloud/dpop.ts, packages/client-runtime/src/relay/managedRelay.ts, packages/client-runtime/src/authorization/service.ts, infra/relay/src/environments] visuals: [credential ladder, relay control-plane and direct data-plane map] -updatedAt: 2026-08-24 +updatedAt: "2026-09-11" --- import Callout from "../../components/Callout.astro"; @@ -103,7 +103,7 @@ managed connector. `--publish-only` deliberately takes a different path: it link activity publishing but asks for no managed tunnel, so a client must reach the environment out of band. - + @@ -115,13 +115,18 @@ HTTP origin, creates a proxied CNAME to `*.cfargotunnel.com`, obtains a connecto token, and marks the allocation ready. The environment runtime launches the managed client as `tunnel run` with that token and supervises it. +Managed exposure accepts only a validated loopback HTTP origin. Link proof checks +reject forwarded authority headers, allocation lookup uses relay-owned records, and +health and mint requests do not follow redirects. Those constraints prevent endpoint +setup from becoming arbitrary relay egress or exposing another host-local service. + That is an endpoint-exposure path: client traffic can travel through the managed Cloudflare tunnel to the environment. It is not a relay data hop. The relay’s own README says that normal API and WebSocket traffic goes directly between client and selected environment after connection; the client runtime independently constructs the environment’s token and WebSocket-ticket requests from the endpoint URL. -
+
-## 6. Evidence boundary: what this pinned source does and does not establish +## 6. Links, allocations, and connectors have different lifetimes + +Cloud authorization, desired exposure, an allocation, and a running connector do +not begin or end together. Linking may record intent while the environment is down; +startup reconciles that intent. A normal CLI-managed shutdown releases its active +tunnel while retaining the hostname reservation and allocation record, so the +environment appears offline rather than unauthorized. + +Two handoffs retain the tunnel: a client-installed link depends on its stored +connector token, and an update starts a replacement environment immediately. +Allocation cleanup claims a generation before deleting external resources so a +delayed finalizer cannot remove a tunnel reused by a restart. Unlink commits +authorization revocation before external teardown and retains enough state to retry +a failed cleanup. + +## 7. Evidence boundary: what the implementation does and does not establish + +DPoP constrains credential replay, but it does not remove the relay trust +assumption. The relay holds the signing authority for environment mint requests; a +compromised relay signing key remains a privileged failure boundary. The relay README specifies the intended direct post-connection API/WebSocket path, diff --git a/src/content/book/360-reconnect-environments.mdx b/src/content/book/360-reconnect-environments.mdx index 6129ea9..1246db8 100644 --- a/src/content/book/360-reconnect-environments.mdx +++ b/src/content/book/360-reconnect-environments.mdx @@ -1,10 +1,10 @@ --- slug: reconnect-environments -order: 360 -number: "36" +order: 510 +number: "51" kind: chapter -part: "Part VII · Reach and ship" -partOrder: 7 +part: "Part VIII · Reach and ship" +partOrder: 8 title: "Reconnect, environments, notifications, and version skew" shortTitle: Reachability recovery summary: "T3 Code recovers reachability one environment at a time: a registry leases a single supervisor and RPC session per environment, projections reconcile independently after a new generation, background work follows declared demand, notifications wake attention rather than synchronize state, and capability plus exact-version recovery makes skew explicit." @@ -18,7 +18,7 @@ objectives: keywords: [reconnect, environment, generation, lease, supervisor, cache, background, APNs, notifications, awareness relay, version skew, self-update] sourceAreas: [packages/client-runtime/src/connection, packages/client-runtime/src/state, apps/mobile/src/connection, apps/mobile/src/features/agent-awareness, infra/relay/src/agentActivity, packages/contracts/src/environment.ts, apps/web/src/versionSkew.ts, apps/server/src/cloud] visuals: [environment recovery boundary, reachability and notification recovery lab] -updatedAt: 2026-08-24 +updatedAt: "2026-09-11" --- import Callout from "../../components/Callout.astro"; @@ -80,10 +80,10 @@ and do not use an environment A session to repair environment B cache. This constraint is useful even when a user has one desktop server exposed through direct LAN, relay, and SSH-assisted routes: access and launch can differ while the -saved environment identity decides which runtime scope owns recovery. [Chapter 34](../access-transports/) +saved environment identity decides which runtime scope owns recovery. [Chapter 49](../access-transports/) separates access from authority; this chapter follows the recovery consequence. -
+
A[Environment A scope] C --> B[Environment B scope] @@ -166,8 +166,8 @@ The safe recovery order is therefore: No step says “replay every command because the connection returned.” Commands resolve their current environment runtime at execution time. Durable command and -outbox semantics belong to [Chapter 24](../permissions-and-input/) and -[Chapter 33](../mobile-client-continuity/); reconnect supplies reachability, not +outbox semantics belong to [Chapter 39](../permissions-and-input/) and +[Chapter 48](../mobile-client-continuity/); reconnect supplies reachability, not permission to duplicate intent. ## 4. Background work has demand and policy, not a hidden always-on connection @@ -266,7 +266,19 @@ Do not make the transport layer a process manager merely to disguise that restar substitute for re-synchronizing after a server restart. -## 7. The compact rule set +## 7. Host-scoped and queued recovery + +Device sessions add a nested recovery problem: the client must reconnect to the T3 +environment, then DeviceService must reconnect to the configured local or SSH host, +then the target and session identities must still match. A simulator with the same +display name on another host is not a valid substitute. + +Mobile drafts, pending thread creation, attachment uploads, and notification launches +can also outlive one connection attempt. Their local records wait for the environment +and then reconcile against server evidence. Shared-setting mismatch is handled in a +separate versioned channel so reconnect does not silently copy machine-local config. + +## 8. The compact rule set Keep these rules together when extending any client surface: diff --git a/src/content/book/370-distribution-artifacts.mdx b/src/content/book/370-distribution-artifacts.mdx index 4d9f9fd..14f95df 100644 --- a/src/content/book/370-distribution-artifacts.mdx +++ b/src/content/book/370-distribution-artifacts.mdx @@ -1,10 +1,10 @@ --- slug: distribution-artifacts -order: 370 -number: "37" +order: 520 +number: "52" kind: chapter -part: "Part VII · Reach and ship" -partOrder: 7 +part: "Part VIII · Reach and ship" +partOrder: 8 title: "Distribution: artifacts, channels, and what actually ships" shortTitle: Distribution artifacts summary: "T3 Code distributes several deliberately different products: an npm CLI that contains a bundled web client, platform-specific Electron installers built from a staged closure, a release-controlled hosted web channel, store binaries plus fingerprint-gated mobile OTAs, and checksum-pinned AUR packages derived from the published Linux AppImage. The release matrix—not every target the builder knows—defines what is actually shipped." @@ -19,7 +19,7 @@ objectives: keywords: [distribution, npm, CLI, Electron, DMG, AppImage, NSIS, GitHub Releases, Vercel, Expo, EAS, fingerprint, AUR, stable, nightly] sourceAreas: [apps/server/package.json, apps/server/scripts/cli.ts, scripts/build-desktop-artifact.ts, .github/workflows/release.yml, .github/workflows/mobile-eas-production.yml, apps/web/vercel.ts, apps/marketing/src, packaging/aur] visuals: [distribution artifact map, interactive artifact factory] -updatedAt: 2026-08-24 +updatedAt: "2026-09-11" --- import ArtifactFactoryLab from "../../components/ArtifactFactoryLab.astro"; @@ -63,7 +63,7 @@ GitHub Release. A successful GitHub Release then unlocks the AUR handoff and hos web deploy. This is dependency ordering, not an assertion that all downstream consumer updates happen simultaneously. -
+
WEB[Web build] REF --> SERVER[Server / CLI bundle] diff --git a/src/content/book/380-release-updates-observability.mdx b/src/content/book/380-release-updates-observability.mdx index 76defad..2caf4c4 100644 --- a/src/content/book/380-release-updates-observability.mdx +++ b/src/content/book/380-release-updates-observability.mdx @@ -1,10 +1,10 @@ --- slug: release-updates-observability -order: 380 -number: "38" +order: 530 +number: "53" kind: chapter -part: "Part VII · Reach and ship" -partOrder: 7 +part: "Part VIII · Reach and ship" +partOrder: 8 title: "Release, update, and observability: three safety boundaries" shortTitle: Release operations summary: "T3 Code's release graph publishes an exact CLI runtime before clients can require it. Desktop, service, and mobile each cross an update boundary differently, while analytics and diagnostics distinguish opt-out product telemetry from local traces, optional OTLP export, authenticated browser ingestion, and demand-driven native resource history." @@ -18,7 +18,7 @@ objectives: keywords: [release, nightly, npm, GitHub Releases, exact version, electron-updater, service launcher, SQLite snapshot, Expo Updates, fingerprint, PostHog, OTLP, resource telemetry, privacy] sourceAreas: [.github/workflows/release.yml, docs/operations/release.md, docs/internals/server-updates.md, apps/desktop/src/updates, apps/server/src/cloud, apps/server/src/telemetry, apps/server/src/observability, apps/server/src/resourceTelemetry, apps/mobile/src/features/updates] visuals: [release dependency graph, updater boundary comparison, release operations lab] -updatedAt: 2026-08-24 +updatedAt: "2026-09-11" --- import Callout from "../../components/Callout.astro"; @@ -65,7 +65,7 @@ operations guide therefore requires `publish_cli` before `release`, and `release before the hosted web deployment. This prevents a newly visible client from issuing an update request for a package version that cannot yet be fetched. -
+
C W --> C C -->|request exact V| S - N -->|"t3@V must already exist"| S + N -->|t3@V must already exist| S R -. nightly selects prerelease\nnightly updater metadata .-> G`} />
@@ -114,6 +114,12 @@ and Windows instances—with a grace budget, destroys windows, then yields contr the updater. That avoids relying on an application shutdown cascade after the OS has begun quitting it. +Updating a desktop-managed remote backend uses a separate prepare/commit token. +Preparation returns while the connection is alive; the client commits only after it +has received that token, then verifies the prepared version after reconnecting. If +installation fails, desktop restarts the stopped backends and retains the failure +for that same token. + ### 2. Boot-service server: stage, preflight, trial, then have the launcher commit @@ -153,6 +159,13 @@ old blocks a target that needs this safety path. After a commit, ordinary servic manager restart policy applies; there is no promise of a second automatic rollback for every later runtime failure. +An accepted update remains pending from the client's perspective. After reconnect, +the client correlates the launcher's update ID with the ready environment, then +checks both outcome and target version; reconnect alone cannot distinguish commit +from rollback. Servers without update-ID support retain version-only correlation. +Foreground CLI processes do not self-update. Chapter 28 owns the restore-marker and +filesystem-sync details of the launcher's recovery journal. + ### 3. Mobile: OTA eligibility is native compatibility plus a safe teardown moment Mobile's Expo Updates configuration uses a fingerprint runtime version. Inference: @@ -176,7 +189,7 @@ update application is a separate immediate path. decision boundary when one is downloaded. -
+
DA[Update available] @@ -207,6 +220,12 @@ are sent in batches only when delivery is enabled; the payload explicitly disabl PostHog person profiles. The service supplies platform, architecture, client type, and T3 Code version alongside the event properties. +Client attribution comes from the WebSocket that produced the event. `clientType` +describes how the server is hosted; `surface` describes the connected web, desktop, +or mobile client. Missing metadata from an earlier client remains unknown. A +server-global “current client” would misattribute simultaneous surfaces, especially +when a desktop-hosted environment serves a phone or remote browser. + @@ -218,6 +237,13 @@ returning it. That reduces direct identifier exposure to the analytics service; does **not** mean all event properties are automatically anonymous, so the payload schema remains an operational privacy boundary. +Active-use reporting should use `client.turn.requested`; `client.connected` counts +reconnections and is therefore sensitive to network behavior. Provider send and +completion totals need not match, and cost comparisons require complete usage with +no observed subagents or mixed-model routing plus matching model, effort, +interaction mode, and terminal status. Partial counts remain useful observations, +but they cannot establish a complete turn total. + Tracing has a different default. The server creates a bounded local file trace sink @@ -239,13 +265,21 @@ deliberately independent delivery lanes. Finally, native resource telemetry is diagnostics-oriented rather than an event -archive. Its native sidecar keeps a one-hour **in-memory**, bounded ring. Periodic -streaming is off until a diagnostics subscription is retained; explicit refresh -still works. The model samples counters and process trees, so processes that begin -and end between samples may not be seen. It is explicitly not syscall, eBPF, ETW, -or endpoint-security tracing. - - +archive. A standalone Rust child owns a bounded in-memory history; collector failure +does not stop the server, and there is no recurring shell-probe fallback. Age, +snapshot count, process rows, and retained bytes are independent limits, so a large +process tree shortens the available history. Periodic streaming is off until a +diagnostics subscription is retained; explicit refresh still works. + +The model samples counters and process trees, so processes that begin and end +between samples may not be seen. PID identity includes process start time, and +snapshot sequence numbers reset with the monitor generation. Historical replay +must not be overwritten with current Electron metrics. Windows packages do not +currently provide the Linux monitor needed inside a WSL backend, although the +inherited Electron power feed remains available. These are sampled diagnostics, +not syscall, eBPF, ETW, or endpoint-security tracing. + + This is an inference from the independently configured paths: product analytics diff --git a/src/content/book/390-six-complete-traces.mdx b/src/content/book/390-six-complete-traces.mdx deleted file mode 100644 index 3f3fc37..0000000 --- a/src/content/book/390-six-complete-traces.mdx +++ /dev/null @@ -1,355 +0,0 @@ ---- -slug: six-complete-traces -order: 390 -number: "39" -kind: chapter -part: "Part VIII · Synthesis" -partOrder: 8 -title: "Six complete traces: ownership, convergence, and failure boundaries" -shortTitle: Six complete traces -summary: "Six source-grounded paths connect the book's modules end to end: a local turn, a T3 Connect mobile turn, an approval, an offline outbox drain, a checkpoint diff and revert, and an exact-version service update. Each trace names the owner of every handoff and the point where the observed implementation stops promising convergence." -status: source-checked -gates: [sources, interaction, links] -objectives: - - Trace six representative operations across client, relay, domain, side-effect, and projection boundaries. - - Distinguish a durable fact from a best-effort handoff, a cached intention, and a completed external effect. - - Reuse the owning chapters without flattening their separate authorization, recovery, and update protocols into one story. - - Identify the exact failure boundary for each trace before treating a local design choice as a general guarantee. -keywords: [synthesis, trace, orchestration, T3 Connect, mobile outbox, approval, checkpoint, revert, exact-version update, convergence] -sourceAreas: [apps/server/src/orchestration, apps/server/src/provider, packages/client-runtime/src, apps/mobile/src/state, infra/relay/src/environments, apps/server/src/checkpointing, apps/server/src/vcs, apps/server/src/cloud, apps/server/src/serviceLauncher.ts, .github/workflows/release.yml, docs/operations/release.md] -visuals: [synchronized trace swimlanes, six-trace stepper] -updatedAt: 2026-08-24 ---- - -import Callout from "../../components/Callout.astro"; -import EvidenceClaim from "../../components/EvidenceClaim.astro"; -import Figure from "../../components/Figure.astro"; -import Mermaid from "../../components/Mermaid.astro"; -import SourceExcerpt from "../../components/SourceExcerpt.astro"; -import SourceList from "../../components/SourceList.astro"; -import SynchronizedTraceLab from "../../components/SynchronizedTraceLab.astro"; - -This chapter is a synthesis, not a new seventh protocol. It puts already-audited -pieces on one time axis so that the ownership changes are visible: a device can -record intent, a relay can authorize connection setup, an environment can commit a -fact, and a provider, Git driver, or stable launcher can perform a later effect. -Those verbs are deliberately not interchangeable. - -The six traces are chosen because they exercise different seams. They do **not** -combine into a global exactly-once guarantee. In particular, a server command may -commit before a hot reactor observes it; a phone file and a server database have no -shared transaction; a checkpoint revert can mutate files before provider rollback; -and a prepared service runtime is not committed until its independent launcher says -so. - - - Read a trace straight through once, then use its linked prerequisite chapter for - the local state machine. The swimlanes deliberately collapse detail; each arrow - means an observed handoff, not an assertion that all arrows are durable, ordered, - or retryable in the same way. - - -## The common reading key: trigger, owner, durable fact, convergence boundary - -Every trace below has four questions: - -1. **What triggered work?** A user action, provider callback, queued file, or - release workflow begins the path. -2. **Which owner may act next?** An owner can pass an intent or fact onward, but - does not automatically own the recipient's state machine. -3. **What became durable, where?** A committed orchestration event, mobile outbox - file, hidden Git ref, or launcher state has a scope. It is not universal proof. -4. **What converges—or stops?** Snapshots and subscriptions converge client views; - some other paths terminate at an honest failure activity, retry policy, or manual - recovery boundary. - -
- >E: authorized turn command - E->>D: commit event + projection + receipt - E-->>W: hot provider intent - W-->>E: runtime facts - E->>D: commit projected result - E-->>C: snapshot / resumed events - end - rect rgb(247, 242, 255) - Note over C,W: 2 · relay-connected mobile turn - C->>R: DPoP-bound connect authorization - R->>E: short-lived bootstrap mint request - E-->>R: proof-bound bootstrap credential - R-->>C: endpoint + bootstrap credential - C->>E: direct bootstrap exchange + WebSocket ticket - C->>E: mobile turn through direct session - E-->>C: environment snapshot / stream - end - rect rgb(255, 248, 234) - Note over C,W: 3 · approval round-trip - W-->>E: native approval request - E->>D: project pending activity - E-->>C: synchronized pending request - C->>E: response command - E->>D: response-requested event - E-->>W: hot provider response - W-->>E: native resolution or stale-request failure - E->>D: later resolution / failure activity - end - rect rgb(239, 251, 241) - Note over C,W: 4 · offline mobile outbox drain - C->>D: atomic queued intent confirmation - C->>E: after connection + live-shell evidence - E->>D: accepted command receipt - E-->>C: accepted command result - C->>D: remove delivered local item - E-->>C: later snapshot / stream reconciliation - end - rect rgb(255, 241, 241) - Note over C,W: 5 · checkpoint diff / revert - E-->>W: completion triggers capture - W->>D: hidden Git ref + patch result - E->>D: checkpoint metadata - C->>E: revert request - E->>D: durable revert request acceptance - E-->>W: ordered restore + provider rollback - W-->>E: restore / rollback outcomes - E->>D: durable completion only after effects - end - rect rgb(241, 246, 250) - Note over A,W: 6 · stable exact-version update - A->>D: publish exact t3@V - A->>C: expose compatible client artifact - C->>E: request server target V - E->>W: preflight then launcher handoff - W->>D: snapshot, trial, commit or rollback - W-->>E: candidate ready / rollback outcome - E-->>C: reconnect / recovery signal - end`} /> -
- -## Trace 1 — local first turn: commit intent before provider work - -An existing-thread turn begins in a local, web, desktop, or mobile renderer. The -client sends the typed command through an authenticated RPC method; the server -normalizes the external input, serializes a decision through its command queue, and -commits the emitted event, read-model work, and accepted receipt in one SQLite -transaction. That transaction is the durable acceptance boundary. - -Only after that commit does `ProviderCommandReactor` observe the committed intent -and ask `ProviderService` to establish or continue the provider session and send the -turn. The provider adapter maps native notifications to the canonical runtime union. -Runtime ingestion turns those facts back into internal commands, whose resulting -projections flow to a client through an HTTP snapshot and resumable subscription. -The detailed command-to-checkpoint path belongs to [Chapter 4](../request-trace/), -with the lifecycle guards expanded in [Chapter 23](../turn-lifecycle/). - - - -The decisive limitation is after the commit: the reactor consumes a hot stream, not -a durable outbox. A crash in the narrow window after the transaction but before the -provider reactor handles the event does not automatically replay a missing send on -restart. Retrying the same command finds the accepted receipt and does not create a -new event. This is intentionally safer than duplicating the durable fact, but it is -not a proof that every accepted turn reached a harness. - - - The observed ordering gives one atomic environment-side acceptance point and a - later, hot side-effect consumer. Client convergence reads committed projections; - it cannot retroactively make an unobserved provider handoff durable. - - -## Trace 2 — relay-connected mobile turn: launch through the relay, work directly with the environment - -A relay-connected phone first holds account authority and a device DPoP key. The -relay authorizes discovery or connection setup and asks the selected environment to -mint a short-lived bootstrap credential bound to that proof key, then returns the -endpoint and bootstrap material to the phone. The phone exchanges that bootstrap -**directly with the environment**, persists the environment-bound access material, -gets a direct WebSocket ticket, and then runs the same shared connection, snapshot, -and command path as another client. - -The tunnel can expose the local environment, but the hosted relay does not carry -normal T3 API or WebSocket traffic after launch. Thus the mobile turn begins only -after the environment session is established; neither a successful Clerk session -nor a successful relay call is a thread update. [Chapter 35](../t3-connect/) owns -the credential ladder and tunnel setup; [Chapter 29](../shared-client-runtime/) and -[Chapter 33](../mobile-client-continuity/) own the client session and remote-native -continuity edges. - - - - - - - The relay is explicitly outside normal post-launch traffic. The safe next step is - environment-scoped session establishment followed by normal snapshot and stream - reconciliation, not treating a control-plane response as authoritative thread - state. - - -## Trace 3 — approval round-trip: persist a request and a response around a native callback - -A provider can request approval or structured user input while processing a turn. -The adapter normalizes that native request; runtime ingestion flushes buffered -assistant content before the interaction pause, records canonical pending activity, -and projection storage exposes it to a selected web or mobile thread. The client -derives a response UI from the synchronized pending request—not from an untrusted -notification payload or a local approximation of provider state. - -When the person answers, the client sends a typed response intent. The decider -creates a durable response-requested event; the provider command reactor later -routes it to the exact bound session, and the adapter uses the provider-native reply -mechanism. A subsequent provider event and projection update are what settle the -visible pending state. The canonical and provider-specific mapping is in -[Chapter 24](../permissions-and-input/). - - - This trace has two durable request sides—provider request projection and user - response intent—but the actual provider callback is a later reactor action. Do not - infer universal approval semantics from a T3 runtime-mode label; adapters map - their native controls differently. - - -## Trace 4 — offline mobile task drain: durable phone intent waits for remote evidence - -While offline, the phone first exposes a queued row optimistically, then serializes -an atomic file write. Before delivery, it confirms that the durable record still -exists behind pending mutations. That prevents an item whose write failed from -escaping merely because it was briefly visible in the UI. - -After reachability returns, the drain does not send every record at once. It handles -one message globally and the first queued message per thread. For a creation, it -waits for a **live shell**: a shell that already contains the stable thread id means -the local cleanup is stale and the item is removed; only a live shell that lacks it -may send. Existing-thread items likewise wait for enough shell evidence before a -missing thread is discarded. Settings reconciliation can precede the final turn -command; the environment then applies its normal receipt and invariant rules. - -Once the start-turn command returns its selected success result, the drain removes -its local file; it does not wait for a later shell/detail projection to make that -cleanup decision. Snapshot and stream state reconcile afterward and remain useful -evidence on later drain passes. That joins no transaction across phone storage and -server SQLite. [Chapter 33](../mobile-client-continuity/) is the full mobile -state-machine account, while [Chapter 10](../events-receipts/) explains the -environment receipt boundary. - - - - - - - Stable identities, serialized confirmation, live-shell evidence, and environment - receipts make retry safer. A crash or network loss between the atomic phone file - and the remote command still leaves a cross-store ambiguity that the drain handles - with evidence and policy rather than an exactly-once claim. - - -## Trace 5 — checkpoint diff and revert: Git content first, then a conditional history rewrite - -For an eligible completed turn, the checkpoint reactor can capture the workspace -through a temporary Git index into a hidden, thread-scoped ref. It derives the patch -against the preceding checkpoint and dispatches a `thread.turn.diff.complete` -command only after that capture/diff work. The engine then makes the checkpoint -summary visible in the durable thread projection. A review screen can compare hidden -turn boundaries; live working-tree and branch modes are separate Git queries. - -Revert begins with a durable request event, but it is an ordered saga rather than -one transaction. It checks the thread, binding, Git workspace, turn count, and -target; restores content and index, refreshes the workspace, asks the bound provider -to roll back later turns, attempts ref pruning, returns those outcomes to the -checkpoint reactor, and only then has the orchestration engine dispatch durable -completion. If provider rollback or a later step fails after Git restore, files may -already match the target while provider history and projected thread history do not. -That partial state is an implementation-path inference, not a promise of automatic -repair. See [Chapter 27](../checkpoints-revert/) for the full preconditions and -provider matrix. - - - The ordered calls show filesystem replacement before provider rollback and before - durable completion. A failure later in that sequence records failure rather than - compensating every changed boundary; a completed projection is therefore not - warranted until the completion path runs. - - -## Trace 6 — stable release and exact-version update: publish, prepare, trial, commit or roll back - -The release workflow first resolves a stable version and publishes the exact -`t3@V` CLI/runtime package. Only then can the GitHub release make clients discoverable -and a hosted deployment follow. A connected client that asks an eligible server to -update targets that exact version, not whichever package currently owns an npm -dist-tag. - -The active server rejects non-exact or concurrent targets, stages the immutable -runtime, preflights it, and hands prepared target information to the stable service -launcher. The launcher owns the migration/trial boundary: it records pending state, -captures the SQLite triplet, stops the old child, starts the new candidate, and -commits only after the candidate reports prepared. A failed or timed-out trial can -restore the snapshot and select the old runtime; after durable commit, later -failures are governed by ordinary restart policy instead. [Chapter -38](../release-updates-observability/) separates this boot-service updater from the -desktop and mobile state machines. - - - - - - - Publication is a release graph invariant, preflight is an active-server check, - and trial/rollback/commit belong to the independent launcher. Conflating these - three boundaries would make a package publish look like a successful machine - update, which the source does not support. - - - - -## What these traces establish—and what they refuse to claim - -The repeatable pattern is not “make every operation a distributed transaction.” It -is more disciplined: give each durable store a narrow contract, serialize the owner -that must make a decision, pass effects through explicit handoff points, and show -the reader or operator what evidence establishes convergence. Inference across -modules is useful only when it keeps the seams visible. - -For example, a relay-connected mobile turn legitimately combines credential launch, -environment supervision, and an ordinary turn. It does **not** imply that the relay -stores the thread. Likewise, an outbox retry and a command receipt together reduce -duplicate work, but they cannot promise that every provider-side effect occurred -once. These distinctions are the basis for the decision review that follows. - - diff --git a/src/content/book/40-runtime-topologies.mdx b/src/content/book/40-runtime-topologies.mdx index b6dba6f..cf91db1 100644 --- a/src/content/book/40-runtime-topologies.mdx +++ b/src/content/book/40-runtime-topologies.mdx @@ -1,10 +1,10 @@ --- slug: runtime-topologies -order: 40 -number: "4" +order: 190 +number: "19" kind: chapter -part: Part I · Boundaries and vocabulary -partOrder: 1 +part: Part II · Boundaries and vocabulary +partOrder: 2 title: Runtime topologies and technology placement shortTitle: Runtime topologies summary: The same contracts appear in several process graphs; local CLI, hosted web, Electron, and mobile move ownership without moving repository execution off the environment server. @@ -17,7 +17,7 @@ objectives: keywords: [topology, Effect, SQLite, React, Electron, Expo, Astro] sourceAreas: [apps/server, apps/web, apps/desktop, apps/mobile, apps/marketing] visuals: [topology switcher, technology placement matrix] -updatedAt: 2026-08-24 +updatedAt: "2026-09-11" --- import Callout from "../../components/Callout.astro"; diff --git a/src/content/book/400-decisions-limitations-roadmap.mdx b/src/content/book/400-decisions-limitations-roadmap.mdx deleted file mode 100644 index c3e0ba1..0000000 --- a/src/content/book/400-decisions-limitations-roadmap.mdx +++ /dev/null @@ -1,413 +0,0 @@ ---- -slug: decisions-limitations-roadmap -order: 400 -number: "40" -kind: chapter -part: "Part VIII · Synthesis" -partOrder: 8 -title: "Decisions, trade-offs, limitations, and an honest roadmap" -shortTitle: Decision ledger -summary: "T3 Code's architecture is a set of bounded choices: server authority, transactional events, hot reactors, cursored projections, adapters, scoped reconnect, durable mobile intent, exact-version updates, and demand-driven background work each make one failure mode tractable while deliberately leaving another visible." -status: source-checked -gates: [sources, interaction] -objectives: - - Read the major architectural choices as pressure, choice, benefit, cost, alternative, and reversal trigger rather than as universal patterns. - - Separate shipped behavior, documented intent, source-bounded inference, latent capability, and explicit future work. - - Identify the persistence and delivery seams, including the conditions under which each choice should be reconsidered. - - Leave a precise inventory of platform asymmetries and repository discrepancies without inventing roadmap commitments. -keywords: [architecture, trade-off, server authority, event sourcing, reactor, projection, provider adapter, reconnect, outbox, update, retention, roadmap] -sourceAreas: [apps/server/src/orchestration, apps/server/src/provider, apps/server/src/auth, packages/client-runtime/src/connection, apps/mobile/src/state, apps/mobile/src/connection, docs/internals/remote.md, docs/operations/release.md] -visuals: [decision ledger, trade-off path, limitation taxonomy] -updatedAt: 2026-08-24 ---- - -import Callout from "../../components/Callout.astro"; -import DecisionLedgerLab from "../../components/DecisionLedgerLab.astro"; -import EvidenceClaim from "../../components/EvidenceClaim.astro"; -import Figure from "../../components/Figure.astro"; -import Mermaid from "../../components/Mermaid.astro"; -import SourceExcerpt from "../../components/SourceExcerpt.astro"; -import SourceList from "../../components/SourceList.astro"; - -This is a ledger, not a claim that T3 Code found the one right architecture. At -the locked revision, its choices consistently put one environment server in charge -of product authority, preserve accepted intent in SQLite, and make provider-native -execution, filesystem work, and client presentation explicitly separate. That -produces a comprehensible control surface across many harnesses. It also produces -real seams: a committed event can miss a hot reactor, an external harness can cross -an ambiguous crash boundary, and not every client or retained artifact has equal -capabilities. - -Use the six columns throughout this chapter precisely: - -| Lens | Question it answers | -| --- | --- | -| Pressure | What failure or product constraint is being controlled? | -| Choice | What is actually implemented at the pinned revision? | -| Benefit | Which guarantee becomes easier to state or test? | -| Cost | What complexity, boundary, or weaker guarantee remains? | -| Alternative | What a different design could optimize instead—not a promise about T3. | -| Reversal trigger | The product pressure that would justify revisiting this choice. | - - - **Shipped** means executable behavior at the lock. **Documented** means an upstream - statement of intent or operations rule. **Inference** is this book's explicitly - bounded conclusion from cited paths. **Future** is only an upstream item named as - unbuilt. A latent builder target or compatibility bridge is neither a shipped - surface nor a future commitment. - - -## 1. Authority and durable intent: make the environment server the product boundary - -### Server authority - -The pressure is remote control without pretending that a browser, desktop shell, or -phone owns a provider process, Git worktree, terminal, or filesystem. T3 puts those -operations behind the environment server. Clients authenticate, use typed RPC, and -hold projections and presentation state; a provider still owns its native reasoning -and context engine. This makes one environment the place where authorization, -orchestration, workspace effects, and product history meet. - - - Server composition acquires authentication, orchestration, provider, VCS, terminal, - filesystem, and remote-endpoint services in one owned runtime. The adapter boundary - keeps provider-native session behavior outside the product's durable domain core. - - -The benefit is a clear remote model: adding another device adds another client of an -environment rather than another competing owner of a workspace. The cost is that -availability, upgrades, and recovery concentrate around that server; clients must -reconnect and reconcile instead of making local state authoritative. A peer-to-peer -or client-owned model could improve disconnected autonomy, but would need a conflict, -credential, and workspace-execution story that this implementation deliberately does -not carry. Revisit the choice if concurrent offline editing or multi-writer workspace -authority becomes a primary product requirement. - -### Transactional event core - -The pressure is accepting a user command exactly enough to answer a retry without -claiming provider completion. The choice is a serialized engine that decides a -command, appends its event batch, applies projections, and records the command receipt -inside one SQLite transaction. The benefit is a sharp acceptance boundary: retrying -the same command id can recover the stored result, and a failed transaction leaves no -accepted receipt. The cost is that normalization-time files, provider calls, and -later reactor work are outside that transaction. - - - -An append-only log is not free: commands and deciders must retain invariants, read -models must be maintained, and a receipt is not an end-to-end idempotence proof for a -multi-step bootstrap saga. A direct CRUD model could lower local complexity when there -is no need to replay or compose state. Revisit the event core when the product no -longer benefits from immutable command history, independently shaped projections, or -receipt-based retry semantics. - -
- S[Environment server\nauthority] - S --> T[(Event + projection + receipt\nSQL transaction)] - T -. committed event .-> R[Hot reactor] - R --> A[Provider adapter] - A --> P[Native harness / process] - T --> Q[Projection cursors\nrebuild read models] - C --> O[Mobile durable\nintent outbox] - S --> U[Exact-version\nstage + preflight] - C -. retained demand .-> B[Scoped background work] - R -. no durable delivery record .-> X[Crash / replay seam] - A -. native state remains external .-> X - P -. result returns hot .-> R`} /> -
- -## 2. Delivery and read state: choose explicit eventual boundaries - -### Hot, best-effort post-commit reactors - -The pressure is to avoid calling a harness while a domain transaction is still open. -T3 publishes only after commit, then runs provider, runtime-ingestion, checkpoint, -deletion, and awareness consumers as hot scoped workers. The benefit is simple: -rollback cannot have caused a provider turn, and one reactor failure is handled without -rolling back accepted domain history. The cost is the **reactor crash window**. A -process can die after the receipt/event commit and before a reactor observes it; the -source contains no durable outbox row, per-reactor delivery cursor, or startup replay -of pending hot work. - - - The provider command reactor consumes current hot events and records duplicate - suppression in process memory before forking its send. Its source explicitly says - pending work is not replayed on a later subscription. - - -That is a reasonable trade where an external operation can be ambiguous to repeat: -a durable outbox still needs idempotency keys, attempt records, and a policy for a -harness that accepted work just before a crash. It would be the alternative when the -product requires guaranteed post-commit execution. The reversal trigger is explicit: -if “accepted turn” must eventually imply “provider send attempted” across server -crashes, add a durable delivery protocol rather than describing the current reactor as -exactly once. - -### Snapshot plus cursor - -The pressure is a fast UI that can restart, replay, and subscribe without claiming -that one monolithic snapshot is globally current. Each projector has a durable cursor; -normal command acceptance advances its projector SQL and cursor together. A composed -snapshot uses the minimum cursor among its required projectors, while live clients -attach before their replay/snapshot read and use global event sequence for gap repair. - - - Projectors bootstrap independently from their own cursors. The snapshot helper - deliberately returns the minimum required projection sequence, not the event-log - head; subscription setup attaches live input before deciding replay or snapshot. - - -The benefit is independently shaped and rebuildable read state. The cost is more -than one watermark, a bounded bootstrap path, ordering-sensitive projectors, and -careful client race guards. A single authoritative document per thread could simplify -some reads but makes fan-out and independently evolved views harder. Revisit cursor -architecture if projection lag, cross-projector joins, or operational replay needs -outgrow the current SQLite/replay ceiling. - -## 3. Harness and client continuity: normalize the boundary, not the world - -### Provider adapters - -The pressure is five harnesses with different session, approval, stream, context, -and process semantics. T3 chooses a narrow adapter lifecycle and canonical runtime -event grammar; `ProviderService` owns routing, bindings, correlation, credentials, -and cross-provider policy. The benefit is one product domain that can preserve native -provenance without forcing all harnesses into a fictional universal feature set. - -The cost is an adapter matrix, capability gaps, and no repository-wide proof that all -providers have semantic parity. A generic “agent protocol only” design could reduce -some integrations but would either lose native features or push product policy into -each provider driver. Revisit this boundary when the normalized contract can no -longer represent an important native lifecycle without pervasive escape hatches. - - - The adapter exposes provider lifecycle and canonical event operations. The service - resolves persisted bindings, adopts/resumes sessions, correlates instance events, - and supplies product-level policy around the concrete adapter. - - -### One reconnect owner per environment - -The pressure is multiple pages, caches, notifications, and devices observing one -remote environment without creating retry storms or merging unrelated authorities. -The choice is one environment registry entry with one supervisor generation and at -most one active RPC lease; shell and thread synchronizers retain separate cache and -cursor responsibilities. The benefit is one place to own transport lifecycle while -each projection remains honest about its own authoritative refresh. - -The cost is a sophisticated supervisor, generations, leases, and surface-specific -resynchronization. A global connection manager is attractive but would blur separate -environment authority and corrupt cache ownership. Revisit this model if the product -adds genuine cross-environment aggregation or a shared write model—not merely a UI -that displays several environment summaries. - - - The runtime registry replaces an environment-scoped lease, and each new generation - leads the shell back through an authoritative refresh rather than elevating cache - continuity into authority. - - -### Durable mobile intent outbox - -The pressure is a phone losing foreground time or connectivity after the user has -pressed send. Mobile uses an optimistic enqueue with durable persistence, serializes -its manager, and asks the user for a delivery decision when a queued turn collides -with a changed thread state. The benefit is preserving user intent locally before it -can be delivered to an environment. The cost is another state machine: existence -guards, capped backoff, confirmations, and explicit replay semantics instead of an -assumption that a compose action immediately became a server turn. - -An always-online client could omit this machinery, but it would trade away the -recovery behavior that matters on a mobile lifecycle. Revisit the outbox when mobile -intent becomes multi-device collaborative work requiring server-issued identities or -when all clients need an equivalent durable intent queue. - - - -### Exact-version updates - -The pressure is a newly visible client asking a connected server to run an incompatible -runtime. T3 publishes the exact CLI package before a release exposes the clients that -can request it; a boot-service server rejects non-exact targets, stages and preflights -that runtime, then hands activation to a stable launcher. The benefit is a precise -compatibility target and a reversible SQLite-bound trial. The cost is release ordering, -launcher protocol compatibility, and platform-specific update machinery. - -A floating channel update can reduce operations friction but makes an update request -less reproducible. Revisit the invariant if a compatibility protocol—not matching -versions—becomes sufficient to prove safe server/client combinations. - - - The operations guide names the npm-before-clients invariant; the self-update path - accepts only an exact target and validates its staged runtime before launcher handoff. - - -### Scope-driven background work - -The pressure is staying useful while mobile is backgrounded without keeping every -environment permanently active. Mobile background activity reports retained demand -through reference-counted environment scopes. The benefit is a bounded reason for -work to continue: a caller declares interest and releases it. The cost is that -subscription ownership and lifecycle cleanup must be correct; background liveness is -not a durable scheduler, and platform APIs constrain what actually runs. - -An always-on global worker would simplify call sites but waste resources and make -ownership leaks more damaging. Revisit scopes when the product needs a durable -background-job contract with OS-managed scheduling and completion receipts. - - - Mobile background work is reported per environment and reference-counted by retained - scopes; the platform layer owns connection cleanup rather than making a background - indicator evidence of durable execution. - - - - -## 4. Limitations are part of the architecture contract - -The ledger above describes choices. This section records the places where their edges -must stay visible in a design review. - -### Shipped asymmetries and discrepancies - -| Classification | What the pinned source establishes | Consequence for a reader or successor | -| --- | --- | --- | -| Shipped platform asymmetry | Desktop, boot-service server, and Expo mobile update through three different state machines; mobile's OTA is fingerprint-gated and its safe reload waits for persistence and lifecycle conditions. | “Update” cannot be one shared abstraction without losing its owner and rollback boundary. | -| Latent artifact | Root build scripts support more target names than the inspected release matrix actually publishes; Windows ARM64 is present as a latent/commented target rather than a distributed artifact at this lock. | Builder support is not evidence that a user can download a release. | -| Transition / compatibility path | The web hides OpenCode's `plan` agent when legacy plan mode is off and heals old stored selections after settings hydrate; mobile retains a device-local legacy plan-mode preference. | The plan UI is transitional compatibility code, not proof of a new universal planning model. | -| Shipped model boundary | Provider resume state is an opaque binding; thread plans, checkpoints, drafts, and native harness history remain distinct records. | There is no universal memory layer or provider-independent continuation guarantee. | - - - The sources persist different identities and lifecycle shapes for bindings, domain - plans, checkpoints, and mobile intent, while plan progress is explicitly in memory. - Calling their collection “universal memory” would overstate what can be restored or - resumed across a provider boundary. - - - - The web hydration pass repairs stale saved selections, the web capability filter - removes the OpenCode plan option while legacy mode is off, and mobile independently - keeps a device-local legacy preference. These are distinct compatibility paths. - - -### Retention and cleanup are selected operations, not one erase button - -Tombstoning a project or thread changes its projected visibility; it does not erase -the append-only event history or command receipts. Thread revert removes selected -derived messages, plans, activities, turns, attachment files, and later checkpoint -refs, but retains event history and records the revert itself. The thread-deletion -reactor performs best-effort provider and terminal cleanup; the web separately offers -an orphan-worktree removal path. Provider binding deletion exists as a repository -operation, yet no caller appeared in the inspected production sources. Hidden checkpoint -refs likewise do not gain a repository-wide lifecycle reconciler simply because a -thread is tombstoned. - - - This conclusion is scoped to the inspected production paths: they distinguish - tombstones, selected revert pruning, hot deletion cleanup, and optional web worktree - cleanup. It is not a promise that future retention behavior will remain unchanged. - - -Attachment cleanup has the opposite shape: normalizers write image bytes before -command dispatch, while projectors perform later filesystem deletion after their SQL -and cursor step. That reduces database/file coupling but makes cross-store atomicity -unavailable. A rejected or retried command can leave an early file; a failed cleanup -is logged, and a crash after cursor advancement can prevent that event from selecting -the same cleanup again. This is **shipped behavior**, not a future garbage collection -protocol. - - - File creation occurs before domain dispatch. The projector's SQL/cursor work occurs - before best-effort filesystem cleanup, and bootstrap resumes from durable cursors. - - -DPoP replay defense records a replay marker under a hashed proof-derived key through -exclusive secret creation. -The source path establishes rejection of a duplicate marker. **Inference:** the -inspected source does not show a corresponding marker-expiry or retention sweep, so -this book cannot claim a bounded replay-marker store merely because proof timestamps -are recorded. That is a retention inference, not a security weakness claim. - - - The code writes a durable marker keyed by proof material and maps an existing marker - to replay rejection. This audit did not locate a deletion or expiry path for those - markers at the pinned revision. - - -### The only roadmap claims here are explicit upstream future work - -The internal remote document labels three items **unbuilt**: additional third-party -tunnel endpoint providers, a **relay-hosted OAuth callback broker**, and richer -multi-environment UI beyond the current connections list. They are future work, not -dates, milestones, or a promise that their current design will ship. The pinned code -and connect documentation do implement a hosted static `/connect/callback` handoff -page. That is not evidence of a relay-hosted backend broker: only the broad claim -that there is *no callback path* is stale, while the literal broker item remains -future work. - - - The source explicitly calls the three remote items unbuilt. Separately, the hosted - callback handoff is executable; it does not establish the future relay broker. The - source provides no release date, priority, or implementation guarantee for the - broker or either other item. - - -## 5. A decision review is more useful than a pattern checklist - -When evaluating T3 Code's choices, the useful question is not “should every system -copy this?” It is: **which pressure does this choice address, which boundary is -durable, and which cost does it impose?** T3's strongest recurring lesson is truthful separation: -accepted intent is not provider completion; a projection is not a universal snapshot; -a remote notification is not authority; a bounded local record is not a retention -policy; and a compatibility branch is not a roadmap. - - diff --git a/src/content/book/50-cli-bootstrap.mdx b/src/content/book/50-cli-bootstrap.mdx index 2adfad4..9c6a4d2 100644 --- a/src/content/book/50-cli-bootstrap.mdx +++ b/src/content/book/50-cli-bootstrap.mdx @@ -1,10 +1,10 @@ --- slug: cli-bootstrap -order: 50 -number: "5" +order: 200 +number: "20" kind: chapter -part: Part II · Boot and connect -partOrder: 2 +part: Part III · Boot and connect +partOrder: 3 title: The `npx t3` bootstrap path shortTitle: CLI bootstrap summary: The published package resolves commands and configuration, locates its copied web client, and hands one readonly ServerConfig value to the layered runtime. @@ -17,7 +17,7 @@ objectives: keywords: [npx t3, CLI, ServerConfig, bootstrap, static assets, npm package] sourceAreas: [apps/server/src/bin.ts, apps/server/src/cli, apps/server/scripts/cli.ts, apps/server/vite.config.ts] visuals: [package exploder, interactive bootstrap stepper, configuration scenario resolver] -updatedAt: 2026-08-24 +updatedAt: "2026-09-11" --- import Callout from "../../components/Callout.astro"; @@ -31,7 +31,7 @@ import SourceList from "../../components/SourceList.astro"; `npx t3` looks like one command, but it crosses three distinct systems: npm selects a published executable, the Effect CLI selects a command and resolves -configuration, and the server runtime acquires the services described in Chapter 6. +configuration, and the server runtime acquires the services described in Chapter 21. Keeping those stages separate makes several otherwise surprising behaviors obvious. ## What npm actually installs @@ -52,7 +52,7 @@ publishes only `dist`. That directory is deliberately more than one JavaScript f
-
+
|bundle| CLI[dist/bin.mjs] L[src/service-launcher.ts] -->|bundle| SL[dist/service-launcher.mjs] @@ -151,7 +151,7 @@ compiled-adjacent `client` directory before the monorepo fallback. If neither ha index, the catch-all route returns HTTP 503 instead of an application shell. - Startup output constructs `/pair#token=…`. URL fragments are not sent in the HTTP request; the browser client extracts and removes the fragment before exchanging the credential in Chapter 8. + Startup output constructs `/pair#token=…`. URL fragments are not sent in the HTTP request; the browser client extracts and removes the fragment before exchanging the credential in Chapter 23. +
P[Bun or Node platform] P --> DB[SQLite + migrations] @@ -53,7 +53,7 @@ activation barrier—controls when already-acquired roots may begin external wor
The graph is broad because the environment server is the execution authority from -Chapter 1. Composition still preserves boundaries: each service exposes a typed +Chapter 16. Composition still preserves boundaries: each service exposes a typed interface, layers declare requirements, and the launched scope owns acquisition and release. `makeServerLayer` is the composition root; `Layer.launch` keeps it alive. @@ -61,7 +61,7 @@ release. `makeServerLayer` is the composition root; `Layer.launch` keeps it aliv The SQLite layer configures a busy timeout, enables foreign keys, selects WAL, and runs a statically ordered migration manifest during acquisition. At this revision -the manifest contains 41 migrations. That number is a fact about the pinned source, +the manifest contains 50 migrations. That number describes the implementation, not a compatibility promise for later revisions. @@ -157,7 +157,7 @@ magically roll back every effect that crossed an external boundary. `docs/internals/overview.md` places command readiness before listener readiness and - describes a different final order. At the pinned revision, executable code places + describes a different final order. Executable code places listener/auxiliary preparation first, then launcher trial, welcome, activation, command readiness, and `ready`. This book follows code and keeps the contradictory documentation as evidence of drift. @@ -169,6 +169,20 @@ magically roll back every effect that crossed an external boundary. ready” erases the command gate and makes the stale overview look plausible. +## Provider, import, review, and device services + +The server graph also assembles four control areas that deliberately remain outside +the provider adapter itself. Model manifests and provider-auth services manage +catalog and sign-in state. Agent-session scanners and importers turn selected native +history into durable project/thread records. Pull-request synchronization observes +linked reviews without making the Git host part of the orchestration transaction. +DeviceService owns local and SSH hosts, targets, sessions, and frame/control traffic. + +Keeping these as services preserves the narrow adapter rule: a provider integration +translates its runtime, while the environment server owns product policy and access. +The authoritative composition is in [server.ts](https://github.com/pingdotgg/t3code/blob/859304b7808ab9a4be87b1bddcd07c6485bf9c4f/apps/server/src/server.ts) and +[serverRuntimeStartup.ts](https://github.com/pingdotgg/t3code/blob/859304b7808ab9a4be87b1bddcd07c6485bf9c4f/apps/server/src/serverRuntimeStartup.ts). + +
H[environment HTTP API] S --> R[Effect RPC definitions] @@ -154,7 +154,7 @@ then inspect authoritative state. The event store makes catch-up possible, but each live subscription buffer is an unbounded in-memory queue bound to that stream's scope. Process loss clears it. Reconnection converges by persisted replay or a fresh snapshot—not by restoring - the old queue. Chapter 12 compares this with the mobile durable outbox. + the prior queue. Chapter 27 compares this with the mobile durable outbox. The `threadSequence` attached to an older-page snapshot is a thread-scoped merge @@ -175,6 +175,19 @@ while its subscription continues to advance on the global sequence. RPC session. +## The typed surface extends beyond thread orchestration + +The RPC catalog also carries agent-session scanning and import, provider setup and +authentication, provider-reported usage limits, device inventory and sessions, +shared-setting patches, and plural pull-request operations. Each family keeps its own +snapshot, subscription, or command semantics; adding them to one WebSocket transport +does not make them one aggregate. + +The broad catalog is useful because version negotiation and authorization remain +method-specific. An older environment can serve core thread state while a client +hides an unsupported device, import, or review action. See the current +[RPC contract](https://github.com/pingdotgg/t3code/blob/859304b7808ab9a4be87b1bddcd07c6485bf9c4f/packages/contracts/src/rpc.ts). + @@ -63,11 +63,11 @@ authorization bypass. its fragment. The hosted client strips that fragment and registers the remote environment with a Bearer session. Clerk authentication to T3 Connect is a separate issuer; a managed-relay authorization later obtains its own DPoP-bound environment - session. Chapter 35 follows that cloud and relay path without conflating it with the + session. Chapter 50 follows that cloud and relay path without conflating it with the four environment-server rows above. -
+
D[auth descriptor] B[desktop bootstrap seed] --> X{exchange shape} @@ -213,13 +213,6 @@ and `relay:read`. The administrative set adds `access:read`, `access:write`, and - - The internal guide says a WebSocket ticket carries scopes. Executable code wins at - this revision: the ticket carries `sid` and timestamps, and verification reloads - scopes from the SQLite session row. The resulting socket is scoped; the ticket - itself is not a scope-bearing snapshot. - - The pinned implementation creates exclusive `dpop-proof-*` marker files. A repository-wide source search found no cleanup path for those markers. That is an @@ -231,7 +224,7 @@ The browser cookie is set `HttpOnly`, `SameSite=Lax`, and path `/`; the code doe set an explicit `Secure` attribute. That is one environment-server session shape, not the credential used by every remote client. Direct hosted pairing stores a Bearer environment session, while the managed-relay path later establishes a -distinct DPoP-bound environment session. Chapter 35 follows those remote boundaries. +distinct DPoP-bound environment session. Chapter 50 follows those remote boundaries. diff --git a/src/content/book/90-commands-invariants.mdx b/src/content/book/90-commands-invariants.mdx index 07244a9..fcd35f4 100644 --- a/src/content/book/90-commands-invariants.mdx +++ b/src/content/book/90-commands-invariants.mdx @@ -1,10 +1,10 @@ --- slug: commands-invariants -order: 90 -number: "9" +order: 240 +number: "24" kind: chapter -part: Part III · Transactional domain core and post-commit delivery -partOrder: 3 +part: Part IV · Transactional domain core and post-commit delivery +partOrder: 4 title: Commands, invariants, and the boundary of atomicity shortTitle: Commands and invariants summary: Client commands are authenticated and normalized before a serialized, Effectful decider plans events; an existing-thread turn commits atomically, while first-turn bootstrap is a WebSocket saga with narrower compensation. @@ -18,7 +18,7 @@ objectives: keywords: [commands, invariants, normalization, decider, aggregate, transaction, bootstrap, compensation] sourceAreas: [packages/contracts/src/orchestration.ts, apps/server/src/orchestration/Normalizer.ts, apps/server/src/orchestration/decider.ts, apps/server/src/orchestration/Layers/OrchestrationEngine.ts, apps/server/src/ws.ts, apps/server/src/orchestration/http.ts] visuals: [command boundary map, existing-turn event batch, bootstrap saga, failure-position explorer] -updatedAt: 2026-08-24 +updatedAt: "2026-09-11" --- import Callout from "../../components/Callout.astro"; @@ -78,7 +78,7 @@ internal commands enter the engine's command union. ## The full command boundary -
+
A[schema + scope] A --> N[normalize + stage] @@ -130,7 +130,7 @@ That placement has two consequences: “Atomic” in the existing-turn path refers to the SQL event, projection, and receipt writes. It does not include normalization-time directories and attachment - bytes. Chapter 13 maps file retention and cleanup in detail. + bytes. Chapter 28 maps file retention and cleanup in detail. ## The decider is isolated, but not a deterministic pure function @@ -206,7 +206,7 @@ setup script, and then start the turn. Those operations cannot share the existing-turn transaction: Git and process launch are external effects, and several domain subcommands each commit independently. -
+
N[normalize] N --> A[Tx A: thread.create] @@ -235,7 +235,7 @@ The saga's real boundaries are easy to miss: The compensation function dispatches thread.delete. The shown failure path contains no worktree or branch removal. That is an absence-of-code inference - at the pinned revision, not a promise that future cleanup will remain narrow. + in the implementation, not a promise that future cleanup will remain narrow. @@ -264,7 +264,7 @@ state exists. Move the boundary and compare what survives. The durable unit is deliberately small: an accepted engine command and its event batch. Everything before it can leave staging residue; everything after it can miss hot delivery; and the bootstrap path composes several such units with external -effects. Chapter 10 now examines the receipt and publication windows inside that +effects. Chapter 25 examines the receipt and publication windows inside that unit. {\n /**\n * Provider kind implemented by this adapter.\n */\n readonly provider: ProviderDriverKind;\n readonly capabilities: ProviderAdapterCapabilities;\n\n /**\n * Start a provider-backed session.\n */\n readonly startSession: (\n input: ProviderSessionStartInput,\n ) => Effect.Effect;\n\n /**\n * Send a turn to an active provider session.\n */\n readonly sendTurn: (\n input: ProviderSendTurnInput,\n ) => Effect.Effect;\n\n /**\n * Interrupt an active turn.\n */\n readonly interruptTurn: (threadId: ThreadId, turnId?: TurnId) => Effect.Effect;", - "checksum": "a75847d120df17015de3fdcd97ab0f5a3b5d3f7c8b19f950e0c0d484441e17cf" + "code": "export interface ProviderAdapterShape {\n /**\n * Provider kind implemented by this adapter.\n */\n readonly provider: ProviderDriverKind;\n readonly capabilities: ProviderAdapterCapabilities;\n\n /**\n * Start a provider-backed session.\n */\n readonly startSession: (\n input: ProviderSessionStartInput,\n ) => Effect.Effect;\n\n /**\n * Send a turn to an active provider session.\n */\n readonly sendTurn: (\n input: ProviderSendTurnInput,\n ) => Effect.Effect;\n\n /** Omitted when this adapter does not support manual context compaction. */\n readonly compaction?: ProviderCompaction;\n\n /**\n * Interrupt an active turn.\n */\n readonly interruptTurn: (threadId: ThreadId, turnId?: TurnId) => Effect.Effect;", + "checksum": "cbef9b58b6cefdda99793bd2e32fcbcb248c832faa26278e22257aca8fb96abe" }, "provider-adapter-interactions": { "id": "provider-adapter-interactions", "path": "apps/server/src/provider/Services/ProviderAdapter.ts", - "start": 73, - "end": 94, + "start": 97, + "end": 117, "language": "typescript", "label": "Provider approval and input contract", - "code": " /**\n * Respond to an interactive approval request.\n */\n readonly respondToRequest: (\n threadId: ThreadId,\n requestId: ApprovalRequestId,\n decision: ProviderApprovalDecision,\n ) => Effect.Effect;\n\n /**\n * Respond to a structured user-input request.\n */\n readonly respondToUserInput: (\n threadId: ThreadId,\n requestId: ApprovalRequestId,\n answers: ProviderUserInputAnswers,\n ) => Effect.Effect;\n\n /**\n * Stop one provider session.\n */\n readonly stopSession: (threadId: ThreadId) => Effect.Effect;", - "checksum": "67b4610571f49faa99826588c7af37dafaf379bc25f4e9d012a429cc31f5858d" + "code": " * Respond to an interactive approval request.\n */\n readonly respondToRequest: (\n threadId: ThreadId,\n requestId: ApprovalRequestId,\n decision: ProviderApprovalDecision,\n ) => Effect.Effect;\n\n /**\n * Respond to a structured user-input request.\n */\n readonly respondToUserInput: (\n threadId: ThreadId,\n requestId: ApprovalRequestId,\n answers: ProviderUserInputAnswers,\n ) => Effect.Effect;\n\n /**\n * Stop one provider session.\n */\n readonly stopSession: (threadId: ThreadId) => Effect.Effect;", + "checksum": "52e87108d4464742547dc5ceb99a3d18923282eb095d3280bbe3e852ec811ffa" }, "provider-adapter-stream": { "id": "provider-adapter-stream", "path": "apps/server/src/provider/Services/ProviderAdapter.ts", - "start": 126, - "end": 134, + "start": 149, + "end": 157, "language": "typescript", "label": "Canonical provider event stream", "code": " /**\n * Stop all sessions owned by this adapter.\n */\n readonly stopAll: () => Effect.Effect;\n\n /**\n * Canonical runtime event stream emitted by this adapter.\n */\n readonly streamEvents: Stream.Stream;", @@ -66,8 +66,8 @@ "end": 259, "language": "typescript", "label": "Atomic command commit", - "code": " const committedCommand = yield* sql\n .withTransaction(\n Effect.gen(function* () {\n const committedEvents: OrchestrationEvent[] = [];\n let nextCommandReadModel = commandReadModel;\n\n for (const nextEvent of eventBases) {\n const savedEvent = yield* eventStore.append(nextEvent);\n nextCommandReadModel = yield* projectEvent(nextCommandReadModel, savedEvent);\n yield* projectionPipeline.projectEvent(savedEvent);\n committedEvents.push(savedEvent);\n }\n\n const lastSavedEvent = committedEvents.at(-1) ?? null;\n if (lastSavedEvent === null) {\n return yield* new OrchestrationCommandInvariantError({\n commandType: envelope.command.type,\n detail: \"Command produced no events.\",\n });\n }\n\n yield* commandReceiptRepository.upsert({\n commandId: envelope.command.commandId,\n aggregateKind: lastSavedEvent.aggregateKind,\n aggregateId: lastSavedEvent.aggregateId,\n acceptedAt: lastSavedEvent.occurredAt,\n resultSequence: lastSavedEvent.sequence,\n status: \"accepted\",\n error: null,\n });\n\n return {\n committedEvents,\n lastSequence: lastSavedEvent.sequence,\n nextCommandReadModel,\n } as const;\n }),\n )\n .pipe(\n Effect.catchTag(\"SqlError\", (sqlError) =>\n Effect.fail(\n toPersistenceSqlError(\"OrchestrationEngine.processEnvelope:transaction\")(sqlError),\n ),\n ),\n );\n\n commandReadModel = committedCommand.nextCommandReadModel;\n for (const [index, event] of committedCommand.committedEvents.entries()) {\n yield* PubSub.publish(eventPubSub, event);\n if (index === 0) {\n yield* Metric.update(\n Metric.withAttributes(\n orchestrationCommandAckDuration,\n metricAttributes({\n ...baseMetricAttributes,\n ackEventType: event.type,\n }),\n ),\n Duration.millis(Math.max(0, (yield* Clock.currentTimeMillis) - envelope.startedAtMs)),\n );\n }\n }\n return { sequence: committedCommand.lastSequence };", - "checksum": "24e00da0c432e501172e3847817fde311d8656b07e3b8261c657ecf6b4c35d4c" + "code": " }))\n ) {\n return yield* new OrchestrationCommandInvariantError({\n commandType: envelope.command.type,\n detail: `thread ${envelope.command.threadId} was recreated before pull request discovery`,\n });\n }\n\n if (\n envelope.command.type === \"thread.auto-settle\" &&\n threadBackgroundLiveness.getThreadBackgroundLiveness(envelope.command.threadId) !== null\n ) {\n return yield* new OrchestrationCommandInvariantError({\n commandType: envelope.command.type,\n detail: `thread ${envelope.command.threadId} has live background work`,\n });\n }\n\n // New and moved projects do not carry a resolved identity in the event-derived\n // command model. Legacy PR edits need it to identify the link they replace.\n if (\n envelope.command.type === \"thread.meta.update\" &&\n envelope.command.linkedPullRequest !== undefined\n ) {\n const threadId = envelope.command.threadId;\n const thread = commandReadModel.threads.find((thread) => thread.id === threadId);\n if (thread !== undefined) {\n const project = yield* projectionSnapshotQuery.getProjectShellById(thread.projectId);\n if (Option.isSome(project)) {\n commandReadModel = {\n ...commandReadModel,\n projects: commandReadModel.projects.map((entry) =>\n entry.id === thread.projectId\n ? { ...entry, repositoryIdentity: project.value.repositoryIdentity }\n : entry,\n ),\n };\n }\n }\n }\n\n // Command snapshots omit activities at startup and cap them while running.\n // Read this request's durable state before deciding how to send the answer.\n const userInputActivity =\n envelope.command.type === \"thread.user-input.respond\" ||\n envelope.command.type === \"thread.user-input.dismiss\"\n ? yield* projectionSnapshotQuery.getUserInputActivity(envelope.command)\n : Option.none();\n const eventBase = yield* decideOrchestrationCommand({\n command: envelope.command,\n readModel: commandReadModel,\n ...(Option.isSome(userInputActivity)\n ? { userInputActivity: userInputActivity.value }\n : {}),\n }).pipe(\n Effect.provideService(Crypto.Crypto, crypto),\n Effect.mapError((cause) =>\n isOrchestrationCommandRejection(cause)\n ? cause\n : new OrchestrationCommandInvariantError({\n commandType: envelope.command.type,\n detail: \"Failed to generate an event identifier.\",\n cause,", + "checksum": "2898a21a30eccdb0c5f3025b4102b4399ed2a52c8bd32d1457c7c4fe6f15e3b1" }, "orchestration-dispatch": { "id": "orchestration-dispatch", @@ -76,8 +76,8 @@ "end": 368, "language": "typescript", "label": "Serialized command dispatch and event fan-out", - "code": " yield* projectionPipeline.bootstrap;\n commandReadModel = yield* projectionSnapshotQuery.getCommandReadModel();\n\n const worker = Effect.forever(Queue.take(commandQueue).pipe(Effect.flatMap(processEnvelope)));\n yield* Effect.forkScoped(worker);\n yield* Effect.logDebug(\"orchestration engine started\").pipe(\n Effect.annotateLogs({ sequence: commandReadModel.snapshotSequence }),\n );\n\n const readEvents: OrchestrationEngineShape[\"readEvents\"] = (fromSequenceExclusive, limit) =>\n eventStore.readFromSequence(fromSequenceExclusive, limit);\n\n const dispatch: OrchestrationEngineShape[\"dispatch\"] = (command, options) =>\n Effect.gen(function* () {\n const result = yield* Deferred.make<{ sequence: number }, OrchestrationDispatchError>();\n yield* Queue.offer(commandQueue, {\n command,\n origin: options?.origin,\n result,\n startedAtMs: yield* Clock.currentTimeMillis,\n });\n return yield* Deferred.await(result);\n });\n\n return {\n readEvents,\n dispatch,\n // Each access creates a fresh PubSub subscription so that multiple\n // consumers (wsServer, ProviderRuntimeIngestion, CheckpointReactor, etc.)\n // each independently receive all domain events.\n get streamDomainEvents(): OrchestrationEngineShape[\"streamDomainEvents\"] {\n return Stream.fromPubSub(eventPubSub);\n },\n // The command read model's snapshotSequence tracks the latest committed\n // event sequence (updated on the worker fiber). A plain property read is a\n // consistent, committed value — reassignment of `commandReadModel` is\n // atomic on the single-threaded event loop.\n latestSequence: Effect.sync(() => commandReadModel.snapshotSequence),", - "checksum": "85e20350d344ef6e865480addc97f37638fb833acbee999092d2a1b537019bfa" + "code": " orchestrationCommandAckDuration,\n metricAttributes({\n ...baseMetricAttributes,\n ackEventType: event.type,\n }),\n ),\n Duration.millis(Math.max(0, (yield* Clock.currentTimeMillis) - envelope.startedAtMs)),\n );\n }\n }\n return { sequence: committedCommand.lastSequence };\n }).pipe(Effect.withSpan(`orchestration.command.${envelope.command.type}`)),\n ).pipe(\n Effect.flatMap((exit) =>\n Effect.gen(function* () {\n const outcome = Exit.isSuccess(exit)\n ? \"success\"\n : Cause.hasInterruptsOnly(exit.cause)\n ? \"interrupt\"\n : \"failure\";\n yield* Metric.update(\n Metric.withAttributes(\n orchestrationCommandDuration,\n metricAttributes(baseMetricAttributes),\n ),\n Duration.millis(Math.max(0, (yield* Clock.currentTimeMillis) - processingStartedAtMs)),\n );\n yield* Metric.update(\n Metric.withAttributes(\n orchestrationCommandsTotal,\n metricAttributes({\n ...baseMetricAttributes,\n outcome,\n }),\n ),\n 1,\n );\n", + "checksum": "b1989f4bfef9d979d1f2042cde9f3ff2c60b5231642c7c8146ba7d222b7aefb3" }, "server-runtime-core": { "id": "server-runtime-core", @@ -86,8 +86,8 @@ "end": 416, "language": "typescript", "label": "Authentication, remote endpoint, provider, and server dependency assembly", - "code": "const AuthLayerLive = EnvironmentAuth.layer.pipe(\n Layer.provideMerge(PersistenceLayerLive),\n Layer.provide(ServerSecretStore.layer),\n);\n\nconst CloudManagedEndpointRuntimeLive = Layer.mergeAll(\n RelayClientLive,\n CloudManagedEndpointRuntime.layer.pipe(\n Layer.provide(ServerSecretStore.layer),\n Layer.provide(RelayClientLive),\n ),\n);\n\nconst ProviderRuntimeLayerLive = ProviderSessionReaperLive.pipe(\n Layer.provideMerge(ProviderLayerLive),\n Layer.provideMerge(OrchestrationLayerLive),\n);\n\nconst RuntimeCoreDependenciesLive = ReactorLayerLive.pipe(\n // Core Services\n Layer.provideMerge(ServerSettingsLayerLive),\n Layer.provideMerge(CheckpointingLayerLive),\n Layer.provideMerge(SourceControlProviderRegistryLayerLive),\n Layer.provideMerge(GitLayerLive),\n Layer.provideMerge(VcsLayerLive),\n Layer.provideMerge(ProviderRuntimeLayerLive),\n Layer.provideMerge(Layer.mergeAll(TerminalLayerLive, PreviewLayerLive)),\n Layer.provideMerge(PersistenceLayerLive),\n Layer.provideMerge(Keybindings.layer),\n Layer.provideMerge(ProviderRegistryLive),\n // The instance registry is the new routing keystone — text generation,\n // adapter lookup, and runtime ingestion all resolve `ProviderInstanceId`\n // through this layer. Built-in drivers come from `BUILT_IN_DRIVERS`;\n // `providerInstances` hydration merges `settings.providers.`\n // with explicit `providerInstances` entries on boot.\n Layer.provideMerge(ProviderInstanceRegistryHydrationLive),\n // Shared native/canonical NDJSON writers used by both the per-instance\n // drivers (native stream, written from inside each `Adapter`) and\n // `ProviderService` (canonical stream, written after event normalization).\n // Provided once at the runtime level so every consumer sees the same\n // logger instances.\n Layer.provideMerge(ProviderEventLoggers.layer),\n // `OpenCodeDriver.create()` yields `OpenCodeRuntime`; previously the old\n // `ProviderRegistryLive` pulled `OpenCodeRuntimeLive` in for itself, but\n // the rewritten registry reads snapshots off the instance registry and\n // no longer transitively provides it. Exposing it at the runtime level\n // keeps a single Live for all opencode consumers.\n Layer.provideMerge(OpenCodeRuntime.OpenCodeRuntimeLive),\n Layer.provideMerge(WorkspaceLayerLive),\n Layer.provideMerge(ProjectFaviconResolverLayerLive),\n Layer.provideMerge(RepositoryIdentityResolver.layer),\n Layer.provideMerge(ServerEnvironment.layer),\n Layer.provideMerge(AuthLayerLive),\n Layer.provideMerge(ServerSecretStore.layer),\n Layer.provideMerge(\n Layer.mergeAll(\n CloudCliTokenManager.layer.pipe(\n Layer.provide(ServerSecretStore.layer),\n Layer.provide(ExternalLauncher.layer),\n ),\n CloudManagedEndpointRuntimeLive,\n ),\n ),\n);", - "checksum": "11cc88459d4db7e4b29b6b738dcdaba486d8385f6cec6e7dde0d7851c7ada9c5" + "code": " Layer.provideMerge(VcsDriverRegistryLayerLive),\n);\n\nconst VcsLayerLive = Layer.empty.pipe(\n Layer.provideMerge(VcsProjectConfig.layer),\n Layer.provideMerge(VcsDriverRegistryLayerLive),\n Layer.provideMerge(VcsProvisioningService.layer.pipe(Layer.provide(VcsDriverRegistryLayerLive))),\n Layer.provideMerge(GitWorkflowLayerLive),\n Layer.provideMerge(ReviewLayerLive),\n Layer.provideMerge(SourceControlRepositoryServiceLayerLive),\n Layer.provideMerge(\n VcsStatusBroadcaster.layer.pipe(\n Layer.provide(GitWorkflowLayerLive),\n Layer.provide(\n VcsStatusBroadcaster.autoPullPolicyLayer.pipe(Layer.provide(ServerSettingsLayerLive)),\n ),\n ),\n ),\n);\n\nconst CheckpointingLayerLive = Layer.empty.pipe(\n Layer.provideMerge(CheckpointDiffQuery.layer),\n Layer.provideMerge(CheckpointStore.layer.pipe(Layer.provide(VcsDriverRegistryLayerLive))),\n);\n\nconst PortScannerLayerLive = PortScanner.layer.pipe(Layer.provide(ProcessRunner.layer));\n\nconst TerminalLayerLive = TerminalManager.layer.pipe(\n Layer.provide(PtyAdapterLive),\n Layer.provide(PortScannerLayerLive),\n Layer.provide(NativeTelemetryLayerLive),\n);\n\nconst PreviewLayerLive = Layer.empty.pipe(\n Layer.provideMerge(PreviewManager.layer),\n Layer.provideMerge(PortScannerLayerLive),\n);\n\nconst DeviceLayerLive = DeviceService.layer.pipe(\n Layer.provide(ServerSettingsLayerLive),\n Layer.provide(ProcessRunner.layer),\n Layer.provide(NetService.layer),\n);\n\nconst WorkspaceEntriesLayerLive = WorkspaceEntries.layer.pipe(Layer.provide(WorkspacePaths.layer));\n\nconst WorkspaceFileSystemLayerLive = WorkspaceFileSystem.layer.pipe(\n Layer.provide(WorkspacePaths.layer),\n Layer.provide(WorkspaceEntriesLayerLive),\n);\n\nconst WorkspaceLayerLive = Layer.mergeAll(\n WorkspacePaths.layer,\n WorkspaceEntriesLayerLive,\n WorkspaceFileSystemLayerLive,\n);\n\nconst ProjectFaviconResolverLayerLive = ProjectFaviconResolver.layer.pipe(\n Layer.provide(WorkspacePaths.layer),\n Layer.provide(T3ProjectFileLoader.layer),\n);\n\nconst ServerEnvironmentLayerLive = ServerEnvironment.layer.pipe(\n Layer.provide(ServerSecretStore.layer),", + "checksum": "4e3a4403c903652b640fcfd7170d247961682838fa01b5f9848dd9f650b0ff7e" }, "dispatch-result": { "id": "dispatch-result", @@ -96,8 +96,8 @@ "end": 1583, "language": "typescript", "label": "Successful client dispatch result", - "code": "export const DispatchResult = Schema.Struct({\n sequence: NonNegativeInt,\n});\nexport type DispatchResult = typeof DispatchResult.Type;", - "checksum": "151b83cdd47b9a449a38bb64d9f7b37ccd31ab6f109753cb6535214e12bf98de" + "code": " modelSelection: ModelSelection,\n runtimeMode: RuntimeMode.pipe(Schema.withDecodingDefault(Effect.succeed(DEFAULT_RUNTIME_MODE))),\n interactionMode: ProviderInteractionMode.pipe(\n Schema.withDecodingDefault(Effect.succeed(DEFAULT_PROVIDER_INTERACTION_MODE)),", + "checksum": "c15ad7fe4de3ed5f5df2a96cd5727d5d036c1889cda00dd343eb3ba47d1bc104" }, "rejected-receipt-best-effort": { "id": "rejected-receipt-best-effort", @@ -106,8 +106,8 @@ "end": 325, "language": "typescript", "label": "Best-effort invariant-rejection receipt", - "code": " if (Exit.isSuccess(exit)) {\n yield* Deferred.succeed(envelope.result, exit.value);\n return;\n }\n\n const error = Cause.squash(exit.cause) as OrchestrationDispatchError;\n if (\n !isOrchestrationCommandPreviouslyRejectedError(error) &&\n !isOrchestrationCommandIdConflictError(error)\n ) {\n yield* reconcileReadModelAfterDispatchFailure.pipe(\n Effect.catch(() =>\n Effect.logWarning(\n \"failed to reconcile orchestration read model after dispatch failure\",\n ).pipe(\n Effect.annotateLogs({\n commandId: envelope.command.commandId,\n snapshotSequence: commandReadModel.snapshotSequence,\n }),\n ),\n ),\n );\n\n if (isOrchestrationCommandInvariantError(error)) {\n yield* commandReceiptRepository\n .upsert({\n commandId: envelope.command.commandId,\n aggregateKind: aggregateRef.aggregateKind,\n aggregateId: aggregateRef.aggregateId,\n acceptedAt: yield* nowIso,\n resultSequence: commandReadModel.snapshotSequence,\n status: \"rejected\",\n error: error.message,\n })\n .pipe(Effect.catch(() => Effect.void));\n }\n }\n\n yield* Deferred.fail(envelope.result, error);", - "checksum": "baa86ecd4a4a583386c539189ea6d26d3b4c6377201a660a9d22b2eb52a7645b" + "code": "\n const lastSavedEvent = committedEvents.at(-1) ?? null;\n if (lastSavedEvent === null) {\n return yield* new OrchestrationCommandInvariantError({\n commandType: envelope.command.type,\n detail: \"Command produced no events.\",\n });\n }\n\n yield* commandReceiptRepository.upsert({\n commandId: envelope.command.commandId,\n aggregateKind: lastSavedEvent.aggregateKind,\n aggregateId: lastSavedEvent.aggregateId,\n acceptedAt: lastSavedEvent.occurredAt,\n resultSequence: lastSavedEvent.sequence,\n status: \"accepted\",\n error: null,\n });\n\n return {\n committedEvents,\n attachmentCleanups,\n lastSequence: lastSavedEvent.sequence,\n nextCommandReadModel,\n } as const;\n }),\n )\n .pipe(\n Effect.catchTag(\"SqlError\", (sqlError) =>\n Effect.fail(\n toPersistenceSqlError(\"OrchestrationEngine.processEnvelope:transaction\")(sqlError),\n ),\n ),\n );\n\n commandReadModel = committedCommand.nextCommandReadModel;\n for (const cleanup of committedCommand.attachmentCleanups) {\n yield* cleanup;\n }", + "checksum": "718ac736f577ea6a48769736cf88f3b0a0508f7965b278d5db2e95a2a7dba89a" }, "hot-reactor-no-replay": { "id": "hot-reactor-no-replay", @@ -116,8 +116,8 @@ "end": 1409, "language": "typescript", "label": "Hot post-commit stream has no pending-work replay", - "code": " yield* forkParked(Stream.runForEach(orchestrationEngine.streamDomainEvents, processEvent));\n\n // The domain event stream is hot, so work pending before this reactor\n // starts cannot be resumed. Correlated completions only clear the request\n // captured here, leaving any newer request untouched.", - "checksum": "fe4e62df9690c497361768207b76ed48bb65364abc5ef5b123321deb6c0277ee" + "code": " }\n yield* providerService.compactThread(\n event.payload.threadId,\n event.payload.modelSelection,\n event.payload.messageId,", + "checksum": "2543de64fa654caf4e5d8b130f721b9cc2b453f0b3ec4dd0d93434f375d0198b" }, "cli-entry-command": { "id": "cli-entry-command", @@ -126,8 +126,8 @@ "end": 71, "language": "typescript", "label": "Root CLI and subcommand dispatch", - "code": "const connectPublicConfigMissingMessage =\n \"T3 Connect commands are unavailable: this build is missing T3 Connect public configuration.\";\n\nclass ConnectPublicConfigMissingError extends CliError.UserError {\n override get message() {\n return connectPublicConfigMissingMessage;\n }\n}\n\nconst connectUnavailableCommand = Command.make(\"connect\", {\n command: Argument.string(\"command\").pipe(Argument.variadic),\n}).pipe(\n Command.withDescription(\"T3 Connect is unavailable in builds without public configuration.\"),\n Command.withHidden,\n Command.withHandler(() =>\n Effect.fail(\n new CliError.ShowHelp({\n commandPath: [\"t3\", \"connect\"],\n errors: [new ConnectPublicConfigMissingError({ cause: connectPublicConfigMissingMessage })],\n }),\n ),\n ),\n);\n\nexport const makeCli = ({ cloudEnabled = hasCloudPublicConfig } = {}) =>\n Command.make(\"t3\", { ...sharedServerCommandFlags }).pipe(\n Command.withDescription(\"Run the T3 Code server.\"),\n Command.withHandler((flags) => runServerCommand(flags)),\n Command.withSubcommands([\n startCommand,\n serveCommand,\n pairCommand,\n authCommand,\n projectCommand,\n serviceCommand,\n servicePreflightCommand,\n triageCommand,\n cloudEnabled ? connectCommand : connectUnavailableCommand,\n ]),\n );\n\nexport const cli = makeCli();\n\nif (import.meta.main) {\n Command.run(cli, { version: packageJson.version }).pipe(\n Effect.scoped,\n Effect.provide(CliRuntimeLayer),\n NodeRuntime.runMain,\n );", - "checksum": "4cbb1e2584966a2f8399dbdf57f860033d79e1da2aba843e49a008d4a4cdc2ab" + "code": "\nconst CliRuntimeLayer = Layer.mergeAll(NodeServices.layer, NetService.layer);\n\nconst connectPublicConfigMissingMessage =\n \"T3 Connect commands are unavailable: this build is missing T3 Connect public configuration.\";\n\nclass ConnectPublicConfigMissingError extends CliError.UserError {\n override get message() {\n return connectPublicConfigMissingMessage;\n }\n}\n\nconst connectUnavailableCommand = Command.make(\"connect\", {\n command: Argument.string(\"command\").pipe(Argument.variadic),\n}).pipe(\n Command.withDescription(\"T3 Connect is unavailable in builds without public configuration.\"),\n Command.unlisted,\n Command.withHandler(() =>\n Effect.fail(\n new CliError.ShowHelp({\n commandPath: [\"t3\", \"connect\"],\n errors: [new ConnectPublicConfigMissingError({ cause: connectPublicConfigMissingMessage })],\n }),\n ),\n ),\n);\n\nexport const makeCli = ({ cloudEnabled = hasCloudPublicConfig } = {}) =>\n Command.make(\"t3\", { ...sharedServerCommandFlags }).pipe(\n Command.withDescription(\"Run the T3 Code server.\"),\n Command.withHandler((flags) => runServerCommand(flags)),\n Command.withSubcommands([\n startCommand,\n serveCommand,\n appCommand,\n pairCommand,\n authCommand,\n projectCommand,\n serviceCommand,\n servicePreflightCommand,\n themeCommand,\n triageCommand,\n cloudEnabled ? connectCommand : connectUnavailableCommand,\n ]),\n );\n\nexport const cli = makeCli();\n\nif (", + "checksum": "b672315a457dcdff8bd676410b58284f5b11ec4210cae9cb2a30091206497259" }, "cli-config-precedence": { "id": "cli-config-precedence", @@ -136,8 +136,8 @@ "end": 280, "language": "typescript", "label": "Mode, port, and base-directory precedence", - "code": " const mode: ServerConfig.RuntimeMode = Option.getOrElse(\n resolveOptionPrecedence(\n normalizedFlags.mode,\n Option.fromUndefinedOr(env.mode),\n Option.fromUndefinedOr(bootstrap?.mode),\n ),\n () => \"web\",\n );\n\n const port = yield* Option.match(\n resolveOptionPrecedence(\n normalizedFlags.port,\n Option.fromUndefinedOr(env.port),\n Option.fromUndefinedOr(bootstrap?.port),\n ),\n {\n onSome: (value) => Effect.succeed(value),\n onNone: () => {\n if (mode === \"desktop\") {\n return Effect.succeed(ServerConfig.DEFAULT_PORT);\n }\n return findAvailablePort(ServerConfig.DEFAULT_PORT);\n },\n },\n );\n const devUrl = Option.getOrElse(\n resolveOptionPrecedence(normalizedFlags.devUrl, Option.fromUndefinedOr(env.devUrl)),\n () => undefined,\n );\n const explicitBaseDir = resolveOptionPrecedence(\n normalizedFlags.baseDir,\n Option.fromUndefinedOr(env.t3Home),\n ).pipe(Option.filter((value) => value.trim().length > 0));\n const baseDir = yield* resolveBaseDir(\n Option.getOrUndefined(\n resolveOptionPrecedence(explicitBaseDir, Option.fromUndefinedOr(bootstrap?.t3Home)),\n ),", - "checksum": "f772af33c3c09ffd6b1641fe1391724758215ae054421feba0e93fc83f736d87" + "code": " normalizedFlags.mode,\n Option.fromUndefinedOr(env.mode),\n Option.fromUndefinedOr(bootstrap?.mode),\n ),\n () => \"web\",\n );\n\n const port = yield* Option.match(\n resolveOptionPrecedence(\n normalizedFlags.port,\n Option.fromUndefinedOr(env.port),\n Option.fromUndefinedOr(bootstrap?.port),\n ),\n {\n onSome: (value) => Effect.succeed(value),\n onNone: () => {\n if (mode === \"desktop\") {\n return Effect.succeed(ServerConfig.DEFAULT_PORT);\n }\n return findAvailablePort(ServerConfig.DEFAULT_PORT);\n },\n },\n );\n const devUrl = Option.getOrElse(\n resolveOptionPrecedence(normalizedFlags.devUrl, Option.fromUndefinedOr(env.devUrl)),\n () => undefined,\n );\n const explicitBaseDir = resolveOptionPrecedence(\n normalizedFlags.baseDir,\n Option.fromUndefinedOr(env.t3Home),\n ).pipe(Option.filter((value) => value.trim().length > 0));\n const baseDir = yield* resolveBaseDir(\n Option.getOrUndefined(\n resolveOptionPrecedence(explicitBaseDir, Option.fromUndefinedOr(bootstrap?.t3Home)),\n ),\n );\n const rawCwd = Option.getOrElse(normalizedFlags.cwd, () => process.cwd());", + "checksum": "605d6e7673ffea12d1baeaa32afc99d1d6da0c74921c1d7707114ac9c90fe863" }, "cli-serve-semantics": { "id": "cli-serve-semantics", @@ -156,8 +156,8 @@ "end": 548, "language": "typescript", "label": "Prepared, activated, command-ready, and ready order", - "code": " yield* Effect.logDebug(\"startup phase: waiting for http listener\");\n yield* runStartupPhase(\"http.wait\", Deferred.await(httpListening));\n yield* runStartupPhase(\n \"auxiliary-roots.parked\",\n options?.awaitAuxiliaryParked ?? Effect.void,\n );\n\n // This is the prepared boundary. Every dependency has been acquired and\n // every runtime root has confirmed that it is parked before this request.\n const updateOutcome = yield* launcher.prepareTrial;\n yield* runStartupPhase(\n \"welcome.publish\",\n lifecycleEvents.publish({\n version: 1,\n type: \"welcome\",\n payload: { environment, ...welcomeBase },\n }),\n );\n yield* options?.activate ?? Effect.void;\n\n yield* Effect.logDebug(\"Accepting commands\");\n yield* commandGate.signalCommandReady;\n yield* runStartupPhase(\n \"ready.publish\",\n lifecycleEvents.publish({\n version: 1,\n type: \"ready\",\n payload: {\n at: DateTime.formatIso(yield* DateTime.now),\n environment,\n ...(updateOutcome === undefined ? {} : { updateOutcome }),\n },\n }),\n );\n yield* Effect.logDebug(\"startup phase: complete\");", - "checksum": "8ed6988b351036573a062e87716f4a1154d4d3e69710d8041f6b849965ced364" + "code": " ),\n ))\n .filter(\n (binding) =>\n readServerUpdateContinuationTurnId(binding.runtimePayload) !== null &&\n readRuntimePayload(binding.runtimePayload).activeTurnId === null &&\n readRuntimePayload(binding.runtimePayload).continueAfterServerUpdatePrepared === true,\n )\n .map((binding) => binding.threadId),\n );\n const orphanedThreads = threads.filter(\n (thread) =>\n thread.session !== null &&\n (thread.session.status === \"starting\" ||\n thread.session.status === \"running\" ||\n thread.session.activeTurnId !== null ||\n (thread.session.status === \"ready\" && preparedThreadIds.has(thread.id))) &&\n !liveThreadIds.has(thread.id),\n );\n\n for (const thread of orphanedThreads) {\n const session = thread.session;\n if (session === null) {\n continue;\n }\n const binding = yield* directory.getBinding(thread.id).pipe(\n Effect.catchCause((cause) =>\n Cause.hasInterrupts(cause)\n ? Effect.failCause(cause)\n : Effect.logWarning(\"failed to read orphaned provider session directory binding\", {\n threadId: thread.id,\n cause,\n }).pipe(Effect.as(Option.none())),\n ),\n );", + "checksum": "1911fc48440331a34e0c2c573728d58f8c23836d86c11ce0c8c9e15adf51e636" }, "command-readiness-middleware": { "id": "command-readiness-middleware", @@ -166,8 +166,8 @@ "end": 437, "language": "typescript", "label": "Global route-execution readiness barrier", - "code": "const commandReadinessLayer = HttpRouter.middleware(\n (httpEffect) =>\n Effect.flatMap(ServerRuntimeStartup.ServerRuntimeStartup, (startup) =>\n startup.awaitCommandReady.pipe(Effect.orDie, Effect.andThen(httpEffect)),\n ),\n { global: true },\n);", - "checksum": "656843f40a4ec8a4e2b1a5dca6671d9264c33edd92731693262a960972ff770f" + "code": ");\n\nconst ProviderRuntimeLayerLive = ProviderSessionReaperLive.pipe(\n // Subscribes to `account.rate-limits.updated` so usage bars track live\n // telemetry instead of waiting for the next status probe.\n Layer.provideMerge(ProviderUsageLimitsIngestionLive),\n Layer.provideMerge(ProviderLayerLive),", + "checksum": "ce8f283b63d93576ed74792f6fae7e07a0642542714ef277983bc4f6aa729af1" }, "rpc-stream-contracts": { "id": "rpc-stream-contracts", @@ -176,8 +176,8 @@ "end": 948, "language": "typescript", "label": "Typed shell and thread streaming RPC contracts", - "code": "export const WsOrchestrationSubscribeShellRpc = Rpc.make(ORCHESTRATION_WS_METHODS.subscribeShell, {\n payload: OrchestrationRpcSchemas.subscribeShell.input,\n success: OrchestrationRpcSchemas.subscribeShell.output,\n error: Schema.Union([OrchestrationGetSnapshotError, EnvironmentAuthorizationError]),\n stream: true,\n});\n\nexport const WsOrchestrationSubscribeThreadRpc = Rpc.make(\n ORCHESTRATION_WS_METHODS.subscribeThread,\n {\n payload: OrchestrationRpcSchemas.subscribeThread.input,\n success: OrchestrationRpcSchemas.subscribeThread.output,\n error: Schema.Union([OrchestrationGetSnapshotError, EnvironmentAuthorizationError]),\n stream: true,\n },\n);", - "checksum": "efb34f4d793fed351ef25bd2e61b7c448bea8748c2edede590ba9a0f72336b0b" + "code": "const WsGitResolvePullRequestRpc = Rpc.make(WS_METHODS.gitResolvePullRequest, {\n payload: GitPullRequestRefInput,\n success: GitResolvePullRequestResult,\n error: Schema.Union([GitManagerServiceError, EnvironmentAuthorizationError]),\n});\n\nconst WsGitPreparePullRequestThreadRpc = Rpc.make(WS_METHODS.gitPreparePullRequestThread, {\n payload: GitPreparePullRequestThreadInput,\n success: GitPreparePullRequestThreadResult,\n error: Schema.Union([GitManagerServiceError, EnvironmentAuthorizationError]),\n});\n\nconst WsVcsListRefsRpc = Rpc.make(WS_METHODS.vcsListRefs, {\n payload: VcsListRefsInput,\n success: VcsListRefsResult,\n error: Schema.Union([GitCommandError, EnvironmentAuthorizationError]),", + "checksum": "5c04d03d47d49264c3385283176aa49c54b12db34b5622215a7bcbedae0ac7af" }, "thread-resume-race": { "id": "thread-resume-race", @@ -186,8 +186,8 @@ "end": 1521, "language": "typescript", "label": "Attach-before-read and bounded thread replay", - "code": " [ORCHESTRATION_WS_METHODS.subscribeThread]: (input) =>\n observeRpcStreamEffect(\n ORCHESTRATION_WS_METHODS.subscribeThread,\n Effect.gen(function* () {\n const isThisThreadDetailEvent = (event: OrchestrationEvent) =>\n event.aggregateKind === \"thread\" &&\n event.aggregateId === input.threadId &&\n isThreadDetailEvent(event);\n\n const liveStream = orchestrationEngine.streamDomainEvents.pipe(\n Stream.filter(isThisThreadDetailEvent),\n Stream.map((event) => ({\n kind: \"event\" as const,\n event: projectActivityEvent(event),\n })),\n );\n\n // Attach live delivery before reading either replay or snapshot state.\n // Otherwise an event published while the snapshot is loading is lost.\n const liveBuffer = yield* Queue.unbounded();\n yield* Effect.forkScoped(\n liveStream.pipe(Stream.runForEach((item) => Queue.offer(liveBuffer, item))),\n );\n const bufferedLiveStream = Stream.fromQueue(liveBuffer);\n\n // When the client already loaded the snapshot over HTTP it passes\n // that snapshot's sequence, and we resume the live subscription by\n // replaying persisted events after it instead of re-sending the\n // (potentially multi-KB) snapshot frame over the socket.\n //\n // The live PubSub subscription must be attached *before* draining\n // the catch-up replay, otherwise events published during the replay\n // window are dropped (they are past the persisted tail the replay\n // read, but the live stream is not yet subscribed). So fork the\n // live stream into a buffer bound to this stream's scope, then emit\n // catch-up followed by the buffered/ongoing live events. Overlapping\n // events are deduped by sequence on the client.\n //\n // The replay is bounded to the projection head captured below. The\n // catch-up range is normally tiny (a fresh HTTP snapshot sequence),\n // but a stale cached cursor can sit hundreds of thousands of global\n // events behind — replaying that decodes every intervening event\n // (including every other thread's tool payloads) only to discard\n // almost all of them, which has OOM-killed servers on large\n // databases. A truncated replay would silently drop this thread's\n // events, so past the gap cap we reset the client with a fresh\n // thread snapshot instead, exactly like subscribeShell above.\n if (input.afterSequence !== undefined) {\n const afterSequence = input.afterSequence;\n const headSequence = yield* orchestrationEngine.latestSequence;\n const replayGap = headSequence - afterSequence;\n if (replayGap >= 0 && replayGap <= THREAD_RESUME_MAX_GAP) {\n const catchUpStream = orchestrationEngine\n .readEvents(afterSequence, replayGap)\n .pipe(\n Stream.filter(isThisThreadDetailEvent),\n Stream.map((event) => ({\n kind: \"event\" as const,\n event: projectActivityEvent(event),\n })),\n Stream.mapError(\n (cause) =>\n new OrchestrationGetSnapshotError({\n message: `Failed to replay thread ${input.threadId} events`,\n cause,\n }),\n ),\n );\n const afterCatchUp =\n input.requestCompletionMarker === true\n ? Stream.concat(\n Stream.fromEffect(\n Queue.offer(liveBuffer, { kind: \"synchronized\" as const }),\n ).pipe(Stream.drain),\n bufferedLiveStream,\n )\n : bufferedLiveStream;\n return Stream.concat(catchUpStream, afterCatchUp);\n }\n // Gap too large (or cursor ahead of authoritative state): fall\n // through to the snapshot path so the client converges from a\n // fresh thread detail instead of an unbounded replay.\n }\n\n const snapshot = yield* projectionSnapshotQuery\n .getThreadDetailSnapshot(\n input.threadId,\n // Windowing the fallback snapshot is opt-in per subscription:\n // clients that don't send turnLimit (including all\n // pre-pagination clients) get the full thread, since they\n // have no way to load older pages.\n input.turnLimit === undefined ? undefined : { turnLimit: input.turnLimit },\n )\n .pipe(\n Effect.mapError(\n (cause) =>\n new OrchestrationGetSnapshotError({\n message: `Failed to load thread ${input.threadId}`,\n cause,\n }),\n ),\n );\n\n if (Option.isNone(snapshot)) {\n return yield* new OrchestrationGetSnapshotError({\n message: `Thread ${input.threadId} was not found`,\n cause: input.threadId,\n });\n }\n\n const afterSnapshot =\n input.requestCompletionMarker === true\n ? Stream.concat(\n Stream.fromEffect(\n Queue.offer(liveBuffer, { kind: \"synchronized\" as const }),\n ).pipe(Stream.drain),\n bufferedLiveStream,\n )\n : bufferedLiveStream;\n return Stream.concat(\n Stream.make({\n kind: \"snapshot\" as const,\n snapshot: projectThreadDetailSnapshot(snapshot.value),\n }),\n afterSnapshot,\n );\n }),\n { \"rpc.aggregate\": \"orchestration\" },", - "checksum": "0f57dcc6b6815e4cd36c8eca74f696252ba35e6d8446a41d66f413eb214f54f3" + "code": " message: \"Failed to dispatch orchestration command\",\n cause,\n }),\n ),\n ),\n { \"rpc.aggregate\": \"orchestration\" },\n ),\n [ORCHESTRATION_WS_METHODS.getWorkflowScript]: (input) =>\n observeRpcEffect(\n ORCHESTRATION_WS_METHODS.getWorkflowScript,\n readWorkflowScript({ scriptPath: input.scriptPath }),\n { \"rpc.aggregate\": \"orchestration\" },\n ),\n [ORCHESTRATION_WS_METHODS.getTurnDiff]: (input) =>\n observeRpcEffect(\n ORCHESTRATION_WS_METHODS.getTurnDiff,\n checkpointDiffQuery.getTurnDiff(input).pipe(\n Effect.mapError(\n (cause) =>\n new OrchestrationGetTurnDiffError({\n message: \"Failed to load turn diff\",\n cause,\n }),\n ),\n ),\n { \"rpc.aggregate\": \"orchestration\" },\n ),\n [ORCHESTRATION_WS_METHODS.getFullThreadDiff]: (input) =>\n observeRpcEffect(\n ORCHESTRATION_WS_METHODS.getFullThreadDiff,\n checkpointDiffQuery.getFullThreadDiff(input).pipe(\n Effect.mapError(\n (cause) =>\n new OrchestrationGetFullThreadDiffError({\n message: \"Failed to load full thread diff\",\n cause,\n }),\n ),\n ),\n { \"rpc.aggregate\": \"orchestration\" },\n ),\n [ORCHESTRATION_WS_METHODS.searchThreads]: (input) =>\n observeRpcEffect(\n ORCHESTRATION_WS_METHODS.searchThreads,\n projectionSnapshotQuery.searchThreads(input).pipe(\n Effect.mapError(\n (cause) =>\n new OrchestrationSearchThreadsError({\n message: \"Failed to search threads\",\n cause,\n }),\n ),\n ),\n { \"rpc.aggregate\": \"orchestration\" },\n ),\n [ORCHESTRATION_WS_METHODS.subscribeShell]: (input) =>\n observeRpcStreamEffect(\n ORCHESTRATION_WS_METHODS.subscribeShell,\n Effect.gen(function* () {\n // Coalesce the live shell stream per aggregate over a small window\n // so bursts of high-frequency events (streaming message deltas,\n // activity appends) collapse into a single shell refetch and never\n // serialize a brand-new thread's `thread.created` behind hundreds\n // of per-event DB reads. See coalesceShellStream.\n // Attach live delivery into a scope-bound buffer BEFORE loading any\n // snapshot or draining catch-up, otherwise an event published while\n // the snapshot query is in flight is lost (it is past the snapshot's\n // sequence but the live subscription is not attached yet). Every\n // path below emits from this same buffered live tail. Overlapping\n // events are deduped by sequence on the client.\n const liveBudget = yield* makeLiveStreamBudget();\n const liveBuffer = yield* Queue.unbounded<\n RetainedLiveItem,\n OrchestrationGetSnapshotError\n >();\n let liveBufferClosed = false;\n const closeLiveBuffer = (error?: OrchestrationGetSnapshotError) =>\n Effect.gen(function* () {\n if (liveBufferClosed) {\n return;\n }\n liveBufferClosed = true;\n liveBudget.release(yield* Queue.clear(liveBuffer).pipe(Effect.orDie));\n if (error) {\n yield* Queue.fail(liveBuffer, error);\n }\n yield* Queue.shutdown(liveBuffer);\n });\n yield* Effect.addFinalizer(() => closeLiveBuffer());\n yield* liveBudget.failed.pipe(\n Effect.catchTags({ OrchestrationGetSnapshotError: closeLiveBuffer }),\n Effect.forkScoped,\n );\n yield* Effect.forkScoped(\n orchestrationEngine.streamDomainEvents.pipe(\n Stream.map(toShellEvent),\n Stream.runForEach((event) =>\n liveBudget.retain({ kind: \"event\" as const, event }, event).pipe(\n Effect.flatMap((item) => Queue.offer(liveBuffer, item)),\n Effect.uninterruptible,\n ),\n ),\n // Stop the PubSub consumer even if RPC delivery is waiting\n // for an ACK and never pulls the failed buffer again.\n Effect.raceFirst(liveBudget.failed),\n Effect.catchTags({ OrchestrationGetSnapshotError: () => Effect.void }),\n ),\n { startImmediately: true },\n );\n const coalesceRetainedInputs = (\n items: ReadonlyArray>,\n ) =>\n coalesceShellLiveInputs(items.map((item) => item.value)).pipe(\n Effect.flatMap((output) => liveBudget.replace(items, output)),\n );\n const bufferedLiveStream = Stream.fromQueue(liveBuffer).pipe(\n Stream.groupedWithin(SHELL_COALESCE_MAX_CHUNK, SHELL_COALESCE_WINDOW),\n Stream.mapEffect(coalesceRetainedInputs),\n Stream.flatMap((items) => Stream.fromIterable(items)),\n );\n\n const loadSnapshot = projectionSnapshotQuery.getShellSnapshot().pipe(\n Effect.tapError((cause) =>\n Effect.logError(\"orchestration shell snapshot load failed\", { cause }),\n ),\n Effect.mapError(\n (cause) =>\n new OrchestrationGetSnapshotError({", + "checksum": "9423f5c9293538ca4abde33fe50a0d1da9b3154d0a039f1555e22761f7356cd0" }, "auth-credential-precedence": { "id": "auth-credential-precedence", @@ -196,8 +196,8 @@ "end": 632, "language": "typescript", "label": "Cookie, Bearer, and DPoP authentication precedence", - "code": " const authenticateRequest = (\n request: HttpServerRequest.HttpServerRequest,\n ): Effect.Effect => {\n const cookieToken = request.cookies[sessions.cookieName];\n const bearerToken = parseBearerToken(request);\n const dpopToken = parseDpopToken(request);\n const credential = cookieToken ?? bearerToken ?? dpopToken;\n if (!credential) {\n return Effect.fail(new ServerAuthMissingCredentialError({}));\n }\n return authenticateToken(credential).pipe(\n Effect.flatMap((session) => {\n if (session.proofKeyThumbprint) {\n if (!dpopToken || dpopToken !== credential) {\n return Effect.fail(\n new ServerAuthInvalidCredentialError({\n diagnostic: \"DPoP-bound access token requires DPoP authorization.\",\n }),\n );\n }\n return verifyRequestDpopProof({\n request,\n expectedThumbprint: session.proofKeyThumbprint,\n expectedAccessToken: dpopToken,\n }).pipe(\n Effect.provideService(ServerSecretStore.ServerSecretStore, secretStore),\n Effect.provideService(Crypto.Crypto, crypto),\n Effect.as(session),\n );\n }\n if (dpopToken) {\n return Effect.fail(\n new ServerAuthInvalidCredentialError({\n diagnostic: \"DPoP authorization requires a proof-bound access token.\",\n }),\n );\n }\n return Effect.succeed(session);\n }),\n );\n };", - "checksum": "d18c91b8d4ceaa79bc875630b87a342328ced1fc8a43556643ea32431af3232d" + "code": "}\n\n/** @public Service construction is part of the canonical Effect module API. */\nexport const make = Effect.gen(function* () {\n const policy = yield* EnvironmentAuthPolicy.EnvironmentAuthPolicy;\n const bootstrapCredentials = yield* PairingGrantStore.PairingGrantStore;\n const sessions = yield* SessionStore.SessionStore;\n const secretStore = yield* ServerSecretStore.ServerSecretStore;\n const crypto = yield* Crypto.Crypto;\n const descriptor = yield* policy.getDescriptor();\n\n const authenticateToken = (\n token: string,\n ): Effect.Effect<\n AuthenticatedSession,\n ServerAuthInvalidCredentialError | ServerAuthInternalError\n > =>\n sessions.verify(token).pipe(\n Effect.tapError((cause) =>\n SessionStore.isSessionCredentialInvalidError(cause)\n ? Effect.logWarning(\"Rejected authenticated session credential.\").pipe(\n Effect.annotateLogs({\n reason: cause.message,\n }),\n )\n : Effect.void,\n ),\n Effect.map((session) => ({\n sessionId: session.sessionId,\n subject: session.subject,\n method: session.method,\n scopes: session.scopes,\n ...(session.proofKeyThumbprint ? { proofKeyThumbprint: session.proofKeyThumbprint } : {}),\n ...(session.expiresAt ? { expiresAt: session.expiresAt } : {}),\n })),\n mapSessionVerificationErrors,\n );\n\n const authenticateRequest = (\n request: HttpServerRequest.HttpServerRequest,\n ): Effect.Effect => {", + "checksum": "4c2332a95e0ae357d5a5622c1913c47860017ca7bf86964014b3081773dbcf96" }, "websocket-ticket-verification": { "id": "websocket-ticket-verification", @@ -206,8 +206,8 @@ "end": 851, "language": "typescript", "label": "WebSocket ticket verification reloads session state", - "code": " const verifyWebSocketToken: SessionStore[\"Service\"][\"verifyWebSocketToken\"] = Effect.fn(\n \"SessionStore.verifyWebSocketToken\",\n )(function* (token) {\n const [encodedPayload, signature] = token.split(\".\");\n if (!encodedPayload || !signature) {\n return yield* new MalformedWebSocketTokenError({});\n }\n\n const expectedSignature = signPayload(encodedPayload, signingSecret);\n if (!timingSafeEqualBase64Url(signature, expectedSignature)) {\n return yield* new InvalidWebSocketTokenSignatureError({});\n }\n\n const claims = yield* decodeWebSocketClaims(base64UrlDecodeUtf8(encodedPayload)).pipe(\n Effect.mapError((cause) => new InvalidWebSocketTokenPayloadError({ cause })),\n );\n\n const observedAt = yield* DateTime.now;\n const expiresAt = DateTime.make(claims.exp);\n if (Option.isNone(expiresAt)) {\n return yield* new InvalidSessionExpirationClaimError({\n sessionId: claims.sid,\n expirationClaim: claims.exp,\n });\n }\n if (claims.exp <= observedAt.epochMilliseconds) {\n return yield* new WebSocketTokenExpiredError({\n sessionId: claims.sid,\n expiresAt: expiresAt.value,\n observedAt,\n });\n }\n\n const row = yield* authSessions\n .getById({ sessionId: claims.sid })\n .pipe(\n Effect.mapError(\n (cause) => new WebSocketTokenVerificationError({ sessionId: claims.sid, cause }),\n ),\n );\n if (Option.isNone(row)) {\n return yield* new UnknownWebSocketSessionError({ sessionId: claims.sid });\n }\n if (row.value.expiresAt.epochMilliseconds <= observedAt.epochMilliseconds) {\n return yield* new WebSocketSessionExpiredError({\n sessionId: claims.sid,\n expiresAt: row.value.expiresAt,\n observedAt,\n });\n }\n if (row.value.revokedAt !== null) {\n return yield* new WebSocketSessionRevokedError({\n sessionId: claims.sid,\n revokedAt: row.value.revokedAt,\n });\n }\n\n return {\n sessionId: row.value.sessionId,\n token,\n method: row.value.method,\n client: toClientMetadata(row.value.client),\n expiresAt: row.value.expiresAt,\n subject: row.value.subject,\n scopes: row.value.scopes,\n } satisfies VerifiedSession;\n });", - "checksum": "dcb5b1a1fbdbcb3efae560e941aeed24343450e553fb345e55b71c1707bd0f9f" + "code": " const expiresAt = DateTime.add(issuedAt, {\n milliseconds: Duration.toMillis(input?.ttl ?? DEFAULT_WEBSOCKET_TOKEN_TTL),\n });\n const claims: WebSocketClaims = {\n v: 1,\n kind: \"websocket\",\n sid: sessionId,\n iat: issuedAt.epochMilliseconds,\n exp: expiresAt.epochMilliseconds,\n };\n const encodedPayload = yield* encodeWsClaims(claims).pipe(\n Effect.map(base64UrlEncode),\n Effect.mapError(\n (cause) =>\n new WebSocketTokenIssueError({\n sessionId,\n cause: new SessionClaimsEncodingError({\n sessionId,\n operation: \"encode_websocket_claims\",\n cause,\n }),\n }),\n ),\n );\n const signature = signPayload(encodedPayload, signingSecret);\n return {\n token: `${encodedPayload}.${signature}`,\n expiresAt,\n };\n });\n\n const verifyWebSocketToken: SessionStore[\"Service\"][\"verifyWebSocketToken\"] = Effect.fn(\n \"SessionStore.verifyWebSocketToken\",\n )(function* (token) {\n const [encodedPayload, signature] = token.split(\".\");\n if (!encodedPayload || !signature) {\n return yield* new MalformedWebSocketTokenError({});\n }\n\n const expectedSignature = signPayload(encodedPayload, signingSecret);\n if (!timingSafeEqualBase64Url(signature, expectedSignature)) {\n return yield* new InvalidWebSocketTokenSignatureError({});\n }\n\n const claims = yield* decodeWebSocketClaims(base64UrlDecodeUtf8(encodedPayload)).pipe(\n Effect.mapError((cause) => new InvalidWebSocketTokenPayloadError({ cause })),\n );\n\n const observedAt = yield* DateTime.now;\n const expiresAt = DateTime.make(claims.exp);\n if (Option.isNone(expiresAt)) {\n return yield* new InvalidSessionExpirationClaimError({\n sessionId: claims.sid,\n expirationClaim: claims.exp,\n });\n }\n if (claims.exp <= observedAt.epochMilliseconds) {\n return yield* new WebSocketTokenExpiredError({\n sessionId: claims.sid,\n expiresAt: expiresAt.value,\n observedAt,\n });\n }\n\n const row = yield* authSessions\n .getById({ sessionId: claims.sid })\n .pipe(", + "checksum": "4b3e8db151728643747f9cdcb91ab8730048a4d9acf13f0c8351677e79690373" }, "command-union-boundary": { "id": "command-union-boundary", @@ -216,8 +216,8 @@ "end": 1057, "language": "typescript", "label": "Client-dispatchable, normalized, and trusted internal command unions", - "code": "const DispatchableClientOrchestrationCommand = Schema.Union([\n ProjectCreateCommand,\n ProjectMetaUpdateCommand,\n ProjectDeleteCommand,\n ThreadCreateCommand,\n ThreadDeleteCommand,\n ThreadArchiveCommand,\n ThreadUnarchiveCommand,\n ThreadSettleCommand,\n ThreadUnsettleCommand,\n ThreadSnoozeCommand,\n ThreadUnsnoozeCommand,\n ThreadPinCommand,\n ThreadUnpinCommand,\n ThreadPinReorderCommand,\n ThreadMetaUpdateCommand,\n ThreadRuntimeModeSetCommand,\n ThreadInteractionModeSetCommand,\n ThreadTurnStartCommand,\n ThreadTurnInterruptCommand,\n ThreadApprovalRespondCommand,\n ThreadUserInputRespondCommand,\n ThreadCheckpointRevertCommand,\n ThreadSessionStopCommand,\n]);\nexport type DispatchableClientOrchestrationCommand =\n typeof DispatchableClientOrchestrationCommand.Type;\n\nexport const ClientOrchestrationCommand = Schema.Union([\n ProjectCreateCommand,\n ProjectMetaUpdateCommand,\n ProjectDeleteCommand,\n ThreadCreateCommand,\n ThreadDeleteCommand,\n ThreadArchiveCommand,\n ThreadUnarchiveCommand,\n ThreadSettleCommand,\n ThreadUnsettleCommand,\n ThreadSnoozeCommand,\n ThreadUnsnoozeCommand,\n ThreadPinCommand,\n ThreadUnpinCommand,\n ThreadPinReorderCommand,\n ThreadMetaUpdateCommand,\n ThreadRuntimeModeSetCommand,\n ThreadInteractionModeSetCommand,\n ClientThreadTurnStartCommand,\n ThreadTurnInterruptCommand,\n ThreadApprovalRespondCommand,\n ThreadUserInputRespondCommand,\n ThreadCheckpointRevertCommand,\n ThreadSessionStopCommand,\n]);\nexport type ClientOrchestrationCommand = typeof ClientOrchestrationCommand.Type;\n\nconst ThreadSessionSetCommand = Schema.Struct({\n type: Schema.Literal(\"thread.session.set\"),\n commandId: CommandId,\n threadId: ThreadId,\n session: OrchestrationSession,\n createdAt: IsoDateTime,\n});\n\nconst ThreadMessageAssistantDeltaCommand = Schema.Struct({\n type: Schema.Literal(\"thread.message.assistant.delta\"),\n commandId: CommandId,\n threadId: ThreadId,\n messageId: MessageId,\n delta: Schema.String,\n turnId: Schema.optional(TurnId),\n createdAt: IsoDateTime,\n});\n\nconst ThreadMessageAssistantCompleteCommand = Schema.Struct({\n type: Schema.Literal(\"thread.message.assistant.complete\"),\n commandId: CommandId,\n threadId: ThreadId,\n messageId: MessageId,\n turnId: Schema.optional(TurnId),\n createdAt: IsoDateTime,\n});\n\nconst ThreadProposedPlanUpsertCommand = Schema.Struct({\n type: Schema.Literal(\"thread.proposed-plan.upsert\"),\n commandId: CommandId,\n threadId: ThreadId,\n proposedPlan: OrchestrationProposedPlan,\n createdAt: IsoDateTime,\n});\n\nconst ThreadTurnDiffCompleteCommand = Schema.Struct({\n type: Schema.Literal(\"thread.turn.diff.complete\"),\n commandId: CommandId,\n threadId: ThreadId,\n turnId: TurnId,\n completedAt: IsoDateTime,\n checkpointRef: CheckpointRef,\n status: OrchestrationCheckpointStatus,\n files: Schema.Array(OrchestrationCheckpointFile),\n assistantMessageId: Schema.optional(MessageId),\n checkpointTurnCount: NonNegativeInt,\n createdAt: IsoDateTime,\n});\n\nconst ThreadActivityAppendCommand = Schema.Struct({\n type: Schema.Literal(\"thread.activity.append\"),\n commandId: CommandId,\n threadId: ThreadId,\n activity: OrchestrationThreadActivity,\n createdAt: IsoDateTime,\n});\n\nconst ThreadRevertCompleteCommand = Schema.Struct({\n type: Schema.Literal(\"thread.revert.complete\"),\n commandId: CommandId,\n threadId: ThreadId,\n turnCount: NonNegativeInt,\n createdAt: IsoDateTime,\n});\n\nconst ThreadTitleRegenerationCompleteCommand = Schema.Struct({\n type: Schema.Literal(\"thread.title.regeneration.complete\"),\n commandId: CommandId,\n threadId: ThreadId,\n requestId: CommandId,\n title: Schema.optional(TrimmedNonEmptyString),\n});\n\nconst InternalOrchestrationCommand = Schema.Union([\n ThreadSessionSetCommand,\n ThreadMessageAssistantDeltaCommand,\n ThreadMessageAssistantCompleteCommand,\n ThreadProposedPlanUpsertCommand,\n ThreadTurnDiffCompleteCommand,\n ThreadActivityAppendCommand,\n ThreadRevertCompleteCommand,\n ThreadTitleRegenerationCompleteCommand,\n]);\nexport type InternalOrchestrationCommand = typeof InternalOrchestrationCommand.Type;\n\nexport const OrchestrationCommand = Schema.Union([\n DispatchableClientOrchestrationCommand,\n InternalOrchestrationCommand,\n]);\nexport type OrchestrationCommand = typeof OrchestrationCommand.Type;", - "checksum": "318896f2aa24bd67ad6b1e40f4fd3b7ac6b7f29240f9290046af5135c8048721" + "code": " afterSequence: Schema.optionalKey(NonNegativeInt),\n /**\n * Requests an explicit marker after the subscription has emitted its initial\n * snapshot or catch-up replay and before it begins emitting live events.\n */\n requestCompletionMarker: Schema.optionalKey(Schema.Boolean),\n /**\n * When provided, the fallback snapshot frame (sent when `afterSequence` is\n * missing or the catch-up gap is too large) is windowed to the last\n * `turnLimit` user-anchored turns and carries `page` metadata. Absent means\n * the fallback snapshot is the full thread, preserving pre-pagination client\n * behavior. Live events are unaffected either way.\n */\n turnLimit: Schema.optionalKey(PositiveInt),\n});\nexport type OrchestrationSubscribeThreadInput = typeof OrchestrationSubscribeThreadInput.Type;\n\n/**\n * Bounds a thread detail read to a window of recent turns. `turnLimit` counts\n * turns with a user pending message (subagent/fan-out turns between them ride\n * along), so the window always contains the last N user prompts. `beforeCursor`\n * requests the disjoint page of older turns strictly before a previously\n * returned cursor. Requests without a window get the full thread; pagination is\n * strictly opt-in so older clients keep today's behavior on both HTTP and the\n * WebSocket fallback snapshot.\n */\nexport const OrchestrationThreadDetailWindow = Schema.Struct({\n turnLimit: Schema.optionalKey(PositiveInt),\n beforeCursor: Schema.optionalKey(TrimmedNonEmptyString),\n});\nexport type OrchestrationThreadDetailWindow = typeof OrchestrationThreadDetailWindow.Type;\n\n/**\n * Page metadata for a windowed thread detail read. `beforeCursor` is opaque and\n * exclusive: passing it back returns the adjacent disjoint slice of older\n * turns. `null` means the thread is fully loaded below this page. The\n * `snapshotSequence` mirrors the top-level snapshot sequence so history pages\n * can be sequence-checked against live state before merging.\n */\nexport const OrchestrationThreadDetailPage = Schema.Struct({\n beforeCursor: Schema.NullOr(TrimmedNonEmptyString),\n hasMore: Schema.Boolean,\n snapshotSequence: NonNegativeInt,\n /**\n * Highest event sequence applied to THIS thread at page read time. The\n * global `snapshotSequence` advances with every thread's events, so a\n * client cannot wait for it via its per-thread subscription; this\n * thread-scoped watermark is reachable. A client merging an older page\n * must first have applied live events up to it — otherwise a streaming\n * turn outside the loaded window could have deltas replayed on top of\n * page content that already includes them, duplicating text.\n */\n threadSequence: Schema.optionalKey(NonNegativeInt),\n});\nexport type OrchestrationThreadDetailPage = typeof OrchestrationThreadDetailPage.Type;\n\nexport const OrchestrationThreadDetailSnapshot = Schema.Struct({\n snapshotSequence: NonNegativeInt,\n thread: OrchestrationThread,\n // Present only on windowed responses. Absent on full snapshots (and from\n // pre-pagination servers), which clients treat as fully loaded.\n page: Schema.optional(OrchestrationThreadDetailPage),\n});\nexport type OrchestrationThreadDetailSnapshot = typeof OrchestrationThreadDetailSnapshot.Type;\n\nexport const ProjectCreateCommand = Schema.Struct({\n type: Schema.Literal(\"project.create\"),\n commandId: CommandId,\n projectId: ProjectId,\n title: TrimmedNonEmptyString,\n workspaceRoot: TrimmedNonEmptyString,\n createWorkspaceRootIfMissing: Schema.optional(Schema.Boolean),\n // Retained for older clients that sent an automatic create-time seed. The\n // server ignores it; explicit project defaults use project.meta.update.\n defaultModelSelection: Schema.optional(Schema.NullOr(ModelSelection)),\n createdAt: IsoDateTime,\n});\n\nconst ProjectMetaUpdateCommand = Schema.Struct({\n type: Schema.Literal(\"project.meta.update\"),\n commandId: CommandId,\n projectId: ProjectId,\n title: Schema.optional(TrimmedNonEmptyString),\n workspaceRoot: Schema.optional(TrimmedNonEmptyString),\n defaultModelSelection: Schema.optional(Schema.NullOr(ModelSelection)),\n // Absent = leave unchanged; null = clear the override.\n defaultThreadEnvMode: Schema.optional(Schema.NullOr(ThreadEnvMode)),\n autoPull: Schema.optional(Schema.Boolean),\n faviconPath: Schema.optional(Schema.NullOr(ProjectFaviconPath)),\n projectIcon: Schema.optional(Schema.NullOr(ProjectIconOverride)),\n scripts: Schema.optional(Schema.Array(ProjectScript)),\n});\n\nconst ProjectDeleteCommand = Schema.Struct({\n type: Schema.Literal(\"project.delete\"),\n commandId: CommandId,\n projectId: ProjectId,\n force: Schema.optional(Schema.Boolean),\n});\n\nconst ThreadCreateCommand = Schema.Struct({\n type: Schema.Literal(\"thread.create\"),\n commandId: CommandId,\n threadId: ThreadId,\n projectId: ProjectId,\n title: TrimmedNonEmptyString,\n modelSelection: ModelSelection,\n runtimeMode: RuntimeMode,\n interactionMode: ProviderInteractionMode.pipe(\n Schema.withDecodingDefault(Effect.succeed(DEFAULT_PROVIDER_INTERACTION_MODE)),\n ),\n branch: Schema.NullOr(TrimmedNonEmptyString),\n worktreePath: Schema.NullOr(TrimmedNonEmptyString),\n createdAt: IsoDateTime,\n historyImport: Schema.optional(Schema.Literal(true)),\n});\n\nconst ThreadDeleteCommand = Schema.Struct({\n type: Schema.Literal(\"thread.delete\"),\n commandId: CommandId,\n threadId: ThreadId,\n});\n\nconst ThreadArchiveCommand = Schema.Struct({\n type: Schema.Literal(\"thread.archive\"),\n commandId: CommandId,\n threadId: ThreadId,\n});\n\nconst ThreadUnarchiveCommand = Schema.Struct({\n type: Schema.Literal(\"thread.unarchive\"),\n commandId: CommandId,\n threadId: ThreadId,\n});\n\nconst ThreadSettleCommand = Schema.Struct({\n type: Schema.Literal(\"thread.settle\"),\n commandId: CommandId,\n threadId: ThreadId,\n});\n\nconst ThreadAutoSettleCommand = Schema.Struct({\n type: Schema.Literal(\"thread.auto-settle\"),\n commandId: CommandId,\n threadId: ThreadId,", + "checksum": "adc2c9922942767a289bb92b44a88eecf9e9fbec37880b6cd70cfcb0e57e543e" }, "normalizer-staging-boundary": { "id": "normalizer-staging-boundary", @@ -226,8 +226,8 @@ "end": 179, "language": "typescript", "label": "Pre-transaction timestamp, workspace, and attachment staging", - "code": "export const canonicalizeClientCommandTimestamps = (\n command: ClientOrchestrationCommand,\n receivedAt: IsoDateTime,\n): ClientOrchestrationCommand => {\n const canonicalCommand =\n \"createdAt\" in command\n ? {\n ...command,\n createdAt: receivedAt,\n }\n : command;\n\n if (canonicalCommand.type !== \"thread.turn.start\" || !canonicalCommand.bootstrap?.createThread) {\n return canonicalCommand;\n }\n\n return {\n ...canonicalCommand,\n bootstrap: {\n ...canonicalCommand.bootstrap,\n createThread: {\n ...canonicalCommand.bootstrap.createThread,\n createdAt: receivedAt,\n },\n },\n };\n};\n\nexport const normalizeDispatchCommand = (command: ClientOrchestrationCommand) =>\n Effect.gen(function* () {\n const receivedAt = DateTime.formatIso(yield* DateTime.now);\n const canonicalCommand = canonicalizeClientCommandTimestamps(command, receivedAt);\n const fileSystem = yield* FileSystem.FileSystem;\n const path = yield* Path.Path;\n const serverConfig = yield* ServerConfig;\n const workspacePaths = yield* WorkspacePaths.WorkspacePaths;\n\n const normalizeProjectWorkspaceRoot = (workspaceRoot: string) =>\n workspacePaths.normalizeWorkspaceRoot(workspaceRoot).pipe(\n Effect.mapError(\n (cause) =>\n new OrchestrationDispatchCommandError({\n message: cause.message,\n }),\n ),\n );\n\n const normalizeProjectWorkspaceRootForCreate = (\n workspaceRoot: string,\n createIfMissing: boolean | undefined,\n ) =>\n workspacePaths\n .normalizeWorkspaceRoot(workspaceRoot, {\n createIfMissing: createIfMissing === true,\n })\n .pipe(\n Effect.mapError(\n (cause) =>\n new OrchestrationDispatchCommandError({\n message: cause.message,\n }),\n ),\n );\n\n if (canonicalCommand.type === \"project.create\") {\n return {\n ...canonicalCommand,\n workspaceRoot: yield* normalizeProjectWorkspaceRootForCreate(\n canonicalCommand.workspaceRoot,\n canonicalCommand.createWorkspaceRootIfMissing,\n ),\n createWorkspaceRootIfMissing: canonicalCommand.createWorkspaceRootIfMissing === true,\n } satisfies OrchestrationCommand;\n }\n\n if (\n canonicalCommand.type === \"project.meta.update\" &&\n canonicalCommand.workspaceRoot !== undefined\n ) {\n return {\n ...canonicalCommand,\n workspaceRoot: yield* normalizeProjectWorkspaceRoot(canonicalCommand.workspaceRoot),\n } satisfies OrchestrationCommand;\n }\n\n if (canonicalCommand.type !== \"thread.turn.start\") {\n return canonicalCommand as OrchestrationCommand;\n }\n\n const normalizedAttachments = yield* Effect.forEach(\n canonicalCommand.message.attachments,\n (attachment) =>\n Effect.gen(function* () {\n const parsed = parseBase64DataUrl(attachment.dataUrl);\n if (!parsed || !parsed.mimeType.startsWith(\"image/\")) {\n return yield* new OrchestrationDispatchCommandError({\n message: `Invalid image attachment payload for '${attachment.name}'.`,\n });\n }\n\n const bytes = Buffer.from(parsed.base64, \"base64\");\n if (bytes.byteLength === 0 || bytes.byteLength > PROVIDER_SEND_TURN_MAX_IMAGE_BYTES) {\n return yield* new OrchestrationDispatchCommandError({\n message: `Image attachment '${attachment.name}' is empty or too large.`,\n });\n }\n\n const attachmentId = createAttachmentId(canonicalCommand.threadId);\n if (!attachmentId) {\n return yield* new OrchestrationDispatchCommandError({\n message: \"Failed to create a safe attachment id.\",\n });\n }\n\n const persistedAttachment = {\n type: \"image\" as const,\n id: attachmentId,\n name: attachment.name,\n mimeType: parsed.mimeType.toLowerCase(),\n sizeBytes: bytes.byteLength,\n };\n\n const attachmentPath = resolveAttachmentPath({\n attachmentsDir: serverConfig.attachmentsDir,\n attachment: persistedAttachment,\n });\n if (!attachmentPath) {\n return yield* new OrchestrationDispatchCommandError({\n message: `Failed to resolve persisted path for '${attachment.name}'.`,\n });\n }\n\n yield* fileSystem.makeDirectory(path.dirname(attachmentPath), { recursive: true }).pipe(\n Effect.mapError(\n () =>\n new OrchestrationDispatchCommandError({\n message: `Failed to create attachment directory for '${attachment.name}'.`,\n }),\n ),\n );\n yield* fileSystem.writeFile(attachmentPath, bytes).pipe(\n Effect.mapError(\n () =>\n new OrchestrationDispatchCommandError({\n message: `Failed to persist attachment '${attachment.name}'.`,\n }),\n ),\n );\n\n return persistedAttachment;\n }),\n { concurrency: 1 },\n );\n\n return {\n ...canonicalCommand,\n message: {\n ...canonicalCommand.message,\n attachments: normalizedAttachments,\n },\n } satisfies OrchestrationCommand;\n });", - "checksum": "a6b4f3d074784de7397eed20ee1bfb7fa2de4eca5f62c5443b1962f1cdb5d6db" + "code": " PENDING_ATTACHMENT_THREAD_SEGMENT,\n parseThreadSegmentFromAttachmentId,\n resolveAttachmentPath,\n} from \"../attachmentStore.ts\";\nimport { ServerConfig } from \"../config.ts\";\nimport { parseBase64DataUrl } from \"../imageMime.ts\";\nimport * as WorkspacePaths from \"../workspace/WorkspacePaths.ts\";\n\nexport const canonicalizeClientCommandTimestamps = (\n command: ClientOrchestrationCommand,\n receivedAt: IsoDateTime,\n): ClientOrchestrationCommand => {\n const canonicalCommand =\n \"createdAt\" in command\n ? {\n ...command,\n createdAt: receivedAt,\n }\n : command;\n\n if (canonicalCommand.type !== \"thread.turn.start\" || !canonicalCommand.bootstrap?.createThread) {\n return canonicalCommand;\n }\n\n return {\n ...canonicalCommand,\n bootstrap: {\n ...canonicalCommand.bootstrap,\n createThread: {\n ...canonicalCommand.bootstrap.createThread,\n createdAt: receivedAt,\n },\n },\n };\n};\n\nconst removeClaimedAttachmentPaths = Effect.fn(\"Normalizer.removeClaimedAttachmentPaths\")(\n function* (attachmentPaths: ReadonlyArray) {\n if (attachmentPaths.length === 0) {\n return;\n }\n const fileSystem = yield* FileSystem.FileSystem;\n yield* Effect.forEach(\n attachmentPaths,\n (attachmentPath) =>\n fileSystem.remove(attachmentPath, { force: true }).pipe(\n Effect.tapError((cause) =>\n Effect.logWarning(\"Failed to remove an unclaimed attachment copy.\", {\n attachmentPath,\n cause,\n }),\n ),\n Effect.orElseSucceed(() => undefined),\n ),\n { concurrency: 1 },\n );\n },\n);\n\nexport const normalizeDispatchCommand = (command: ClientOrchestrationCommand) =>\n Effect.gen(function* () {\n const receivedAt = DateTime.formatIso(yield* DateTime.now);\n const canonicalCommand = canonicalizeClientCommandTimestamps(command, receivedAt);\n const fileSystem = yield* FileSystem.FileSystem;\n const path = yield* Path.Path;\n const serverConfig = yield* ServerConfig;\n const workspacePaths = yield* WorkspacePaths.WorkspacePaths;\n\n const normalizeProjectWorkspaceRoot = (workspaceRoot: string) =>\n workspacePaths.normalizeWorkspaceRoot(workspaceRoot).pipe(\n Effect.mapError(\n (cause) =>\n new OrchestrationDispatchCommandError({\n message: cause.message,\n }),\n ),\n );\n\n const normalizeProjectWorkspaceRootForCreate = (\n workspaceRoot: string,\n createIfMissing: boolean | undefined,\n ) =>\n workspacePaths\n .normalizeWorkspaceRoot(workspaceRoot, {\n createIfMissing: createIfMissing === true,\n })\n .pipe(\n Effect.mapError(\n (cause) =>\n new OrchestrationDispatchCommandError({\n message: cause.message,\n }),\n ),\n );\n\n if (canonicalCommand.type === \"project.create\") {\n return {\n ...canonicalCommand,\n workspaceRoot: yield* normalizeProjectWorkspaceRootForCreate(\n canonicalCommand.workspaceRoot,\n canonicalCommand.createWorkspaceRootIfMissing,\n ),\n createWorkspaceRootIfMissing: canonicalCommand.createWorkspaceRootIfMissing === true,\n } satisfies OrchestrationCommand;\n }\n\n if (\n canonicalCommand.type === \"project.meta.update\" &&\n canonicalCommand.workspaceRoot !== undefined\n ) {\n return {\n ...canonicalCommand,\n workspaceRoot: yield* normalizeProjectWorkspaceRoot(canonicalCommand.workspaceRoot),\n } satisfies OrchestrationCommand;\n }\n\n if (\n canonicalCommand.type !== \"thread.turn.start\" &&\n canonicalCommand.type !== \"thread.user-input.respond\"\n ) {\n return canonicalCommand as OrchestrationCommand;\n }\n\n const attachments =\n canonicalCommand.type === \"thread.turn.start\"\n ? canonicalCommand.message.attachments\n : Object.values(canonicalCommand.attachmentsByQuestionId ?? {}).flat();\n if (\n canonicalCommand.type === \"thread.user-input.respond\" &&\n attachments.length > PROVIDER_SEND_TURN_MAX_ATTACHMENTS\n ) {\n return yield* new OrchestrationDispatchCommandError({\n message: `You can attach up to ${PROVIDER_SEND_TURN_MAX_ATTACHMENTS} files per question response.`,\n });\n }\n const claimedAttachmentPaths: string[] = [];\n const normalizedAttachments = yield* Effect.forEach(\n attachments,\n (attachment) =>\n Effect.gen(function* () {\n if (!(\"dataUrl\" in attachment)) {\n const claim = planAttachmentClaim({\n attachmentsDir: serverConfig.attachmentsDir,\n threadId: canonicalCommand.threadId,\n attachmentId: attachment.id,\n });\n if (!claim.ok) {\n return yield* new OrchestrationDispatchCommandError({\n message: `Attachment '${attachment.name}' cannot be sent: ${claim.reason}.`,\n });\n }\n\n const info = yield* fileSystem.stat(claim.currentPath).pipe(\n Effect.mapError(\n (cause) =>\n new OrchestrationDispatchCommandError({\n message: `Attachment '${attachment.name}' cannot be sent: attachment not found.`,\n cause,\n }),\n ),\n );\n if (Number(info.size) !== attachment.sizeBytes) {", + "checksum": "403c94c75dbccdef63997d5e1deebb623472361f55b9f85ccaca5102350e9b4d" }, "turn-atomic-batch": { "id": "turn-atomic-batch", @@ -236,8 +236,8 @@ "end": 1036, "language": "typescript", "label": "Existing-thread turn plans a two-to-four-event batch", - "code": " case \"thread.turn.start\": {\n const targetThread = yield* requireThread({\n readModel,\n command,\n threadId: command.threadId,\n });\n const sourceProposedPlan = command.sourceProposedPlan;\n const sourceThread = sourceProposedPlan\n ? yield* requireThread({\n readModel,\n command,\n threadId: sourceProposedPlan.threadId,\n })\n : null;\n const sourcePlan =\n sourceProposedPlan && sourceThread\n ? sourceThread.proposedPlans.find((entry) => entry.id === sourceProposedPlan.planId)\n : null;\n if (sourceProposedPlan && !sourcePlan) {\n return yield* new OrchestrationCommandInvariantError({\n commandType: command.type,\n detail: `Proposed plan '${sourceProposedPlan.planId}' does not exist on thread '${sourceProposedPlan.threadId}'.`,\n });\n }\n if (sourceThread && sourceThread.projectId !== targetThread.projectId) {\n return yield* new OrchestrationCommandInvariantError({\n commandType: command.type,\n detail: `Proposed plan '${sourceProposedPlan?.planId}' belongs to thread '${sourceThread.id}' in a different project.`,\n });\n }\n const userMessageEvent: Omit = {\n ...(yield* withEventBase({\n aggregateKind: \"thread\",\n aggregateId: command.threadId,\n occurredAt: command.createdAt,\n commandId: command.commandId,\n })),\n type: \"thread.message-sent\",\n payload: {\n threadId: command.threadId,\n messageId: command.message.messageId,\n role: \"user\",\n text: command.message.text,\n attachments: command.message.attachments,\n turnId: null,\n streaming: false,\n createdAt: command.createdAt,\n updatedAt: command.createdAt,\n },\n };\n const turnStartRequestedEvent: Omit = {\n ...(yield* withEventBase({\n aggregateKind: \"thread\",\n aggregateId: command.threadId,\n occurredAt: command.createdAt,\n commandId: command.commandId,\n })),\n causationEventId: userMessageEvent.eventId,\n type: \"thread.turn-start-requested\",\n payload: {\n threadId: command.threadId,\n messageId: command.message.messageId,\n ...(command.modelSelection !== undefined\n ? { modelSelection: command.modelSelection }\n : {}),\n ...(command.titleSeed !== undefined ? { titleSeed: command.titleSeed } : {}),\n runtimeMode: targetThread.runtimeMode,\n interactionMode: targetThread.interactionMode,\n ...(sourceProposedPlan !== undefined ? { sourceProposedPlan } : {}),\n createdAt: command.createdAt,\n },\n };\n // Real activity resets ANY override: it wakes an explicitly settled\n // thread, and it clears a keep-active pin back to neutral so the\n // thread can auto-settle again after this burst of work goes stale.\n // A snooze clears the same way — sending a message to a snoozed\n // thread is the user re-engaging, so the return ticket is spent.\n const lifecycleResetEvents: Array> = [];\n if (targetThread.settledOverride !== null) {\n lifecycleResetEvents.push({\n ...(yield* withEventBase({\n aggregateKind: \"thread\",\n aggregateId: command.threadId,\n occurredAt: command.createdAt,\n commandId: command.commandId,\n })),\n type: \"thread.unsettled\",\n payload: {\n threadId: command.threadId,\n reason: \"activity\",\n updatedAt: command.createdAt,\n },\n });\n }\n if (targetThread.snoozedUntil != null) {\n lifecycleResetEvents.push({\n ...(yield* withEventBase({\n aggregateKind: \"thread\",\n aggregateId: command.threadId,\n occurredAt: command.createdAt,\n commandId: command.commandId,\n })),\n type: \"thread.unsnoozed\",\n payload: {\n threadId: command.threadId,\n reason: \"activity\",\n updatedAt: command.createdAt,\n },\n });\n }\n return [...lifecycleResetEvents, userMessageEvent, turnStartRequestedEvent];", - "checksum": "0d0fe115f9ae7b07e903de3cca6dfd815044b54aa8a56b75417acb4a17410c4d" + "code": " {\n type: \"thread.pull-request.unlink\" as const,\n commandId: command.commandId,\n threadId: command.threadId,\n host: currentPullRequest.host,\n repository: currentPullRequest.repository,\n number: currentPullRequest.number,\n },\n ]\n : []),\n {\n type: \"thread.pull-request.link\",\n commandId: command.commandId,\n threadId: command.threadId,\n ...legacyThreadPullRequestKey(linked, host),\n url: linked.url,\n source: \"manual\",\n },\n ],\n });\n }\n\n if (command.linkedPullRequest === null && currentPullRequest !== null) {\n const { linkedPullRequest: _linkedPullRequest, ...metadata } = command;\n const hasMetadata = Object.entries(metadata).some(\n ([key, value]) => ![\"type\", \"commandId\", \"threadId\"].includes(key) && value !== undefined,\n );\n return yield* decideCommandSequence({\n readModel,\n commands: [\n ...(hasMetadata ? [metadata] : []),\n {\n type: \"thread.pull-request.unlink\",\n commandId: command.commandId,\n threadId: command.threadId,\n host: currentPullRequest.host,\n repository: currentPullRequest.repository,\n number: currentPullRequest.number,\n },\n ],\n });\n }\n const branch =\n command.branch !== undefined &&\n command.expectedBranch !== undefined &&\n thread.branch !== command.expectedBranch\n ? thread.branch\n : command.branch;\n const occurredAt = yield* nowIso;\n return {\n ...(yield* withEventBase({\n aggregateKind: \"thread\",\n aggregateId: command.threadId,\n occurredAt,\n commandId: command.commandId,\n })),\n type: \"thread.meta-updated\",\n payload: {\n threadId: command.threadId,\n ...(command.title !== undefined ? { title: command.title } : {}),\n ...(command.regenerateTitle === true\n ? {\n regenerateTitle: true as const,\n previousTitle: thread.title,\n titleRegeneration: {\n requestId: command.commandId,\n startedAt: occurredAt,\n },\n }\n : {}),\n ...(command.title !== undefined && thread.titleRegeneration != null\n ? { titleRegeneration: null }\n : {}),\n ...(command.modelSelection !== undefined\n ? { modelSelection: command.modelSelection }\n : {}),\n ...(branch !== undefined ? { branch } : {}),\n ...(command.worktreePath !== undefined ? { worktreePath: command.worktreePath } : {}),\n ...(command.linkedPullRequest !== undefined\n ? { linkedPullRequest: command.linkedPullRequest }\n : {}),\n updatedAt: occurredAt,\n },\n };\n }\n\n case \"thread.pull-request.link\": {\n const thread = yield* requireThread({\n readModel,\n command,\n threadId: command.threadId,\n });\n const key = normalizeThreadPullRequestKey(command);\n const existing = findPullRequestLink(thread, key);\n // An explicit link on a dismissed stack member un-dismisses it; any\n // other duplicate is a no-op the engine would reject as zero-event.\n const undismisses =\n existing?.source === \"stack-dismissed\" &&\n (command.source === \"manual\" || command.source === \"agent\" || command.source === \"created\");\n if (existing !== undefined && !undismisses) {\n return yield* new OrchestrationCommandInvariantError({\n commandType: command.type,\n detail: `pull request ${key.host}/${key.repository}#${key.number} is already linked to thread ${command.threadId}`,\n });\n }\n const occurredAt = yield* nowIso;\n return {\n ...(yield* withEventBase({\n aggregateKind: \"thread\",\n aggregateId: command.threadId,\n occurredAt,", + "checksum": "83c68709ef8681a8fc3fe134c1244b017637541825ba2e680895427723606e73" }, "engine-commit-publication": { "id": "engine-commit-publication", @@ -246,8 +246,8 @@ "end": 259, "language": "typescript", "label": "SQL commit followed by in-memory fold and hot publication", - "code": " const committedCommand = yield* sql\n .withTransaction(\n Effect.gen(function* () {\n const committedEvents: OrchestrationEvent[] = [];\n let nextCommandReadModel = commandReadModel;\n\n for (const nextEvent of eventBases) {\n const savedEvent = yield* eventStore.append(nextEvent);\n nextCommandReadModel = yield* projectEvent(nextCommandReadModel, savedEvent);\n yield* projectionPipeline.projectEvent(savedEvent);\n committedEvents.push(savedEvent);\n }\n\n const lastSavedEvent = committedEvents.at(-1) ?? null;\n if (lastSavedEvent === null) {\n return yield* new OrchestrationCommandInvariantError({\n commandType: envelope.command.type,\n detail: \"Command produced no events.\",\n });\n }\n\n yield* commandReceiptRepository.upsert({\n commandId: envelope.command.commandId,\n aggregateKind: lastSavedEvent.aggregateKind,\n aggregateId: lastSavedEvent.aggregateId,\n acceptedAt: lastSavedEvent.occurredAt,\n resultSequence: lastSavedEvent.sequence,\n status: \"accepted\",\n error: null,\n });\n\n return {\n committedEvents,\n lastSequence: lastSavedEvent.sequence,\n nextCommandReadModel,\n } as const;\n }),\n )\n .pipe(\n Effect.catchTag(\"SqlError\", (sqlError) =>\n Effect.fail(\n toPersistenceSqlError(\"OrchestrationEngine.processEnvelope:transaction\")(sqlError),\n ),\n ),\n );\n\n commandReadModel = committedCommand.nextCommandReadModel;\n for (const [index, event] of committedCommand.committedEvents.entries()) {\n yield* PubSub.publish(eventPubSub, event);\n if (index === 0) {\n yield* Metric.update(\n Metric.withAttributes(\n orchestrationCommandAckDuration,\n metricAttributes({\n ...baseMetricAttributes,\n ackEventType: event.type,\n }),\n ),\n Duration.millis(Math.max(0, (yield* Clock.currentTimeMillis) - envelope.startedAtMs)),\n );\n }\n }\n return { sequence: committedCommand.lastSequence };", - "checksum": "24e00da0c432e501172e3847817fde311d8656b07e3b8261c657ecf6b4c35d4c" + "code": " }))\n ) {\n return yield* new OrchestrationCommandInvariantError({\n commandType: envelope.command.type,\n detail: `thread ${envelope.command.threadId} was recreated before pull request discovery`,\n });\n }\n\n if (\n envelope.command.type === \"thread.auto-settle\" &&\n threadBackgroundLiveness.getThreadBackgroundLiveness(envelope.command.threadId) !== null\n ) {\n return yield* new OrchestrationCommandInvariantError({\n commandType: envelope.command.type,\n detail: `thread ${envelope.command.threadId} has live background work`,\n });\n }\n\n // New and moved projects do not carry a resolved identity in the event-derived\n // command model. Legacy PR edits need it to identify the link they replace.\n if (\n envelope.command.type === \"thread.meta.update\" &&\n envelope.command.linkedPullRequest !== undefined\n ) {\n const threadId = envelope.command.threadId;\n const thread = commandReadModel.threads.find((thread) => thread.id === threadId);\n if (thread !== undefined) {\n const project = yield* projectionSnapshotQuery.getProjectShellById(thread.projectId);\n if (Option.isSome(project)) {\n commandReadModel = {\n ...commandReadModel,\n projects: commandReadModel.projects.map((entry) =>\n entry.id === thread.projectId\n ? { ...entry, repositoryIdentity: project.value.repositoryIdentity }\n : entry,\n ),\n };\n }\n }\n }\n\n // Command snapshots omit activities at startup and cap them while running.\n // Read this request's durable state before deciding how to send the answer.\n const userInputActivity =\n envelope.command.type === \"thread.user-input.respond\" ||\n envelope.command.type === \"thread.user-input.dismiss\"\n ? yield* projectionSnapshotQuery.getUserInputActivity(envelope.command)\n : Option.none();\n const eventBase = yield* decideOrchestrationCommand({\n command: envelope.command,\n readModel: commandReadModel,\n ...(Option.isSome(userInputActivity)\n ? { userInputActivity: userInputActivity.value }\n : {}),\n }).pipe(\n Effect.provideService(Crypto.Crypto, crypto),\n Effect.mapError((cause) =>\n isOrchestrationCommandRejection(cause)\n ? cause\n : new OrchestrationCommandInvariantError({\n commandType: envelope.command.type,\n detail: \"Failed to generate an event identifier.\",\n cause,", + "checksum": "2898a21a30eccdb0c5f3025b4102b4399ed2a52c8bd32d1457c7c4fe6f15e3b1" }, "command-receipt-schema": { "id": "command-receipt-schema", @@ -266,8 +266,8 @@ "end": 1678, "language": "typescript", "label": "Ordered projectors, transactional cursor updates, and attachment side effects", - "code": " const projectors: ReadonlyArray = [\n {\n name: ORCHESTRATION_PROJECTOR_NAMES.projects,\n apply: applyProjectsProjection,\n },\n {\n name: ORCHESTRATION_PROJECTOR_NAMES.threadMessages,\n apply: applyThreadMessagesProjection,\n },\n {\n name: ORCHESTRATION_PROJECTOR_NAMES.threadProposedPlans,\n apply: applyThreadProposedPlansProjection,\n },\n {\n name: ORCHESTRATION_PROJECTOR_NAMES.threadActivities,\n apply: applyThreadActivitiesProjection,\n },\n {\n name: ORCHESTRATION_PROJECTOR_NAMES.threadSessions,\n apply: applyThreadSessionsProjection,\n },\n {\n name: ORCHESTRATION_PROJECTOR_NAMES.threadTurns,\n apply: applyThreadTurnsProjection,\n },\n {\n name: ORCHESTRATION_PROJECTOR_NAMES.checkpoints,\n apply: applyCheckpointsProjection,\n },\n {\n name: ORCHESTRATION_PROJECTOR_NAMES.pendingApprovals,\n apply: applyPendingApprovalsProjection,\n },\n {\n name: ORCHESTRATION_PROJECTOR_NAMES.threads,\n apply: applyThreadsProjection,\n },\n ];\n\n const runProjectorForEvent = Effect.fn(\"runProjectorForEvent\")(function* (\n projector: ProjectorDefinition,\n event: OrchestrationEvent,\n ) {\n const attachmentSideEffects: AttachmentSideEffects = {\n deletedThreadIds: new Set(),\n prunedThreadRelativePaths: new Map>(),\n };\n\n yield* sql.withTransaction(\n projector.apply(event, attachmentSideEffects).pipe(\n Effect.flatMap(() =>\n projectionStateRepository.upsert({\n projector: projector.name,\n lastAppliedSequence: event.sequence,\n updatedAt: event.occurredAt,\n }),\n ),\n ),\n );\n\n yield* runAttachmentSideEffects(attachmentSideEffects).pipe(\n Effect.catch((cause) =>\n Effect.logWarning(\"failed to apply projected attachment side-effects\", {\n projector: projector.name,\n sequence: event.sequence,\n eventType: event.type,\n cause,\n }),\n ),\n );", - "checksum": "ec226c4bd720884378ba5cb08e9527043641bfaac002455052b75f36ee02daa0" + "code": " return;\n }\n\n case \"thread.turn-interrupt-requested\": {\n if (event.payload.turnId === undefined) {\n return;\n }\n const existingTurn = yield* projectionTurnRepository.getByTurnId({\n threadId: event.payload.threadId,\n turnId: event.payload.turnId,\n });\n if (Option.isSome(existingTurn)) {\n yield* projectionTurnRepository.upsertByTurnId({\n ...existingTurn.value,\n state: \"interrupted\",\n completedAt: existingTurn.value.completedAt ?? event.payload.createdAt,\n startedAt: existingTurn.value.startedAt ?? event.payload.createdAt,\n requestedAt: existingTurn.value.requestedAt ?? event.payload.createdAt,\n });\n return;\n }\n yield* projectionTurnRepository.upsertByTurnId({\n turnId: event.payload.turnId,\n threadId: event.payload.threadId,\n pendingMessageId: null,\n sourceProposedPlanThreadId: null,\n sourceProposedPlanId: null,\n assistantMessageId: null,\n state: \"interrupted\",\n requestedAt: event.payload.createdAt,\n startedAt: event.payload.createdAt,\n completedAt: event.payload.createdAt,\n checkpointTurnCount: null,\n checkpointRef: null,\n checkpointStatus: null,\n checkpointFiles: [],\n });\n return;\n }\n\n case \"thread.turn-diff-completed\": {\n // Mid-turn diff updates produce placeholder checkpoints; record the\n // checkpoint, but don't settle a turn its session is still running.\n const session = yield* projectionThreadSessionRepository.getByThreadId({\n threadId: event.payload.threadId,\n });\n const turnStillRunning =\n Option.isSome(session) &&\n session.value.status === \"running\" &&\n session.value.activeTurnId === event.payload.turnId;\n const existingTurn = yield* projectionTurnRepository.getByTurnId({\n threadId: event.payload.threadId,\n turnId: event.payload.turnId,\n });\n const nextState = event.payload.status === \"error\" ? \"error\" : \"completed\";\n yield* projectionTurnRepository.clearCheckpointTurnConflict({\n threadId: event.payload.threadId,\n turnId: event.payload.turnId,\n checkpointTurnCount: event.payload.checkpointTurnCount,\n });\n\n if (Option.isSome(existingTurn)) {\n yield* projectionTurnRepository.upsertByTurnId({\n ...existingTurn.value,\n assistantMessageId: event.payload.assistantMessageId,\n state:\n turnStillRunning || existingTurn.value.state === \"interrupted\"\n ? existingTurn.value.state\n : nextState,\n checkpointTurnCount: event.payload.checkpointTurnCount,", + "checksum": "b4586260079076deb87dc4af04a0c38a477b3470c97ea23b193ce363bd60d4a7" }, "snapshot-safe-watermark": { "id": "snapshot-safe-watermark", @@ -276,8 +276,8 @@ "end": 258, "language": "typescript", "label": "Snapshot watermark is the minimum required projector cursor", - "code": "const REQUIRED_SNAPSHOT_PROJECTORS = [\n ORCHESTRATION_PROJECTOR_NAMES.projects,\n ORCHESTRATION_PROJECTOR_NAMES.threads,\n ORCHESTRATION_PROJECTOR_NAMES.threadMessages,\n ORCHESTRATION_PROJECTOR_NAMES.threadProposedPlans,\n ORCHESTRATION_PROJECTOR_NAMES.threadActivities,\n ORCHESTRATION_PROJECTOR_NAMES.threadSessions,\n ORCHESTRATION_PROJECTOR_NAMES.checkpoints,\n] as const;\n\nfunction maxIso(left: string | null, right: string): string {\n if (left === null) {\n return right;\n }\n return left > right ? left : right;\n}\n\nfunction escapeLikePattern(value: string): string {\n return value.replaceAll(\"!\", \"!!\").replaceAll(\"%\", \"!%\").replaceAll(\"_\", \"!_\");\n}\n\nfunction foldAsciiCase(value: string): string {\n return value.replace(/[A-Z]/g, (character) => character.toLowerCase());\n}\n\nfunction buildSearchSnippet(text: string, query: string): string {\n const normalizedText = text.replace(/\\s+/g, \" \").trim();\n if (normalizedText.length <= 240) {\n return normalizedText;\n }\n\n const normalizedQuery = foldAsciiCase(query.replace(/\\s+/g, \" \").trim());\n const matchIndex = foldAsciiCase(normalizedText).indexOf(normalizedQuery);\n const bodyLength = 236;\n const idealStart = Math.max(0, matchIndex - 72);\n const start = Math.min(idealStart, normalizedText.length - bodyLength);\n const end = Math.min(normalizedText.length, start + bodyLength);\n return `${start > 0 ? \"…\" : \"\"}${normalizedText.slice(start, end)}${\n end < normalizedText.length ? \"…\" : \"\"\n }`;\n}\n\nfunction computeSnapshotSequence(\n stateRows: ReadonlyArray>,\n): number {\n if (stateRows.length === 0) {\n return 0;\n }\n const sequenceByProjector = new Map(\n stateRows.map((row) => [row.projector, row.lastAppliedSequence] as const),\n );\n\n let minSequence = Number.POSITIVE_INFINITY;\n for (const projector of REQUIRED_SNAPSHOT_PROJECTORS) {\n const sequence = sequenceByProjector.get(projector);\n if (sequence === undefined) {\n return 0;\n }\n if (sequence < minSequence) {\n minSequence = sequence;\n }\n }\n\n return Number.isFinite(minSequence) ? minSequence : 0;", - "checksum": "1e59d916d6fd2d68ce72a50afe86973454487d7b5b81d8215e632767ef82f3a4" + "code": " threadId: ThreadId,\n runtimePayload: Schema.Unknown,\n});\nconst ThreadIdLookupInput = Schema.Struct({\n threadId: ThreadId,\n});\nconst TurnStartMessageLookupInput = Schema.Struct({\n threadId: ThreadId,\n messageId: MessageId,\n});\nconst ThreadActivityKindsLookupInput = Schema.Struct({\n threadId: ThreadId,\n activityKinds: Schema.Array(Schema.String),\n});\nconst ThreadActivityIdsLookupInput = Schema.Struct({\n activityIds: Schema.Array(ProjectionThreadActivity.fields.activityId),\n});\n// Windowed reads order turns by the stable keyset (anchor, turn key), where\n// anchor is requested_at and turn key is\n// COALESCE(turn_id, ''). Both are event-derived, so cursors survive the\n// revert projector's row-id rewrite and full projection rebuilds.\nconst ThreadTurnWindowLookupInput = Schema.Struct({\n threadId: ThreadId,\n // Exclusive keyset upper bound. Sentinels \"~\"/\"\" mean unbounded (\"~\" sorts\n // after every ISO timestamp).\n beforeAnchorAt: Schema.String,\n beforeTurnKey: Schema.String,\n userTurnLimit: Schema.Number,\n maxRawTurns: Schema.Number,\n});\nconst ProjectionTurnWindowRowSchema = Schema.Struct({\n // The turn's timeline anchor, used to bound rows that have no turn linkage\n // (user messages and turnless activities) to the same page window.\n anchorAt: Schema.String,\n turnKey: Schema.String,\n});\nconst ThreadTurnRangeLookupInput = Schema.Struct({\n threadId: ThreadId,\n // Turn-linked rows are bounded by the keyset range [min, before) over\n // (anchor, turn key); turnless rows by the matching [minAnchorAt,\n // beforeAnchorAt) time range. Unbounded ends use sentinels: \"\" for the\n // lower bound, \"~\" (sorts after ISO dates) for the upper bound.\n minAnchorAt: Schema.String,\n minTurnKey: Schema.String,\n beforeAnchorAt: Schema.String,\n beforeTurnKey: Schema.String,\n});\nconst ProjectionProjectLookupRowSchema = ProjectionProjectDbRowSchema;\nconst ProjectionThreadIdLookupRowSchema = Schema.Struct({\n threadId: ThreadId,\n});\nconst ProjectionThreadCheckpointContextThreadRowSchema = Schema.Struct({\n threadId: ThreadId,\n projectId: ProjectId,\n workspaceRoot: Schema.String,\n worktreePath: Schema.NullOr(Schema.String),\n});\nconst FullThreadDiffContextLookupInput = Schema.Struct({\n threadId: ThreadId,\n checkpointTurnCount: NonNegativeInt,\n});\nconst ProjectionFullThreadDiffContextRowSchema = Schema.Struct({\n threadId: ThreadId,\n projectId: ProjectId,", + "checksum": "12c8e828c469e66e0dbddf9fbac6dabdc0f05acd3556feb761e2b03d60f073f2" }, "provider-reactor-forked-send": { "id": "provider-reactor-forked-send", @@ -286,8 +286,8 @@ "end": 1174, "language": "typescript", "label": "Volatile turn dedupe and forked provider send", - "code": " const processTurnStartRequested = Effect.fn(\"processTurnStartRequested\")(function* (\n event: Extract,\n ) {\n const key = turnStartKeyForEvent(event);\n if (yield* hasHandledTurnStartRecently(key)) {\n return;\n }\n\n const thread = yield* resolveThread(event.payload.threadId);\n if (!thread) {\n return;\n }\n\n const message = thread.messages.find((entry) => entry.id === event.payload.messageId);\n if (!message || message.role !== \"user\") {\n yield* appendProviderFailureActivity({\n threadId: event.payload.threadId,\n kind: \"provider.turn.start.failed\",\n summary: \"Provider turn start failed\",\n detail: `User message '${event.payload.messageId}' was not found for turn start request.`,\n turnId: null,\n createdAt: event.payload.createdAt,\n });\n return;\n }\n\n const isFirstUserMessageTurn =\n thread.messages.filter((entry) => entry.role === \"user\").length === 1;\n if (isFirstUserMessageTurn) {\n const project = yield* resolveProject(thread.projectId);\n const generationCwd =\n resolveThreadWorkspaceCwd({\n thread,\n projects: project ? [project] : [],\n }) ?? process.cwd();\n const generationInput = {\n messageText: message.text,\n ...(message.attachments !== undefined ? { attachments: message.attachments } : {}),\n ...(event.payload.titleSeed !== undefined ? { titleSeed: event.payload.titleSeed } : {}),\n };\n\n yield* maybeGenerateAndRenameWorktreeBranchForFirstTurn({\n threadId: event.payload.threadId,\n branch: thread.branch,\n worktreePath: thread.worktreePath,\n ...generationInput,\n }).pipe(Effect.forkScoped);\n\n if (canReplaceThreadTitle(thread.title, event.payload.titleSeed)) {\n yield* maybeGenerateThreadTitleForFirstTurn({\n threadId: event.payload.threadId,\n cwd: generationCwd,\n ...generationInput,\n }).pipe(Effect.forkScoped);\n }\n }\n\n const handleTurnStartFailure = (cause: Cause.Cause) => {\n if (Cause.hasInterruptsOnly(cause)) {\n return Effect.void;\n }\n const detail = formatFailureDetail(cause);\n return setThreadSessionErrorOnTurnStartFailure({\n threadId: event.payload.threadId,\n detail,\n createdAt: event.payload.createdAt,\n }).pipe(\n Effect.flatMap(() =>\n appendProviderFailureActivity({\n threadId: event.payload.threadId,\n kind: \"provider.turn.start.failed\",\n summary: \"Provider turn start failed\",\n detail,\n turnId: null,\n createdAt: event.payload.createdAt,\n }),\n ),\n Effect.asVoid,\n );\n };\n\n const recoverTurnStartFailure = (cause: Cause.Cause) =>\n handleTurnStartFailure(cause).pipe(\n Effect.catchCause((recoveryCause) =>\n Effect.logWarning(\"provider command reactor failed to recover turn start failure\", {\n eventType: event.type,\n threadId: event.payload.threadId,\n cause: Cause.pretty(recoveryCause),\n originalCause: Cause.pretty(cause),\n }),\n ),\n );\n\n const sendTurnRequest = yield* buildSendTurnRequestForThread({\n threadId: event.payload.threadId,\n messageText: message.text,\n ...(message.attachments !== undefined ? { attachments: message.attachments } : {}),\n ...(event.payload.modelSelection !== undefined\n ? { modelSelection: event.payload.modelSelection }\n : {}),\n interactionMode: event.payload.interactionMode,\n createdAt: event.payload.createdAt,\n }).pipe(\n Effect.map(Option.some),\n Effect.catchCause((cause) => handleTurnStartFailure(cause).pipe(Effect.as(Option.none()))),\n );\n\n if (Option.isNone(sendTurnRequest)) {\n return;\n }\n\n yield* providerService\n .sendTurn(sendTurnRequest.value)\n .pipe(Effect.catchCause(recoverTurnStartFailure), Effect.forkScoped);\n });", - "checksum": "e612e2a0295ab2afbf9d9b7a9d4c7da0578c4adf5b052b24892d929ec9995f00" + "code": " return { _tag: \"Superseded\" } as const;\n }\n\n return { _tag: \"Completed\", title: generated.title } as const;\n });\n const dispatchThreadTitleRegenerationCompletion = Effect.fn(\n \"dispatchThreadTitleRegenerationCompletion\",\n )(function* (input: {\n readonly threadId: ThreadId;\n readonly requestId: CommandId;\n readonly title?: string;\n }) {\n yield* orchestrationEngine.dispatch({\n type: \"thread.title.regeneration.complete\",\n commandId: yield* serverCommandId(\"thread-title-regeneration-complete\"),\n threadId: input.threadId,\n requestId: input.requestId,\n ...(input.title !== undefined ? { title: input.title } : {}),\n });\n });\n const findInterruptedThreadTitleRegenerations = Effect.fn(\n \"findInterruptedThreadTitleRegenerations\",\n )(function* () {\n const readModel = yield* projectionSnapshotQuery.getCommandReadModel();\n return readModel.threads.flatMap((thread) => {\n const requestId = thread.titleRegeneration?.requestId;\n return requestId === undefined ? [] : [{ threadId: thread.id, requestId }];\n });\n });\n const clearInterruptedThreadTitleRegenerations = Effect.fn(\n \"clearInterruptedThreadTitleRegenerations\",\n )(function* (\n interrupted: ReadonlyArray<{ readonly threadId: ThreadId; readonly requestId: CommandId }>,\n ) {\n yield* Effect.forEach(\n interrupted,\n ({ threadId, requestId }) => {\n return dispatchThreadTitleRegenerationCompletion({\n threadId,\n requestId,\n }).pipe(\n Effect.catchCause((cause) => {\n if (Cause.hasInterruptsOnly(cause)) {\n return Effect.interrupt;\n }\n return Effect.logWarning(\n \"provider command reactor failed to clear interrupted title regeneration\",\n {\n threadId,\n cause: Cause.pretty(cause),\n },\n );\n }),\n );\n },\n { discard: true },\n );\n });\n const processThreadTitleRegenerationSafely = Effect.fn(\"processThreadTitleRegenerationSafely\")(\n function* (event: Extract) {\n if (event.payload.regenerateTitle !== true) {\n return;\n }\n\n const requestId = event.payload.titleRegeneration?.requestId ?? event.commandId;\n if (requestId === null) {\n return;\n }\n const result = yield* regenerateThreadTitle(event, requestId).pipe(\n Effect.catchCause((cause) => {\n if (Cause.hasInterruptsOnly(cause)) {\n return Effect.failCause(cause);\n }\n return Effect.logWarning(\"provider command reactor failed to regenerate thread title\", {\n threadId: event.payload.threadId,\n cause: Cause.pretty(cause),\n }).pipe(Effect.as({ _tag: \"Completed\", title: undefined } as const));\n }),\n );\n if (result._tag === \"Superseded\") {\n return;\n }\n\n const completion = {\n threadId: event.payload.threadId,\n requestId,\n ...(result.title !== undefined ? { title: result.title } : {}),\n };\n yield* dispatchThreadTitleRegenerationCompletion(completion).pipe(\n Effect.catchCause((cause) => {\n if (Cause.hasInterruptsOnly(cause)) {\n return Effect.failCause(cause);\n }\n return Effect.logWarning(\n \"provider command reactor retrying title regeneration completion\",\n {\n threadId: event.payload.threadId,\n cause: Cause.pretty(cause),\n },\n ).pipe(Effect.andThen(dispatchThreadTitleRegenerationCompletion(completion)));\n }),\n );\n },\n (effect, event) =>\n effect.pipe(\n Effect.catchCause((cause) => {\n if (Cause.hasInterruptsOnly(cause)) {\n return Effect.failCause(cause);\n }\n return Effect.logWarning(\n \"provider command reactor failed to complete title regeneration\",\n {\n threadId: event.payload.threadId,\n cause: Cause.pretty(cause),\n },", + "checksum": "e7b8d37aeaaad8d247211fe9e4d780501e06f11faa37d2b8559f0ccf789c4ab6" }, "sqlite-runtime-pragmas": { "id": "sqlite-runtime-pragmas", @@ -316,8 +316,8 @@ "end": 117, "language": "typescript", "label": "Scoped provider driver and instance records", - "code": "/**\n * One materialized provider instance. Held by the registry, looked up by\n * `instanceId`, torn down by closing the scope it was created in.\n *\n * The three \"shape\" fields are captured closures owned by this instance —\n * stopping one instance cannot affect another, and starting a second\n * instance of the same driver does not reach into the first instance's\n * state.\n */\nexport interface ProviderInstance {\n readonly instanceId: ProviderInstanceId;\n readonly driverKind: ProviderDriverKind;\n readonly continuationIdentity: ProviderContinuationIdentity;\n readonly displayName: string | undefined;\n readonly accentColor?: string | undefined;\n readonly enabled: boolean;\n readonly snapshot: ServerProviderShape;\n readonly adapter: ProviderAdapterShape;\n readonly textGeneration: TextGeneration.TextGeneration[\"Service\"];\n}\n\nexport interface ProviderContinuationIdentity {\n readonly driverKind: ProviderDriverKind;\n readonly continuationKey: string;\n}\n\nexport function defaultProviderContinuationIdentity(input: {\n readonly driverKind: ProviderDriverKind;\n readonly instanceId: ProviderInstanceId;\n}): ProviderContinuationIdentity {\n return {\n driverKind: input.driverKind,\n continuationKey: `${input.driverKind}:instance:${input.instanceId}`,\n };\n}\n\n/**\n * Inputs the registry passes to a driver's `create` function.\n *\n * `config` is the typed payload — already decoded by the registry through\n * `driver.configSchema`. Drivers never decode their own raw envelope.\n */\nexport interface ProviderDriverCreateInput {\n readonly instanceId: ProviderInstanceId;\n readonly displayName: string | undefined;\n readonly accentColor?: string | undefined;\n readonly environment: ProviderInstanceEnvironment;\n readonly enabled: boolean;\n readonly config: Config;\n}\n\n/**\n * Driver SPI — registered as a plain value, not a Layer.\n *\n * `Config` is whatever the driver decoded from\n * `ProviderInstanceConfig.config`. `R` is the union of infrastructure\n * services the driver depends on; the registry layer aggregates `R` across\n * all registered drivers and the runtime supplies them.\n *\n * `create` is responsible for *all* per-instance state — process handles,\n * pubsub topics, refs, file watchers — and must release them when its\n * scope closes. Two calls to `create` with different `instanceId` /\n * `config` MUST yield instances with no shared mutable state.", - "checksum": "d6fe18a1ab845da33fc05cb2ef822ef1057706345468a8b485daa76abaa9fce6" + "code": " readonly supportsMultipleInstances?: boolean;\n}\n\n/**\n * One materialized provider instance. Held by the registry, looked up by\n * `instanceId`, torn down by closing the scope it was created in.\n *\n * The three \"shape\" fields are captured closures owned by this instance —\n * stopping one instance cannot affect another, and starting a second\n * instance of the same driver does not reach into the first instance's\n * state.\n */\nexport interface ProviderInstance {\n readonly instanceId: ProviderInstanceId;\n readonly driverKind: ProviderDriverKind;\n readonly continuationIdentity: ProviderContinuationIdentity;\n readonly displayName: string | undefined;\n readonly accentColor?: string | undefined;\n readonly enabled: boolean;\n readonly snapshot: ServerProviderShape;\n readonly snapshotForCwd?: (cwd: string) => Effect.Effect;\n readonly refreshModels?: () => Effect.Effect;\n /**\n * Redeem one banked rate-limit reset credit on the signed-in account, then\n * re-probe so the snapshot reflects the cleared windows. Account-level,\n * not thread-level, which is why it lives here rather than on the adapter.\n */\n readonly consumeResetCredit?: () => Effect.Effect<\n ProviderConsumeResetCreditOutcome,\n ProviderDriverError\n >;\n readonly adapter: ProviderAdapterShape;\n readonly textGeneration: TextGeneration.TextGeneration[\"Service\"];\n readonly auth?: ProviderAuthController;\n}\n\nexport interface ProviderContinuationIdentity {\n readonly driverKind: ProviderDriverKind;\n readonly continuationKey: string;\n}\n\nexport function defaultProviderContinuationIdentity(input: {\n readonly driverKind: ProviderDriverKind;\n readonly instanceId: ProviderInstanceId;\n}): ProviderContinuationIdentity {\n return {\n driverKind: input.driverKind,\n continuationKey: `${input.driverKind}:instance:${input.instanceId}`,\n };\n}\n\n/**\n * Inputs the registry passes to a driver's `create` function.\n *\n * `config` is the typed payload — already decoded by the registry through\n * `driver.configSchema`. Drivers never decode their own raw envelope.\n */\nexport interface ProviderDriverCreateInput {\n readonly instanceId: ProviderInstanceId;\n readonly displayName: string | undefined;\n readonly accentColor?: string | undefined;\n readonly environment: ProviderInstanceEnvironment;\n readonly enabled: boolean;", + "checksum": "de0a664479c4720c11e4f6b3b7149dbb5f3a4c3ce5bd000e7c22b44cf2e18f12" }, "provider-driver-instance-records": { "id": "provider-driver-instance-records", @@ -346,8 +346,8 @@ "end": 1693, "language": "typescript", "label": "Codex session replacement and runtime configuration", - "code": " const startSession: CodexAdapterShape[\"startSession\"] = (input) =>\n Effect.scoped(\n Effect.gen(function* () {\n if (input.provider !== undefined && input.provider !== PROVIDER) {\n return yield* new ProviderAdapterValidationError({\n provider: PROVIDER,\n operation: \"startSession\",\n issue: `Expected provider '${PROVIDER}' but received '${input.provider}'.`,\n });\n }\n\n const existing = sessions.get(input.threadId);\n if (existing && !existing.stopped) {\n yield* Effect.suspend(() => stopSessionInternal(existing));\n }\n\n const serviceTier =\n input.modelSelection?.instanceId === boundInstanceId\n ? getCodexServiceTierOptionValue(input.modelSelection)\n : undefined;\n const mcpSession = McpProviderSession.readMcpProviderSession(input.threadId);\n const runtimeInput: CodexSessionRuntimeOptions = {\n threadId: input.threadId,\n providerInstanceId: boundInstanceId,\n cwd: input.cwd ?? process.cwd(),\n binaryPath: codexConfig.binaryPath,\n launchArgs: resolveCodexLaunchArgs(codexConfig.launchArgs, options?.environment),\n ...(options?.environment ? { environment: options.environment } : {}),\n ...(codexConfig.homePath ? { homePath: codexConfig.homePath } : {}),\n ...(isCodexResumeCursorSchema(input.resumeCursor)\n ? { resumeCursor: input.resumeCursor }\n : {}),\n runtimeMode: input.runtimeMode,\n ...(input.modelSelection?.instanceId === boundInstanceId\n ? { model: input.modelSelection.model }\n : {}),\n ...(serviceTier ? { serviceTier } : {}),\n ...(mcpSession\n ? {\n environment: {\n ...(options?.environment ?? process.env),\n T3_MCP_BEARER_TOKEN: mcpSession.authorizationHeader.replace(/^Bearer\\s+/, \"\"),\n },\n appServerArgs: [\n \"-c\",\n `mcp_servers.t3-code.url=${mcpSession.endpoint}`,\n \"-c\",\n 'mcp_servers.t3-code.bearer_token_env_var=\"T3_MCP_BEARER_TOKEN\"',\n ],\n }\n : {}),\n };", - "checksum": "6dd08ac4e760401826162f8d3a0d739b58ed890361c817cb7b726329818dee33" + "code": " },\n ];\n }\n\n if (event.method === \"turn/diff/updated\") {\n const payload = readPayload(EffectCodexSchema.V2TurnDiffUpdatedNotification, event.payload);\n if (!payload) {\n return [];\n }\n return [\n {\n ...runtimeEventBase(event, canonicalThreadId),\n type: \"turn.diff.updated\",\n payload: {\n unifiedDiff: payload.diff,\n },\n },\n ];\n }\n\n if (event.method === \"item/started\") {\n const started = mapItemLifecycle(event, canonicalThreadId, \"item.started\");\n return started ? [started] : [];\n }\n\n if (event.method === \"item/completed\") {\n const payload = readPayload(EffectCodexSchema.V2ItemCompletedNotification, event.payload);\n const item = payload?.item;\n if (!item) {\n return [];\n }\n if (item.type === \"agentMessage\" && item.delivery === \"async\" && item.questions?.length) {\n return [\n {\n ...runtimeEventBase(event, canonicalThreadId),\n type: \"user-input.requested\",\n requestId: RuntimeRequestId.make(`codex-async:${canonicalThreadId}:${item.id}`),\n eventId: EventId.make(`codex-async:${canonicalThreadId}:${item.id}`),\n payload: {\n responseMode: \"message\",\n questions: item.questions.map((question, index) => ({\n id: String(index),\n header: \"Question\",\n question: question.title,\n options: (question.options ?? []).map((label) => ({ label, description: \"\" })),\n allowCustomAnswer: true,\n multiSelect: false,\n })),\n },\n },\n ];\n }", + "checksum": "164c03b117c2c909d9df6780dbfb4f473c416322b6fa4ec0c958f4b1e5bfc1b8" }, "codex-adapter-runtime-events": { "id": "codex-adapter-runtime-events", @@ -356,8 +356,8 @@ "end": 1084, "language": "typescript", "label": "Codex usage, turn, and plan event normalization", - "code": " if (event.method === \"thread/tokenUsage/updated\") {\n const payload = readPayload(\n EffectCodexSchema.V2ThreadTokenUsageUpdatedNotification,\n event.payload,\n );\n const normalizedUsage = payload ? normalizeCodexTokenUsage(payload.tokenUsage) : undefined;\n if (!normalizedUsage) {\n return [];\n }\n return [\n {\n type: \"thread.token-usage.updated\",\n ...runtimeEventBase(event, canonicalThreadId),\n payload: {\n usage: normalizedUsage,\n },\n },\n ];\n }\n\n if (event.method === \"turn/started\") {\n const turnId = event.turnId;\n if (!turnId) {\n return [];\n }\n return [\n {\n ...runtimeEventBase(event, canonicalThreadId),\n turnId,\n type: \"turn.started\",\n payload: {},\n },\n ];\n }\n\n if (event.method === \"turn/completed\") {\n const payload = readPayload(EffectCodexSchema.V2TurnCompletedNotification, event.payload);\n if (!payload) {\n return [];\n }\n const errorMessage = trimText(payload.turn.error?.message);\n return [\n {\n ...runtimeEventBase(event, canonicalThreadId),\n type: \"turn.completed\",\n payload: {\n state: toTurnStatus(payload.turn.status),\n ...(errorMessage ? { errorMessage } : {}),\n },\n },\n ];\n }\n\n if (event.method === \"turn/aborted\") {\n return [\n {\n ...runtimeEventBase(event, canonicalThreadId),\n type: \"turn.aborted\",\n payload: {\n reason: event.message ?? \"Turn aborted\",\n },\n },\n ];\n }\n\n if (event.method === \"turn/plan/updated\") {\n const payload = readPayload(EffectCodexSchema.V2TurnPlanUpdatedNotification, event.payload);\n if (!payload) {\n return [];\n }\n return [\n {\n ...runtimeEventBase(event, canonicalThreadId),\n type: \"turn.plan.updated\",\n payload: {\n ...(trimText(payload.explanation) ? { explanation: trimText(payload.explanation) } : {}),\n plan: payload.plan.map((step) => ({\n step: trimText(step.step) ?? \"step\",\n status:\n step.status === \"completed\" || step.status === \"inProgress\" ? step.status : \"pending\",\n })),\n },\n },\n ];\n }", - "checksum": "4b82fdd81df8d8a061c9f7f0b036c32d5f5bf061f9511189548f386a038f4d9f" + "code": " readPayload(EffectCodexSchema.V2ItemCompletedNotification, event.payload);\n const item = payload?.item;\n if (!item) {\n return undefined;\n }\n const itemType = toCanonicalItemType(item.type);\n if (itemType === \"unknown\" && lifecycle !== \"item.updated\") {\n return undefined;\n }\n\n const detail = itemDetail(itemType, item);\n const toolPresentation = item.type === \"mcpToolCall\" ? mcpToolPresentation(item) : {};\n const title = itemTitle(itemType, item, toolPresentation);\n const status =\n lifecycle === \"item.started\"\n ? \"inProgress\"\n : lifecycle === \"item.completed\"\n ? \"status\" in item && (item.status === \"failed\" || item.status === \"declined\")\n ? item.status\n : \"completed\"\n : undefined;\n\n return {\n ...runtimeEventBase(event, canonicalThreadId),\n type: lifecycle,\n payload: {\n itemType,\n ...(status ? { status } : {}),\n ...(title ? { title } : {}),\n ...(detail ? { detail } : {}),\n ...toolPresentation,\n ...(event.payload !== undefined ? { data: event.payload } : {}),\n },\n };\n}\n\n/**\n * Maps the session runtime's synthetic `collabAgent/*` events (native\n * multi-agent v2 child-thread signals) into the shared task.* lifecycle.\n * Agent identity = child thread id; nickname is the display title, role is\n * agentRole (fallback: last agentPath segment, then \"general-purpose\").\n * A completed child turn is idle (resumable), not terminal. timelineBypass\n * keeps these rows out of the parent chat.\n */\nfunction mapCollabAgentEvent(\n event: ProviderEvent,\n canonicalThreadId: ThreadId,\n): ReadonlyArray {\n const payload =\n typeof event.payload === \"object\" && event.payload !== null\n ? (event.payload as Record)\n : undefined;\n const agentThreadId = typeof payload?.agentThreadId === \"string\" ? payload.agentThreadId : \"\";\n if (!payload || agentThreadId.length === 0) {\n return [];\n }\n const base = runtimeEventBase(event, canonicalThreadId);\n const taskId = RuntimeTaskId.make(agentThreadId);\n const agentPath = typeof payload.agentPath === \"string\" ? payload.agentPath : undefined;\n const pathLeaf = agentPath?.split(\"/\").findLast((segment) => segment.length > 0);\n const nickname = typeof payload.nickname === \"string\" ? payload.nickname : undefined;\n const role =\n (typeof payload.role === \"string\" ? payload.role : undefined) ?? pathLeaf ?? \"general-purpose\";\n // A bare thread id is not a name. Omitting the title lets the client fold\n // keep the real one from task.started instead of clobbering it (probe\n // finding: progress rows renamed math_one to its UUID).\n const knownName = nickname ?? pathLeaf;\n const title = knownName ?? agentThreadId;\n const model = typeof payload.model === \"string\" ? payload.model.trim() : \"\";\n const effort = typeof payload.effort === \"string\" ? payload.effort.trim() : \"\";\n // Identity repeated on every status patch so rows are self-describing when\n // the start row ages out of activity retention (review finding: a\n // reconstructed agent had a UUID name and no role/path).\n const linkage = {\n role,\n ...(knownName ? { title: knownName } : {}),\n ...(model ? { model } : {}),\n ...(effort ? { effort } : {}),\n ...(agentPath ? { agentPath } : {}),\n timelineBypass: true,\n } as const;\n\n switch (event.method) {\n case \"collabAgent/started\":\n return [", + "checksum": "3771639d53637c69037443605ea8849eae6e99b75625feae0f520e86ef5092e7" }, "claude-adapter-query-config": { "id": "claude-adapter-query-config", @@ -366,8 +366,8 @@ "end": 4196, "language": "typescript", "label": "Claude permission mapping and query option assembly", - "code": " const runtimeModeToPermission: Record = {\n \"auto-accept-edits\": \"acceptEdits\",\n auto: \"auto\",\n \"full-access\": \"bypassPermissions\",\n };\n const permissionMode = runtimeModeToPermission[input.runtimeMode];\n const settings = {\n ...(typeof thinking === \"boolean\" ? { alwaysThinkingEnabled: thinking } : {}),\n ...(fastMode ? { fastMode: true } : {}),\n ...(ultracode ? { ultracode: true } : {}),\n };\n const mcpSession = McpProviderSession.readMcpProviderSession(input.threadId);\n // The attachments dir grant lets the agent Read/copy pasted images at\n // the paths ProviderService injects into the turn text, without an\n // approval prompt. It is a leaf directory holding only attachment\n // files; siblings like secrets/ and state.sqlite stay ungranted.\n const additionalDirectories = [\n ...(input.cwd ? [input.cwd] : []),\n serverConfig.attachmentsDir,\n ];\n const queryOptions: ClaudeQueryOptions = {\n ...(input.cwd ? { cwd: input.cwd } : {}),\n ...(apiModelId ? { model: apiModelId } : {}),\n pathToClaudeCodeExecutable: claudeBinaryPath,\n systemPrompt: { type: \"preset\", preset: \"claude_code\" },\n settingSources: [...CLAUDE_SETTING_SOURCES],\n // `ultracode` is a Claude Code setting, not an API effort level. It is\n // normalized to `xhigh` above and paired with `settings.ultracode`.\n ...(effectiveEffort\n ? {\n effort: effectiveEffort as unknown as NonNullable,\n }\n : {}),\n ...(permissionMode ? { permissionMode } : {}),\n ...(permissionMode === \"bypassPermissions\"\n ? { allowDangerouslySkipPermissions: true }\n : {}),\n ...(Object.keys(settings).length > 0 ? { settings } : {}),\n ...(existingResumeSessionId ? { resume: existingResumeSessionId } : {}),\n ...(newSessionId ? { sessionId: newSessionId } : {}),\n includePartialMessages: true,\n canUseTool,\n env: claudeEnvironment,\n additionalDirectories,\n ...(Object.keys(extraArgs).length > 0 ? { extraArgs } : {}),\n ...(mcpSession\n ? {\n mcpServers: {\n \"t3-code\": {\n type: \"http\",\n url: mcpSession.endpoint,\n headers: {\n Authorization: mcpSession.authorizationHeader,\n },\n },\n },\n }\n : {}),\n };", - "checksum": "0b8c0d482fc624fb131e748d735394497afa9d437867ad9c1bee9f2f2119de08" + "code": " },\n providerRefs: {},\n });\n }\n\n if (sessions.get(context.session.threadId) === context) {\n sessions.delete(context.session.threadId);\n }\n });\n\n const requireSession = (\n threadId: ThreadId,\n ): Effect.Effect => {\n const context = sessions.get(threadId);\n if (!context) {\n return Effect.fail(\n new ProviderAdapterSessionNotFoundError({\n provider: PROVIDER,\n threadId,\n }),\n );\n }\n if (context.stopped || context.session.status === \"closed\") {\n return Effect.fail(\n new ProviderAdapterSessionClosedError({\n provider: PROVIDER,\n threadId,\n }),\n );\n }\n return Effect.succeed(context);\n };\n\n const startSession: ClaudeAdapterShape[\"startSession\"] = Effect.fn(\"startSession\")(\n function* (input) {\n const modelCatalog = yield* modelCatalogEffect;\n if (input.provider !== undefined && input.provider !== PROVIDER) {\n return yield* new ProviderAdapterValidationError({\n provider: PROVIDER,\n operation: \"startSession\",\n issue: `Expected provider '${PROVIDER}' but received '${input.provider}'.`,\n });\n }\n\n const existingContext = sessions.get(input.threadId);\n if (existingContext) {\n yield* Effect.logWarning(\"claude.session.replacing\", {\n threadId: input.threadId,\n existingSessionStatus: existingContext.session.status,\n reason: \"startSession called with existing active session\",\n });\n yield* stopSessionInternal(existingContext, {\n emitExitEvent: false,\n });\n }\n\n const startedAt = yield* nowIso;\n const resumeState = readClaudeResumeState(input.resumeCursor);\n const threadId = input.threadId;", + "checksum": "d750f3bc39c5ec9a9b2b180cbbfb4d4a74adad582f9e510bba3b1cfb16d4e052" }, "claude-query-invocation": { "id": "claude-query-invocation", @@ -376,8 +376,8 @@ "end": 4235, "language": "typescript", "label": "Claude SDK query invocation", - "code": " const queryRuntime = yield* Effect.try({\n try: () =>\n createQuery({\n prompt,\n options: queryOptions,\n }),\n catch: (cause) =>\n new ProviderAdapterProcessError({\n provider: PROVIDER,\n threadId,\n detail: \"Failed to start Claude runtime session.\",\n cause,\n }),", - "checksum": "293fa0c20180cf51eb8c621ce2d48809cb91db55d182270896f857d245b8a7a1" + "code": "\n const contextRef = yield* Ref.make(undefined);\n\n /**\n * Handle AskUserQuestion tool calls by emitting a `user-input.requested`\n * runtime event and waiting for the user to respond via `respondToUserInput`.\n */\n const handleAskUserQuestion = Effect.fn(\"handleAskUserQuestion\")(function* (\n context: ClaudeSessionContext,\n toolInput: Record,\n callbackOptions: {\n readonly signal: AbortSignal;\n readonly toolUseID?: string;", + "checksum": "cd41b6c05f86f4517dca3286f196f5af1673ed20ac34ab06d44ff17690179f76" }, "claude-adapter-interaction-handlers": { "id": "claude-adapter-interaction-handlers", @@ -386,8 +386,8 @@ "end": 4529, "language": "typescript", "label": "Claude approval and structured-input deferreds", - "code": " const respondToRequest: ClaudeAdapterShape[\"respondToRequest\"] = Effect.fn(\"respondToRequest\")(\n function* (threadId, requestId, decision) {\n const context = yield* requireSession(threadId);\n const pending = context.pendingApprovals.get(requestId);\n if (!pending) {\n return yield* new ProviderAdapterRequestError({\n provider: PROVIDER,\n method: \"item/requestApproval/decision\",\n detail: `Unknown pending approval request: ${requestId}`,\n });\n }\n\n context.pendingApprovals.delete(requestId);\n yield* Deferred.succeed(pending.decision, decision);\n },\n );\n\n const respondToUserInput: ClaudeAdapterShape[\"respondToUserInput\"] = Effect.fn(\n \"respondToUserInput\",\n )(function* (threadId, requestId, answers) {\n const context = yield* requireSession(threadId);\n const pending = context.pendingUserInputs.get(requestId);\n if (!pending) {\n return yield* new ProviderAdapterRequestError({\n provider: PROVIDER,\n method: \"item/tool/respondToUserInput\",\n detail: `Unknown pending user-input request: ${requestId}`,\n });\n }\n\n context.pendingUserInputs.delete(requestId);\n yield* Deferred.succeed(pending.answers, answers);\n });", - "checksum": "e41de225714771b18fce1f2bcaff53bc79bfff542df321078c5ab4d1ff8086d2" + "code": " decision: decisionDeferred,\n ...(callbackOptions.suggestions ? { suggestions: callbackOptions.suggestions } : {}),\n };\n\n const requestedStamp = yield* makeEventStamp();\n yield* offerRuntimeEvent({\n type: \"request.opened\",\n eventId: requestedStamp.eventId,\n provider: PROVIDER,\n createdAt: requestedStamp.createdAt,\n threadId: context.session.threadId,\n ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}),\n requestId: asRuntimeRequestId(requestId),\n payload: {\n requestType,\n detail,\n args: {\n toolName,\n input: toolInput,\n ...(callbackOptions.toolUseID ? { toolUseId: callbackOptions.toolUseID } : {}),\n },\n },\n providerRefs: nativeProviderRefs(context, {\n providerItemId: callbackOptions.toolUseID,\n }),\n raw: {\n source: \"claude.sdk.permission\",\n method: \"canUseTool/request\",\n payload: {\n toolName,\n input: toolInput,\n },\n },", + "checksum": "000089b799260ccdec2d6b21b77ebfcb9a5e4d98adffe0e80fae351492cc9a13" }, "acp-client-typed-surface": { "id": "acp-client-typed-surface", @@ -396,8 +396,8 @@ "end": 134, "language": "typescript", "label": "Typed ACP session command surface", - "code": " {\n readonly raw: AcpClientRaw;\n readonly agent: {\n /**\n * Initializes the ACP session and negotiates capabilities.\n * @see https://agentclientprotocol.com/protocol/schema#initialize\n */\n readonly initialize: (\n payload: AcpSchema.InitializeRequest,\n ) => Effect.Effect;\n /**\n * Performs ACP authentication when the agent requires it.\n * @see https://agentclientprotocol.com/protocol/schema#authenticate\n */\n readonly authenticate: (\n payload: AcpSchema.AuthenticateRequest,\n ) => Effect.Effect;\n /**\n * Logs out the current ACP identity.\n * @see https://agentclientprotocol.com/protocol/schema#logout\n */\n readonly logout: (\n payload: AcpSchema.LogoutRequest,\n ) => Effect.Effect;\n /**\n * Starts a new ACP session.\n * @see https://agentclientprotocol.com/protocol/schema#session/new\n */\n readonly createSession: (\n payload: AcpSchema.NewSessionRequest,\n ) => Effect.Effect;\n /**\n * Loads a previously saved ACP session.\n * @see https://agentclientprotocol.com/protocol/schema#session/load\n */\n readonly loadSession: (\n payload: AcpSchema.LoadSessionRequest,\n ) => Effect.Effect;\n /**\n * Lists available ACP sessions.\n * @see https://agentclientprotocol.com/protocol/schema#session/list\n */\n readonly listSessions: (\n payload: AcpSchema.ListSessionsRequest,\n ) => Effect.Effect;\n /**\n * Forks an ACP session.\n * @see https://agentclientprotocol.com/protocol/schema#session/fork\n */\n readonly forkSession: (\n payload: AcpSchema.ForkSessionRequest,\n ) => Effect.Effect;\n /**\n * Resumes an ACP session.\n * @see https://agentclientprotocol.com/protocol/schema#session/resume\n */\n readonly resumeSession: (\n payload: AcpSchema.ResumeSessionRequest,\n ) => Effect.Effect;\n /**\n * Closes an ACP session.\n * @see https://agentclientprotocol.com/protocol/schema#session/close\n */\n readonly closeSession: (\n payload: AcpSchema.CloseSessionRequest,\n ) => Effect.Effect;\n /**\n * Selects the active model for a session.\n * @see https://agentclientprotocol.com/protocol/schema#session/set_model\n */\n readonly setSessionModel: (\n payload: AcpSchema.SetSessionModelRequest,\n ) => Effect.Effect;\n /**\n * Updates a session configuration option.\n * @see https://agentclientprotocol.com/protocol/schema#session/set_config_option\n */\n readonly setSessionConfigOption: (\n payload: AcpSchema.SetSessionConfigOptionRequest,\n ) => Effect.Effect;\n /**\n * Sends a prompt turn to the agent.\n * @see https://agentclientprotocol.com/protocol/schema#session/prompt\n */\n readonly prompt: (\n payload: AcpSchema.PromptRequest,\n ) => Effect.Effect;\n /**\n * Sends a real ACP `session/cancel` notification.\n * @see https://agentclientprotocol.com/protocol/schema#session/cancel\n */\n readonly cancel: (\n payload: AcpSchema.CancelNotification,\n ) => Effect.Effect;\n };", - "checksum": "7df29bfbf9bbea09d570a845e6123253b3cee19ee9af1f4837fbf6d050040856" + "code": "\ntype AcpClientRaw = {\n readonly notifications: Stream.Stream;\n readonly request: (method: string, payload: unknown) => Effect.Effect;\n readonly notify: (method: string, payload: unknown) => Effect.Effect;\n};\n\nexport class AcpClient extends Context.Service<\n AcpClient,\n {\n readonly raw: AcpClientRaw;\n readonly agent: {\n /**\n * Initializes the ACP session and negotiates capabilities.\n * @see https://agentclientprotocol.com/protocol/schema#initialize\n */\n readonly initialize: (\n payload: AcpSchema.InitializeRequest,\n ) => Effect.Effect;\n /**\n * Performs ACP authentication when the agent requires it.\n * @see https://agentclientprotocol.com/protocol/schema#authenticate\n */\n readonly authenticate: (\n payload: AcpSchema.AuthenticateRequest,\n ) => Effect.Effect;\n /**\n * Logs out the current ACP identity.\n * @see https://agentclientprotocol.com/protocol/schema#logout\n */\n readonly logout: (\n payload: AcpSchema.LogoutRequest,\n ) => Effect.Effect;\n /**\n * Starts a new ACP session.\n * @see https://agentclientprotocol.com/protocol/schema#session/new\n */\n readonly createSession: (\n payload: AcpSchema.NewSessionRequest,\n ) => Effect.Effect;\n /**\n * Loads a previously saved ACP session.\n * @see https://agentclientprotocol.com/protocol/schema#session/load\n */\n readonly loadSession: (\n payload: AcpSchema.LoadSessionRequest,\n ) => Effect.Effect;\n /**\n * Lists available ACP sessions.\n * @see https://agentclientprotocol.com/protocol/schema#session/list\n */\n readonly listSessions: (\n payload: AcpSchema.ListSessionsRequest,\n ) => Effect.Effect;\n /**\n * Forks an ACP session.\n * @see https://agentclientprotocol.com/protocol/schema#session/fork\n */\n readonly forkSession: (\n payload: AcpSchema.ForkSessionRequest,\n ) => Effect.Effect;\n /**\n * Resumes an ACP session.\n * @see https://agentclientprotocol.com/protocol/schema#session/resume\n */\n readonly resumeSession: (\n payload: AcpSchema.ResumeSessionRequest,\n ) => Effect.Effect;\n /**\n * Closes an ACP session.\n * @see https://agentclientprotocol.com/protocol/schema#session/close\n */\n readonly closeSession: (\n payload: AcpSchema.CloseSessionRequest,\n ) => Effect.Effect;\n /**\n * Selects the active model for a session.\n * @see https://agentclientprotocol.com/protocol/schema#session/set_model\n */\n readonly setSessionModel: (\n payload: AcpSchema.SetSessionModelRequest,\n ) => Effect.Effect;\n /**\n * Updates a session configuration option.\n * @see https://agentclientprotocol.com/protocol/schema#session/set_config_option\n */\n readonly setSessionConfigOption: (\n payload: AcpSchema.SetSessionConfigOptionRequest,\n ) => Effect.Effect;\n /**\n * Sends a prompt turn to the agent.\n * @see https://agentclientprotocol.com/protocol/schema#session/prompt\n */\n readonly prompt: (\n payload: AcpSchema.PromptRequest,", + "checksum": "c1752c16896150006bb70e6467b75618792fc5bbffbef8a707d22daf34da0c7b" }, "cursor-session-mode-selection": { "id": "cursor-session-mode-selection", @@ -406,8 +406,8 @@ "end": 294, "language": "typescript", "label": "Cursor negotiated mode and model selection", - "code": "function resolveRequestedModeId(input: {\n readonly interactionMode: ProviderInteractionMode | undefined;\n readonly runtimeMode: RuntimeMode;\n readonly modeState: AcpSessionModeState | undefined;\n}): string | undefined {\n const modeState = input.modeState;\n if (!modeState) {\n return undefined;\n }\n\n if (input.interactionMode === \"plan\") {\n return findModeByAliases(modeState.availableModes, ACP_PLAN_MODE_ALIASES)?.id;\n }\n\n if (input.runtimeMode === \"approval-required\") {\n return (\n findModeByAliases(modeState.availableModes, ACP_APPROVAL_MODE_ALIASES)?.id ??\n findModeByAliases(modeState.availableModes, ACP_IMPLEMENT_MODE_ALIASES)?.id ??\n modeState.availableModes.find((mode) => !isPlanMode(mode))?.id ??\n modeState.currentModeId\n );\n }\n\n return (\n findModeByAliases(modeState.availableModes, ACP_IMPLEMENT_MODE_ALIASES)?.id ??\n findModeByAliases(modeState.availableModes, ACP_APPROVAL_MODE_ALIASES)?.id ??\n modeState.availableModes.find((mode) => !isPlanMode(mode))?.id ??\n modeState.currentModeId\n );\n}\n\nfunction applyRequestedSessionConfiguration(input: {\n readonly runtime: AcpSessionRuntime.AcpSessionRuntime[\"Service\"];\n readonly runtimeMode: RuntimeMode;\n readonly interactionMode: ProviderInteractionMode | undefined;\n readonly modelSelection:\n | {\n readonly model: string;\n readonly options?: ReadonlyArray | null | undefined;\n }\n | undefined;\n readonly mapError: (context: {\n readonly cause: import(\"effect-acp/errors\").AcpError;\n readonly method: \"session/set_config_option\" | \"session/set_mode\";\n }) => E;\n}): Effect.Effect {\n return Effect.gen(function* () {\n if (input.modelSelection) {\n yield* applyCursorAcpModelSelection({\n runtime: input.runtime,\n model: input.modelSelection.model,\n selections: input.modelSelection.options,\n mapError: ({ cause }) =>\n input.mapError({\n cause,\n method: \"session/set_config_option\",\n }),\n });\n }\n\n const requestedModeId = resolveRequestedModeId({\n interactionMode: input.interactionMode,\n runtimeMode: input.runtimeMode,\n modeState: yield* input.runtime.getModeState,\n });\n if (!requestedModeId) {\n return;\n }\n\n yield* input.runtime.setMode(requestedModeId).pipe(\n Effect.mapError((cause) =>\n input.mapError({\n cause,\n method: \"session/set_mode\",\n }),\n ),\n );\n });", - "checksum": "496d6f291b2b479eb2836b32aeafe501dcc9f66a5d207cb94adf592c9f4a6a78" + "code": " }\n }\n return undefined;\n}\n\nfunction isPlanMode(mode: AcpSessionMode): boolean {\n return findModeByAliases([mode], ACP_PLAN_MODE_ALIASES) !== undefined;\n}\n\nfunction resolveRequestedModeId(input: {\n readonly interactionMode: ProviderInteractionMode | undefined;\n readonly runtimeMode: RuntimeMode;\n readonly modeState: AcpSessionModeState | undefined;\n}): string | undefined {\n const modeState = input.modeState;\n if (!modeState) {\n return undefined;\n }\n\n if (input.interactionMode === \"plan\") {\n return findModeByAliases(modeState.availableModes, ACP_PLAN_MODE_ALIASES)?.id;\n }\n\n if (input.runtimeMode === \"approval-required\") {\n return (\n findModeByAliases(modeState.availableModes, ACP_APPROVAL_MODE_ALIASES)?.id ??\n findModeByAliases(modeState.availableModes, ACP_IMPLEMENT_MODE_ALIASES)?.id ??\n modeState.availableModes.find((mode) => !isPlanMode(mode))?.id ??\n modeState.currentModeId\n );\n }\n\n return (\n findModeByAliases(modeState.availableModes, ACP_IMPLEMENT_MODE_ALIASES)?.id ??\n findModeByAliases(modeState.availableModes, ACP_APPROVAL_MODE_ALIASES)?.id ??\n modeState.availableModes.find((mode) => !isPlanMode(mode))?.id ??\n modeState.currentModeId\n );\n}\n\nfunction applyRequestedSessionConfiguration(input: {\n readonly runtime: AcpSessionRuntime.AcpSessionRuntime[\"Service\"];\n readonly runtimeMode: RuntimeMode;\n readonly interactionMode: ProviderInteractionMode | undefined;\n readonly modelSelection:\n | {\n readonly model: string;\n readonly options?: ReadonlyArray | null | undefined;\n }\n | undefined;\n readonly mapError: (context: {\n readonly cause: import(\"effect-acp/errors\").AcpError;\n readonly method: \"session/set_config_option\" | \"session/set_mode\";\n }) => E;\n}): Effect.Effect {\n return Effect.gen(function* () {\n if (input.modelSelection) {\n yield* applyCursorAcpModelSelection({\n runtime: input.runtime,\n model: input.modelSelection.model,\n selections: input.modelSelection.options,\n mapError: ({ cause }) =>\n input.mapError({\n cause,\n method: \"session/set_config_option\",\n }),\n });\n }\n\n const requestedModeId = resolveRequestedModeId({\n interactionMode: input.interactionMode,\n runtimeMode: input.runtimeMode,\n modeState: yield* input.runtime.getModeState,\n });\n if (!requestedModeId) {\n return;\n }\n", + "checksum": "b35831c6d05469540c5d6296ddc4189ca772fa2468c7f1fd7da7f7861825143b" }, "grok-acp-runtime-support": { "id": "grok-acp-runtime-support", @@ -416,8 +416,8 @@ "end": 108, "language": "typescript", "label": "Grok ACP launch, authentication, and model selection", - "code": "\nexport function buildGrokAcpSpawnInput(\n grokSettings: GrokAcpRuntimeGrokSettings | null | undefined,\n cwd: string,\n environment?: NodeJS.ProcessEnv,\n): AcpSessionRuntime.AcpSpawnInput {\n return {\n command: grokSettings?.binaryPath || \"grok\",\n args: [\"agent\", \"stdio\"],\n cwd,\n env: {\n ...environment,\n [GROK_OAUTH2_REFERRER_ENV]: T3_CODE_OAUTH_REFERRER,\n },\n };\n}\n\nfunction resolveGrokAuthMethodId(environment: NodeJS.ProcessEnv | undefined): string {\n return environment?.[GROK_API_KEY_ENV]?.trim()\n ? GROK_AUTH_METHOD_API_KEY\n : GROK_AUTH_METHOD_CACHED_TOKEN;\n}\n\nexport const makeGrokAcpRuntime = (\n input: GrokAcpRuntimeInput,\n): Effect.Effect<\n AcpSessionRuntime.AcpSessionRuntime[\"Service\"],\n EffectAcpErrors.AcpError,\n Crypto.Crypto | Scope.Scope\n> =>\n Effect.gen(function* () {\n const acpContext = yield* Layer.build(\n AcpSessionRuntime.layer({\n ...input,\n spawn: buildGrokAcpSpawnInput(input.grokSettings, input.cwd, input.environment),\n authMethodId: resolveGrokAuthMethodId(input.environment),\n }).pipe(\n Layer.provide(\n Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, input.childProcessSpawner),\n ),\n ),\n );\n const runtime = yield* Effect.service(AcpSessionRuntime.AcpSessionRuntime).pipe(\n Effect.provide(acpContext),\n );\n return yield* makeXAiPromptCompletionRuntime(runtime);\n });\n\nexport function resolveGrokAcpBaseModelId(model: string | null | undefined): string {\n const trimmed = model?.trim();\n const base = trimmed && trimmed.length > 0 ? trimmed : \"grok-build\";\n return normalizeModelSlug(base, GROK_DRIVER_KIND) ?? \"grok-build\";\n}\n\nexport function currentGrokModelIdFromSessionSetup(\n sessionSetupResult:\n | EffectAcpSchema.LoadSessionResponse\n | EffectAcpSchema.NewSessionResponse\n | EffectAcpSchema.ResumeSessionResponse,\n): string | undefined {\n return sessionSetupResult.models?.currentModelId?.trim() || undefined;\n}\n\nexport function applyGrokAcpModelSelection(input: {\n readonly runtime: Pick;\n readonly currentModelId: string | undefined;\n readonly requestedModelId: string | undefined;\n readonly mapError: (cause: EffectAcpErrors.AcpError) => E;\n}): Effect.Effect {\n const shouldSwitchModel =\n input.requestedModelId !== undefined && input.requestedModelId !== input.currentModelId;\n if (!shouldSwitchModel) {\n return Effect.succeed(input.currentModelId);\n }\n return input.runtime\n .setSessionModel(input.requestedModelId)\n .pipe(Effect.mapError(input.mapError), Effect.as(input.requestedModelId));\n}", - "checksum": "d76d1518eb9e2c11e37e359f1d1324099034f92d77f9e6f5af7a3d167bcd168a" + "code": "}\n\nexport function grokAcpSpawnArgs(runtimeMode?: RuntimeMode): ReadonlyArray {\n switch (runtimeMode) {\n case \"approval-required\":\n return [\"--permission-mode\", \"default\", \"agent\", \"stdio\"];\n case \"auto-accept-edits\":\n return [\"--permission-mode\", \"acceptEdits\", \"agent\", \"stdio\"];\n case \"auto\":\n return [\"--permission-mode\", \"auto\", \"agent\", \"stdio\"];\n case \"full-access\":\n return [\"agent\", \"--always-approve\", \"stdio\"];\n default:\n return [\"agent\", \"stdio\"];\n }\n}\n\nexport function buildGrokAcpSpawnInput(\n grokSettings: GrokAcpRuntimeGrokSettings | null | undefined,\n cwd: string,\n environment?: NodeJS.ProcessEnv,\n runtimeMode?: RuntimeMode,\n): AcpSessionRuntime.AcpSpawnInput {\n return {\n command: grokSettings?.binaryPath || \"grok\",\n args: [...grokAcpSpawnArgs(runtimeMode)],\n cwd,\n env: {\n ...environment,\n [GROK_OAUTH2_REFERRER_ENV]: T3_CODE_OAUTH_REFERRER,\n },\n };\n}\n\nfunction resolveGrokAuthMethodId(environment: NodeJS.ProcessEnv | undefined): string {\n return environment?.[GROK_API_KEY_ENV]?.trim()\n ? GROK_AUTH_METHOD_API_KEY\n : GROK_AUTH_METHOD_CACHED_TOKEN;\n}\n\nexport const makeGrokAcpRuntime = (\n input: GrokAcpRuntimeInput,\n): Effect.Effect<\n AcpSessionRuntime.AcpSessionRuntime[\"Service\"],\n EffectAcpErrors.AcpError,\n Crypto.Crypto | Scope.Scope\n> =>\n Effect.gen(function* () {\n const acpContext = yield* Layer.build(\n AcpSessionRuntime.layer({\n ...input,\n spawn: buildGrokAcpSpawnInput(\n input.grokSettings,\n input.cwd,\n input.environment,\n input.runtimeMode,\n ),\n authMethodId: resolveGrokAuthMethodId(input.environment),\n }).pipe(\n Layer.provide(\n Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, input.childProcessSpawner),\n ),\n ),\n );\n const runtime = yield* Effect.service(AcpSessionRuntime.AcpSessionRuntime).pipe(\n Effect.provide(acpContext),\n );\n return yield* makeXAiPromptCompletionRuntime(runtime);\n });\n\n/**\n * T3's built-in Grok slug. It is the CLI's product name, not a model id the ACP accepts,\n * so selecting it means \"use whatever model the Grok session currently runs on\".\n */\nexport const GROK_DEFAULT_MODEL_SLUG = \"grok-build\";\n\nexport function resolveGrokAcpBaseModelId(model: string | null | undefined): string {\n const trimmed = model?.trim();", + "checksum": "0f4663c6b1f9dcfd00297cd46ec030193d2f6c3a48558195d972701d274929c0" }, "opencode-runtime-ownership": { "id": "opencode-runtime-ownership", @@ -426,8 +426,8 @@ "end": 177, "language": "typescript", "label": "Local and external OpenCode server ownership", - "code": "export interface OpenCodeRuntimeShape {\n /**\n * Spawns a local OpenCode server process. Its lifetime is bound to the caller's\n * `Scope.Scope` — the child is killed automatically when that scope closes.\n * Consumers that want a long-lived server must create and hold a scope explicitly\n * (see {@link Scope.make}) and close it when done.\n */\n readonly startOpenCodeServerProcess: (input: {\n readonly binaryPath: string;\n readonly environment?: NodeJS.ProcessEnv;\n readonly port?: number;\n readonly hostname?: string;\n readonly timeoutMs?: number;\n }) => Effect.Effect;\n /**\n * Returns a handle to either an externally-managed OpenCode server (when\n * `serverUrl` is provided — no lifetime is attached to the caller's scope) or a\n * freshly spawned local server whose lifetime is bound to the caller's scope.\n */\n readonly connectToOpenCodeServer: (input: {\n readonly binaryPath: string;\n readonly serverUrl?: string | null;\n readonly environment?: NodeJS.ProcessEnv;\n readonly port?: number;\n readonly hostname?: string;\n readonly timeoutMs?: number;\n }) => Effect.Effect;\n readonly runOpenCodeCommand: (input: {\n readonly binaryPath: string;\n readonly args: ReadonlyArray;\n readonly environment?: NodeJS.ProcessEnv;\n readonly cwd?: string;\n }) => Effect.Effect;\n readonly createOpenCodeSdkClient: (input: {\n readonly baseUrl: string;\n readonly directory: string;\n readonly serverPassword?: string;\n }) => OpencodeClient;", - "checksum": "846dada188ac95cfa4616580a5fa9aacefea446eb2542ea69ed44236f20b6dea" + "code": " }).pipe(Effect.withSpan(`opencode.${operation}`));\n\nexport const verifyOpenCodeServerVersion = Effect.fn(\"verifyOpenCodeServerVersion\")(function* (\n client: OpencodeClient,\n) {\n const healthOption = yield* runOpenCodeSdk(\"global.health\", (signal) =>\n client.global.health({ signal }),\n ).pipe(Effect.timeoutOption(OPENCODE_HEALTH_TIMEOUT));\n if (Option.isNone(healthOption)) {\n return yield* new OpenCodeRuntimeError({\n operation: \"global.health\",\n detail: \"Timed out while checking the OpenCode server version.\",\n });\n }\n\n const health = yield* decodeOpenCodeHealth(healthOption.value.data).pipe(\n Effect.mapError(\n (cause) =>\n new OpenCodeRuntimeError({\n operation: \"global.health\",\n detail: `OpenCode server returned an invalid health response. T3 Code requires OpenCode v${MINIMUM_OPENCODE_VERSION} or newer.`,\n cause,\n }),\n ),\n );\n if (parseSemver(health.version) === null) {\n return yield* new OpenCodeRuntimeError({\n operation: \"global.health\",\n detail: `OpenCode server returned an invalid version. T3 Code requires OpenCode v${MINIMUM_OPENCODE_VERSION} or newer.`,\n });\n }\n if (compareSemverVersions(health.version, MINIMUM_OPENCODE_VERSION) < 0) {\n return yield* new OpenCodeRuntimeError({\n operation: \"global.health\",\n detail: `OpenCode v${health.version} is too old. Upgrade to v${MINIMUM_OPENCODE_VERSION} or newer.`,\n });\n }\n return health.version;", + "checksum": "2b7a1598d997972a1048f4de644aeda21fcafbcef45d001c057aa8e66c784e84" }, "opencode-turn-and-revert": { "id": "opencode-turn-and-revert", @@ -436,8 +436,8 @@ "end": 1509, "language": "typescript", "label": "OpenCode prompt and steering path", - "code": " const sendTurn: OpenCodeAdapterShape[\"sendTurn\"] = Effect.fn(\"sendTurn\")(function* (input) {\n const context = yield* ensureSessionContext(sessions, input.threadId);\n // A sendTurn while a turn is active is a steer: OpenCode queues the\n // prompt into the busy session and the work continues as one turn, so\n // the active turn id is reused instead of opening a new turn.\n const steeringTurnId = context.activeTurnId;\n const turnId = steeringTurnId ?? TurnId.make(`opencode-turn-${yield* randomUUIDv4}`);\n const modelSelection =\n input.modelSelection ??\n (context.session.model\n ? { instanceId: boundInstanceId, model: context.session.model }\n : undefined);\n if (modelSelection !== undefined && modelSelection.instanceId !== boundInstanceId) {\n return yield* new ProviderAdapterValidationError({\n provider: PROVIDER,\n operation: \"sendTurn\",\n issue: `OpenCode model selection is bound to instance '${modelSelection?.instanceId}', expected '${boundInstanceId}'.`,\n });\n }\n const parsedModel = parseOpenCodeModelSlug(modelSelection?.model);\n if (!parsedModel) {\n return yield* new ProviderAdapterValidationError({\n provider: PROVIDER,\n operation: \"sendTurn\",\n issue: \"OpenCode model selection must use the 'provider/model' format.\",\n });\n }\n\n const text = input.input?.trim();\n const fileParts = toOpenCodeFileParts({\n attachments: input.attachments,\n resolveAttachmentPath: (attachment) =>\n resolveAttachmentPath({\n attachmentsDir: serverConfig.attachmentsDir,\n attachment,\n }),\n });\n if ((!text || text.length === 0) && fileParts.length === 0) {\n return yield* new ProviderAdapterValidationError({\n provider: PROVIDER,\n operation: \"sendTurn\",\n issue: \"OpenCode turns require text input or at least one attachment.\",\n });\n }\n\n const agent = getModelSelectionStringOptionValue(modelSelection, \"agent\");\n const variant = getModelSelectionStringOptionValue(modelSelection, \"variant\");\n\n context.activeTurnId = turnId;\n context.activeAgent = agent ?? (input.interactionMode === \"plan\" ? \"plan\" : undefined);\n context.activeVariant = variant;\n yield* updateProviderSession(\n context,\n {\n status: \"running\",\n activeTurnId: turnId,\n model: modelSelection?.model ?? context.session.model,\n },\n { clearLastError: true },\n );\n\n if (steeringTurnId === undefined) {\n yield* emit({\n ...(yield* buildEventBase({ threadId: input.threadId, turnId })),\n type: \"turn.started\",\n payload: {\n model: modelSelection?.model ?? context.session.model,\n ...(variant ? { effort: variant } : {}),\n },\n });\n }\n\n yield* runOpenCodeSdk(\"session.promptAsync\", () =>\n context.client.session.promptAsync({\n sessionID: context.openCodeSessionId,\n model: parsedModel,\n ...(context.activeAgent ? { agent: context.activeAgent } : {}),\n ...(context.activeVariant ? { variant: context.activeVariant } : {}),\n parts: [...(text ? [{ type: \"text\" as const, text }] : []), ...fileParts],\n }),", - "checksum": "bd23ad46a4ad8ad0613a82192a06a3329c10808cfda57391fd77f2ba14fcccf9" + "code": " context.awaitingBusyAfterInterruption = false;\n yield* scheduleIdleReconciliation(context, promptAdmission.turnId, idle.raw);\n return;\n }\n if (isIdle && promptAdmission.messageObserved) {\n promptAdmission.idleStatusConfirmations += 1;\n if (promptAdmission.idleStatusConfirmations >= 2) {\n context.promptAdmission = undefined;\n context.awaitingBusyAfterInterruption = false;\n yield* completeOpenCodeTurn(\n context,\n promptAdmission.turnId,\n promptAdmission.generation,\n {\n type: \"session.status.recovered\",\n status: statusData,\n },\n );\n return;\n }\n } else if (!isIdle) {\n promptAdmission.idleStatusConfirmations = 0;\n }\n if (\n isIdle &&\n promptAdmission.messageObserved &&\n promptAdmission.recoveryRaw !== undefined\n ) {\n context.promptAdmission = undefined;\n context.awaitingBusyAfterInterruption = false;\n yield* scheduleIdleReconciliation(\n context,\n promptAdmission.turnId,\n promptAdmission.recoveryRaw,\n );\n return;\n }\n\n const delayMs = Math.min(250 * 2 ** retryCount, 2_000);\n yield* Effect.sleep(`${delayMs} millis`);\n }\n yield* failPromptAdmissionRecovery(context, promptAdmission);\n }).pipe(\n Effect.catchCause(() => Effect.void),\n Effect.ensuring(\n Effect.sync(() => {\n delete promptAdmission.recoveryFiber;\n }),\n ),\n );\n promptAdmission.recoveryFiber = yield* recover.pipe(Effect.forkIn(context.sessionScope));\n });\n\n const interruptOpenCodeTurn = Effect.fn(\"interruptOpenCodeTurn\")(function* (\n context: OpenCodeSessionContext,\n turnId: TurnId,\n raw?: unknown,\n ) {\n if (context.interruptedTurnId === turnId) {\n return;\n }\n yield* cancelIdleReconciliation(context);\n context.interruptedTurnId = turnId;\n context.reconcileIdleStatus = true;\n context.awaitingBusyAfterInterruption = false;\n const cancellation =\n context.cancellation?.turnId === turnId ? context.cancellation : undefined;\n if (cancellation) {\n context.cancellation = undefined;\n }\n let tokenUsage: TurnTokenUsage = {\n usageStatus: \"unavailable\",\n usageScope: \"main_agent\",\n hasSubagents: false,\n };\n if (context.activeTurnId === turnId) {\n tokenUsage = takeOpenCodeTurnTokenUsage(context, false);\n context.activeTurnId = undefined;\n context.activeAgent = undefined;\n context.activeVariant = undefined;", + "checksum": "a2e98ecb437d2e78ec9380422c9181706049049c996b4012b174ca5ca1ec4feb" }, "usage-live-contract": { "id": "usage-live-contract", @@ -446,8 +446,8 @@ "end": 331, "language": "typescript", "label": "Live thread token-usage snapshot contract", - "code": "export const ThreadTokenUsageSnapshot = Schema.Struct({\n usedTokens: NonNegativeInt,\n totalProcessedTokens: Schema.optional(NonNegativeInt),\n maxTokens: Schema.optional(PositiveInt),\n inputTokens: Schema.optional(NonNegativeInt),\n cachedInputTokens: Schema.optional(NonNegativeInt),\n outputTokens: Schema.optional(NonNegativeInt),\n reasoningOutputTokens: Schema.optional(NonNegativeInt),\n lastUsedTokens: Schema.optional(NonNegativeInt),\n lastInputTokens: Schema.optional(NonNegativeInt),\n lastCachedInputTokens: Schema.optional(NonNegativeInt),\n lastOutputTokens: Schema.optional(NonNegativeInt),\n lastReasoningOutputTokens: Schema.optional(NonNegativeInt),\n toolUses: Schema.optional(NonNegativeInt),\n durationMs: Schema.optional(NonNegativeInt),\n compactsAutomatically: Schema.optional(Schema.Boolean),\n});\nexport type ThreadTokenUsageSnapshot = typeof ThreadTokenUsageSnapshot.Type;\n\nconst ThreadTokenUsageUpdatedPayload = Schema.Struct({\n usage: ThreadTokenUsageSnapshot,\n});\nexport type ThreadTokenUsageUpdatedPayload = typeof ThreadTokenUsageUpdatedPayload.Type;", - "checksum": "d63a433efcb36c6017efca4dfa4c29f8e8186b060316152534c0befba263d700" + "code": "const ThreadMetadataUpdatedPayload = Schema.Struct({\n name: Schema.optional(TrimmedNonEmptyStringSchema),\n metadata: Schema.optional(UnknownRecordSchema),\n});\nexport type ThreadMetadataUpdatedPayload = typeof ThreadMetadataUpdatedPayload.Type;\n\nexport const ThreadTokenUsageSnapshot = Schema.Struct({\n usedTokens: NonNegativeInt,\n totalProcessedTokens: Schema.optional(NonNegativeInt),\n maxTokens: Schema.optional(PositiveInt),\n inputTokens: Schema.optional(NonNegativeInt),\n cachedInputTokens: Schema.optional(NonNegativeInt),\n outputTokens: Schema.optional(NonNegativeInt),\n reasoningOutputTokens: Schema.optional(NonNegativeInt),\n lastUsedTokens: Schema.optional(NonNegativeInt),\n lastInputTokens: Schema.optional(NonNegativeInt),\n lastCachedInputTokens: Schema.optional(NonNegativeInt),\n lastOutputTokens: Schema.optional(NonNegativeInt),\n lastReasoningOutputTokens: Schema.optional(NonNegativeInt),\n toolUses: Schema.optional(NonNegativeInt),\n durationMs: Schema.optional(NonNegativeInt),\n compactsAutomatically: Schema.optional(Schema.Boolean),\n autoCompactThreshold: Schema.optional(PositiveInt),", + "checksum": "a1a7e41bdc54cbd69f757707d29037d7d2aba9c097566870f1c6e117594536cc" }, "usage-codex-normalization": { "id": "usage-codex-normalization", @@ -456,8 +456,8 @@ "end": 190, "language": "typescript", "label": "Codex live token-usage normalization", - "code": "function normalizeCodexTokenUsage(\n usage: EffectCodexSchema.V2ThreadTokenUsageUpdatedNotification[\"tokenUsage\"],\n): ThreadTokenUsageSnapshot | undefined {\n const totalProcessedTokens = usage.total.totalTokens;\n const usedTokens = usage.last.totalTokens;\n if (usedTokens === undefined || usedTokens <= 0) {\n return undefined;\n }\n\n const maxTokens = usage.modelContextWindow ?? undefined;\n const inputTokens = usage.last.inputTokens;\n const cachedInputTokens = usage.last.cachedInputTokens;\n const outputTokens = usage.last.outputTokens;\n const reasoningOutputTokens = usage.last.reasoningOutputTokens;\n\n return {\n usedTokens,\n ...(totalProcessedTokens !== undefined && totalProcessedTokens > usedTokens\n ? { totalProcessedTokens }\n : {}),\n ...(maxTokens !== undefined ? { maxTokens } : {}),\n ...(inputTokens !== undefined ? { inputTokens } : {}),\n ...(cachedInputTokens !== undefined ? { cachedInputTokens } : {}),\n ...(outputTokens !== undefined ? { outputTokens } : {}),\n ...(reasoningOutputTokens !== undefined ? { reasoningOutputTokens } : {}),\n ...(usedTokens !== undefined ? { lastUsedTokens: usedTokens } : {}),\n ...(inputTokens !== undefined ? { lastInputTokens: inputTokens } : {}),\n ...(cachedInputTokens !== undefined ? { lastCachedInputTokens: cachedInputTokens } : {}),\n ...(outputTokens !== undefined ? { lastOutputTokens: outputTokens } : {}),\n ...(reasoningOutputTokens !== undefined\n ? { lastReasoningOutputTokens: reasoningOutputTokens }\n : {}),", - "checksum": "fec887f108f8d86e020a989334ff325a8b42f40bb30b26f61e69fe87041df522" + "code": " method,\n detail: error.message,\n cause: error,\n });\n}\n\ntype CodexLifecycleItem =\n | EffectCodexSchema.V2ItemStartedNotification[\"item\"]\n | EffectCodexSchema.V2ItemCompletedNotification[\"item\"];\n\ntype CodexToolUserInputQuestion =\n | EffectCodexSchema.ServerRequest__ToolRequestUserInputQuestion\n | EffectCodexSchema.ToolRequestUserInputParams__ToolRequestUserInputQuestion;\n\nconst ApprovalDecisionPayload = Schema.Struct({\n decision: ProviderApprovalDecision,\n});\n\nfunction readPayload(\n schema: Schema.Schema,\n payload: ProviderEvent[\"payload\"],\n): A | undefined {\n const isPayload = Schema.is(schema);\n return isPayload(payload) ? payload : undefined;\n}\n\nfunction trimText(value: string | undefined | null): string | undefined {\n const trimmed = value?.trim();\n return trimmed && trimmed.length > 0 ? trimmed : undefined;\n}\n\nfunction asUnknownRecord(value: unknown): Record | undefined {", + "checksum": "e401d51bcf0f2ba3730c62f28009062a4ae7bf01947efd57b2b4d9093e160bc3" }, "usage-codex-emission": { "id": "usage-codex-emission", @@ -466,8 +466,8 @@ "end": 1018, "language": "typescript", "label": "Codex live usage event emission", - "code": " if (event.method === \"thread/tokenUsage/updated\") {\n const payload = readPayload(\n EffectCodexSchema.V2ThreadTokenUsageUpdatedNotification,\n event.payload,\n );\n const normalizedUsage = payload ? normalizeCodexTokenUsage(payload.tokenUsage) : undefined;\n if (!normalizedUsage) {\n return [];\n }\n return [\n {\n type: \"thread.token-usage.updated\",\n ...runtimeEventBase(event, canonicalThreadId),\n payload: {\n usage: normalizedUsage,\n },\n },\n ];\n }", - "checksum": "b5443e4a9d8358ced1bc548a51227ab8796667482f262f5ebc6513cda5220cf1" + "code": " readPayload(EffectCodexSchema.V2ItemCompletedNotification, event.payload);\n const item = payload?.item;\n if (!item) {\n return undefined;\n }\n const itemType = toCanonicalItemType(item.type);\n if (itemType === \"unknown\" && lifecycle !== \"item.updated\") {\n return undefined;\n }\n\n const detail = itemDetail(itemType, item);\n const toolPresentation = item.type === \"mcpToolCall\" ? mcpToolPresentation(item) : {};\n const title = itemTitle(itemType, item, toolPresentation);\n const status =\n lifecycle === \"item.started\"\n ? \"inProgress\"\n : lifecycle === \"item.completed\"\n ? \"status\" in item && (item.status === \"failed\" || item.status === \"declined\")\n ? item.status", + "checksum": "b9c3a0d0365c6c9f054237cb8c6d4bfb181df69ab4083ed70493a00e60cb9035" }, "usage-claude-emission": { "id": "usage-claude-emission", @@ -476,8 +476,8 @@ "end": 2088, "language": "typescript", "label": "Claude live usage event emission", - "code": " const emitThreadTokenUsage = Effect.fn(\"emitThreadTokenUsage\")(function* (\n context: ClaudeSessionContext,\n usage: ThreadTokenUsageSnapshot | undefined,\n options?: {\n readonly rawMethod?: string;\n readonly rawPayload?: unknown;\n },\n ) {\n if (!usage) {\n return;\n }\n\n context.lastKnownTokenUsage = usage;\n context.lastKnownTotalProcessedTokens =\n usage.totalProcessedTokens ?? context.lastKnownTotalProcessedTokens;\n\n const turnState = context.turnState;\n const stamp = yield* makeEventStamp();\n yield* offerRuntimeEvent({\n type: \"thread.token-usage.updated\",\n eventId: stamp.eventId,\n provider: PROVIDER,\n createdAt: stamp.createdAt,\n threadId: context.session.threadId,\n ...(turnState ? { turnId: turnState.turnId } : {}),\n payload: {\n usage,\n },\n providerRefs: nativeProviderRefs(context),\n ...(options?.rawMethod || options?.rawPayload\n ? {\n raw: {\n source: \"claude.sdk.message\" as const,\n ...(options.rawMethod ? { method: options.rawMethod } : {}),\n payload: options.rawPayload,\n },\n }\n : {}),", - "checksum": "1358344a9ecfbb8fb89639a941e39879abdeb0f8da803ccfa6e9b66cb69478f1" + "code": " resumeCursor,\n updatedAt: yield* nowIso,\n };\n });\n\n const ensureAssistantTextBlock = Effect.fn(\"ensureAssistantTextBlock\")(function* (\n context: ClaudeSessionContext,\n blockIndex: number,\n options?: {\n readonly fallbackText?: string;\n readonly streamClosed?: boolean;\n },\n ) {\n const turnState = context.turnState;\n if (!turnState) {\n return undefined;\n }\n\n const existing = turnState.assistantTextBlocks.get(blockIndex);\n if (existing && !existing.completionEmitted) {\n if (existing.fallbackText.length === 0 && options?.fallbackText) {\n existing.fallbackText = options.fallbackText;\n }\n if (options?.streamClosed) {\n existing.streamClosed = true;\n }\n return { blockIndex, block: existing };\n }\n\n const block: AssistantTextBlockState = {\n itemId: yield* randomUUIDv4,\n blockIndex,\n emittedTextDelta: false,\n fallbackText: options?.fallbackText ?? \"\",\n streamClosed: options?.streamClosed ?? false,\n completionEmitted: false,\n };\n turnState.assistantTextBlocks.set(blockIndex, block);", + "checksum": "8d97f57f5b9c5a4d711c35f03d2203ec34262404dde9a224358b083cde6777b7" }, "usage-live-activity": { "id": "usage-live-activity", @@ -486,18 +486,18 @@ "end": 784, "language": "typescript", "label": "Live usage becomes context-window activity", - "code": " case \"thread.token-usage.updated\": {\n const payload = buildContextWindowActivityPayload(event);\n if (!payload) {\n return [];\n }\n\n return [\n {\n id: event.eventId,\n createdAt: event.createdAt,\n tone: \"info\",\n kind: \"context-window.updated\",\n summary: \"Context window updated\",\n payload,\n turnId: toTurnId(event.turnId) ?? null,\n ...maybeSequence,\n },\n ];\n }", - "checksum": "32de8fdc5bf965dcd67f6e3ebef7b896b6605e8c2a5d2d0a5544a915dccf9c1e" + "code": " },\n ];\n }\n\n case \"thread.token-usage.updated\": {\n const payload = buildContextWindowActivityPayload(event);\n if (!payload) {\n return [];\n }\n\n return [\n {\n id: event.eventId,\n createdAt: event.createdAt,\n tone: \"info\",\n kind: \"context-window.updated\",\n summary: \"Context window updated\",\n payload,\n turnId: toTurnId(event.turnId) ?? null,", + "checksum": "b7b9aaab84d817bfe4e7c4d20175a3eee4783508aacabc0bce66f9b138cc1a1c" }, "usage-web-context-meter": { "id": "usage-web-context-meter", "path": "apps/web/src/lib/contextWindow.ts", "start": 50, - "end": 96, + "end": 90, "language": "typescript", "label": "Latest valid context-window snapshot derivation", - "code": "export function deriveLatestContextWindowSnapshot(\n activities: ReadonlyArray,\n): ContextWindowSnapshot | null {\n for (let index = activities.length - 1; index >= 0; index -= 1) {\n const activity = activities[index];\n if (!activity || activity.kind !== \"context-window.updated\") {\n continue;\n }\n\n const payload = asRecord(activity.payload);\n const usedTokens = asFiniteNumber(payload?.usedTokens);\n if (usedTokens === null || usedTokens < 0) {\n continue;\n }\n\n const maxTokens = asFiniteNumber(payload?.maxTokens);\n const usedPercentage =\n maxTokens !== null && maxTokens > 0 ? Math.min(100, (usedTokens / maxTokens) * 100) : null;\n const remainingTokens =\n maxTokens !== null ? Math.max(0, Math.round(maxTokens - usedTokens)) : null;\n const remainingPercentage = usedPercentage !== null ? Math.max(0, 100 - usedPercentage) : null;\n\n return {\n usedTokens,\n totalProcessedTokens: asFiniteNumber(payload?.totalProcessedTokens),\n maxTokens,\n remainingTokens,\n usedPercentage,\n remainingPercentage,\n inputTokens: asFiniteNumber(payload?.inputTokens),\n cachedInputTokens: asFiniteNumber(payload?.cachedInputTokens),\n outputTokens: asFiniteNumber(payload?.outputTokens),\n reasoningOutputTokens: asFiniteNumber(payload?.reasoningOutputTokens),\n lastUsedTokens: asFiniteNumber(payload?.lastUsedTokens),\n lastInputTokens: asFiniteNumber(payload?.lastInputTokens),\n lastCachedInputTokens: asFiniteNumber(payload?.lastCachedInputTokens),\n lastOutputTokens: asFiniteNumber(payload?.lastOutputTokens),\n lastReasoningOutputTokens: asFiniteNumber(payload?.lastReasoningOutputTokens),\n toolUses: asFiniteNumber(payload?.toolUses),\n durationMs: asFiniteNumber(payload?.durationMs),\n compactsAutomatically: asBoolean(payload?.compactsAutomatically) ?? false,\n updatedAt: activity.createdAt,\n };\n }\n\n return null;\n}", - "checksum": "f414738aaad3b6f8526e5760a436944757fefe35662bb9ce36fe9375f1c0c35b" + "code": " return {\n usedTokens,\n totalProcessedTokens: asFiniteNumber(payload?.totalProcessedTokens),\n maxTokens,\n remainingTokens,\n usedPercentage,\n remainingPercentage,\n inputTokens: asFiniteNumber(payload?.inputTokens),\n cachedInputTokens: asFiniteNumber(payload?.cachedInputTokens),\n outputTokens: asFiniteNumber(payload?.outputTokens),\n reasoningOutputTokens: asFiniteNumber(payload?.reasoningOutputTokens),\n lastUsedTokens: asFiniteNumber(payload?.lastUsedTokens),\n lastInputTokens: asFiniteNumber(payload?.lastInputTokens),\n lastCachedInputTokens: asFiniteNumber(payload?.lastCachedInputTokens),\n lastOutputTokens: asFiniteNumber(payload?.lastOutputTokens),\n lastReasoningOutputTokens: asFiniteNumber(payload?.lastReasoningOutputTokens),\n toolUses: asFiniteNumber(payload?.toolUses),\n durationMs: asFiniteNumber(payload?.durationMs),\n compactsAutomatically: asBoolean(payload?.compactsAutomatically) ?? false,\n autoCompactThreshold: asFiniteNumber(payload?.autoCompactThreshold),\n updatedAt: activity.createdAt,\n };\n }\n\n return null;\n}\n\nexport function formatContextWindowTokens(value: number | null): string {\n if (value === null || !Number.isFinite(value)) {\n return \"0\";\n }\n if (value < 1_000) {\n return `${Math.round(value)}`;\n }\n if (value < 10_000) {\n return `${(value / 1_000).toFixed(1).replace(/\\.0$/, \"\")}k`;\n }\n if (value < 1_000_000) {\n return `${Math.round(value / 1_000)}k`;\n }\n return `${(value / 1_000_000).toFixed(1).replace(/\\.0$/, \"\")}m`;", + "checksum": "7f8275c0c9616c94a566e78443ac6b69be3120b5fe4cc9caf0ddea3365747434" }, "usage-summary-contract": { "id": "usage-summary-contract", @@ -506,8 +506,8 @@ "end": 101, "language": "typescript", "label": "Historical usage version, providers, tokens, and cost buckets", - "code": "/**\n * Usage reporting contract.\n *\n * Each environment scans the provider CLIs' own on-disk session transcripts\n * (`~/.claude/projects/**\\/*.jsonl`, `~/.codex/sessions/**\\/*.jsonl`) rather than\n * relying on T3 Code's own orchestration projections, so usage stays complete\n * even for turns that were never driven through T3 Code. This mirrors the\n * approach `ccusage` takes.\n *\n * Environments return pre-aggregated `(day, hourStart?, provider, model)`\n * buckets. Raw transcript records never cross the wire.\n *\n * @module usage\n */\nimport * as Schema from \"effect/Schema\";\n\nimport { NonNegativeInt, TrimmedNonEmptyString } from \"./baseSchemas.ts\";\n\n/**\n * Bumped whenever the shape of {@link UsageSummary} changes incompatibly. The\n * client renders partial coverage when an environment reports an older version\n * rather than failing the whole page.\n */\nexport const USAGE_CONTRACT_VERSION = 4 as const;\n\nexport const UsageProviderKind = Schema.Literals([\"claude\", \"codex\"]);\nexport type UsageProviderKind = typeof UsageProviderKind.Type;\n\n/**\n * A calendar day in the reporting time zone, formatted `YYYY-MM-DD`.\n *\n * Days are bucketed server-side so that a turn always lands on the day the user\n * experienced it, not the UTC day.\n */\nconst USAGE_DAY_PATTERN = /^\\d{4}-\\d{2}-\\d{2}$/;\n\nexport const UsageDay = TrimmedNonEmptyString.check(Schema.isPattern(USAGE_DAY_PATTERN)).pipe(\n Schema.brand(\"UsageDay\"),\n);\nexport type UsageDay = typeof UsageDay.Type;\n\nexport const UsageResolution = Schema.Literals([\"day\", \"hour\"]);\nexport type UsageResolution = typeof UsageResolution.Type;\n\n/**\n * Why a bucket's cost is what it is.\n *\n * - `providerReported` - the transcript carried an explicit cost figure.\n * - `modelPriced` - we matched the model against the LiteLLM rate table.\n * - `unpriced` - tokens are known, rates are not. Counted in totals, excluded\n * from cost.\n */\nexport const UsageCostSource = Schema.Literals([\"providerReported\", \"modelPriced\", \"unpriced\"]);\nexport type UsageCostSource = typeof UsageCostSource.Type;\n\n/**\n * Token counts for a bucket.\n *\n * `cachedInputTokens` and `cacheCreationTokens` are disjoint from\n * `uncachedInputTokens`; summing all three gives total input. `reasoningTokens`\n * is a *subset* of `outputTokens` (Codex reports it that way, and Anthropic\n * folds thinking into output), so it must never be added on top.\n */\nexport const UsageTokenTotals = Schema.Struct({\n uncachedInputTokens: NonNegativeInt,\n cachedInputTokens: NonNegativeInt,\n cacheCreationTokens: NonNegativeInt,\n outputTokens: NonNegativeInt,\n reasoningTokens: NonNegativeInt,\n});\nexport type UsageTokenTotals = typeof UsageTokenTotals.Type;\n\n/**\n * One `(day, hourStart?, provider, model)` cell. `hourStart` is the UTC start\n * instant of a rolling bucket and is present only for hourly requests.\n *\n * `costUsd` is the raw API-equivalent cost of these tokens. It is not money\n * spent: subscription plans bill separately. `unpricedRecords` counts records\n * whose tokens are included in the token totals but which contributed nothing\n * to `costUsd`.\n */\nexport const UsageBucket = Schema.Struct({\n day: UsageDay,\n hourStart: Schema.optional(TrimmedNonEmptyString),\n provider: UsageProviderKind,\n model: TrimmedNonEmptyString,\n totals: UsageTokenTotals,\n costUsd: Schema.Number,\n /**\n * What the cached input would have cost at full input rates minus what it\n * actually cost. Requires the rate table, so it is computed alongside cost\n * rather than derived on the client.\n */\n cacheSavingsUsd: Schema.Number,\n costSource: UsageCostSource,\n /** Distinct assistant responses, after de-duplication. */\n records: NonNegativeInt,\n unpricedRecords: NonNegativeInt,\n /** Distinct transcript sessions that contributed to this cell. */\n sessions: NonNegativeInt,\n});", - "checksum": "f93dd8ada5db38b9c283d950acc7da22def26fa5c8ae2352aca8ab2aa31b3040" + "code": "/**\n * Usage reporting contract.\n *\n * Each environment scans the provider CLIs' own on-disk session transcripts\n * (`~/.claude/projects/**\\/*.jsonl`, `~/.codex/sessions/**\\/*.jsonl`,\n * `~/.grok/sessions/**\\/updates.jsonl`) rather than relying on T3 Code's own\n * orchestration projections, so usage stays complete even for turns that were\n * never driven through T3 Code. This mirrors the approach `ccusage` takes.\n *\n * Environments return pre-aggregated `(day, hourStart?, provider, model)`\n * buckets. Raw transcript records never cross the wire.\n *\n * @module usage\n */\nimport * as Schema from \"effect/Schema\";\n\nimport { NonNegativeInt, TrimmedNonEmptyString } from \"./baseSchemas.ts\";\n\n/**\n * Bumped whenever the shape of {@link UsageSummary} changes incompatibly. The\n * client renders partial coverage when an environment reports an older version\n * rather than failing the whole page.\n */\nexport const USAGE_CONTRACT_VERSION = 5 as const;\n\n/**\n * Oldest {@link UsageSummary} version a current client will still merge.\n *\n * v5 only adds `grok` to {@link UsageProviderKind}; v4 Claude/Codex buckets\n * remain valid, so mixed-version environments keep those totals instead of\n * treating every older server as stale.\n */\nexport const USAGE_MERGE_COMPATIBLE_SINCE = 4 as const;\n\nexport const UsageProviderKind = Schema.Literals([\"claude\", \"codex\", \"grok\"]);\nexport type UsageProviderKind = typeof UsageProviderKind.Type;\n\n/**\n * A calendar day in the reporting time zone, formatted `YYYY-MM-DD`.\n *\n * Days are bucketed server-side so that a turn always lands on the day the user\n * experienced it, not the UTC day.\n */\nconst USAGE_DAY_PATTERN = /^\\d{4}-\\d{2}-\\d{2}$/;\n\nexport const UsageDay = TrimmedNonEmptyString.check(Schema.isPattern(USAGE_DAY_PATTERN)).pipe(\n Schema.brand(\"UsageDay\"),\n);\nexport type UsageDay = typeof UsageDay.Type;\n\nexport const UsageResolution = Schema.Literals([\"day\", \"hour\"]);\nexport type UsageResolution = typeof UsageResolution.Type;\n\n/**\n * Why a bucket's cost is what it is.\n *\n * - `providerReported` - the transcript carried an explicit cost figure.\n * - `modelPriced` - we used a custom price override or the LiteLLM rate table.\n * - `unpriced` - tokens are known, rates are not. Counted in totals, excluded\n * from cost.\n */\nexport const UsageCostSource = Schema.Literals([\"providerReported\", \"modelPriced\", \"unpriced\"]);\nexport type UsageCostSource = typeof UsageCostSource.Type;\n\n/**\n * Token counts for a bucket.\n *\n * `cachedInputTokens` and `cacheCreationTokens` are disjoint from\n * `uncachedInputTokens`; summing all three gives total input. `reasoningTokens`\n * is a *subset* of `outputTokens` (Codex reports it that way, and Anthropic\n * folds thinking into output), so it must never be added on top.\n */\nexport const UsageTokenTotals = Schema.Struct({\n uncachedInputTokens: NonNegativeInt,\n cachedInputTokens: NonNegativeInt,\n cacheCreationTokens: NonNegativeInt,\n outputTokens: NonNegativeInt,\n reasoningTokens: NonNegativeInt,\n});\nexport type UsageTokenTotals = typeof UsageTokenTotals.Type;\n\n/**\n * One `(day, hourStart?, provider, model)` cell. `hourStart` is the UTC start\n * instant of a rolling bucket and is present only for hourly requests.\n *\n * `costUsd` is the raw API-equivalent cost of these tokens. It is not money\n * spent: subscription plans bill separately. `unpricedRecords` counts records\n * whose tokens are included in the token totals but which contributed nothing\n * to `costUsd`.\n */\nexport const UsageBucket = Schema.Struct({\n day: UsageDay,\n hourStart: Schema.optional(TrimmedNonEmptyString),\n provider: UsageProviderKind,\n model: TrimmedNonEmptyString,\n totals: UsageTokenTotals,\n costUsd: Schema.Number,\n /**\n * What the cached input would have cost at full input rates minus what it\n * actually cost. Requires the rate table, so it is computed alongside cost\n * rather than derived on the client.", + "checksum": "d41abdde2550475fbe1399f35e5a653c7b4152c2dc2dcdf66bde4a0b8629a36c" }, "usage-home-discovery": { "id": "usage-home-discovery", @@ -516,8 +516,8 @@ "end": 225, "language": "typescript", "label": "Claude and Codex transcript home resolution", - "code": " /**\n * Claude's config dir is the home itself when overridden, but a default\n * install nests transcripts under `~/.claude/projects`. Probe both.\n */\n const resolveClaudeTranscriptDir = (homePath: string) =>\n Effect.gen(function* () {\n const nested = path.join(homePath, \".claude\", \"projects\");\n const nestedExists = yield* fileSystem\n .exists(nested)\n .pipe(Effect.catchCause(() => Effect.succeed(false)));\n return nestedExists ? nested : path.join(homePath, \"projects\");\n });\n\n /** Resolves the transcript directory for each provider. */\n const resolveTranscriptDirs = Effect.fn(\"UsageService.resolveTranscriptDirs\")(function* () {\n // A settings failure must surface as an error: swallowing it here would\n // present \"zero usage from every provider\" as a valid answer.\n const settings = yield* settingsService.getSettings.pipe(\n Effect.catchCause(\n (cause) =>\n new UsageReadError({\n reason: \"scanFailed\",\n // Bounded description; the squashed failure travels as the cause.\n // Squashed, not the Cause tree: a full tree in a Defect field is\n // the unbounded wire payload the bounded detail exists to avoid.\n detail: \"Server settings could not be read.\",\n cause: Cause.squash(cause),\n }),\n ),\n );\n\n const claudeHome = yield* resolveClaudeHomePath(settings.providers.claudeAgent);\n const claudeDir = yield* resolveClaudeTranscriptDir(claudeHome);\n const codexLayout = yield* resolveCodexHomeLayout(settings.providers.codex);\n\n return [\n { provider: \"claude\" as const, dir: claudeDir },\n { provider: \"codex\" as const, dir: path.join(codexLayout.sharedHomePath, \"sessions\") },\n ];", - "checksum": "d6526ea3f89bb7c8877e0d60c06a6f36809be5b51834b133045a40debecabafd" + "code": " }\n }\n\n const fetched = yield* httpClient.get(LITELLM_RATES_URL).pipe(\n Effect.flatMap(HttpClientResponse.filterStatusOk),\n Effect.flatMap((response) => response.json),\n Effect.timeout(10_000),\n Effect.catchCause(() => Effect.succeed(null)),\n );\n if (fetched === null) {\n // The refresh failed; whatever we are serving is now past its TTL and\n // must not keep claiming to be fresh.\n if (rates.size > 0) ratesStatus = \"cached\";\n return;\n }\n\n const parsed = parseRateTable(fetched);\n if (parsed.size === 0) return;\n\n rates = parsed;\n ratesFetchedAtMs = now;\n ratesStatus = \"fresh\";\n\n yield* encodeRatesCache({ fetchedAtMs: now, document: fetched }).pipe(\n Effect.flatMap((serialized) => fileSystem.writeFileString(ratesCachePath, serialized)),\n Effect.catchCause(() => Effect.void),\n );\n });\n\n const ensureRates = (force: boolean) => ratesLock.withPermit(loadRates(force));\n\n const refreshRates = ensureRates(true).pipe(\n Effect.map(pricing),\n Effect.withSpan(\"UsageService.refreshRates\"),\n );\n\n /**\n * Claude's config dir is the home itself when overridden, but a default\n * install nests transcripts under `~/.claude/projects`. Probe both.", + "checksum": "b74098d7789cb4e51703255dcbc8137747cb524da3e3ce94d8a12607ac991762" }, "usage-transcript-parsers": { "id": "usage-transcript-parsers", @@ -526,8 +526,8 @@ "end": 137, "language": "typescript", "label": "Claude transcript usage normalization", - "code": "/**\n * Parses one line of a Claude Code transcript.\n *\n * T3 Code writes one record per assistant *content block*, and every one of\n * those records repeats the same complete `usage` object for the parent\n * message. Summing them overcounts by roughly 2.4x on a real workload, so the\n * caller must drop repeats by `dedupeKey` and keep the first.\n */\nexport function parseClaudeLine(line: string): UsageRecord | null {\n let parsed: unknown;\n try {\n parsed = JSON.parse(line);\n } catch {\n return null;\n }\n if (typeof parsed !== \"object\" || parsed === null) return null;\n\n const record = parsed as Record;\n if (record[\"type\"] !== \"assistant\") return null;\n\n const message = record[\"message\"];\n if (typeof message !== \"object\" || message === null) return null;\n const messageRecord = message as Record;\n\n const usage = messageRecord[\"usage\"];\n if (typeof usage !== \"object\" || usage === null) return null;\n const usageRecord = usage as Record;\n\n const timestampMs = parseTimestampMs(record[\"timestamp\"]);\n if (timestampMs === null) return null;\n\n const model = typeof messageRecord[\"model\"] === \"string\" ? messageRecord[\"model\"] : \"\";\n if (model.length === 0) return null;\n\n const messageId = typeof messageRecord[\"id\"] === \"string\" ? messageRecord[\"id\"] : null;\n const requestId = typeof record[\"requestId\"] === \"string\" ? record[\"requestId\"] : null;\n // Matches ccusage: prefer the message/request pair, fall back to whichever\n // half exists. Records with neither cannot be de-duplicated.\n const dedupeKey =\n messageId === null && requestId === null ? null : `${messageId ?? \"\"}:${requestId ?? \"\"}`;\n\n const cost = record[\"costUSD\"];\n\n return {\n provider: \"claude\",\n timestampMs,\n model,\n sessionId: typeof record[\"sessionId\"] === \"string\" ? record[\"sessionId\"] : \"\",\n totals: {\n uncachedInputTokens: int(usageRecord[\"input_tokens\"]),\n cachedInputTokens: int(usageRecord[\"cache_read_input_tokens\"]),\n cacheCreationTokens: int(usageRecord[\"cache_creation_input_tokens\"]),\n outputTokens: int(usageRecord[\"output_tokens\"]),\n // Anthropic folds thinking tokens into output and does not break them out.\n reasoningTokens: 0,\n },\n reportedCostUsd: typeof cost === \"number\" && Number.isFinite(cost) ? cost : null,\n dedupeKey,\n };\n}", - "checksum": "98addede4c92886630a8f69b8ec46e776ac775bbb00896814ba7a17e244c4531" + "code": " * headless `total_cost_usd_ticks`. Convert to dollars for pricing.\n */\nexport const GROK_COST_USD_TICKS_PER_DOLLAR = 10_000_000_000;\n\nfunction grokCostTicksToUsd(ticks: unknown): number | null {\n if (typeof ticks !== \"number\" || !Number.isFinite(ticks) || ticks < 0) return null;\n return ticks / GROK_COST_USD_TICKS_PER_DOLLAR;\n}\n\n/* -------------------------------------------------------------------------- */\n/* Claude Code */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Parses one line of a Claude Code transcript.\n *\n * T3 Code writes one record per assistant *content block*, and every one of\n * those records repeats the same complete `usage` object for the parent\n * message. Summing them overcounts by roughly 2.4x on a real workload, so the\n * caller must drop repeats by `dedupeKey` and keep the first.\n */\nexport function parseClaudeLine(line: string): UsageRecord | null {\n let parsed: unknown;\n try {\n parsed = JSON.parse(line);\n } catch {\n return null;\n }\n if (typeof parsed !== \"object\" || parsed === null) return null;\n\n const record = parsed as Record;\n if (record[\"type\"] !== \"assistant\") return null;\n\n const message = record[\"message\"];\n if (typeof message !== \"object\" || message === null) return null;\n const messageRecord = message as Record;\n\n const usage = messageRecord[\"usage\"];\n if (typeof usage !== \"object\" || usage === null) return null;\n const usageRecord = usage as Record;\n\n const timestampMs = parseTimestampMs(record[\"timestamp\"]);\n if (timestampMs === null) return null;\n\n const model = typeof messageRecord[\"model\"] === \"string\" ? messageRecord[\"model\"] : \"\";\n if (model.length === 0) return null;\n\n const messageId = typeof messageRecord[\"id\"] === \"string\" ? messageRecord[\"id\"] : null;\n const requestId = typeof record[\"requestId\"] === \"string\" ? record[\"requestId\"] : null;\n // Matches ccusage: prefer the message/request pair, fall back to whichever\n // half exists. Records with neither cannot be de-duplicated.\n const dedupeKey =\n messageId === null && requestId === null ? null : `${messageId ?? \"\"}:${requestId ?? \"\"}`;\n\n const cost = record[\"costUSD\"];\n\n return {\n provider: \"claude\",\n timestampMs,\n model,", + "checksum": "0e09c48fc3881974c7d4fd3a578c4ed7c3f787dd80af2b09836945a9f28dfc60" }, "usage-dedup-buckets": { "id": "usage-dedup-buckets", @@ -536,8 +536,8 @@ "end": 178, "language": "typescript", "label": "Global deduplication, bounds, bucketing, and pricing fold", - "code": " /**\n * Folds one record in. Returns whether it actually contributed, so callers\n * can derive per-window facts (distinct sessions, for one) from the records\n * that landed rather than everything the mtime prefilter happened to admit.\n */\n add(record: UsageRecord): boolean {\n if (record.dedupeKey !== null) {\n if (this.#seen.has(record.dedupeKey)) {\n this.#duplicatesDropped += 1;\n return false;\n }\n this.#seen.add(record.dedupeKey);\n }\n\n if (\n this.#hourlyWindow !== null &&\n (record.timestampMs < this.#hourlyWindow.sinceTimeMs ||\n record.timestampMs >= this.#hourlyWindow.untilTimeMs)\n ) {\n this.#outOfWindow += 1;\n return false;\n }\n\n const day = this.#toDay(record.timestampMs);\n if (\n this.#hourlyWindow === null &&\n (day < this.#options.sinceDay || day > this.#options.untilDay)\n ) {\n this.#outOfWindow += 1;\n return false;\n }\n\n const hourStart =\n this.#hourlyWindow === null\n ? \"\"\n : new Date(\n this.#hourlyWindow.sinceTimeMs +\n Math.floor((record.timestampMs - this.#hourlyWindow.sinceTimeMs) / HOUR_MS) * HOUR_MS,\n ).toISOString();\n const key = `${day}\\u0000${hourStart}\\u0000${record.provider}\\u0000${record.model}`;\n let bucket = this.#buckets.get(key);\n if (bucket === undefined) {\n bucket = {\n totals: EMPTY_TOTALS,\n costUsd: 0,\n cacheSavingsUsd: 0,\n records: 0,\n unpricedRecords: 0,\n providerReportedRecords: 0,\n sessions: new Set(),\n };\n this.#buckets.set(key, bucket);\n }\n\n const priced = priceUsage(\n this.#options.rates,\n record.model,\n record.totals,\n record.reportedCostUsd,\n );\n\n bucket.totals = addTotals(bucket.totals, record.totals);\n bucket.costUsd += priced.costUsd;\n bucket.cacheSavingsUsd += cacheSavingsUsd(this.#options.rates, record.model, record.totals);\n bucket.records += 1;\n if (priced.costSource === \"unpriced\") bucket.unpricedRecords += 1;\n if (priced.costSource === \"providerReported\") bucket.providerReportedRecords += 1;\n if (record.sessionId.length > 0) bucket.sessions.add(record.sessionId);\n return true;\n }", - "checksum": "ca4bb237d353ca5cbf384f3e91c0d802c24e1764b19871add182ac894cc19677" + "code": "\n /**\n * Folds one record in. Returns whether it actually contributed, so callers\n * can derive per-window facts (distinct sessions, for one) from the records\n * that landed rather than everything the mtime prefilter happened to admit.\n */\n add(record: UsageRecord): boolean {\n if (record.dedupeKey !== null) {\n if (this.#seen.has(record.dedupeKey)) {\n this.#duplicatesDropped += 1;\n return false;\n }\n this.#seen.add(record.dedupeKey);\n }\n\n if (\n this.#hourlyWindow !== null &&\n (record.timestampMs < this.#hourlyWindow.sinceTimeMs ||\n record.timestampMs >= this.#hourlyWindow.untilTimeMs)\n ) {\n this.#outOfWindow += 1;\n return false;\n }\n\n const day = this.#toDay(record.timestampMs);\n if (\n this.#hourlyWindow === null &&\n (day < this.#options.sinceDay || day > this.#options.untilDay)\n ) {\n this.#outOfWindow += 1;\n return false;\n }\n\n const hourStart =\n this.#hourlyWindow === null\n ? \"\"\n : new Date(\n this.#hourlyWindow.sinceTimeMs +\n Math.floor((record.timestampMs - this.#hourlyWindow.sinceTimeMs) / HOUR_MS) * HOUR_MS,\n ).toISOString();\n const key = `${day}\\u0000${hourStart}\\u0000${record.provider}\\u0000${record.model}`;\n let bucket = this.#buckets.get(key);\n if (bucket === undefined) {\n bucket = {\n totals: EMPTY_TOTALS,\n costUsd: 0,\n cacheSavingsUsd: 0,\n records: 0,\n unpricedRecords: 0,\n providerReportedRecords: 0,\n sessions: new Set(),\n };\n this.#buckets.set(key, bucket);\n }\n\n const priced = priceUsage(\n this.#options.rates,\n record.model,\n record.totals,\n record.reportedCostUsd,\n this.#options.priceOverrides,\n );\n\n bucket.totals = addTotals(bucket.totals, record.totals);\n bucket.costUsd += priced.costUsd;\n bucket.cacheSavingsUsd += cacheSavingsUsd(\n this.#options.rates,\n record.model,\n record.totals,\n this.#options.priceOverrides,", + "checksum": "f68e5e1f8e561c6e8c24cdf66526e1da204e4f54e63e7c03f2bd259d9f2717b1" }, "usage-pricing-cache": { "id": "usage-pricing-cache", @@ -546,8 +546,8 @@ "end": 148, "language": "typescript", "label": "Historical usage rate lookup and pricing arithmetic", - "code": "/**\n * Models we never price, regardless of the table.\n *\n * `` marks locally generated messages that were never billed. Bare\n * family names (\"opus\", \"sonnet\") are genuinely ambiguous across generations,\n * so we report them as unpriced instead of guessing a generation.\n */\nconst UNPRICEABLE_MODELS = new Set([\n \"\",\n \"synthetic\",\n \"opus\",\n \"sonnet\",\n \"haiku\",\n \"fable\",\n]);\n\nexport function lookupRate(table: RateTable, model: string): ModelRate | null {\n const normalized = normalizeModelName(model);\n if (normalized.length === 0 || UNPRICEABLE_MODELS.has(normalized)) return null;\n return table.get(normalized) ?? null;\n}\n\nexport interface PricedUsage {\n readonly costUsd: number;\n readonly costSource: UsageCostSource;\n}\n\n/**\n * Prices a bucket's tokens.\n *\n * `reasoningTokens` is intentionally not charged separately: it is already\n * counted inside `outputTokens`.\n */\nexport function priceUsage(\n table: RateTable,\n model: string,\n totals: UsageTokenTotals,\n reportedCostUsd: number | null,\n): PricedUsage {\n if (reportedCostUsd !== null && Number.isFinite(reportedCostUsd)) {\n return { costUsd: reportedCostUsd, costSource: \"providerReported\" };\n }\n\n const rate = lookupRate(table, model);\n if (rate === null) return { costUsd: 0, costSource: \"unpriced\" };\n\n const costUsd =\n totals.uncachedInputTokens * rate.inputCostPerToken +\n totals.cachedInputTokens * rate.cacheReadCostPerToken +\n totals.cacheCreationTokens * rate.cacheCreationCostPerToken +\n totals.outputTokens * rate.outputCostPerToken;\n\n return { costUsd, costSource: \"modelPriced\" };\n}\n\n/**\n * What the cached input would have cost at full input rates, minus what it\n * actually cost. Drives the \"cache savings\" figure.\n */\nexport function cacheSavingsUsd(table: RateTable, model: string, totals: UsageTokenTotals): number {\n const rate = lookupRate(table, model);\n if (rate === null) return 0;\n return totals.cachedInputTokens * (rate.inputCostPerToken - rate.cacheReadCostPerToken);\n}", - "checksum": "c1d6f1e6a20765222fbbd5c94a90766711a846430088f37c1242011fd9ec1518" + "code": " const key = normalizeRateKey(name);\n if (key.length === 0) continue;\n table.set(key, {\n inputCostPerToken: input,\n outputCostPerToken: output,\n // Anthropic bills cache reads at a discount and cache writes at a\n // premium. When a model omits them, cached input is priced as plain\n // input rather than as free.\n cacheReadCostPerToken: finiteNumber(entry.cache_read_input_token_cost) ?? input,\n cacheCreationCostPerToken: finiteNumber(entry.cache_creation_input_token_cost) ?? input,\n });\n }\n\n // `null` marks a bare name claimed at conflicting rates: no alias for it.\n const aliasCandidates = new Map();\n for (const [key, rate] of table) {\n const alias = bareModelName(key);\n if (alias.length === 0 || alias === key || table.has(alias)) continue;\n const held = aliasCandidates.get(alias);\n if (held === undefined) {\n aliasCandidates.set(alias, rate);\n } else if (held !== null && !sameRate(held, rate)) {\n aliasCandidates.set(alias, null);\n }\n }\n for (const [alias, rate] of aliasCandidates) {\n if (rate !== null) table.set(alias, rate);\n }\n\n return table;\n}\n\nfunction sameRate(a: ModelRate, b: ModelRate): boolean {\n return (\n a.inputCostPerToken === b.inputCostPerToken &&\n a.outputCostPerToken === b.outputCostPerToken &&\n a.cacheReadCostPerToken === b.cacheReadCostPerToken &&\n a.cacheCreationCostPerToken === b.cacheCreationCostPerToken\n );\n}\n\nfunction normalizeRateKey(model: string): string {\n return model.trim().toLowerCase();\n}\n\nfunction bareModelName(key: string): string {\n const slash = key.lastIndexOf(\"/\");\n return slash === -1 ? key : key.slice(slash + 1);\n}\n\n/**\n * Drops a bracketed variant suffix such as `claude-fable-5-1[1m]`, which\n * Claude Code writes for the 1M context tier. The rate table only knows the\n * base name, and we price at the base tier anyway.\n */\nfunction stripVariantSuffix(key: string): string {\n const bracket = key.indexOf(\"[\");\n return bracket === -1 ? key : key.slice(0, bracket);\n}\n\n/**\n * Models we never price, regardless of the table.\n *\n * `` marks locally generated messages that were never billed. Bare", + "checksum": "5490c87f87b40cd244f7a6c8779fec5dcf41e9a78990ef28764105eb677ddcdf" }, "usage-rpc-contract": { "id": "usage-rpc-contract", @@ -556,8 +556,8 @@ "end": 284, "language": "typescript", "label": "Typed usage summary RPC", - "code": " previewAutomationFocusHost: \"previewAutomation.focusHost\",\n\n // Server meta\n serverProbe: \"server.probe\",\n serverGetConfig: \"server.getConfig\",\n serverRefreshProviders: \"server.refreshProviders\",\n serverUpdateProvider: \"server.updateProvider\",\n serverUpdateServer: \"server.updateServer\",\n serverUpdateServerWithProgress: \"server.updateServerWithProgress\",\n serverUpsertKeybinding: \"server.upsertKeybinding\",\n serverRemoveKeybinding: \"server.removeKeybinding\",\n serverGetSettings: \"server.getSettings\",\n serverUpdateSettings: \"server.updateSettings\",\n serverDiscoverSourceControl: \"server.discoverSourceControl\",\n serverGetTraceDiagnostics: \"server.getTraceDiagnostics\",\n serverGetProcessDiagnostics: \"server.getProcessDiagnostics\",\n serverGetProcessResourceHistory: \"server.getProcessResourceHistory\",\n serverGetResourceTelemetryHistory: \"server.getResourceTelemetryHistory\",\n serverRetryResourceTelemetry: \"server.retryResourceTelemetry\",\n serverSignalProcess: \"server.signalProcess\",\n serverReportClientActivity: \"server.reportClientActivity\",\n serverReportHostPowerState: \"server.reportHostPowerState\",\n serverGetBackgroundPolicy: \"server.getBackgroundPolicy\",\n serverGetUsageSummary: \"server.getUsageSummary\",\n", - "checksum": "a9bdb475c42adc730d43788b4336aa36b6c0de90e60fc9dcd2139b1f929e9ea6" + "code": " projectsListEntries: \"projects.listEntries\",\n projectsReadFile: \"projects.readFile\",\n projectsSearchContents: \"projects.searchContents\",\n projectsSearchEntries: \"projects.searchEntries\",\n projectsWriteFile: \"projects.writeFile\",\n\n // Shell methods\n shellOpenInEditor: \"shell.openInEditor\",\n\n // Filesystem methods\n filesystemBrowse: \"filesystem.browse\",\n agentSessionsScan: \"agentSessions.scan\",\n agentSessionsImport: \"agentSessions.import\",\n assetsCreateUrl: \"assets.createUrl\",\n attachmentsCreateUploadUrl: \"attachments.createUploadUrl\",\n attachmentsDelete: \"attachments.delete\",\n\n // Provider methods\n providerUploadFeedback: \"provider.uploadFeedback\",\n providerAuthStart: \"provider.auth.start\",\n providerConsumeResetCredit: \"provider.consumeResetCredit\",\n providerAuthComplete: \"provider.auth.complete\",\n providerAuthCancel: \"provider.auth.cancel\",\n providerAuthLogout: \"provider.auth.logout\",\n providerAuthSubscribe: \"provider.auth.subscribe\",", + "checksum": "41f4187f609e35c8c1b624bb0abb62ba5d0dcc32f8675c6a965a2e5d3cd0b970" }, "usage-environment-merge": { "id": "usage-environment-merge", @@ -566,8 +566,8 @@ "end": 163, "language": "typescript", "label": "Deterministic physical-source ownership across environments", - "code": "/**\n * Decides which environment owns each physical transcript directory.\n *\n * Several environments on one machine (worktree servers, for instance) resolve\n * the same provider home and would otherwise double count every token. The\n * first environment in a stable order claims a fingerprint; the rest have that\n * provider's buckets dropped. Environments are sorted by id so the winner does\n * not change between renders.\n */\nfunction claimSources(environments: readonly EnvironmentUsage[]): {\n readonly ownerByFingerprint: ReadonlyMap;\n readonly duplicates: readonly string[];\n} {\n const ownerByFingerprint = new Map();\n const duplicates: string[] = [];\n\n const ordered = [...environments].sort((a, b) => a.environmentId.localeCompare(b.environmentId));\n\n for (const environment of ordered) {\n for (const source of environment.summary.sources) {\n if (source.status === \"missing\") continue;\n const key = fingerprintKey(source.fingerprint);\n if (ownerByFingerprint.has(key)) {\n duplicates.push(`${environment.label}: ${source.fingerprint.resolvedHomePath}`);\n continue;\n }\n ownerByFingerprint.set(key, environment.environmentId);\n }\n }\n\n return { ownerByFingerprint, duplicates };\n}\n\n/** Sources this environment owns after fingerprint claims, plus their buckets. */\nfunction ownedContribution(\n environment: EnvironmentUsage,\n ownerByFingerprint: ReadonlyMap,\n): {\n readonly buckets: readonly UsageBucket[];\n readonly sessionsByProvider: ReadonlyMap;\n} {\n const ownedProviders = new Set();\n const sessionsByProvider = new Map();\n for (const source of environment.summary.sources) {\n if (source.status === \"missing\") continue;\n const key = fingerprintKey(source.fingerprint);\n if (ownerByFingerprint.get(key) === environment.environmentId) {\n const provider = source.fingerprint.provider;\n ownedProviders.add(provider);\n // Distinct within a directory. Summing per-bucket session counts instead\n // would count a session once per day and model it spans.\n sessionsByProvider.set(\n provider,\n (sessionsByProvider.get(provider) ?? 0) + source.distinctSessions,\n );\n }\n }\n return {\n buckets: environment.summary.buckets.filter((bucket) => ownedProviders.has(bucket.provider)),\n sessionsByProvider,\n };\n}", - "checksum": "2ef3c27f0875e553cc334289b2ffa24682b812e8a15f401be76eb06240ccfb98" + "code": "\n/**\n * Decides which environment owns each physical transcript directory.\n *\n * Several environments on one machine (worktree servers, for instance) resolve\n * the same provider home and would otherwise double count every token. The\n * first environment in a stable order claims a fingerprint; the rest have that\n * provider's buckets dropped. Environments are sorted by id so the winner does\n * not change between renders.\n */\nfunction claimSources(environments: readonly EnvironmentUsage[]): {\n readonly ownerByFingerprint: ReadonlyMap;\n readonly duplicates: readonly string[];\n} {\n const ownerByFingerprint = new Map();\n const duplicates: string[] = [];\n\n const ordered = [...environments].sort((a, b) => a.environmentId.localeCompare(b.environmentId));\n\n for (const environment of ordered) {\n for (const source of environment.summary.sources) {\n if (source.status === \"missing\") continue;\n const key = fingerprintKey(source.fingerprint);\n if (ownerByFingerprint.has(key)) {\n duplicates.push(`${environment.label}: ${source.fingerprint.resolvedHomePath}`);\n continue;\n }\n ownerByFingerprint.set(key, environment.environmentId);\n }\n }\n\n return { ownerByFingerprint, duplicates };\n}\n\n/** Sources this environment owns after fingerprint claims, plus their buckets. */\nfunction ownedContribution(\n environment: EnvironmentUsage,\n ownerByFingerprint: ReadonlyMap,\n): {\n readonly buckets: readonly UsageBucket[];\n readonly sessionsByProvider: ReadonlyMap;\n} {\n const ownedProviders = new Set();\n const sessionsByProvider = new Map();\n for (const source of environment.summary.sources) {\n if (source.status === \"missing\") continue;\n const key = fingerprintKey(source.fingerprint);\n if (ownerByFingerprint.get(key) === environment.environmentId) {\n const provider = source.fingerprint.provider;\n ownedProviders.add(provider);\n // Distinct within a directory. Summing per-bucket session counts instead\n // would count a session once per day and model it spans.\n sessionsByProvider.set(\n provider,\n (sessionsByProvider.get(provider) ?? 0) + source.distinctSessions,\n );\n }\n }\n return {\n buckets: environment.summary.buckets.filter((bucket) => ownedProviders.has(bucket.provider)),\n sessionsByProvider,\n };", + "checksum": "acc6d30d19f08fe6679c3e149a5afd7bc8f67d8d8ea0f25db8591c87f4c844f9" }, "usage-reader-failure-policy": { "id": "usage-reader-failure-policy", @@ -576,8 +576,8 @@ "end": 140, "language": "typescript", "label": "Streaming transcript read and null-on-read-failure policy", - "code": "/**\n * Streams one transcript and returns the usage records it contains, or `null`\n * when the file could not be read.\n *\n * The distinction matters to the caller's cache: a genuinely empty transcript\n * is a stable fact worth memoising, while a transient read failure memoised\n * under the same `(size, mtime)` key would silently drop that file's usage\n * until the file next changes.\n *\n * Codex carries the active model on `turn_context` lines that hold no usage of\n * their own, so those still have to pass through the reducer to keep model\n * attribution correct.\n */\nexport async function readTranscriptRecords(\n filePath: string,\n provider: UsageProviderKind,\n): Promise {\n const records: UsageRecord[] = [];\n const codexState = initialCodexScanState();\n\n try {\n const lines = NodeReadline.createInterface({\n input: NodeFS.createReadStream(filePath, { encoding: \"utf8\" }),\n crlfDelay: Infinity,\n });\n\n for await (const line of lines) {\n if (provider === \"codex\") {\n if (\n !mightCarryUsage(line, provider) &&\n !line.includes('\"turn_context\"') &&\n !line.includes('\"session_meta\"')\n ) {\n continue;\n }\n const record = parseCodexLine(line, codexState);\n if (record !== null) records.push(record);\n continue;\n }\n\n if (!mightCarryUsage(line, provider)) continue;\n const record = parseClaudeLine(line);\n if (record !== null) records.push(record);\n }\n } catch {\n return null;\n }\n\n return records;", - "checksum": "a4603871e61cae2e5575a2df75f84ff9f5684262b1cb59e638f3009e66fdfa58" + "code": " * Errors on individual entries are swallowed: session files rotate and get\n * removed while the walk is in flight, and a partial listing is far better than\n * failing the page.\n *\n * `fileName` restricts the walk to a single basename (Grok's `updates.jsonl`).\n * Grok sessions also ship multi-megabyte `chat_history` and `events` logs that\n * never carry usage, so the basename filter keeps a cold scan off those files.\n */\nexport async function listTranscriptFiles(\n root: string,\n sinceMs: number,\n options?: { readonly fileName?: string },\n): Promise {\n const found: TranscriptFile[] = [];\n const fileName = options?.fileName;\n\n const walk = async (dir: string): Promise => {\n let entries;\n try {\n entries = await NodeFSP.readdir(dir, { withFileTypes: true });\n } catch {\n return;\n }\n for (const entry of entries) {\n const child = NodePath.join(dir, entry.name);\n if (entry.isDirectory()) {\n await walk(child);\n continue;\n }\n if (fileName !== undefined) {\n if (entry.name !== fileName) continue;\n } else if (!entry.name.endsWith(\".jsonl\")) {\n continue;\n }\n try {\n const stats = await NodeFSP.stat(child);\n if (stats.mtimeMs >= sinceMs) {\n found.push({ path: child, size: stats.size, mtimeMs: stats.mtimeMs });\n }\n } catch {\n // Vanished between readdir and stat.\n }\n }\n };\n\n await walk(root);\n return found;\n}\n", + "checksum": "71265f90c3e9871a9e7391eadc30b7b9b78749bc44f5df198e402e8737d4f952" }, "usage-source-status": { "id": "usage-source-status", @@ -586,8 +586,8 @@ "end": 408, "language": "typescript", "label": "Current historical source status and session accounting", - "code": " const sources: UsageSource[] = [];\n const livePaths = new Set();\n const walkedRoots: string[] = [];\n\n for (const { provider, dir } of dirs) {\n const volumeId = yield* Effect.promise(() => readDirectoryVolumeId(dir));\n const exists = yield* fileSystem\n .exists(dir)\n .pipe(Effect.catchCause(() => Effect.succeed(false)));\n\n if (!exists) {\n sources.push({\n fingerprint: { hostId, provider, resolvedHomePath: dir, volumeId },\n status: \"missing\",\n scannedFiles: 0,\n skippedFiles: 0,\n malformedRecords: 0,\n distinctSessions: 0,\n message: \"No transcript directory on this environment.\",\n });\n continue;\n }\n\n walkedRoots.push(dir);\n const files = yield* Effect.promise(() => listTranscriptFiles(dir, windowStartMs));\n let scannedFiles = 0;\n let skippedFiles = 0;\n // Distinct per directory. Buckets carry per-cell session counts, but a\n // session spans days and models, so clients total this figure instead.\n const sessionIds = new Set();\n\n for (const file of files) {\n livePaths.add(file.path);\n const records = yield* readFileRecords(file.path, file.size, file.mtimeMs, provider);\n if (records.length === 0) {\n skippedFiles += 1;\n continue;\n }\n scannedFiles += 1;\n for (const record of records) {\n // Only sessions that contributed in-window count: the mtime slack\n // admits boundary files whose records fall outside the range.\n if (aggregator.add(record) && record.sessionId.length > 0) {\n sessionIds.add(record.sessionId);\n }\n }\n }\n\n sources.push({\n fingerprint: { hostId, provider, resolvedHomePath: dir, volumeId },\n status: \"ok\",\n scannedFiles,\n skippedFiles,\n malformedRecords: 0,\n distinctSessions: sessionIds.size,\n message: null,\n });", - "checksum": "619a5fb4c66a09b2bf6abed238f22779341b997262bc48736bd927aa19f042f1" + "code": " // resumed parse dedupes exactly like a full one.\n const base = parsed.resumed && cached !== undefined ? cached.records : [];\n const seen = new Set();\n const records = dedupeWithinFile([...base, ...parsed.records], seen);\n const tailRecords = dedupeWithinFile(parsed.tailRecords, seen);\n\n fileCache.set(filePath, {\n size,\n mtimeMs,\n provider,\n records,\n tailRecords,\n position: parsed.position,\n });\n cacheDirty = true;\n return tailRecords.length === 0 ? records : [...records, ...tailRecords];\n });\n\n /** One provider directory's walk and parse, before rates are involved. */\n interface ScannedDir {\n readonly provider: UsageProviderKind;\n readonly dir: string;\n readonly volumeId: string;\n /** Parsed records per file, or `null` when the directory does not exist. */\n readonly files:\n | readonly { readonly path: string; readonly records: readonly UsageRecord[] }[]\n | null;\n }\n\n const collectDirs = Effect.fn(\"UsageService.collectDirs\")(function* (\n windowStartMs: number,\n settings: ServerSettingsValue,\n ) {\n // The home resolvers ask for `Path` themselves; satisfy them from the\n // instance we already hold so the scan stays context-free.\n const dirs = yield* resolveTranscriptDirs(settings).pipe(\n Effect.provideService(Path.Path, path),\n );\n const scanned: ScannedDir[] = [];\n for (const { provider, dir, fileName } of dirs) {\n const volumeId = yield* Effect.promise(() => readDirectoryVolumeId(dir));\n const exists = yield* fileSystem\n .exists(dir)\n .pipe(Effect.catchCause(() => Effect.succeed(false)));\n if (!exists) {\n scanned.push({ provider, dir, volumeId, files: null });\n continue;\n }\n const files = yield* Effect.promise(() =>\n listTranscriptFiles(dir, windowStartMs, fileName === undefined ? undefined : { fileName }),\n );\n const parsedFiles: { path: string; records: readonly UsageRecord[] }[] = [];\n for (const file of files) {\n const records = yield* readFileRecords(file.path, file.size, file.mtimeMs, provider);\n parsedFiles.push({ path: file.path, records });\n }\n scanned.push({ provider, dir, volumeId, files: parsedFiles });", + "checksum": "7de0ee2dda5367904c1b586892fc6ffa83f0193e20041f5f010e51fb22f06819" }, "project-root-normalization": { "id": "project-root-normalization", @@ -596,8 +596,8 @@ "end": 200, "language": "typescript", "label": "Workspace-root resolution, validation, and opt-in creation", - "code": " const normalizeWorkspaceRoot: WorkspacePaths[\"Service\"][\"normalizeWorkspaceRoot\"] = Effect.fn(\n \"WorkspacePaths.normalizeWorkspaceRoot\",\n )(function* (workspaceRoot, options) {\n const normalizedWorkspaceRoot = path.resolve(expandHomePath(workspaceRoot.trim(), path));\n let workspaceStat = yield* statWorkspaceRoot(\n workspaceRoot,\n normalizedWorkspaceRoot,\n \"validate-existing\",\n );\n if (!workspaceStat && options?.createIfMissing) {\n yield* fileSystem.makeDirectory(normalizedWorkspaceRoot, { recursive: true }).pipe(\n Effect.mapError(\n (cause) =>\n new WorkspaceRootCreateFailedError({\n workspaceRoot,\n normalizedWorkspaceRoot,\n cause,\n }),\n ),\n );\n workspaceStat = yield* statWorkspaceRoot(\n workspaceRoot,\n normalizedWorkspaceRoot,\n \"verify-created\",\n );\n }\n if (!workspaceStat) {\n return yield* new WorkspaceRootNotExistsError({\n workspaceRoot,\n normalizedWorkspaceRoot,\n });\n }\n if (workspaceStat.type !== \"Directory\") {\n return yield* new WorkspaceRootNotDirectoryError({\n workspaceRoot,\n normalizedWorkspaceRoot,\n });\n }\n return normalizedWorkspaceRoot;\n });", - "checksum": "dc1befa9f427fd48c5c9988360c52a9532a82d89a2e243b0dee2c25f2794fc12" + "code": " if (!workspaceStat && options?.createIfMissing) {\n yield* fileSystem.makeDirectory(normalizedWorkspaceRoot, { recursive: true }).pipe(\n Effect.mapError(\n (cause) =>\n new WorkspaceRootCreateFailedError({\n workspaceRoot,\n normalizedWorkspaceRoot,\n cause,\n }),\n ),\n );\n workspaceStat = yield* statWorkspaceRoot(\n workspaceRoot,\n normalizedWorkspaceRoot,\n \"verify-created\",\n );\n }\n if (!workspaceStat) {\n return yield* new WorkspaceRootNotExistsError({\n workspaceRoot,\n normalizedWorkspaceRoot,\n });\n }\n if (workspaceStat.type !== \"Directory\") {\n return yield* new WorkspaceRootNotDirectoryError({\n workspaceRoot,\n normalizedWorkspaceRoot,\n });\n }\n return normalizedWorkspaceRoot;\n });\n\n const resolveRelativePathWithinRoot: WorkspacePaths[\"Service\"][\"resolveRelativePathWithinRoot\"] =\n Effect.fn(\"WorkspacePaths.resolveRelativePathWithinRoot\")(function* (input) {\n const normalizedInputPath = input.relativePath.trim();\n if (path.isAbsolute(normalizedInputPath)) {\n return yield* new WorkspacePathOutsideRootError({\n workspaceRoot: input.workspaceRoot,\n relativePath: input.relativePath,\n });", + "checksum": "544b261224c96192e448aeff959d3981b1bf9c353ac05f05f8065a6323a627a9" }, "project-create-normalization": { "id": "project-create-normalization", @@ -606,8 +606,8 @@ "end": 90, "language": "typescript", "label": "Project creation normalization before dispatch", - "code": " const normalizeProjectWorkspaceRoot = (workspaceRoot: string) =>\n workspacePaths.normalizeWorkspaceRoot(workspaceRoot).pipe(\n Effect.mapError(\n (cause) =>\n new OrchestrationDispatchCommandError({\n message: cause.message,\n }),\n ),\n );\n\n const normalizeProjectWorkspaceRootForCreate = (\n workspaceRoot: string,\n createIfMissing: boolean | undefined,\n ) =>\n workspacePaths\n .normalizeWorkspaceRoot(workspaceRoot, {\n createIfMissing: createIfMissing === true,\n })\n .pipe(\n Effect.mapError(\n (cause) =>\n new OrchestrationDispatchCommandError({\n message: cause.message,\n }),\n ),\n );\n\n if (canonicalCommand.type === \"project.create\") {\n return {\n ...canonicalCommand,\n workspaceRoot: yield* normalizeProjectWorkspaceRootForCreate(\n canonicalCommand.workspaceRoot,\n canonicalCommand.createWorkspaceRootIfMissing,\n ),\n createWorkspaceRootIfMissing: canonicalCommand.createWorkspaceRootIfMissing === true,\n } satisfies OrchestrationCommand;", - "checksum": "6b0a20d1e9548a92779defd07120ed3e684de23de08d6de2f159a36edf6b1f57" + "code": " function* (attachmentPaths: ReadonlyArray) {\n if (attachmentPaths.length === 0) {\n return;\n }\n const fileSystem = yield* FileSystem.FileSystem;\n yield* Effect.forEach(\n attachmentPaths,\n (attachmentPath) =>\n fileSystem.remove(attachmentPath, { force: true }).pipe(\n Effect.tapError((cause) =>\n Effect.logWarning(\"Failed to remove an unclaimed attachment copy.\", {\n attachmentPath,\n cause,\n }),\n ),\n Effect.orElseSucceed(() => undefined),\n ),\n { concurrency: 1 },\n );\n },\n);\n\nexport const normalizeDispatchCommand = (command: ClientOrchestrationCommand) =>\n Effect.gen(function* () {\n const receivedAt = DateTime.formatIso(yield* DateTime.now);\n const canonicalCommand = canonicalizeClientCommandTimestamps(command, receivedAt);\n const fileSystem = yield* FileSystem.FileSystem;\n const path = yield* Path.Path;\n const serverConfig = yield* ServerConfig;\n const workspacePaths = yield* WorkspacePaths.WorkspacePaths;\n\n const normalizeProjectWorkspaceRoot = (workspaceRoot: string) =>\n workspacePaths.normalizeWorkspaceRoot(workspaceRoot).pipe(\n Effect.mapError(\n (cause) =>\n new OrchestrationDispatchCommandError({", + "checksum": "3ab75dc3fdc3de94954d69a2e3a66c26077bad99857d32c095f23c8fb7dde374" }, "project-root-uniqueness": { "id": "project-root-uniqueness", @@ -636,8 +636,8 @@ "end": 105, "language": "typescript", "label": "Best-effort t3.json loading and validation", - "code": " /**\n * Load and decode `t3.json` at the workspace root.\n *\n * Never fails: missing, unreadable, or invalid files resolve to\n * `Option.none` (invalid files are logged as warnings).\n */\n readonly load: (workspaceRoot: string) => Effect.Effect>;\n }\n>()(\"t3/project/T3ProjectFileLoader\") {}\n\nconst logT3ProjectFileLoadError = (error: T3ProjectFileLoadError) =>\n Effect.logWarning(error).pipe(\n Effect.annotateLogs({\n operation: error.operation,\n workspaceRoot: error.workspaceRoot,\n filePath: error.filePath,\n errorTag: error._tag,\n }),\n );\n\nexport const make = Effect.gen(function* () {\n const fileSystem = yield* FileSystem.FileSystem;\n const path = yield* Path.Path;\n\n const load: T3ProjectFileLoader[\"Service\"][\"load\"] = Effect.fn(\"T3ProjectFileLoader.load\")(\n function* (workspaceRoot) {\n const filePath = path.join(workspaceRoot, T3_PROJECT_FILE_NAME);\n const raw = yield* fileSystem.readFileString(filePath).pipe(\n Effect.map(Option.some),\n Effect.catchTags({\n PlatformError: (error) =>\n error.reason._tag === \"NotFound\"\n ? Effect.succeed(Option.none())\n : logT3ProjectFileLoadError(\n new T3ProjectFileLoadError({\n operation: \"read\",\n workspaceRoot,\n filePath,\n cause: error,\n }),\n ).pipe(Effect.as(Option.none())),\n }),\n );\n if (Option.isNone(raw)) {\n return Option.none();\n }\n return yield* decodeT3ProjectFileJson(raw.value).pipe(\n Effect.map(Option.some),\n Effect.catchTags({\n SchemaError: (error) =>\n logT3ProjectFileLoadError(\n new T3ProjectFileLoadError({\n operation: \"decode\",\n workspaceRoot,\n filePath,\n cause: error,\n }),\n ).pipe(Effect.as(Option.none())),\n }),\n );\n },\n );\n\n return T3ProjectFileLoader.of({ load });", - "checksum": "1a2c82901fba9d86213563abdee2569b7f48283533b7c7cc23b5bfd46b7df753" + "code": " /**\n * Load and decode `t3.json` at the workspace root.\n *\n * Never fails: missing, unreadable, or invalid files resolve to\n * `Option.none` (invalid files are logged as warnings).\n */\n readonly load: (workspaceRoot: string) => Effect.Effect>;\n }\n>()(\"t3/project/T3ProjectFileLoader\") {}\n\nconst logT3ProjectFileLoadError = (error: T3ProjectFileLoadError) =>\n Effect.logWarning(error).pipe(\n Effect.annotateLogs({\n operation: error.operation,\n workspaceRoot: error.workspaceRoot,\n filePath: error.filePath,\n errorTag: error._tag,\n }),\n );\n\n/** @public Service construction is part of the canonical Effect module API. */\nexport const make = Effect.gen(function* () {\n const fileSystem = yield* FileSystem.FileSystem;\n const path = yield* Path.Path;\n\n const load: T3ProjectFileLoader[\"Service\"][\"load\"] = Effect.fn(\"T3ProjectFileLoader.load\")(\n function* (workspaceRoot) {\n const filePath = path.join(workspaceRoot, T3_PROJECT_FILE_NAME);\n const raw = yield* fileSystem.readFileString(filePath).pipe(\n Effect.map(Option.some),\n Effect.catchTags({\n PlatformError: (error) =>\n error.reason._tag === \"NotFound\"\n ? Effect.succeed(Option.none())\n : logT3ProjectFileLoadError(\n new T3ProjectFileLoadError({\n operation: \"read\",\n workspaceRoot,\n filePath,\n cause: error,\n }),\n ).pipe(Effect.as(Option.none())),\n }),\n );\n if (Option.isNone(raw)) {\n return Option.none();\n }\n return yield* decodeT3ProjectFileJson(raw.value).pipe(\n Effect.map(Option.some),\n Effect.catchTags({\n SchemaError: (error) =>\n logT3ProjectFileLoadError(\n new T3ProjectFileLoadError({\n operation: \"decode\",\n workspaceRoot,\n filePath,\n cause: error,\n }),\n ).pipe(Effect.as(Option.none())),\n }),\n );\n },\n );\n", + "checksum": "e71dbe2e8454dfab825cb4783e54ab5b3e8f3471f499e37b04062a7c87552b47" }, "project-t3-file-loader-tests": { "id": "project-t3-file-loader-tests", @@ -666,8 +666,8 @@ "end": 482, "language": "tsx", "label": "Inherited project environment default in web settings", - "code": " const t3File = useT3ProjectFileState(\n selectedCheckout.environmentId,\n selectedCheckout.workspaceRoot,\n );\n // What the \"Default\" option resolves to while no override is set: the\n // repo's t3.json value when present, otherwise the global setting.\n const inheritedEnvMode = t3File.file?.defaultThreadEnvMode ?? settings.defaultThreadEnvMode;\n const inheritedEnvModeSource = t3File.file?.defaultThreadEnvMode != null ? \"t3.json\" : \"global\";", - "checksum": "7ab6370096f28cd8e0175e8d2a40f3e7e4e29a087f725975228b5ffdff9f68a2" + "code": " }\n } finally {\n savingBrowserAccessRef.current = false;\n setSavingBrowserAccess(false);\n }\n };\n const setBrowserAccess = (enabled: boolean | undefined) =>\n setBooleanOverride(\"projectAgentBrowserAccessOverrides\", enabled);", + "checksum": "fbf5916ff291a48b4e78b778cc5d8f3668a52e64220ed7e31626b4d240333d8e" }, "project-settings-project-override": { "id": "project-settings-project-override", @@ -676,8 +676,8 @@ "end": 905, "language": "tsx", "label": "Project environment override controls", - "code": " setDefaultThreadEnvMode(null)}\n />\n ) : null\n }\n control={\n {\n if (value === \"worktree\" || value === \"local\") {\n setDefaultThreadEnvMode(value);\n } else if (value === \"inherit\") {\n setDefaultThreadEnvMode(null);\n }\n }}\n >\n \n \n {storedEnvMode === null\n ? group.memberProjects.length > 1\n ? \"Default (per checkout)\"\n : `Default (${resolveEnvModeLabel(inheritedEnvMode).toLowerCase()})`\n : resolveEnvModeLabel(storedEnvMode)}\n \n \n \n \n {group.memberProjects.length > 1\n ? \"Default (each checkout's t3.json or global setting)\"\n : `Default (${inheritedEnvModeSource}: ${resolveEnvModeLabel(inheritedEnvMode).toLowerCase()})`}\n \n {resolveEnvModeLabel(\"worktree\")}\n {resolveEnvModeLabel(\"local\")}\n ", - "checksum": "8bfa93d2fed3d5a4362c1c30abedf00d2d1811052808a02f9ec63ed3338c8d35" + "code": " if (result._tag === \"Failure\") {\n reportFailure(`Failed to remove \"${member.title}\"`, result);\n return;\n }\n const projectRef = scopeProjectRef(member.environmentId, member.id);\n releaseProjectDraftUploads(\n projectRef,\n memberThreads.map((thread) => scopeThreadRef(thread.environmentId, thread.id)),\n );\n const projectDraftThread = draftStore.getDraftThreadByProjectRef(projectRef);\n if (projectDraftThread) {\n draftStore.clearDraftThread(projectDraftThread.draftId);\n }\n draftStore.clearProjectDraftThreadId(projectRef);\n }\n\n if (isWholeGroup) {\n if (hasOtherMembers) {\n void navigate({\n to: \"/settings/projects\",\n search: { project: group.projectKey, machine: undefined },\n replace: true,\n });\n } else {\n void navigate({ to: \"/\", replace: true });\n }\n }\n },\n [\n deleteProject,\n group.displayName,\n group.memberProjects.length,\n group.projectKey,\n hasOtherMembers,\n navigate,\n reportFailure,\n threads,\n ],\n );\n", + "checksum": "5d98470270d121dadc6325d8bdbf1756b1d78a8c6b12ba5892380fb0b6628080" }, "project-repository-identity": { "id": "project-repository-identity", @@ -686,8 +686,8 @@ "end": 166, "language": "typescript", "label": "Best-effort Git remote repository identity resolution", - "code": "function pickPrimaryRemote(\n remotes: ReadonlyMap,\n): { readonly remoteName: string; readonly remoteUrl: string } | null {\n for (const preferredRemoteName of [\"upstream\", \"origin\"] as const) {\n const remoteUrl = remotes.get(preferredRemoteName);\n if (remoteUrl) {\n return { remoteName: preferredRemoteName, remoteUrl };\n }\n }\n\n const [remoteName, remoteUrl] =\n [...remotes.entries()].toSorted(([left], [right]) => left.localeCompare(right))[0] ?? [];\n return remoteName && remoteUrl ? { remoteName, remoteUrl } : null;\n}\n\nfunction buildRepositoryIdentity(input: {\n readonly remoteName: string;\n readonly remoteUrl: string;\n readonly rootPath: string;\n}): RepositoryIdentity {\n const canonicalKey = normalizeGitRemoteUrl(input.remoteUrl);\n const sourceControlProvider = detectSourceControlProviderFromGitRemoteUrl(input.remoteUrl);\n const repositoryPath = canonicalKey.split(\"/\").slice(1).join(\"/\");\n const repositoryPathSegments = repositoryPath.split(\"/\").filter((segment) => segment.length > 0);\n const [owner] = repositoryPathSegments;\n const repositoryName = repositoryPathSegments.at(-1);\n\n return {\n canonicalKey,\n locator: {\n source: \"git-remote\",\n remoteName: input.remoteName,\n remoteUrl: input.remoteUrl,\n },\n rootPath: input.rootPath,\n ...(repositoryPath ? { displayName: repositoryPath } : {}),\n ...(sourceControlProvider ? { provider: sourceControlProvider.kind } : {}),\n ...(owner ? { owner } : {}),\n ...(repositoryName ? { name: repositoryName } : {}),\n };\n}\n\nconst resolveRepositoryIdentityCacheKey = Effect.fn(\"RepositoryIdentityResolver.resolveCacheKey\")(\n function* (cwd: string) {\n const processRunner = yield* ProcessRunner.ProcessRunner;\n let cacheKey = cwd;\n\n // git is a real executable on every platform — no cmd.exe shell mode, which\n // would split paths containing spaces during cmd's re-tokenization.\n const topLevelResult = yield* processRunner\n .run({\n command: \"git\",\n args: [\"-C\", cwd, \"rev-parse\", \"--show-toplevel\"],\n timeoutBehavior: \"timedOutResult\",\n })\n .pipe(Effect.option);\n if (topLevelResult._tag === \"None\" || topLevelResult.value.code !== 0) {\n return cacheKey;\n }\n\n const candidate = topLevelResult.value.stdout.trim();\n if (candidate.length > 0) {\n cacheKey = candidate;\n }\n\n return cacheKey;\n },\n);\n\nconst resolveRepositoryIdentityFromCacheKey = Effect.fn(\n \"RepositoryIdentityResolver.resolveFromCacheKey\",\n)(function* (\n cacheKey: string,\n): Effect.fn.Return {\n const processRunner = yield* ProcessRunner.ProcessRunner;\n const remoteResult = yield* processRunner\n .run({\n command: \"git\",\n args: [\"-C\", cacheKey, \"remote\", \"-v\"],\n timeoutBehavior: \"timedOutResult\",\n })\n .pipe(Effect.option);\n if (remoteResult._tag === \"None\" || remoteResult.value.code !== 0) {\n return null;\n }\n\n const remote = pickPrimaryRemote(parseRemoteFetchUrls(remoteResult.value.stdout));\n return remote ? buildRepositoryIdentity({ ...remote, rootPath: cacheKey }) : null;\n});\n\nexport const make = Effect.fn(\"RepositoryIdentityResolver.make\")(function* (\n options: RepositoryIdentityResolverOptions = {},\n) {\n const processRunner = yield* ProcessRunner.ProcessRunner;\n\n const repositoryIdentityCache = yield* Cache.makeWith(\n (cacheKey) =>\n resolveRepositoryIdentityFromCacheKey(cacheKey).pipe(\n Effect.provideService(ProcessRunner.ProcessRunner, processRunner),\n ),\n {\n capacity: options.cacheCapacity ?? DEFAULT_REPOSITORY_IDENTITY_CACHE_CAPACITY,\n timeToLive: Exit.match({\n onSuccess: (value) =>\n value === null\n ? (options.negativeCacheTtl ?? DEFAULT_NEGATIVE_CACHE_TTL)\n : (options.positiveCacheTtl ?? DEFAULT_POSITIVE_CACHE_TTL),\n onFailure: () => Duration.zero,\n }),\n },\n );\n\n const resolve: RepositoryIdentityResolver[\"Service\"][\"resolve\"] = Effect.fn(\n \"RepositoryIdentityResolver.resolve\",\n )(function* (cwd) {\n const cacheKey = yield* resolveRepositoryIdentityCacheKey(cwd).pipe(\n Effect.provideService(ProcessRunner.ProcessRunner, processRunner),\n );\n return yield* Cache.get(repositoryIdentityCache, cacheKey);", - "checksum": "4a4f8be0eeff3ff2856b99be1dc3e109597f73ef9dfb6c89fb54aef34a4f6fff" + "code": " return remotes;\n}\n\nfunction pickPrimaryRemote(\n remotes: ReadonlyMap,\n): { readonly remoteName: string; readonly remoteUrl: string } | null {\n for (const preferredRemoteName of [\"upstream\", \"origin\"] as const) {\n const remoteUrl = remotes.get(preferredRemoteName);\n if (remoteUrl) {\n return { remoteName: preferredRemoteName, remoteUrl };\n }\n }\n\n const [remoteName, remoteUrl] =\n [...remotes.entries()].toSorted(([left], [right]) => left.localeCompare(right))[0] ?? [];\n return remoteName && remoteUrl ? { remoteName, remoteUrl } : null;\n}\n\nfunction buildRepositoryIdentity(input: {\n readonly remoteName: string;\n readonly remoteUrl: string;\n readonly rootPath: string;\n}): RepositoryIdentity {\n const canonicalKey = normalizeGitRemoteUrl(input.remoteUrl);\n const sourceControlProvider = detectSourceControlProviderFromGitRemoteUrl(input.remoteUrl);\n const repositoryPath = canonicalKey.split(\"/\").slice(1).join(\"/\");\n const repositoryPathSegments = repositoryPath.split(\"/\").filter((segment) => segment.length > 0);\n const [owner] = repositoryPathSegments;\n const repositoryName = repositoryPathSegments.at(-1);\n\n return {\n canonicalKey,\n locator: {\n source: \"git-remote\",\n remoteName: input.remoteName,\n remoteUrl: input.remoteUrl,\n },\n rootPath: input.rootPath,\n ...(repositoryPath ? { displayName: repositoryPath } : {}),\n ...(sourceControlProvider ? { provider: sourceControlProvider.kind } : {}),\n ...(owner ? { owner } : {}),\n ...(repositoryName ? { name: repositoryName } : {}),\n };\n}\n\nconst resolveRepositoryIdentityCacheKey = Effect.fn(\"RepositoryIdentityResolver.resolveCacheKey\")(\n function* (cwd: string) {\n const processRunner = yield* ProcessRunner.ProcessRunner;\n\n // git is a real executable on every platform — no cmd.exe shell mode, which\n // would split paths containing spaces during cmd's re-tokenization.\n const topLevelResult = yield* processRunner\n .run({\n command: \"git\",\n args: [\"-C\", cwd, \"rev-parse\", \"--show-toplevel\"],\n timeoutBehavior: \"timedOutResult\",\n })\n .pipe(Effect.option);\n if (topLevelResult._tag === \"None\" || topLevelResult.value.code !== 0) {\n return null;\n }\n\n const candidate = topLevelResult.value.stdout.trim();\n return candidate.length > 0 ? candidate : null;\n },\n);\n\nconst resolveRepositoryIdentityFromCacheKey = Effect.fn(\n \"RepositoryIdentityResolver.resolveFromCacheKey\",\n)(function* (\n cacheKey: string,\n): Effect.fn.Return {\n const processRunner = yield* ProcessRunner.ProcessRunner;\n const remoteResult = yield* processRunner\n .run({\n command: \"git\",\n args: [\"-C\", cacheKey, \"remote\", \"-v\"],\n timeoutBehavior: \"timedOutResult\",\n })\n .pipe(Effect.option);\n if (remoteResult._tag === \"None\" || remoteResult.value.code !== 0) {\n return null;\n }\n\n const remote = pickPrimaryRemote(parseRemoteFetchUrls(remoteResult.value.stdout));\n return remote ? buildRepositoryIdentity({ ...remote, rootPath: cacheKey }) : null;\n});\n\nexport const make = Effect.fn(\"RepositoryIdentityResolver.make\")(function* (\n options: RepositoryIdentityResolverOptions = {},\n) {\n const processRunner = yield* ProcessRunner.ProcessRunner;\n const cacheCapacity = options.cacheCapacity ?? DEFAULT_REPOSITORY_IDENTITY_CACHE_CAPACITY;\n\n const repositoryRootCache = yield* Cache.makeWith(\n (cwd) =>\n resolveRepositoryIdentityCacheKey(cwd).pipe(\n Effect.provideService(ProcessRunner.ProcessRunner, processRunner),\n ),\n {\n capacity: cacheCapacity,\n timeToLive: Exit.match({\n onSuccess: (value) =>\n value === null ? Duration.zero : (options.positiveCacheTtl ?? DEFAULT_POSITIVE_CACHE_TTL),\n onFailure: () => Duration.zero,\n }),\n },\n );\n\n const repositoryIdentityCache = yield* Cache.makeWith(\n (cacheKey) =>\n resolveRepositoryIdentityFromCacheKey(cacheKey).pipe(\n Effect.provideService(ProcessRunner.ProcessRunner, processRunner),\n ),\n {\n capacity: cacheCapacity,\n timeToLive: Exit.match({\n onSuccess: (value) =>\n value === null", + "checksum": "540edcaa27000ca909054957357f6f076c46e910d1c3d4c20d85a68f0e2fd3b6" }, "project-identity-read-projection": { "id": "project-identity-read-projection", @@ -696,8 +696,8 @@ "end": 388, "language": "typescript", "label": "Snapshot-time repository identity enrichment", - "code": "function mapProjectShellRow(\n row: Schema.Schema.Type,\n repositoryIdentity: OrchestrationProject[\"repositoryIdentity\"],\n): OrchestrationProjectShell {\n return {\n id: row.projectId,\n title: row.title,\n workspaceRoot: row.workspaceRoot,\n repositoryIdentity,\n defaultModelSelection: row.defaultModelSelection,\n defaultThreadEnvMode: row.defaultThreadEnvMode,\n faviconPath: row.faviconPath ?? null,\n scripts: row.scripts,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n };\n}\n\nfunction mapProposedPlanRow(\n row: Schema.Schema.Type,\n): OrchestrationProposedPlan {\n return {\n id: row.planId,\n turnId: row.turnId,\n planMarkdown: row.planMarkdown,\n implementedAt: row.implementedAt,\n implementationThreadId: row.implementationThreadId,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n };\n}\n\nfunction toPersistenceSqlOrDecodeError(sqlOperation: string, decodeOperation: string) {\n return (cause: unknown): ProjectionRepositoryError =>\n Schema.isSchemaError(cause)\n ? toPersistenceDecodeError(decodeOperation)(cause)\n : toPersistenceSqlError(sqlOperation)(cause);\n}\n\nconst makeProjectionSnapshotQuery = Effect.gen(function* () {\n const threadBackgroundLiveness = yield* ThreadBackgroundLivenessService;\n const threadPlanProgress = yield* ThreadPlanProgressService;\n const sql = yield* SqlClient.SqlClient;\n const repositoryIdentityResolver = yield* RepositoryIdentityResolver.RepositoryIdentityResolver;\n const repositoryIdentityResolutionConcurrency = 4;\n const resolveRepositoryIdentitiesForProjects = Effect.fn(\n \"ProjectionSnapshotQuery.resolveRepositoryIdentitiesForProjects\",\n )(function* (\n projectRows: ReadonlyArray>,\n options?: {\n readonly includeDeleted?: boolean;\n },\n ) {\n const filteredProjectRows =\n options?.includeDeleted === true\n ? projectRows\n : projectRows.filter((row) => row.deletedAt === null);\n const uniqueWorkspaceRoots = [...new Set(filteredProjectRows.map((row) => row.workspaceRoot))];\n const repositoryIdentityByWorkspaceRoot = new Map(\n yield* Effect.forEach(\n uniqueWorkspaceRoots,\n (workspaceRoot) =>\n repositoryIdentityResolver\n .resolve(workspaceRoot)\n .pipe(Effect.map((identity) => [workspaceRoot, identity] as const)),\n { concurrency: repositoryIdentityResolutionConcurrency },\n ),\n );\n\n return new Map(\n filteredProjectRows.map((row) => [\n row.projectId,\n repositoryIdentityByWorkspaceRoot.get(row.workspaceRoot) ?? null,\n ]),\n );\n });", - "checksum": "7d141cd37e227fc76fc88c9537c8bb8dd65842f91b9e2ec7fdec540a2913c122" + "code": " const sequenceByProjector = new Map(\n stateRows.map((row) => [row.projector, row.lastAppliedSequence] as const),\n );\n\n let minSequence = Number.POSITIVE_INFINITY;\n for (const projector of REQUIRED_SNAPSHOT_PROJECTORS) {\n const sequence = sequenceByProjector.get(projector);\n if (sequence === undefined) {\n return 0;\n }\n if (sequence < minSequence) {\n minSequence = sequence;\n }\n }\n\n return Number.isFinite(minSequence) ? minSequence : 0;\n}\n\nfunction mapLatestTurn(\n row: Schema.Schema.Type,\n): OrchestrationLatestTurn {\n return {\n turnId: row.turnId,\n state:\n row.state === \"error\"\n ? \"error\"\n : row.state === \"interrupted\"\n ? \"interrupted\"\n : row.state === \"completed\"\n ? \"completed\"\n : \"running\",\n requestedAt: row.requestedAt,\n startedAt: row.startedAt,\n completedAt: row.completedAt,\n assistantMessageId: row.assistantMessageId,\n ...(row.sourceProposedPlanThreadId !== null && row.sourceProposedPlanId !== null\n ? {\n sourceProposedPlan: {\n threadId: row.sourceProposedPlanThreadId,\n planId: row.sourceProposedPlanId,\n },\n }\n : {}),\n };\n}\n\nfunction mapTitleRegeneration(row: Schema.Schema.Type) {\n return row.titleRegenerationRequestId != null && row.titleRegenerationStartedAt != null\n ? {\n requestId: row.titleRegenerationRequestId,\n startedAt: row.titleRegenerationStartedAt,\n }\n : null;\n}\n\nfunction mapSessionRow(\n row: Schema.Schema.Type,\n): OrchestrationSession {\n return {\n threadId: row.threadId,\n status: row.status,\n providerName: row.providerName,\n ...(row.providerInstanceId !== null ? { providerInstanceId: row.providerInstanceId } : {}),\n runtimeMode: row.runtimeMode,\n activeTurnId: row.activeTurnId,\n lastError: row.lastError,\n updatedAt: row.updatedAt,\n };\n}\n\nfunction mapProjectShellRow(\n row: Schema.Schema.Type,\n repositoryIdentity: OrchestrationProject[\"repositoryIdentity\"],\n): OrchestrationProjectShell {\n return {\n id: row.projectId,", + "checksum": "f45f6ab00ef0d92463ce1614e11a5dee07103a8f1c01970d8821b52554321326" }, "project-durable-projection": { "id": "project-durable-projection", @@ -706,8 +706,8 @@ "end": 298, "language": "typescript", "label": "Project creation and metadata event decisions", - "code": " case \"project.create\": {\n yield* requireProjectAbsent({\n readModel,\n command,\n projectId: command.projectId,\n });\n yield* requireActiveProjectWorkspaceRootAbsent({\n readModel,\n command,\n workspaceRoot: command.workspaceRoot,\n exceptProjectId: command.projectId,\n });\n\n return {\n ...(yield* withEventBase({\n aggregateKind: \"project\",\n aggregateId: command.projectId,\n occurredAt: command.createdAt,\n commandId: command.commandId,\n })),\n type: \"project.created\",\n payload: {\n projectId: command.projectId,\n title: command.title,\n workspaceRoot: command.workspaceRoot,\n defaultModelSelection: command.defaultModelSelection ?? null,\n faviconPath: null,\n scripts: [],\n createdAt: command.createdAt,\n updatedAt: command.createdAt,\n },\n };\n }\n\n case \"project.meta.update\": {\n yield* requireProject({\n readModel,\n command,\n projectId: command.projectId,\n });\n if (command.workspaceRoot !== undefined) {\n yield* requireActiveProjectWorkspaceRootAbsent({\n readModel,\n command,\n workspaceRoot: command.workspaceRoot,\n exceptProjectId: command.projectId,\n });\n }\n const occurredAt = yield* nowIso;\n return {\n ...(yield* withEventBase({\n aggregateKind: \"project\",\n aggregateId: command.projectId,\n occurredAt,\n commandId: command.commandId,\n })),\n type: \"project.meta-updated\",\n payload: {\n projectId: command.projectId,\n ...(command.title !== undefined ? { title: command.title } : {}),\n ...(command.workspaceRoot !== undefined ? { workspaceRoot: command.workspaceRoot } : {}),\n ...(command.defaultModelSelection !== undefined\n ? { defaultModelSelection: command.defaultModelSelection }\n : {}),\n ...(command.defaultThreadEnvMode !== undefined\n ? { defaultThreadEnvMode: command.defaultThreadEnvMode }\n : {}),\n ...(command.faviconPath !== undefined ? { faviconPath: command.faviconPath } : {}),\n ...(command.scripts !== undefined ? { scripts: command.scripts } : {}),\n updatedAt: occurredAt,\n },\n };", - "checksum": "e788184b10630834970883f5f18d9c6b555e7ecc2993eaedf944acaf4afddf0b" + "code": " projectId: command.projectId,\n });\n yield* requireActiveProjectWorkspaceRootAbsent({\n readModel,\n command,\n workspaceRoot: command.workspaceRoot,\n exceptProjectId: command.projectId,\n });\n\n return {\n ...(yield* withEventBase({\n aggregateKind: \"project\",\n aggregateId: command.projectId,\n occurredAt: command.createdAt,\n commandId: command.commandId,\n })),\n type: \"project.created\",\n payload: {\n projectId: command.projectId,\n title: command.title,\n workspaceRoot: command.workspaceRoot,\n // Project creation has no user model choice. Older clients sent an\n // automatic seed here, but only a metadata update records an\n // explicit project default.\n defaultModelSelection: null,\n faviconPath: null,\n projectIcon: null,\n scripts: [],\n createdAt: command.createdAt,\n updatedAt: command.createdAt,\n },\n };\n }\n\n case \"project.meta.update\": {\n const project = yield* requireProject({\n readModel,\n command,\n projectId: command.projectId,\n });\n if (command.scripts !== undefined) {\n // Persisted IDs predate shortcut validation. Let users edit or remove them\n // without allowing another invalid ID to enter the project.\n const existingIds = new Set(project.scripts.map((script) => script.id));\n for (const script of command.scripts) {\n if (!existingIds.has(script.id) && !isScriptRunCommand(`script.${script.id}.run`)) {\n return yield* new OrchestrationCommandInvariantError({\n commandType: command.type,\n detail: `Script ID '${script.id}' must be 1-${MAX_SCRIPT_ID_LENGTH} lowercase letters, digits or hyphens, starting with a letter or digit.`,\n });\n }\n }\n }\n if (command.workspaceRoot !== undefined) {\n yield* requireActiveProjectWorkspaceRootAbsent({\n readModel,\n command,\n workspaceRoot: command.workspaceRoot,\n exceptProjectId: command.projectId,\n });\n }\n const occurredAt = yield* nowIso;\n return {\n ...(yield* withEventBase({\n aggregateKind: \"project\",\n aggregateId: command.projectId,\n occurredAt,\n commandId: command.commandId,\n })),\n type: \"project.meta-updated\",\n payload: {\n projectId: command.projectId,", + "checksum": "53d09d295b615243ef50268e53ded34add3a8163f8cc601373e6b7948ba50cc9" }, "project-projector": { "id": "project-projector", @@ -716,8 +716,8 @@ "end": 263, "language": "typescript", "label": "Durable project read-model projection", - "code": " switch (event.type) {\n case \"project.created\":\n return decodeForEvent(ProjectCreatedPayload, event.payload, event.type, \"payload\").pipe(\n Effect.map((payload) => {\n const existing = nextBase.projects.find((entry) => entry.id === payload.projectId);\n const nextProject = {\n id: payload.projectId,\n title: payload.title,\n workspaceRoot: payload.workspaceRoot,\n defaultModelSelection: payload.defaultModelSelection,\n defaultThreadEnvMode: null,\n faviconPath: payload.faviconPath ?? null,\n scripts: payload.scripts,\n createdAt: payload.createdAt,\n updatedAt: payload.updatedAt,\n deletedAt: null,\n };\n\n return {\n ...nextBase,\n projects: existing\n ? nextBase.projects.map((entry) =>\n entry.id === payload.projectId ? nextProject : entry,\n )\n : [...nextBase.projects, nextProject],\n };\n }),\n );\n\n case \"project.meta-updated\":\n return decodeForEvent(ProjectMetaUpdatedPayload, event.payload, event.type, \"payload\").pipe(\n Effect.map((payload) => ({\n ...nextBase,\n projects: nextBase.projects.map((project) =>\n project.id === payload.projectId\n ? {\n ...project,\n ...(payload.title !== undefined ? { title: payload.title } : {}),\n ...(payload.workspaceRoot !== undefined\n ? { workspaceRoot: payload.workspaceRoot }\n : {}),\n ...(payload.defaultModelSelection !== undefined\n ? { defaultModelSelection: payload.defaultModelSelection }\n : {}),\n ...(payload.defaultThreadEnvMode !== undefined\n ? { defaultThreadEnvMode: payload.defaultThreadEnvMode }\n : {}),\n ...(payload.faviconPath !== undefined\n ? { faviconPath: payload.faviconPath }\n : {}),\n ...(payload.scripts !== undefined ? { scripts: payload.scripts } : {}),\n updatedAt: payload.updatedAt,\n }\n : project,\n ),\n })),\n );", - "checksum": "729a2f33d9502369b95c9f126893ba31605baac1c1ae7f5ecfabadb71399f48e" + "code": " const retainedMessageIds = new Set();\n for (const message of messages) {\n if (message.role === \"system\" || isImportedAgentSessionMessageId(message.id)) {\n retainedMessageIds.add(message.id);\n continue;\n }\n if (message.turnId !== null && retainedTurnIds.has(message.turnId)) {\n retainedMessageIds.add(message.id);\n }\n }\n\n const retainedUserCount = messages.filter(\n (message) =>\n message.role === \"user\" &&\n !isImportedAgentSessionMessageId(message.id) &&\n retainedMessageIds.has(message.id),\n ).length;\n const missingUserCount = Math.max(0, turnCount - retainedUserCount);\n if (missingUserCount > 0) {\n const fallbackUserMessages = messages\n .filter(\n (message) =>\n message.role === \"user\" &&\n !retainedMessageIds.has(message.id) &&\n (message.turnId === null || retainedTurnIds.has(message.turnId)),\n )\n .toSorted(\n (left, right) =>\n compareDateTimeStrings(left.createdAt, right.createdAt) ||\n left.id.localeCompare(right.id),\n )\n .slice(0, missingUserCount);\n for (const message of fallbackUserMessages) {\n retainedMessageIds.add(message.id);\n }\n }\n\n const retainedAssistantCount = messages.filter(\n (message) =>\n message.role === \"assistant\" &&\n !isImportedAgentSessionMessageId(message.id) &&\n retainedMessageIds.has(message.id),\n ).length;\n const missingAssistantCount = Math.max(0, turnCount - retainedAssistantCount);\n if (missingAssistantCount > 0) {\n const fallbackAssistantMessages = messages\n .filter(\n (message) =>\n message.role === \"assistant\" &&\n !retainedMessageIds.has(message.id) &&\n (message.turnId === null || retainedTurnIds.has(message.turnId)),\n )\n .toSorted(\n (left, right) =>\n compareDateTimeStrings(left.createdAt, right.createdAt) ||\n left.id.localeCompare(right.id),\n )", + "checksum": "3b43fa6abfff7d93e1a26ca1e2e9db1305724a017683a69a68cef3dcaaabdfc2" }, "project-logical-grouping": { "id": "project-logical-grouping", @@ -736,8 +736,8 @@ "end": 115, "language": "typescript", "label": "Environment labels retained in grouped sidebar projects", - "code": "export function buildSidebarProjectSnapshots(input: {\n projects: ReadonlyArray;\n settings: ProjectGroupingSettings;\n primaryEnvironmentId: EnvironmentId | null;\n resolveEnvironmentLabel: (environmentId: EnvironmentId) => string | null;\n // Returns true when an env id maps to a desktopLocal saved-env\n // record (today: the WSL backend). Defaults to \"false for every\n // env\" so callers that don't care about the distinction get the\n // legacy behavior.\n isDesktopLocalEnvironment?: (environmentId: EnvironmentId) => boolean;\n}): SidebarProjectSnapshot[] {\n return buildProjectGroups({\n projects: input.projects,\n settings: input.settings,\n preferredEnvironmentId: input.primaryEnvironmentId,\n }).map((group): SidebarProjectSnapshot => {\n const members = group.members.map(\n ({ physicalProjectKey, project }): SidebarProjectGroupMember => ({\n ...project,\n physicalProjectKey,\n environmentLabel: input.resolveEnvironmentLabel(project.environmentId),\n }),\n );\n const representative =\n members.find(\n (member) =>\n member.environmentId === group.representative.environmentId &&\n member.id === group.representative.id,\n ) ?? members[0]!;\n\n const hasLocal =\n input.primaryEnvironmentId !== null &&\n members.some((member) => member.environmentId === input.primaryEnvironmentId);\n const hasRemote =\n input.primaryEnvironmentId !== null\n ? members.some((member) => member.environmentId !== input.primaryEnvironmentId)\n : false;\n const remoteMembers = members.filter(\n (member) =>\n input.primaryEnvironmentId !== null && member.environmentId !== input.primaryEnvironmentId,\n );\n const remoteEnvironmentLabels = remoteMembers\n .flatMap((member) => (member.environmentLabel ? [member.environmentLabel] : []))\n .filter((label, index, labels) => labels.indexOf(label) === index);\n const isDesktopLocal = input.isDesktopLocalEnvironment ?? (() => false);\n const allRemoteMembersAreDesktopLocal =\n remoteMembers.length > 0 &&\n remoteMembers.every((member) => isDesktopLocal(member.environmentId));\n\n return {\n ...representative,\n projectKey: group.key,\n displayName: group.label,\n groupedProjectCount: members.length,\n environmentPresence:\n hasLocal && hasRemote ? \"mixed\" : hasRemote ? \"remote-only\" : \"local-only\",\n allRemoteMembersAreDesktopLocal,\n memberProjects: members,\n memberProjectRefs: group.memberProjectRefs,\n remoteEnvironmentLabels,\n };\n });\n}", - "checksum": "991d462dd85ddad3333fca1606c7beac65a41aa4b90282ba4794991ef9e2fb6e" + "code": "\nexport function buildSidebarProjectSnapshots(input: {\n projects: ReadonlyArray;\n settings: ProjectGroupingSettings;\n primaryEnvironmentId: EnvironmentId | null;\n resolveEnvironmentLabel: (environmentId: EnvironmentId) => string | null;\n // Returns true when an env id maps to a desktop-local saved-env\n // record. Defaults to \"false for every\n // env\" so callers that don't care about the distinction get the\n // legacy behavior.\n isDesktopLocalEnvironment?: (environmentId: EnvironmentId) => boolean;\n isWslEnvironment?: (environmentId: EnvironmentId) => boolean;\n}): SidebarProjectSnapshot[] {\n return buildProjectGroups({\n projects: input.projects,\n settings: input.settings,\n preferredEnvironmentId: input.primaryEnvironmentId,\n }).map((group): SidebarProjectSnapshot => {\n const members = group.members.map(\n ({ physicalProjectKey, project }): SidebarProjectGroupMember => ({\n ...project,\n physicalProjectKey,\n environmentLabel: input.resolveEnvironmentLabel(project.environmentId),\n }),\n );\n const representative =\n members.find(\n (member) =>\n member.environmentId === group.representative.environmentId &&\n member.id === group.representative.id,\n ) ?? members[0]!;\n\n const hasLocal =\n input.primaryEnvironmentId !== null &&\n members.some((member) => member.environmentId === input.primaryEnvironmentId);\n const hasRemote =\n input.primaryEnvironmentId !== null\n ? members.some((member) => member.environmentId !== input.primaryEnvironmentId)\n : false;\n const remoteMembers = members.filter(\n (member) =>\n input.primaryEnvironmentId !== null && member.environmentId !== input.primaryEnvironmentId,\n );\n const remoteEnvironmentLabels = remoteMembers\n .flatMap((member) => (member.environmentLabel ? [member.environmentLabel] : []))\n .filter((label, index, labels) => labels.indexOf(label) === index);\n const isDesktopLocal = input.isDesktopLocalEnvironment ?? (() => false);\n const isWsl = input.isWslEnvironment ?? (() => false);\n const allRemoteMembersAreDesktopLocal =\n remoteMembers.length > 0 &&\n remoteMembers.every((member) => isDesktopLocal(member.environmentId));\n const allRemoteMembersAreWsl =\n remoteMembers.length > 0 && remoteMembers.every((member) => isWsl(member.environmentId));\n\n return {\n ...representative,\n projectKey: group.key,\n displayName: group.label,\n groupedProjectCount: members.length,\n environmentPresence:\n hasLocal && hasRemote ? \"mixed\" : hasRemote ? \"remote-only\" : \"local-only\",\n allRemoteMembersAreDesktopLocal,\n allRemoteMembersAreWsl,", + "checksum": "05d19eb676c474593da430f6bb77de6e2cab6959c802577432d83ba657e44e88" }, "project-setup-script-selection": { "id": "project-setup-script-selection", @@ -746,8 +746,8 @@ "end": 37, "language": "typescript", "label": "Worktree setup-script selection and environment construction", - "code": "export function projectScriptCwd(input: {\n project: {\n cwd: string;\n };\n worktreePath?: string | null;\n}): string {\n return input.worktreePath ?? input.project.cwd;\n}\n\nexport function projectScriptRuntimeEnv(\n input: ProjectScriptRuntimeEnvInput,\n): Record {\n const env: Record = {\n T3CODE_PROJECT_ROOT: input.project.cwd,\n };\n if (input.worktreePath) {\n env.T3CODE_WORKTREE_PATH = input.worktreePath;\n }\n if (input.extraEnv) {\n return { ...env, ...input.extraEnv };\n }\n return env;\n}\n\nexport function setupProjectScript(scripts: readonly ProjectScript[]): ProjectScript | null {\n return scripts.find((script) => script.runOnWorktreeCreate) ?? null;\n}", - "checksum": "78e76784914a8a10cc610f1b31943af278b003d98790ddffe5243b8ddeb8f7d8" + "code": " override ?? (project.scripts.length > 0 ? project.scripts : settings.defaultProjectScripts)\n );\n}\n\nexport function projectScriptsInheritDefaults(\n settings: Pick,\n project: { id: ProjectId; scripts: readonly ProjectScript[] },\n): boolean {\n const override = settings.projectScriptOverrides[project.id];\n return override === null || (override === undefined && project.scripts.length === 0);\n}\n\ninterface ProjectScriptRuntimeEnvInput {\n project: {\n cwd: string;\n };\n worktreePath?: string | null;\n extraEnv?: Record;\n}\n\nexport function projectScriptCwd(input: {\n project: {\n cwd: string;\n };\n worktreePath?: string | null;\n}): string {\n return input.worktreePath ?? input.project.cwd;", + "checksum": "789173ea9418b1a4bc05d6e2f62202aac4b436584c60658aa64b16443852da87" }, "project-setup-script-runner": { "id": "project-setup-script-runner", @@ -756,8 +756,8 @@ "end": 183, "language": "typescript", "label": "Setup script launched in the linked worktree terminal", - "code": " if (!project) {\n return yield* new ProjectSetupScriptProjectNotFoundError(errorContext);\n }\n\n const script = setupProjectScript(project.scripts);\n if (!script) {\n return {\n status: \"no-script\",\n } as const;\n }\n\n const terminalId = input.preferredTerminalId ?? `setup-${script.id}`;\n const cwd = input.worktreePath;\n const env = projectScriptRuntimeEnv({\n project: { cwd: project.workspaceRoot },\n worktreePath: input.worktreePath,\n });\n\n yield* terminalManager\n .open({\n threadId: input.threadId,\n terminalId,\n cwd,\n worktreePath: input.worktreePath,\n env,\n })\n .pipe(\n Effect.mapError(\n (cause) =>\n new ProjectSetupScriptOperationError({\n ...errorContext,\n operation: \"openTerminal\",\n cause,\n }),\n ),\n );\n yield* terminalManager\n .write({\n threadId: input.threadId,\n terminalId,\n data: `${script.command}\\r`,\n })\n .pipe(\n Effect.mapError(\n (cause) =>\n new ProjectSetupScriptOperationError({\n ...errorContext,\n operation: \"writeCommand\",\n cause,\n }),\n ),\n );\n\n return {\n status: \"started\",\n scriptId: script.id,\n scriptName: script.name,\n terminalId,\n cwd,\n } as const;\n });", - "checksum": "cff81f3c6730404adcbbfda69be3407b3d365eda8730a8f780e36b80a0fe2395" + "code": " operation: \"resolveProject\",\n cause,\n }),\n ),\n )\n : null);\n\n if (!project) {\n return yield* new ProjectSetupScriptProjectNotFoundError(errorContext);\n }\n\n const settings = yield* serverSettings.getSettings.pipe(\n Effect.mapError(\n (cause) =>\n new ProjectSetupScriptOperationError({\n ...errorContext,\n operation: \"readSettings\",\n cause,\n }),\n ),\n );\n const script = setupProjectScript(resolveProjectScripts(settings, project));\n if (!script) {\n return {\n status: \"no-script\",\n } as const;\n }\n\n const terminalId = input.preferredTerminalId ?? `setup-${script.id}`;\n const cwd = input.worktreePath;\n const env = projectScriptRuntimeEnv({\n project: { cwd: project.workspaceRoot },\n worktreePath: input.worktreePath,\n });\n\n yield* terminalManager\n .open({\n threadId: input.threadId,\n terminalId,\n cwd,\n worktreePath: input.worktreePath,\n env,\n })\n .pipe(\n Effect.mapError(\n (cause) =>\n new ProjectSetupScriptOperationError({\n ...errorContext,\n operation: \"openTerminal\",\n cause,\n }),\n ),\n );\n yield* terminalManager\n .write({\n threadId: input.threadId,\n terminalId,\n data: `${script.command}\\r`,\n })\n .pipe(\n Effect.mapError(", + "checksum": "601e99785f56276c9e6668a63ff6ba766fafc58144bfd61fa50a364ce5245f4e" }, "worktree-non-git-fallback": { "id": "worktree-non-git-fallback", @@ -766,8 +766,8 @@ "end": 327, "language": "typescript", "label": "Non-Git projects fall back from worktree to local mode", - "code": "export function resolveSendEnvMode(input: {\n requestedEnvMode: DraftThreadEnvMode;\n isGitRepo: boolean;\n}): DraftThreadEnvMode {\n return input.isGitRepo ? input.requestedEnvMode : \"local\";\n}", - "checksum": "f7e6d7316e8bb7115262ee669771235b851a046fb5943c03ae99edc63a2d8cbd" + "code": " if (!modelSelectionChanged && !branchChanged) {\n return null;\n }\n return {\n ...(modelSelectionChanged ? { modelSelection: nextModelSelection } : {}),\n ...(branchChanged ? { branch: input.nextBranch, worktreePath: null } : {}),", + "checksum": "3434eaae23de5a762d3978fb97003128d373873527fbac282c0a4f51eb29834b" }, "worktree-ref-occupancy": { "id": "worktree-ref-occupancy", @@ -776,8 +776,8 @@ "end": 2559, "language": "typescript", "label": "Git worktree listing decorates occupied branch references", - "code": " const readGitRefsSnapshot = Effect.fn(\"readGitRefsSnapshot\")(function* (gitCommonDir: string) {\n const fetchCwd =\n path.basename(gitCommonDir) === \".git\" ? path.dirname(gitCommonDir) : gitCommonDir;\n const gitDirArgs = [\"--git-dir\", gitCommonDir] as const;\n const [refsResult, defaultRefResult, worktreeListResult, remoteNamesResult] = yield* Effect.all(\n [\n executeGitWithStableDiagnostics(\n \"GitVcsDriver.listRefs.snapshotRefs\",\n fetchCwd,\n [\n ...gitDirArgs,\n \"for-each-ref\",\n \"--format=%(refname)%09%(committerdate:unix)%09%(symref)\",\n \"refs/heads\",\n \"refs/remotes\",\n ],\n {\n timeoutMs: 30_000,\n maxOutputBytes: 16 * 1024 * 1024,\n fallbackErrorDetail: \"Git ref snapshot enumeration failed.\",\n },\n ),\n executeGit(\n \"GitVcsDriver.listRefs.defaultRef\",\n fetchCwd,\n [...gitDirArgs, \"symbolic-ref\", \"refs/remotes/origin/HEAD\"],\n {\n timeoutMs: 5_000,\n allowNonZeroExit: true,\n },\n ),\n executeGit(\n \"GitVcsDriver.listRefs.worktreeList\",\n fetchCwd,\n [...gitDirArgs, \"worktree\", \"list\", \"--porcelain\", \"-z\"],\n {\n timeoutMs: 30_000,\n allowNonZeroExit: true,\n maxOutputBytes: 16 * 1024 * 1024,\n },\n ),\n executeGit(\"GitVcsDriver.listRefs.remoteNames\", fetchCwd, [...gitDirArgs, \"remote\"], {\n timeoutMs: 5_000,\n allowNonZeroExit: true,\n }),\n ],\n { concurrency: 2 },\n );\n\n const remoteNames =\n remoteNamesResult.exitCode === 0 ? parseRemoteNames(remoteNamesResult.stdout) : [];\n if (remoteNamesResult.exitCode !== 0 && remoteNamesResult.stderr.trim().length > 0) {\n yield* Effect.logWarning(\n `GitVcsDriver.listRefs: remote name lookup returned code ${remoteNamesResult.exitCode} for ${gitCommonDir}: ${remoteNamesResult.stderr.trim()}. Falling back to an empty remote name list.`,\n );\n }\n const defaultBranch =\n defaultRefResult.exitCode === 0\n ? defaultRefResult.stdout.trim().replace(/^refs\\/remotes\\/origin\\//, \"\")\n : null;\n const parsedWorktreeEntries =\n worktreeListResult.exitCode === 0\n ? [...parseWorktreeBranchPaths(worktreeListResult.stdout)].map(\n ([branchName, worktreePath]) =>\n [branchName, path.normalize(path.resolve(worktreePath))] as const,\n )\n : [];\n const existingWorktreeEntries = yield* Effect.filter(\n parsedWorktreeEntries,\n ([, worktreePath]) =>\n fileSystem.stat(worktreePath).pipe(\n Effect.as(true),\n Effect.orElseSucceed(() => false),\n ),\n { concurrency: 16 },\n );\n const worktreeMap = new Map(existingWorktreeEntries);\n const localBranches: Array<{ readonly ref: VcsRef; readonly lastCommit: number }> = [];\n const remoteBranches: Array<{ readonly ref: VcsRef; readonly lastCommit: number }> = [];\n\n for (const line of refsResult.stdout.split(\"\\n\")) {\n if (line.length === 0) continue;\n const [fullRefName, lastCommitRaw, symbolicTarget] = line.split(\"\\t\");\n if (!fullRefName || symbolicTarget) continue;\n const parsedLastCommit = Number.parseInt(lastCommitRaw ?? \"0\", 10);\n const lastCommit = Number.isFinite(parsedLastCommit) ? parsedLastCommit : 0;\n\n if (fullRefName.startsWith(\"refs/heads/\")) {\n const name = fullRefName.slice(\"refs/heads/\".length);\n localBranches.push({\n ref: {\n name,\n current: false,\n isRemote: false,\n isDefault: name === defaultBranch,\n worktreePath: worktreeMap.get(name) ?? null,\n },", - "checksum": "0d568abc710d939aac21b967910cb9bd479b61ebd13ad26bcbc87c919cac7743" + "code": " }\n\n const [realRepositoryRoot, realTarget] = yield* Effect.all([\n fileSystem.realPath(repositoryRoot),\n fileSystem.realPath(requestedPath),\n ]).pipe(\n Effect.mapError((cause) =>\n fileError(\"fs.realPath\", `Could not resolve diff file '${input.newPath}'.`, cause),\n ),\n );\n if (!isPathWithinRoot(realRepositoryRoot, realTarget)) {\n return yield* fileError(\n \"fs.realPath\",\n `Diff file '${input.newPath}' resolves outside the review workspace.`,\n );\n }\n\n const info = yield* fileSystem\n .stat(realTarget)\n .pipe(\n Effect.mapError((cause) =>\n fileError(\"fs.stat\", `Could not inspect diff file '${input.newPath}'.`, cause),\n ),\n );\n if (info.type !== \"File\") {\n return yield* fileError(\"fs.stat\", `Diff path '${input.newPath}' is not a file.`);\n }\n if (info.size > BigInt(REVIEW_DIFF_FILE_MAX_OUTPUT_BYTES)) {\n return yield* fileError(\n \"fs.stat\",\n `Diff file '${input.newPath}' exceeds the 1 MB expansion limit.`,\n );\n }\n\n const bytes = yield* fileSystem\n .readFile(realTarget)\n .pipe(\n Effect.mapError((cause) =>\n fileError(\"fs.readFile\", `Could not read diff file '${input.newPath}'.`, cause),\n ),\n );\n if (bytes.includes(0)) {\n return yield* fileError(\"fs.readFile\", `Cannot expand binary file '${input.newPath}'.`);\n }\n return new TextDecoder(\"utf-8\").decode(bytes);\n });\n\n const getReviewDiffFileContents = Effect.fn(\"getReviewDiffFileContents\")(function* (\n input: ReviewDiffFileContentsInput,\n ) {\n if (input.sourceKind === \"working-tree\") {\n const repositoryRoot = yield* runGitStdout(\n \"GitVcsDriver.getReviewDiffFileContents.repositoryRoot\",\n input.cwd,\n [\"rev-parse\", \"--show-toplevel\"],\n ).pipe(Effect.map((value) => value.trim()));\n if (repositoryRoot.length === 0) {\n return yield* reviewDiffFileError(input, \"Could not resolve the Git repository root.\");\n }\n const [oldContents, newContents] = yield* Effect.all(\n [\n input.changeType === \"new\"\n ? Effect.succeed(\"\")\n : readReviewFileAtRevision(input, input.baseRef ?? \"HEAD\", input.oldPath),\n input.changeType === \"deleted\"\n ? Effect.succeed(\"\")\n : readWorkingTreeReviewFile(input, repositoryRoot),\n ],\n { concurrency: 2 },\n );\n return { oldContents, newContents };\n }\n\n if (!input.baseRef || !input.headRef) {\n return yield* reviewDiffFileError(\n input,\n \"Branch diff file expansion requires both base and head refs.\",\n );\n }\n const mergeBase = yield* runGitStdout(\n \"GitVcsDriver.getReviewDiffFileContents.mergeBase\",\n input.cwd,\n [\"merge-base\", input.baseRef, input.headRef],\n ).pipe(Effect.map((value) => value.trim()));\n if (mergeBase.length === 0) {\n return yield* reviewDiffFileError(input, \"Could not resolve the branch comparison base.\");\n }\n const [oldContents, newContents] = yield* Effect.all(\n [\n input.changeType === \"new\"\n ? Effect.succeed(\"\")\n : readReviewFileAtRevision(input, mergeBase, input.oldPath),\n input.changeType === \"deleted\"\n ? Effect.succeed(\"\")\n : readReviewFileAtRevision(input, input.headRef, input.newPath),\n ],\n { concurrency: 2 },", + "checksum": "d5269dcadff0e187afb7f9e4d35f3a870f9e085701bb8954b2efe3697adebef3" }, "worktree-create-remove-contract": { "id": "worktree-create-remove-contract", @@ -786,8 +786,8 @@ "end": 165, "language": "typescript", "label": "Worktree creation and removal RPC inputs", - "code": "export const VcsListRefsInput = Schema.Struct({\n cwd: TrimmedNonEmptyStringSchema,\n query: Schema.optional(TrimmedNonEmptyStringSchema.check(Schema.isMaxLength(256))),\n cursor: Schema.optional(NonNegativeInt),\n includeMatchingRemoteRefs: Schema.optional(Schema.Boolean),\n refKind: Schema.optional(Schema.Literals([\"all\", \"local\", \"remote\"])),\n refresh: Schema.optional(Schema.Boolean),\n limit: Schema.optional(\n PositiveInt.check(Schema.isLessThanOrEqualTo(GIT_LIST_BRANCHES_MAX_LIMIT)),\n ),\n});\nexport type VcsListRefsInput = typeof VcsListRefsInput.Type;\n\nexport const VcsCreateWorktreeInput = Schema.Struct({\n cwd: TrimmedNonEmptyStringSchema,\n refName: TrimmedNonEmptyStringSchema,\n newRefName: Schema.optional(TrimmedNonEmptyStringSchema),\n baseRefName: Schema.optional(TrimmedNonEmptyStringSchema),\n path: Schema.NullOr(TrimmedNonEmptyStringSchema),\n});\nexport type VcsCreateWorktreeInput = typeof VcsCreateWorktreeInput.Type;\n\nexport const GitPullRequestRefInput = Schema.Struct({\n cwd: TrimmedNonEmptyStringSchema,\n reference: GitPullRequestReference,\n});\nexport type GitPullRequestRefInput = typeof GitPullRequestRefInput.Type;\n\nexport const GitPreparePullRequestThreadInput = Schema.Struct({\n cwd: TrimmedNonEmptyStringSchema,\n reference: GitPullRequestReference,\n mode: GitPreparePullRequestThreadMode,\n threadId: Schema.optional(ThreadId),\n});\nexport type GitPreparePullRequestThreadInput = typeof GitPreparePullRequestThreadInput.Type;\n\nexport const VcsRemoveWorktreeInput = Schema.Struct({\n cwd: TrimmedNonEmptyStringSchema,\n path: TrimmedNonEmptyStringSchema,\n force: Schema.optional(Schema.Boolean),\n});\nexport type VcsRemoveWorktreeInput = typeof VcsRemoveWorktreeInput.Type;", - "checksum": "38f837fcc1bd73ea692ff819c248771f4eba476d0336357698d09ffdfa20bad2" + "code": "});\nexport type GitRunStackedActionInput = typeof GitRunStackedActionInput.Type;\n\nexport const VcsListRefsInput = Schema.Struct({\n cwd: TrimmedNonEmptyStringSchema,\n query: Schema.optional(TrimmedNonEmptyStringSchema.check(Schema.isMaxLength(256))),\n cursor: Schema.optional(NonNegativeInt),\n includeMatchingRemoteRefs: Schema.optional(Schema.Boolean),\n refKind: Schema.optional(Schema.Literals([\"all\", \"local\", \"remote\"])),\n refresh: Schema.optional(Schema.Boolean),\n limit: Schema.optional(\n PositiveInt.check(Schema.isLessThanOrEqualTo(GIT_LIST_BRANCHES_MAX_LIMIT)),\n ),\n});\nexport type VcsListRefsInput = typeof VcsListRefsInput.Type;\n\nexport const VcsCreateWorktreeInput = Schema.Struct({\n cwd: TrimmedNonEmptyStringSchema,\n refName: TrimmedNonEmptyStringSchema,\n newRefName: Schema.optional(TrimmedNonEmptyStringSchema),\n baseRefName: Schema.optional(TrimmedNonEmptyStringSchema),\n path: Schema.NullOr(TrimmedNonEmptyStringSchema),\n});\nexport type VcsCreateWorktreeInput = typeof VcsCreateWorktreeInput.Type;\n\nexport const GitPullRequestRefInput = Schema.Struct({\n cwd: TrimmedNonEmptyStringSchema,\n reference: GitPullRequestReference,\n});\nexport type GitPullRequestRefInput = typeof GitPullRequestRefInput.Type;\n\nexport const GitPreparePullRequestThreadInput = Schema.Struct({\n cwd: TrimmedNonEmptyStringSchema,\n reference: GitPullRequestReference,\n mode: GitPreparePullRequestThreadMode,\n threadId: Schema.optional(ThreadId),\n});\nexport type GitPreparePullRequestThreadInput = typeof GitPreparePullRequestThreadInput.Type;\n\nexport const VcsRemoveWorktreeInput = Schema.Struct({\n cwd: TrimmedNonEmptyStringSchema,\n path: TrimmedNonEmptyStringSchema,", + "checksum": "eb666ab2994a9dc3797de1f75448df3c93943dbf58b7376f301ce8f532fc1c6c" }, "worktree-result-contract": { "id": "worktree-result-contract", @@ -796,8 +796,8 @@ "end": 294, "language": "typescript", "label": "Worktree and pull-request preparation results", - "code": "export const VcsListRefsResult = Schema.Struct({\n refs: Schema.Array(VcsRef),\n isRepo: Schema.Boolean,\n hasPrimaryRemote: Schema.Boolean,\n nextCursor: NonNegativeInt.pipe(Schema.NullOr),\n totalCount: NonNegativeInt,\n});\nexport type VcsListRefsResult = typeof VcsListRefsResult.Type;\n\nexport const VcsCreateWorktreeResult = Schema.Struct({\n worktree: VcsWorktree,\n});\nexport type VcsCreateWorktreeResult = typeof VcsCreateWorktreeResult.Type;\n\nexport const GitResolvePullRequestResult = Schema.Struct({\n pullRequest: GitResolvedPullRequest,\n});\nexport type GitResolvePullRequestResult = typeof GitResolvePullRequestResult.Type;\n\nexport const GitPreparePullRequestThreadResult = Schema.Struct({\n pullRequest: GitResolvedPullRequest,\n branch: TrimmedNonEmptyStringSchema,\n worktreePath: TrimmedNonEmptyStringSchema.pipe(Schema.NullOr),\n /**\n * False when the checkout could not be brought to the pull request head — a reused worktree\n * holding local commits or uncommitted changes keeps its own state, so the code being handed\n * over is older than the pull request.\n */\n isOnPullRequestHead: Schema.Boolean,\n});\nexport type GitPreparePullRequestThreadResult = typeof GitPreparePullRequestThreadResult.Type;", - "checksum": "be9e1709dcb7b1c171e5a679852c15f2b5e16949f1bde705f05f5d01086a63ee" + "code": "]);\nexport type VcsStatusStreamEvent = typeof VcsStatusStreamEvent.Type;\n\nexport const VcsListRefsResult = Schema.Struct({\n refs: Schema.Array(VcsRef),\n isRepo: Schema.Boolean,\n hasPrimaryRemote: Schema.Boolean,\n nextCursor: NonNegativeInt.pipe(Schema.NullOr),\n totalCount: NonNegativeInt,\n});\nexport type VcsListRefsResult = typeof VcsListRefsResult.Type;\n\nexport const VcsCreateWorktreeResult = Schema.Struct({\n worktree: VcsWorktree,\n});\nexport type VcsCreateWorktreeResult = typeof VcsCreateWorktreeResult.Type;\n\nexport const GitResolvePullRequestResult = Schema.Struct({\n pullRequest: GitResolvedPullRequest,\n});\nexport type GitResolvePullRequestResult = typeof GitResolvePullRequestResult.Type;\n\nexport const GitPreparePullRequestThreadResult = Schema.Struct({\n pullRequest: GitResolvedPullRequest,\n branch: TrimmedNonEmptyStringSchema,\n worktreePath: TrimmedNonEmptyStringSchema.pipe(Schema.NullOr),\n /**\n * False when the checkout could not be brought to the pull request head — a reused worktree\n * holding local commits or uncommitted changes keeps its own state, so the code being handed\n * over is older than the pull request.\n */", + "checksum": "e05212a7dd2402706ea5aba7e3479a2272d7998fd2f80ef691bd419e8ec0db57" }, "worktree-bootstrap-path": { "id": "worktree-bootstrap-path", @@ -806,8 +806,8 @@ "end": 1022, "language": "typescript", "label": "Origin-aware worktree bootstrap saga", - "code": " if (bootstrap?.prepareWorktree) {\n let worktreeBaseRef = bootstrap.prepareWorktree.baseBranch;\n // \"Start from origin\" is a stored default; repos without an\n // origin remote fall back to the local base branch instead of\n // failing the whole bootstrap on `git fetch origin`.\n const startFromOrigin =\n bootstrap.prepareWorktree.startFromOrigin === true &&\n (yield* gitWorkflow.remoteExists({\n cwd: bootstrap.prepareWorktree.projectCwd,\n remoteName: \"origin\",\n }));\n if (startFromOrigin) {\n yield* gitWorkflow.fetchRemote({\n cwd: bootstrap.prepareWorktree.projectCwd,\n remoteName: \"origin\",\n });\n const resolvedRemoteBase = yield* gitWorkflow.resolveRemoteTrackingCommit({\n cwd: bootstrap.prepareWorktree.projectCwd,\n refName: bootstrap.prepareWorktree.baseBranch,\n fallbackRemoteName: \"origin\",\n });\n worktreeBaseRef = resolvedRemoteBase.commitSha;\n }\n const worktree = yield* gitWorkflow.createWorktree({\n cwd: bootstrap.prepareWorktree.projectCwd,\n refName: worktreeBaseRef,\n newRefName: bootstrap.prepareWorktree.branch,\n baseRefName: bootstrap.prepareWorktree.baseBranch,\n path: null,\n });\n targetWorktreePath = worktree.worktree.path;\n yield* dispatchFromClient({\n type: \"thread.meta.update\",\n commandId: yield* serverCommandId(\"bootstrap-thread-meta-update\"),\n threadId: command.threadId,\n branch: worktree.worktree.refName,\n worktreePath: targetWorktreePath,\n });\n yield* refreshGitStatus(targetWorktreePath);\n }\n\n yield* runSetupProgram();\n\n return yield* dispatchFromClient(finalTurnStartCommand);", - "checksum": "d243bf017c0fece3cfeca6fcec59f80633bae6e4358beecd85c6afdff52e31c1" + "code": " Effect.gen(function* () {\n const bootstrap = command.bootstrap;\n const { bootstrap: _bootstrap, ...finalTurnStartCommand } = command;\n let createdThread = false;\n let targetProjectId = bootstrap?.createThread?.projectId;\n let targetProjectCwd = bootstrap?.prepareWorktree?.projectCwd;\n let targetWorktreePath = bootstrap?.createThread?.worktreePath ?? null;\n\n const cleanupCreatedThread = () =>\n createdThread\n ? serverCommandId(\"bootstrap-thread-delete\").pipe(\n Effect.flatMap((commandId) =>\n dispatchFromClient({\n type: \"thread.delete\",\n commandId,\n threadId: command.threadId,\n }),\n ),\n Effect.as(true),\n )\n : Effect.succeed(false);\n\n const recordSetupScriptLaunchFailure = (input: {\n readonly error: ProjectSetupScriptRunner.ProjectSetupScriptRunnerError;\n readonly requestedAt: string;\n readonly worktreePath: string;\n }) => {\n const detail = projectSetupScriptCompatibilityDetail(input.error);\n return appendSetupScriptActivity({\n threadId: command.threadId,\n kind: \"setup-script.failed\",\n summary: \"Setup script failed to start\",\n createdAt: input.requestedAt,\n payload: {\n detail,\n worktreePath: input.worktreePath,\n },\n tone: \"error\",\n }).pipe(\n Effect.ignoreCause({ log: false }),\n Effect.flatMap(() =>\n Effect.logWarning(\"bootstrap turn start failed to launch setup script\", {\n threadId: command.threadId,\n worktreePath: input.worktreePath,", + "checksum": "111e1c08a9b64f52e1c1d5ba8492aa8aab42542206e448d91a06277afdbc6809" }, "worktree-origin-base-test": { "id": "worktree-origin-base-test", @@ -816,8 +816,8 @@ "end": 1565, "language": "typescript", "label": "Remote base reference worktree creation test", - "code": " it.effect(\"creates a worktree from the latest fetched remote commit\", () =>\n Effect.gen(function* () {\n const cwd = yield* makeTmpDir();\n const remote = yield* makeTmpDir(\"git-remote-\");\n const peer = yield* makeTmpDir(\"git-peer-\");\n const { initialBranch } = yield* initRepoWithCommit(cwd);\n yield* git(remote, [\"init\", \"--bare\"]);\n yield* git(cwd, [\"remote\", \"add\", \"origin\", remote]);\n yield* git(cwd, [\"push\", \"-u\", \"origin\", initialBranch]);\n yield* git(remote, [\"symbolic-ref\", \"HEAD\", `refs/heads/${initialBranch}`]);\n const beforeFetch = yield* git(cwd, [\"rev-parse\", `refs/remotes/origin/${initialBranch}`]);\n\n yield* git(peer, [\"clone\", remote, \".\"]);\n yield* git(peer, [\"config\", \"user.email\", \"test@test.com\"]);\n yield* git(peer, [\"config\", \"user.name\", \"Test\"]);\n yield* writeTextFile(peer, \"remote-change.txt\", \"remote\\n\");\n yield* git(peer, [\"add\", \"remote-change.txt\"]);\n yield* git(peer, [\"commit\", \"-m\", \"remote change\"]);\n yield* git(peer, [\"push\", \"origin\", initialBranch]);\n const remoteHead = yield* git(peer, [\"rev-parse\", \"HEAD\"]);\n assert.notEqual(beforeFetch, remoteHead);\n\n const driver = yield* GitVcsDriver.GitVcsDriver;\n yield* driver.fetchRemote({ cwd, remoteName: \"origin\" });\n\n const resolvedBase = yield* driver.resolveRemoteTrackingCommit({\n cwd,\n refName: initialBranch,\n fallbackRemoteName: \"origin\",\n });\n const explicitlyResolvedBase = yield* driver.resolveRemoteTrackingCommit({\n cwd,\n refName: `origin/${initialBranch}`,\n fallbackRemoteName: \"origin\",\n });\n\n assert.deepEqual(resolvedBase, {\n commitSha: remoteHead,\n remoteRefName: `origin/${initialBranch}`,\n });\n assert.deepEqual(explicitlyResolvedBase, resolvedBase);\n assert.equal(yield* git(cwd, [\"rev-parse\", initialBranch]), beforeFetch);\n\n const pathService = yield* Path.Path;\n const worktreePath = pathService.join(\n yield* makeTmpDir(\"git-fetched-worktrees-\"),\n \"fetched-origin\",\n );\n yield* driver.createWorktree({\n cwd,\n path: worktreePath,\n refName: resolvedBase.commitSha,\n newRefName: \"t3code/fetched-origin\",\n baseRefName: resolvedBase.remoteRefName,\n });\n\n assert.equal(yield* git(worktreePath, [\"rev-parse\", \"HEAD\"]), remoteHead);\n assert.equal(\n yield* driver.readConfigValue(worktreePath, \"branch.t3code/fetched-origin.gh-merge-base\"),\n initialBranch,\n );", - "checksum": "3079485524aec02260b358299da023b6dda92f6b257cd7abe203530cca242022" + "code": " yield* fileSystem.exists(pathService.join(worktreePath, \"shared\", \"SHARED.md\")),\n true,\n );\n }),\n );\n\n it.effect(\"still creates the worktree when submodule checkout fails\", () =>\n Effect.gen(function* () {\n const fileSystem = yield* FileSystem.FileSystem;\n const pathService = yield* Path.Path;\n\n const cwd = yield* makeTmpDir();\n const { initialBranch } = yield* initRepoWithCommit(cwd);\n // Points at a repository that does not exist, so the checkout fails the\n // way an unreachable private remote would. Creation must still succeed.\n yield* writeTextFile(\n cwd,\n \".gitmodules\",\n '[submodule \"missing\"]\\n\\tpath = missing\\n\\turl = /nonexistent/repo.git\\n',\n );\n yield* git(cwd, [\"add\", \".\"]);\n yield* git(cwd, [\"commit\", \"-m\", \"add unreachable submodule\"]);\n\n const worktreePath = pathService.join(\n yield* makeTmpDir(\"git-worktrees-\"),\n \"broken-submodule-worktree\",\n );\n const driver = yield* GitVcsDriver.GitVcsDriver;\n const created = yield* driver.createWorktree({\n cwd,\n path: worktreePath,\n refName: initialBranch,\n newRefName: \"feature/broken-submodules\",\n });\n\n assert.equal(created.worktree.path, worktreePath);\n assert.equal(yield* fileSystem.exists(worktreePath), true);\n }),\n );\n\n it.effect(\"creates and removes a worktree for a new refName\", () =>\n Effect.gen(function* () {\n const cwd = yield* makeTmpDir();\n const { initialBranch } = yield* initRepoWithCommit(cwd);\n const pathService = yield* Path.Path;\n const worktreePath = pathService.join(\n yield* makeTmpDir(\"git-worktrees-\"),\n \"feature-worktree\",\n );\n const driver = yield* GitVcsDriver.GitVcsDriver;\n\n const created = yield* driver.createWorktree({\n cwd,\n path: worktreePath,\n refName: initialBranch,\n newRefName: \"feature/worktree\",\n });\n\n assert.equal(created.worktree.path, worktreePath);\n assert.equal(created.worktree.refName, \"feature/worktree\");\n assert.equal(yield* git(worktreePath, [\"branch\", \"--show-current\"]), \"feature/worktree\");", + "checksum": "3825dad162e15f5bccb06dd3e016ae9dedbc033d83cd9eb3dc9ea0e24847fb6d" }, "worktree-create-command": { "id": "worktree-create-command", @@ -826,8 +826,8 @@ "end": 2799, "language": "typescript", "label": "Git worktree add command and default linked path", - "code": " const createWorktree: GitVcsDriver.GitVcsDriver[\"Service\"][\"createWorktree\"] = Effect.fn(\n \"createWorktree\",\n )(function* (input) {\n const targetBranch = input.newRefName ?? input.refName;\n const sanitizedBranch = targetBranch.replace(/\\//g, \"-\");\n const repoName = path.basename(input.cwd);\n const worktreePath = input.path ?? path.join(worktreesDir, repoName, sanitizedBranch);\n const args = input.newRefName\n ? [\"worktree\", \"add\", \"-b\", input.newRefName, worktreePath, input.refName]\n : [\"worktree\", \"add\", worktreePath, input.refName];\n\n yield* executeGit(\"GitVcsDriver.createWorktree\", input.cwd, args, {\n fallbackErrorDetail: \"git worktree add failed\",\n timeoutMs: WORKTREE_ADD_TIMEOUT_MS,\n });\n\n if (input.newRefName && input.baseRefName) {\n const remoteNames = yield* listRemoteNames(input.cwd).pipe(Effect.orElseSucceed(() => []));\n const parsedBaseRef = parseRemoteRefWithRemoteNames(\n input.baseRefName,\n remoteNames.toSorted((left, right) => right.length - left.length),\n );\n const baseBranch = parsedBaseRef?.branchName ?? input.baseRefName;\n yield* runGit(\"GitVcsDriver.createWorktree.configureBaseRef\", input.cwd, [\n \"config\",\n `branch.${input.newRefName}.gh-merge-base`,\n baseBranch,\n ]);\n }\n\n return {\n worktree: {\n path: worktreePath,\n refName: targetBranch,\n },\n };\n });", - "checksum": "4c15353dd8e82554c229e870a3625673d90574db5b6c7ae4396b332f837f61c3" + "code": " refresh: boolean,\n ) {\n while (true) {\n const generation = currentListRefsGeneration(gitCommonDir);\n const currentEpoch = listRefsEpochByCommonDir.get(gitCommonDir);\n const snapshot =\n refresh || currentEpoch === undefined\n ? // The refresh cache owns the complete snapshot read, rather than only the\n // epoch bump. Slow repositories therefore remain singleflight for the\n // entire Git scan even when more refresh requests arrive after the\n // coalescing TTL would otherwise have elapsed.\n yield* Cache.get(\n listRefsRefreshSnapshotCache,\n new GitRefsRefreshCacheKey({ gitCommonDir, generation }),\n )\n : yield* Cache.get(\n listRefsSnapshotCache,\n new GitRefsSnapshotCacheKey({ gitCommonDir, epoch: currentEpoch }),\n );\n if (currentListRefsGeneration(gitCommonDir) === generation) {\n return snapshot;\n }\n }\n });\n const invalidateListRefsSnapshot = Effect.fn(\"invalidateListRefsSnapshot\")(function* (\n cwd: string,\n ) {\n const repositoryPathsCacheKey = normalizeRepositoryPathsCacheKey(cwd);\n const repositoryPaths = yield* Cache.get(repositoryPathsCache, repositoryPathsCacheKey);\n if (repositoryPaths === null) return;\n const previousGeneration = currentListRefsGeneration(repositoryPaths.gitCommonDir);\n bumpListRefsGeneration(repositoryPaths.gitCommonDir);\n bumpListRefsEpoch(repositoryPaths.gitCommonDir);\n yield* Cache.invalidate(\n listRefsRefreshSnapshotCache,\n new GitRefsRefreshCacheKey({\n gitCommonDir: repositoryPaths.gitCommonDir,", + "checksum": "6a1953b4a31f955e6bc2e82f9f7f2db4c0bf788f9349a3f158c900926121e04b" }, "worktree-setup-runner": { "id": "worktree-setup-runner", @@ -836,8 +836,8 @@ "end": 183, "language": "typescript", "label": "Post-creation setup script runner", - "code": " if (!project) {\n return yield* new ProjectSetupScriptProjectNotFoundError(errorContext);\n }\n\n const script = setupProjectScript(project.scripts);\n if (!script) {\n return {\n status: \"no-script\",\n } as const;\n }\n\n const terminalId = input.preferredTerminalId ?? `setup-${script.id}`;\n const cwd = input.worktreePath;\n const env = projectScriptRuntimeEnv({\n project: { cwd: project.workspaceRoot },\n worktreePath: input.worktreePath,\n });\n\n yield* terminalManager\n .open({\n threadId: input.threadId,\n terminalId,\n cwd,\n worktreePath: input.worktreePath,\n env,\n })\n .pipe(\n Effect.mapError(\n (cause) =>\n new ProjectSetupScriptOperationError({\n ...errorContext,\n operation: \"openTerminal\",\n cause,\n }),\n ),\n );\n yield* terminalManager\n .write({\n threadId: input.threadId,\n terminalId,\n data: `${script.command}\\r`,\n })\n .pipe(\n Effect.mapError(\n (cause) =>\n new ProjectSetupScriptOperationError({\n ...errorContext,\n operation: \"writeCommand\",\n cause,\n }),\n ),\n );\n\n return {\n status: \"started\",\n scriptId: script.id,\n scriptName: script.name,\n terminalId,\n cwd,\n } as const;\n });", - "checksum": "cff81f3c6730404adcbbfda69be3407b3d365eda8730a8f780e36b80a0fe2395" + "code": " operation: \"resolveProject\",\n cause,\n }),\n ),\n )\n : null);\n\n if (!project) {\n return yield* new ProjectSetupScriptProjectNotFoundError(errorContext);\n }\n\n const settings = yield* serverSettings.getSettings.pipe(\n Effect.mapError(\n (cause) =>\n new ProjectSetupScriptOperationError({\n ...errorContext,\n operation: \"readSettings\",\n cause,\n }),\n ),\n );\n const script = setupProjectScript(resolveProjectScripts(settings, project));\n if (!script) {\n return {\n status: \"no-script\",\n } as const;\n }\n\n const terminalId = input.preferredTerminalId ?? `setup-${script.id}`;\n const cwd = input.worktreePath;\n const env = projectScriptRuntimeEnv({\n project: { cwd: project.workspaceRoot },\n worktreePath: input.worktreePath,\n });\n\n yield* terminalManager\n .open({\n threadId: input.threadId,\n terminalId,\n cwd,\n worktreePath: input.worktreePath,\n env,\n })\n .pipe(\n Effect.mapError(\n (cause) =>\n new ProjectSetupScriptOperationError({\n ...errorContext,\n operation: \"openTerminal\",\n cause,\n }),\n ),\n );\n yield* terminalManager\n .write({\n threadId: input.threadId,\n terminalId,\n data: `${script.command}\\r`,\n })\n .pipe(\n Effect.mapError(", + "checksum": "601e99785f56276c9e6668a63ff6ba766fafc58144bfd61fa50a364ce5245f4e" }, "worktree-setup-test": { "id": "worktree-setup-test", @@ -846,8 +846,8 @@ "end": 152, "language": "typescript", "label": "Setup terminal cwd and environment test", - "code": " it.effect(\n \"opens the deterministic setup terminal with worktree env and writes the command\",\n () => {\n const open = vi.fn(() =>\n Effect.succeed({\n threadId: \"thread-1\",\n terminalId: \"setup-setup\",\n cwd: \"/repo/worktrees/a\",\n worktreePath: \"/repo/worktrees/a\",\n status: \"running\" as const,\n pid: 123,\n history: \"\",\n exitCode: null,\n exitSignal: null,\n label: \"setup-setup\",\n updatedAt: \"2026-01-01T00:00:00.000Z\",\n }),\n );\n const write = vi.fn(() => Effect.void);\n const project = makeProject([\n {\n id: \"setup\",\n name: \"Setup\",\n command: \"bun install\",\n icon: \"configure\",\n runOnWorktreeCreate: true,\n },\n ]);\n\n return Effect.gen(function* () {\n const runner = yield* ProjectSetupScriptRunner.ProjectSetupScriptRunner;\n const result = yield* runner.runForThread({\n threadId: \"thread-1\",\n projectCwd: \"/repo/project\",\n worktreePath: \"/repo/worktrees/a\",\n });\n\n expect(result).toEqual({\n status: \"started\",\n scriptId: \"setup\",\n scriptName: \"Setup\",\n terminalId: \"setup-setup\",\n cwd: \"/repo/worktrees/a\",\n });\n expect(open).toHaveBeenCalledWith({\n threadId: \"thread-1\",\n terminalId: \"setup-setup\",\n cwd: \"/repo/worktrees/a\",\n worktreePath: \"/repo/worktrees/a\",\n env: {\n T3CODE_PROJECT_ROOT: \"/repo/project\",\n T3CODE_WORKTREE_PATH: \"/repo/worktrees/a\",\n },\n });\n expect(write).toHaveBeenCalledWith({\n threadId: \"thread-1\",\n terminalId: \"setup-setup\",\n data: \"bun install\\r\",\n });\n }).pipe(Effect.provide(testLayer(project, { open, write })));", - "checksum": "97cc43e347199b1d18b1a9e4c57bc8933996c2e29b4db41a3d8b700fbf69ffaf" + "code": " exitSignal: null,\n label: \"setup-default-setup\",\n updatedAt: \"2026-01-01T00:00:00.000Z\",\n }),\n );\n const write = vi.fn(() => Effect.void);\n return Effect.gen(function* () {\n const runner = yield* ProjectSetupScriptRunner.ProjectSetupScriptRunner;\n const result = yield* runner.runForThread({\n threadId: \"thread-1\",\n projectId: \"project-1\",\n worktreePath: \"/repo/worktrees/a\",\n });\n expect(result).toMatchObject({ status: \"started\", scriptId: \"default-setup\" });\n expect(open).toHaveBeenCalledWith({\n threadId: \"thread-1\",\n terminalId: \"setup-default-setup\",\n cwd: \"/repo/worktrees/a\",\n worktreePath: \"/repo/worktrees/a\",\n env: { T3CODE_PROJECT_ROOT: \"/repo/project\", T3CODE_WORKTREE_PATH: \"/repo/worktrees/a\" },\n });\n expect(write).toHaveBeenCalledWith({\n threadId: \"thread-1\",\n terminalId: \"setup-default-setup\",\n data: \"npm install\\r\",\n });\n }).pipe(\n Effect.provide(\n testLayer(\n makeProject([]),\n { open, write },\n ServerSettings.layerTest({\n defaultProjectScripts: [\n {\n id: \"default-setup\",\n name: \"Setup\",\n command: \"npm install\",\n icon: \"configure\",\n runOnWorktreeCreate: true,\n },\n ],\n }),\n ),\n ),\n );\n });\n\n it.effect(\"returns no-script when no setup script exists\", () => {\n const open = vi.fn(() => Effect.die(\"unexpected open\"));\n const write = vi.fn(() => Effect.die(\"unexpected write\"));\n const project = makeProject([]);\n\n return Effect.gen(function* () {\n const runner = yield* ProjectSetupScriptRunner.ProjectSetupScriptRunner;\n const result = yield* runner.runForThread({\n threadId: \"thread-1\",\n projectId: \"project-1\",\n worktreePath: \"/repo/worktrees/a\",\n });\n", + "checksum": "a44de0a01dde5e6ef143978a7a731c52889ecd6d79eab0c4336e181866502a7e" }, "worktree-remove-command": { "id": "worktree-remove-command", @@ -856,8 +856,8 @@ "end": 2994, "language": "typescript", "label": "Git worktree removal command", - "code": " const removeWorktree: GitVcsDriver.GitVcsDriver[\"Service\"][\"removeWorktree\"] = Effect.fn(\n \"removeWorktree\",\n )(function* (input) {\n const args = [\"worktree\", \"remove\"];\n if (input.force) {\n args.push(\"--force\");\n }\n args.push(input.path);\n yield* executeGit(\"GitVcsDriver.removeWorktree\", input.cwd, args, {\n timeoutMs: 15_000,\n fallbackErrorDetail: \"git worktree remove failed\",\n });\n });", - "checksum": "af8c3225d2530ef3532d7840a4f688233911c75f8750cb1287777d8a7b4dbf98" + "code": " const { commitSha: headCommit } = yield* resolveCommit({ cwd: input.cwd, revision: \"HEAD\" });\n if (headCommit === input.targetCommit) {\n return { headCommit, moved: false, onTarget: true };\n }\n\n const worktreeChanges = yield* runGitStdout(\n \"GitVcsDriver.refreshCheckedOutBranch.status\",\n input.cwd,\n [\"status\", \"--porcelain\"],\n );\n if (worktreeChanges.trim().length > 0) {\n return { headCommit, moved: false, onTarget: false };\n }", + "checksum": "a2729d8d2b9a24aa18554f9c321237382fbb262f0250aba2705e3daa5d76105c" }, "worktree-create-remove-test": { "id": "worktree-create-remove-test", @@ -866,8 +866,8 @@ "end": 1403, "language": "typescript", "label": "Worktree create and remove integration test", - "code": " it.effect(\"creates and removes a worktree for a new refName\", () =>\n Effect.gen(function* () {\n const cwd = yield* makeTmpDir();\n const { initialBranch } = yield* initRepoWithCommit(cwd);\n const pathService = yield* Path.Path;\n const worktreePath = pathService.join(\n yield* makeTmpDir(\"git-worktrees-\"),\n \"feature-worktree\",\n );\n const driver = yield* GitVcsDriver.GitVcsDriver;\n\n const created = yield* driver.createWorktree({\n cwd,\n path: worktreePath,\n refName: initialBranch,\n newRefName: \"feature/worktree\",\n });\n\n assert.equal(created.worktree.path, worktreePath);\n assert.equal(created.worktree.refName, \"feature/worktree\");\n assert.equal(yield* git(worktreePath, [\"branch\", \"--show-current\"]), \"feature/worktree\");\n\n yield* driver.removeWorktree({ cwd, path: worktreePath });\n const fileSystem = yield* FileSystem.FileSystem;\n assert.equal(yield* fileSystem.exists(worktreePath), false);\n }),\n );", - "checksum": "7a00307d552baf90ff2b75e89cf5b2578a924b5f13fef6b4784fbf9f03a92160" + "code": "\n const refs = yield* driver.listRefs({ cwd });\n const remoteDefault = refs.refs.find((ref) => ref.name === `origin/${initialBranch}`);\n assert.equal(remoteDefault?.isRemote, true);\n assert.equal(remoteDefault?.isDefault, true);\n }),\n );\n\n it.effect(\"creates, checks out, renames, and lists refs\", () =>\n Effect.gen(function* () {\n const cwd = yield* makeTmpDir();\n yield* initRepoWithCommit(cwd);\n const driver = yield* GitVcsDriver.GitVcsDriver;\n\n yield* driver.createRef({ cwd, refName: \"feature/original\" });\n const switchRef = yield* driver.switchRef({ cwd, refName: \"feature/original\" });\n assert.equal(switchRef.refName, \"feature/original\");\n\n const renamed = yield* driver.renameBranch({\n cwd,\n oldBranch: \"feature/original\",\n newBranch: \"feature/renamed\",\n });\n assert.equal(renamed.branch, \"feature/renamed\");\n assert.equal(yield* git(cwd, [\"branch\", \"--show-current\"]), \"feature/renamed\");\n\n const refs = yield* driver.listRefs({ cwd });", + "checksum": "c616bd3d4708c901885bef2613bfba08269a3e0fc5c3c0f3fd742d357c54cef3" }, "worktree-bootstrap-failure-cleanup": { "id": "worktree-bootstrap-failure-cleanup", @@ -876,8 +876,8 @@ "end": 1053, "language": "typescript", "label": "Bootstrap failure compensation for a newly created thread", - "code": " return yield* bootstrapProgram.pipe(\n Effect.catchCause((cause) => {\n const dispatchError = toBootstrapDispatchCommandCauseError(cause);\n if (Cause.hasInterruptsOnly(cause)) {\n return Effect.fail(dispatchError);\n }\n return Effect.uninterruptible(cleanupCreatedThread()).pipe(\n Effect.matchCauseEffect({\n onFailure: (cleanupCause) =>\n Effect.logWarning(\"bootstrap thread cleanup failed\", {\n threadId: command.threadId,\n detail: Cause.pretty(cleanupCause),\n }).pipe(Effect.flatMap(() => Effect.fail(dispatchError))),\n onSuccess: (threadDeleted) =>\n Effect.fail(\n threadDeleted\n ? new OrchestrationDispatchCommandError({\n message: dispatchError.message,\n ...(dispatchError.cause !== undefined\n ? { cause: dispatchError.cause }\n : {}),\n bootstrapThreadDisposition: \"deleted\",\n })\n : dispatchError,\n ),\n }),\n );\n }),\n );", - "checksum": "d2c16f4de66195ff88ab5b79da7e0944fd287f3bc58ca2743d12e972a780cd57" + "code": " ),\n );\n };\n\n const recordSetupScriptStarted = (input: {\n readonly requestedAt: string;\n readonly worktreePath: string;\n readonly scriptId: string;\n readonly scriptName: string;\n readonly terminalId: string;\n }) =>\n Effect.gen(function* () {\n const startedAt = yield* nowIso;\n const payload = {\n scriptId: input.scriptId,\n scriptName: input.scriptName,\n terminalId: input.terminalId,\n worktreePath: input.worktreePath,\n };\n yield* Effect.all([\n appendSetupScriptActivity({\n threadId: command.threadId,\n kind: \"setup-script.requested\",\n summary: \"Starting setup script\",\n createdAt: input.requestedAt,\n payload,\n tone: \"info\",\n }),\n appendSetupScriptActivity({", + "checksum": "00a9cb175a0cfc289b50c630b99a2a8939ebdeefce0ad81300669bf0c29047bc" }, "turn-lifecycle-start-decider": { "id": "turn-lifecycle-start-decider", @@ -886,8 +886,8 @@ "end": 1036, "language": "typescript", "label": "Turn start intent and atomic lifecycle-reset event batch", - "code": " case \"thread.turn.start\": {\n const targetThread = yield* requireThread({\n readModel,\n command,\n threadId: command.threadId,\n });\n const sourceProposedPlan = command.sourceProposedPlan;\n const sourceThread = sourceProposedPlan\n ? yield* requireThread({\n readModel,\n command,\n threadId: sourceProposedPlan.threadId,\n })\n : null;\n const sourcePlan =\n sourceProposedPlan && sourceThread\n ? sourceThread.proposedPlans.find((entry) => entry.id === sourceProposedPlan.planId)\n : null;\n if (sourceProposedPlan && !sourcePlan) {\n return yield* new OrchestrationCommandInvariantError({\n commandType: command.type,\n detail: `Proposed plan '${sourceProposedPlan.planId}' does not exist on thread '${sourceProposedPlan.threadId}'.`,\n });\n }\n if (sourceThread && sourceThread.projectId !== targetThread.projectId) {\n return yield* new OrchestrationCommandInvariantError({\n commandType: command.type,\n detail: `Proposed plan '${sourceProposedPlan?.planId}' belongs to thread '${sourceThread.id}' in a different project.`,\n });\n }\n const userMessageEvent: Omit = {\n ...(yield* withEventBase({\n aggregateKind: \"thread\",\n aggregateId: command.threadId,\n occurredAt: command.createdAt,\n commandId: command.commandId,\n })),\n type: \"thread.message-sent\",\n payload: {\n threadId: command.threadId,\n messageId: command.message.messageId,\n role: \"user\",\n text: command.message.text,\n attachments: command.message.attachments,\n turnId: null,\n streaming: false,\n createdAt: command.createdAt,\n updatedAt: command.createdAt,\n },\n };\n const turnStartRequestedEvent: Omit = {\n ...(yield* withEventBase({\n aggregateKind: \"thread\",\n aggregateId: command.threadId,\n occurredAt: command.createdAt,\n commandId: command.commandId,\n })),\n causationEventId: userMessageEvent.eventId,\n type: \"thread.turn-start-requested\",\n payload: {\n threadId: command.threadId,\n messageId: command.message.messageId,\n ...(command.modelSelection !== undefined\n ? { modelSelection: command.modelSelection }\n : {}),\n ...(command.titleSeed !== undefined ? { titleSeed: command.titleSeed } : {}),\n runtimeMode: targetThread.runtimeMode,\n interactionMode: targetThread.interactionMode,\n ...(sourceProposedPlan !== undefined ? { sourceProposedPlan } : {}),\n createdAt: command.createdAt,\n },\n };\n // Real activity resets ANY override: it wakes an explicitly settled\n // thread, and it clears a keep-active pin back to neutral so the\n // thread can auto-settle again after this burst of work goes stale.\n // A snooze clears the same way — sending a message to a snoozed\n // thread is the user re-engaging, so the return ticket is spent.\n const lifecycleResetEvents: Array> = [];\n if (targetThread.settledOverride !== null) {\n lifecycleResetEvents.push({\n ...(yield* withEventBase({\n aggregateKind: \"thread\",\n aggregateId: command.threadId,\n occurredAt: command.createdAt,\n commandId: command.commandId,\n })),\n type: \"thread.unsettled\",\n payload: {\n threadId: command.threadId,\n reason: \"activity\",\n updatedAt: command.createdAt,\n },\n });\n }\n if (targetThread.snoozedUntil != null) {\n lifecycleResetEvents.push({\n ...(yield* withEventBase({\n aggregateKind: \"thread\",\n aggregateId: command.threadId,\n occurredAt: command.createdAt,\n commandId: command.commandId,\n })),\n type: \"thread.unsnoozed\",\n payload: {\n threadId: command.threadId,\n reason: \"activity\",\n updatedAt: command.createdAt,\n },\n });\n }\n return [...lifecycleResetEvents, userMessageEvent, turnStartRequestedEvent];", - "checksum": "0d0fe115f9ae7b07e903de3cca6dfd815044b54aa8a56b75417acb4a17410c4d" + "code": " {\n type: \"thread.pull-request.unlink\" as const,\n commandId: command.commandId,\n threadId: command.threadId,\n host: currentPullRequest.host,\n repository: currentPullRequest.repository,\n number: currentPullRequest.number,\n },\n ]\n : []),\n {\n type: \"thread.pull-request.link\",\n commandId: command.commandId,\n threadId: command.threadId,\n ...legacyThreadPullRequestKey(linked, host),\n url: linked.url,\n source: \"manual\",\n },\n ],\n });\n }\n\n if (command.linkedPullRequest === null && currentPullRequest !== null) {\n const { linkedPullRequest: _linkedPullRequest, ...metadata } = command;\n const hasMetadata = Object.entries(metadata).some(\n ([key, value]) => ![\"type\", \"commandId\", \"threadId\"].includes(key) && value !== undefined,\n );\n return yield* decideCommandSequence({\n readModel,\n commands: [\n ...(hasMetadata ? [metadata] : []),\n {\n type: \"thread.pull-request.unlink\",\n commandId: command.commandId,\n threadId: command.threadId,\n host: currentPullRequest.host,\n repository: currentPullRequest.repository,\n number: currentPullRequest.number,\n },\n ],\n });\n }\n const branch =\n command.branch !== undefined &&\n command.expectedBranch !== undefined &&\n thread.branch !== command.expectedBranch\n ? thread.branch\n : command.branch;\n const occurredAt = yield* nowIso;\n return {\n ...(yield* withEventBase({\n aggregateKind: \"thread\",\n aggregateId: command.threadId,\n occurredAt,\n commandId: command.commandId,\n })),\n type: \"thread.meta-updated\",\n payload: {\n threadId: command.threadId,\n ...(command.title !== undefined ? { title: command.title } : {}),\n ...(command.regenerateTitle === true\n ? {\n regenerateTitle: true as const,\n previousTitle: thread.title,\n titleRegeneration: {\n requestId: command.commandId,\n startedAt: occurredAt,\n },\n }\n : {}),\n ...(command.title !== undefined && thread.titleRegeneration != null\n ? { titleRegeneration: null }\n : {}),\n ...(command.modelSelection !== undefined\n ? { modelSelection: command.modelSelection }\n : {}),\n ...(branch !== undefined ? { branch } : {}),\n ...(command.worktreePath !== undefined ? { worktreePath: command.worktreePath } : {}),\n ...(command.linkedPullRequest !== undefined\n ? { linkedPullRequest: command.linkedPullRequest }\n : {}),\n updatedAt: occurredAt,\n },\n };\n }\n\n case \"thread.pull-request.link\": {\n const thread = yield* requireThread({\n readModel,\n command,\n threadId: command.threadId,\n });\n const key = normalizeThreadPullRequestKey(command);\n const existing = findPullRequestLink(thread, key);\n // An explicit link on a dismissed stack member un-dismisses it; any\n // other duplicate is a no-op the engine would reject as zero-event.\n const undismisses =\n existing?.source === \"stack-dismissed\" &&\n (command.source === \"manual\" || command.source === \"agent\" || command.source === \"created\");\n if (existing !== undefined && !undismisses) {\n return yield* new OrchestrationCommandInvariantError({\n commandType: command.type,\n detail: `pull request ${key.host}/${key.repository}#${key.number} is already linked to thread ${command.threadId}`,\n });\n }\n const occurredAt = yield* nowIso;\n return {\n ...(yield* withEventBase({\n aggregateKind: \"thread\",\n aggregateId: command.threadId,\n occurredAt,", + "checksum": "83c68709ef8681a8fc3fe134c1244b017637541825ba2e680895427723606e73" }, "turn-lifecycle-command-reactor": { "id": "turn-lifecycle-command-reactor", @@ -896,8 +896,8 @@ "end": 1174, "language": "typescript", "label": "Committed turn intent forwarded to the provider", - "code": " const processTurnStartRequested = Effect.fn(\"processTurnStartRequested\")(function* (\n event: Extract,\n ) {\n const key = turnStartKeyForEvent(event);\n if (yield* hasHandledTurnStartRecently(key)) {\n return;\n }\n\n const thread = yield* resolveThread(event.payload.threadId);\n if (!thread) {\n return;\n }\n\n const message = thread.messages.find((entry) => entry.id === event.payload.messageId);\n if (!message || message.role !== \"user\") {\n yield* appendProviderFailureActivity({\n threadId: event.payload.threadId,\n kind: \"provider.turn.start.failed\",\n summary: \"Provider turn start failed\",\n detail: `User message '${event.payload.messageId}' was not found for turn start request.`,\n turnId: null,\n createdAt: event.payload.createdAt,\n });\n return;\n }\n\n const isFirstUserMessageTurn =\n thread.messages.filter((entry) => entry.role === \"user\").length === 1;\n if (isFirstUserMessageTurn) {\n const project = yield* resolveProject(thread.projectId);\n const generationCwd =\n resolveThreadWorkspaceCwd({\n thread,\n projects: project ? [project] : [],\n }) ?? process.cwd();\n const generationInput = {\n messageText: message.text,\n ...(message.attachments !== undefined ? { attachments: message.attachments } : {}),\n ...(event.payload.titleSeed !== undefined ? { titleSeed: event.payload.titleSeed } : {}),\n };\n\n yield* maybeGenerateAndRenameWorktreeBranchForFirstTurn({\n threadId: event.payload.threadId,\n branch: thread.branch,\n worktreePath: thread.worktreePath,\n ...generationInput,\n }).pipe(Effect.forkScoped);\n\n if (canReplaceThreadTitle(thread.title, event.payload.titleSeed)) {\n yield* maybeGenerateThreadTitleForFirstTurn({\n threadId: event.payload.threadId,\n cwd: generationCwd,\n ...generationInput,\n }).pipe(Effect.forkScoped);\n }\n }\n\n const handleTurnStartFailure = (cause: Cause.Cause) => {\n if (Cause.hasInterruptsOnly(cause)) {\n return Effect.void;\n }\n const detail = formatFailureDetail(cause);\n return setThreadSessionErrorOnTurnStartFailure({\n threadId: event.payload.threadId,\n detail,\n createdAt: event.payload.createdAt,\n }).pipe(\n Effect.flatMap(() =>\n appendProviderFailureActivity({\n threadId: event.payload.threadId,\n kind: \"provider.turn.start.failed\",\n summary: \"Provider turn start failed\",\n detail,\n turnId: null,\n createdAt: event.payload.createdAt,\n }),\n ),\n Effect.asVoid,\n );\n };\n\n const recoverTurnStartFailure = (cause: Cause.Cause) =>\n handleTurnStartFailure(cause).pipe(\n Effect.catchCause((recoveryCause) =>\n Effect.logWarning(\"provider command reactor failed to recover turn start failure\", {\n eventType: event.type,\n threadId: event.payload.threadId,\n cause: Cause.pretty(recoveryCause),\n originalCause: Cause.pretty(cause),\n }),\n ),\n );\n\n const sendTurnRequest = yield* buildSendTurnRequestForThread({\n threadId: event.payload.threadId,\n messageText: message.text,\n ...(message.attachments !== undefined ? { attachments: message.attachments } : {}),\n ...(event.payload.modelSelection !== undefined\n ? { modelSelection: event.payload.modelSelection }\n : {}),\n interactionMode: event.payload.interactionMode,\n createdAt: event.payload.createdAt,\n }).pipe(\n Effect.map(Option.some),\n Effect.catchCause((cause) => handleTurnStartFailure(cause).pipe(Effect.as(Option.none()))),\n );\n\n if (Option.isNone(sendTurnRequest)) {\n return;\n }\n\n yield* providerService\n .sendTurn(sendTurnRequest.value)\n .pipe(Effect.catchCause(recoverTurnStartFailure), Effect.forkScoped);\n });", - "checksum": "e612e2a0295ab2afbf9d9b7a9d4c7da0578c4adf5b052b24892d929ec9995f00" + "code": " return { _tag: \"Superseded\" } as const;\n }\n\n return { _tag: \"Completed\", title: generated.title } as const;\n });\n const dispatchThreadTitleRegenerationCompletion = Effect.fn(\n \"dispatchThreadTitleRegenerationCompletion\",\n )(function* (input: {\n readonly threadId: ThreadId;\n readonly requestId: CommandId;\n readonly title?: string;\n }) {\n yield* orchestrationEngine.dispatch({\n type: \"thread.title.regeneration.complete\",\n commandId: yield* serverCommandId(\"thread-title-regeneration-complete\"),\n threadId: input.threadId,\n requestId: input.requestId,\n ...(input.title !== undefined ? { title: input.title } : {}),\n });\n });\n const findInterruptedThreadTitleRegenerations = Effect.fn(\n \"findInterruptedThreadTitleRegenerations\",\n )(function* () {\n const readModel = yield* projectionSnapshotQuery.getCommandReadModel();\n return readModel.threads.flatMap((thread) => {\n const requestId = thread.titleRegeneration?.requestId;\n return requestId === undefined ? [] : [{ threadId: thread.id, requestId }];\n });\n });\n const clearInterruptedThreadTitleRegenerations = Effect.fn(\n \"clearInterruptedThreadTitleRegenerations\",\n )(function* (\n interrupted: ReadonlyArray<{ readonly threadId: ThreadId; readonly requestId: CommandId }>,\n ) {\n yield* Effect.forEach(\n interrupted,\n ({ threadId, requestId }) => {\n return dispatchThreadTitleRegenerationCompletion({\n threadId,\n requestId,\n }).pipe(\n Effect.catchCause((cause) => {\n if (Cause.hasInterruptsOnly(cause)) {\n return Effect.interrupt;\n }\n return Effect.logWarning(\n \"provider command reactor failed to clear interrupted title regeneration\",\n {\n threadId,\n cause: Cause.pretty(cause),\n },\n );\n }),\n );\n },\n { discard: true },\n );\n });\n const processThreadTitleRegenerationSafely = Effect.fn(\"processThreadTitleRegenerationSafely\")(\n function* (event: Extract) {\n if (event.payload.regenerateTitle !== true) {\n return;\n }\n\n const requestId = event.payload.titleRegeneration?.requestId ?? event.commandId;\n if (requestId === null) {\n return;\n }\n const result = yield* regenerateThreadTitle(event, requestId).pipe(\n Effect.catchCause((cause) => {\n if (Cause.hasInterruptsOnly(cause)) {\n return Effect.failCause(cause);\n }\n return Effect.logWarning(\"provider command reactor failed to regenerate thread title\", {\n threadId: event.payload.threadId,\n cause: Cause.pretty(cause),\n }).pipe(Effect.as({ _tag: \"Completed\", title: undefined } as const));\n }),\n );\n if (result._tag === \"Superseded\") {\n return;\n }\n\n const completion = {\n threadId: event.payload.threadId,\n requestId,\n ...(result.title !== undefined ? { title: result.title } : {}),\n };\n yield* dispatchThreadTitleRegenerationCompletion(completion).pipe(\n Effect.catchCause((cause) => {\n if (Cause.hasInterruptsOnly(cause)) {\n return Effect.failCause(cause);\n }\n return Effect.logWarning(\n \"provider command reactor retrying title regeneration completion\",\n {\n threadId: event.payload.threadId,\n cause: Cause.pretty(cause),\n },\n ).pipe(Effect.andThen(dispatchThreadTitleRegenerationCompletion(completion)));\n }),\n );\n },\n (effect, event) =>\n effect.pipe(\n Effect.catchCause((cause) => {\n if (Cause.hasInterruptsOnly(cause)) {\n return Effect.failCause(cause);\n }\n return Effect.logWarning(\n \"provider command reactor failed to complete title regeneration\",\n {\n threadId: event.payload.threadId,\n cause: Cause.pretty(cause),\n },", + "checksum": "e7b8d37aeaaad8d247211fe9e4d780501e06f11faa37d2b8559f0ccf789c4ab6" }, "turn-lifecycle-runtime-correlation": { "id": "turn-lifecycle-runtime-correlation", @@ -906,8 +906,8 @@ "end": 213, "language": "typescript", "label": "Provider runtime event identity validation", - "code": "const correlateRuntimeEventWithInstance = (\n source: {\n readonly instanceId: ProviderInstanceId;\n readonly provider: ProviderDriverKind;\n },\n event: ProviderRuntimeEvent,\n): ProviderRuntimeEvent => {\n if (event.provider !== source.provider) {\n throw new Error(\n `ProviderService.streamEvents: provider instance '${source.instanceId}' is backed by driver '${source.provider}' but emitted driver '${event.provider}'.`,\n );\n }\n if (event.providerInstanceId !== undefined && event.providerInstanceId !== source.instanceId) {\n throw new Error(\n `ProviderService.streamEvents: provider instance '${source.instanceId}' emitted event for instance '${event.providerInstanceId}'.`,\n );\n }\n return { ...event, providerInstanceId: source.instanceId };\n};", - "checksum": "43acc45594b4f0975e8a79e5f7a2dd9f0ce5233962b23cbf441038991a1f3082" + "code": " ) {\n return [];\n }\n return [compacted];\n}\n\nfunction accessibilityNodeHasBounds(node: SnapShotPromptAccessibilityNode): boolean {\n return Boolean(node.bounds || node.children?.some(accessibilityNodeHasBounds));\n}\n\nfunction compactAccessibilityForPrompt(\n accessibility: SnapShotAccessibility,\n): SnapShotPromptAccessibility {\n if (accessibility.format === \"flat-text\") {\n return {\n format: \"flat-text\",\n text: accessibility.text,\n ...(accessibility.truncated ? { truncated: true } : {}),\n };", + "checksum": "9ead33d2920212e084a505f67cf05a02ba3f92b41a07d70930f64a3591102ee5" }, "turn-lifecycle-runtime-guards": { "id": "turn-lifecycle-runtime-guards", @@ -916,8 +916,8 @@ "end": 1656, "language": "typescript", "label": "Runtime lifecycle correlation and durable session folding", - "code": " const now = event.createdAt;\n const eventTurnId = toTurnId(event.turnId);\n const activeTurnId = thread.session?.activeTurnId ?? null;\n const pendingTurnStart = yield* projectionTurnRepository.getPendingTurnStartByThreadId({\n threadId: thread.id,\n });\n const hasPendingTurnStart =\n Option.isSome(pendingTurnStart) && thread.session?.status === \"starting\";\n\n const conflictsWithActiveTurn =\n activeTurnId !== null && eventTurnId !== undefined && !sameId(activeTurnId, eventTurnId);\n const missingTurnForActiveTurn = activeTurnId !== null && eventTurnId === undefined;\n\n // A turn.started that conflicts with the active turn is legitimate when\n // the server itself has a turn start pending for this thread AND the\n // provider session already tracks the event's turn as its active turn:\n // steering a running turn makes some providers (e.g. opencode) open a\n // new turn without ever completing the superseded one. A stale\n // turn.started for some other turn id still gets rejected.\n const conflictingTurnStartIsPendingTurnStart =\n event.type === \"turn.started\" && conflictsWithActiveTurn\n ? sameId(yield* getExpectedProviderTurnIdForThread(thread.id), eventTurnId) &&\n Option.isSome(pendingTurnStart)\n : false;\n\n const shouldApplyThreadLifecycle = (() => {\n if (!STRICT_PROVIDER_LIFECYCLE_GUARD) {\n return true;\n }\n switch (event.type) {\n case \"session.exited\":\n return true;\n case \"session.started\":\n case \"thread.started\":\n return true;\n case \"turn.started\":\n return !conflictsWithActiveTurn || conflictingTurnStartIsPendingTurnStart;\n case \"turn.completed\":\n if (conflictsWithActiveTurn || missingTurnForActiveTurn) {\n return false;\n }\n // Only the active turn may close the lifecycle state.\n if (activeTurnId !== null && eventTurnId !== undefined) {\n return sameId(activeTurnId, eventTurnId);\n }\n // No active turn tracked: accept only completions that name their\n // turn (covers a real completion whose turn.started was lost). An\n // untargeted completion cannot prove it belongs to any turn this\n // thread ran — the known emitter was the Claude resume handshake\n // (system/init + result(num_turns: 0)), which is not a turn at\n // all — and applying it here stomps the \"starting\" lifecycle\n // state while a turn start is pending.\n return eventTurnId !== undefined;\n default:\n return true;\n }\n })();\n const acceptedTurnStartedSourcePlan =\n event.type === \"turn.started\" && shouldApplyThreadLifecycle\n ? yield* getSourceProposedPlanReferenceForAcceptedTurnStart(thread.id, eventTurnId)\n : null;\n\n if (\n event.type === \"session.started\" ||\n event.type === \"session.state.changed\" ||\n event.type === \"session.exited\" ||\n event.type === \"thread.started\" ||\n event.type === \"turn.started\" ||\n event.type === \"turn.completed\"\n ) {\n const status = (() => {\n switch (event.type) {\n case \"session.state.changed\": {\n const runtimeStatus = orchestrationSessionStatusFromRuntimeState(event.payload.state);\n return hasPendingTurnStart && runtimeStatus === \"ready\" ? \"starting\" : runtimeStatus;\n }\n case \"turn.started\":\n return \"running\";\n case \"session.exited\":\n return \"stopped\";\n case \"turn.completed\":\n return normalizeRuntimeTurnState(event.payload.state) === \"failed\"\n ? \"error\"\n : \"ready\";\n case \"session.started\":\n case \"thread.started\":\n // Provider thread/session start notifications can arrive during an\n // active or pending turn; preserve that lifecycle state.\n return activeTurnId !== null ? \"running\" : hasPendingTurnStart ? \"starting\" : \"ready\";\n }\n })();\n const nextActiveTurnId =\n event.type === \"turn.started\"\n ? (eventTurnId ?? null)\n : event.type === \"turn.completed\" || event.type === \"session.exited\"\n ? null\n : event.type === \"session.state.changed\" &&\n !sessionStatusAllowsActiveTurn(\n orchestrationSessionStatusFromRuntimeState(event.payload.state),\n )\n ? null\n : activeTurnId;\n const lastError =\n event.type === \"session.state.changed\" && event.payload.state === \"error\"\n ? (event.payload.reason ?? thread.session?.lastError ?? \"Provider session error\")\n : event.type === \"turn.completed\" &&\n normalizeRuntimeTurnState(event.payload.state) === \"failed\"\n ? (event.payload.errorMessage ?? thread.session?.lastError ?? \"Turn failed\")\n : status === \"ready\"\n ? null\n : (thread.session?.lastError ?? null);\n\n if (shouldApplyThreadLifecycle) {\n if (event.type === \"turn.started\" && acceptedTurnStartedSourcePlan !== null) {\n yield* markSourceProposedPlanImplemented(\n acceptedTurnStartedSourcePlan.sourceThreadId,\n acceptedTurnStartedSourcePlan.sourcePlanId,\n thread.id,\n now,\n ).pipe(\n Effect.catchCause((cause) =>\n Effect.logWarning(\n \"provider runtime ingestion failed to mark source proposed plan\",\n {\n eventId: event.eventId,\n eventType: event.type,\n cause: Cause.pretty(cause),\n },\n ),\n ),\n );\n }\n\n yield* orchestrationEngine.dispatch({\n type: \"thread.session.set\",\n commandId: yield* providerCommandId(event, \"thread-session-set\"),\n threadId: thread.id,\n session: {\n threadId: thread.id,\n status,\n providerName: event.provider,\n ...(event.providerInstanceId !== undefined\n ? { providerInstanceId: event.providerInstanceId }\n : {}),\n runtimeMode: thread.session?.runtimeMode ?? \"full-access\",\n activeTurnId: nextActiveTurnId,\n lastError,\n updatedAt: now,\n },\n createdAt: now,\n });\n }", - "checksum": "c7bc77bba03bd67251974ffa4e634dad8af26720466ddde046f4be45c4110947" + "code": " Option.isSome(pendingTurnStart) && thread.session?.status === \"starting\";\n\n const conflictsWithActiveTurn =\n activeTurnId !== null && eventTurnId !== undefined && !sameId(activeTurnId, eventTurnId);\n const missingTurnForActiveTurn = activeTurnId !== null && eventTurnId === undefined;\n\n // A turn.started that conflicts with the active turn is legitimate when\n // the server itself has a turn start pending for this thread AND the\n // provider session already tracks the event's turn as its active turn:\n // steering a running turn makes some providers (e.g. opencode) open a\n // new turn without ever completing the superseded one. A stale\n // turn.started for some other turn id still gets rejected.\n const conflictingTurnStartIsPendingTurnStart =\n event.type === \"turn.started\" && conflictsWithActiveTurn\n ? sameId(yield* getExpectedProviderTurnIdForThread(thread.id), eventTurnId) &&\n Option.isSome(pendingTurnStart)\n : false;\n\n const shouldApplyThreadLifecycle = (() => {\n if (!STRICT_PROVIDER_LIFECYCLE_GUARD) {\n return true;\n }\n switch (event.type) {\n case \"session.exited\":\n return true;\n case \"session.started\":\n case \"thread.started\":\n return true;\n case \"turn.started\":\n return !conflictsWithActiveTurn || conflictingTurnStartIsPendingTurnStart;\n case \"turn.completed\":\n case \"turn.aborted\":\n if (conflictsWithActiveTurn || missingTurnForActiveTurn) {\n return false;\n }\n // Only the active turn may close the lifecycle state.\n if (activeTurnId !== null && eventTurnId !== undefined) {\n return sameId(activeTurnId, eventTurnId);\n }\n // A named completion can recover a lost turn.started event.\n // An abort needs an active turn so a delayed stop cannot replace\n // a ready session or clear a newer pending start.\n return event.type === \"turn.completed\" && eventTurnId !== undefined;\n default:\n return true;\n }\n })();\n const acceptedTurnStartedSourcePlan =\n event.type === \"turn.started\" && shouldApplyThreadLifecycle\n ? yield* getSourceProposedPlanReferenceForAcceptedTurnStart(thread.id, eventTurnId)\n : null;\n\n if (\n event.type === \"session.started\" ||\n event.type === \"session.state.changed\" ||\n event.type === \"session.exited\" ||\n event.type === \"thread.started\" ||\n event.type === \"turn.started\" ||\n isTerminalTurn\n ) {\n const status = (() => {\n switch (event.type) {\n case \"session.state.changed\": {\n const runtimeStatus = orchestrationSessionStatusFromRuntimeState(event.payload.state);\n return hasPendingTurnStart && runtimeStatus === \"ready\" ? \"starting\" : runtimeStatus;\n }\n case \"turn.started\":\n return \"running\";\n case \"session.exited\":\n return \"stopped\";\n case \"turn.aborted\":\n return \"interrupted\";\n case \"turn.completed\":\n return normalizeRuntimeTurnState(event.payload.state) === \"failed\"\n ? \"error\"\n : \"ready\";\n case \"session.started\":\n case \"thread.started\":\n // Provider thread/session start notifications can arrive during an\n // active or pending turn; preserve that lifecycle state.\n return activeTurnId !== null ? \"running\" : hasPendingTurnStart ? \"starting\" : \"ready\";\n }\n })();\n const nextActiveTurnId =\n event.type === \"turn.started\"\n ? (eventTurnId ?? null)\n : isTerminalTurn || event.type === \"session.exited\"\n ? null\n : event.type === \"session.state.changed\" &&\n !sessionStatusAllowsActiveTurn(\n orchestrationSessionStatusFromRuntimeState(event.payload.state),\n )\n ? null\n : activeTurnId;\n const lastError =\n event.type === \"session.state.changed\" && event.payload.state === \"error\"\n ? (event.payload.reason ?? thread.session?.lastError ?? \"Provider session error\")\n : event.type === \"turn.completed\" &&\n normalizeRuntimeTurnState(event.payload.state) === \"failed\"\n ? (event.payload.errorMessage ?? thread.session?.lastError ?? \"Turn failed\")\n : status === \"ready\" || status === \"interrupted\"\n ? null\n : (thread.session?.lastError ?? null);\n\n if (shouldApplyThreadLifecycle) {\n if (event.type === \"turn.started\" && acceptedTurnStartedSourcePlan !== null) {\n yield* markSourceProposedPlanImplemented(\n acceptedTurnStartedSourcePlan.sourceThreadId,\n acceptedTurnStartedSourcePlan.sourcePlanId,\n thread.id,\n now,\n ).pipe(\n Effect.catchCause((cause) =>\n Effect.logWarning(\n \"provider runtime ingestion failed to mark source proposed plan\",\n {\n eventId: event.eventId,\n eventType: event.type,\n cause: Cause.pretty(cause),\n },\n ),\n ),\n );\n }\n\n yield* orchestrationEngine.dispatch({\n type: \"thread.session.set\",\n commandId: yield* providerCommandId(event, \"thread-session-set\"),\n threadId: thread.id,\n session: {\n threadId: thread.id,\n status,\n providerName: event.provider,\n ...(event.providerInstanceId !== undefined\n ? { providerInstanceId: event.providerInstanceId }\n : {}),\n runtimeMode: thread.session?.runtimeMode ?? \"full-access\",\n activeTurnId: nextActiveTurnId,\n lastError,\n updatedAt: now,\n },\n createdAt: now,\n });\n }\n }\n\n const assistantDelta =\n event.type === \"content.delta\" && event.payload.streamKind === \"assistant_text\"\n ? event.payload.delta\n : undefined;\n const proposedPlanDelta =\n event.type === \"turn.proposed.delta\" ? event.payload.delta : undefined;", + "checksum": "4fde66056b2d04d3ffdce5654ad64b71de580363632007fffebf7f8e4f16da41" }, "turn-lifecycle-buffering": { "id": "turn-lifecycle-buffering", @@ -926,8 +926,8 @@ "end": 1750, "language": "typescript", "label": "Buffered assistant delivery and pause-boundary flush", - "code": " const assistantDelta =\n event.type === \"content.delta\" && event.payload.streamKind === \"assistant_text\"\n ? event.payload.delta\n : undefined;\n const proposedPlanDelta =\n event.type === \"turn.proposed.delta\" ? event.payload.delta : undefined;\n\n if (assistantDelta && assistantDelta.length > 0) {\n const turnId = toTurnId(event.turnId);\n const assistantMessageId = yield* getOrCreateAssistantMessageId({\n threadId: thread.id,\n event,\n ...(turnId ? { turnId } : {}),\n });\n if (turnId) {\n yield* rememberAssistantMessageId(thread.id, turnId, assistantMessageId);\n }\n\n const assistantDeliveryMode: AssistantDeliveryMode = yield* Effect.map(\n serverSettingsService.getSettings,\n (settings) => (settings.enableLegacyTokenStreaming ? \"streaming\" : \"buffered\"),\n );\n if (assistantDeliveryMode === \"buffered\") {\n const spillChunk = yield* appendBufferedAssistantText(assistantMessageId, assistantDelta);\n if (spillChunk.length > 0) {\n yield* orchestrationEngine.dispatch({\n type: \"thread.message.assistant.delta\",\n commandId: yield* providerCommandId(event, \"assistant-delta-buffer-spill\"),\n threadId: thread.id,\n messageId: assistantMessageId,\n delta: spillChunk,\n ...(turnId ? { turnId } : {}),\n createdAt: now,\n });\n }\n } else {\n yield* orchestrationEngine.dispatch({\n type: \"thread.message.assistant.delta\",\n commandId: yield* providerCommandId(event, \"assistant-delta\"),\n threadId: thread.id,\n messageId: assistantMessageId,\n delta: assistantDelta,\n ...(turnId ? { turnId } : {}),\n createdAt: now,\n });\n }\n }\n\n const pauseForUserTurnId =\n event.type === \"request.opened\" || event.type === \"user-input.requested\"\n ? toTurnId(event.turnId)\n : undefined;\n if (pauseForUserTurnId) {\n const detailedThread = yield* getLoadedThreadDetail();\n const assistantDeliveryMode: AssistantDeliveryMode = yield* Effect.map(\n serverSettingsService.getSettings,\n (settings) => (settings.enableLegacyTokenStreaming ? \"streaming\" : \"buffered\"),\n );\n const flushedMessageIds =\n assistantDeliveryMode === \"buffered\"\n ? yield* flushBufferedAssistantMessagesForTurn({\n event,\n threadId: thread.id,\n turnId: pauseForUserTurnId,\n createdAt: now,\n commandTag:\n event.type === \"request.opened\"\n ? \"assistant-delta-flush-on-request-opened\"\n : \"assistant-delta-flush-on-user-input-requested\",\n })\n : new Set();\n yield* finalizeActiveAssistantSegmentForTurn({\n event,\n threadId: thread.id,\n turnId: pauseForUserTurnId,\n createdAt: now,\n commandTag:\n event.type === \"request.opened\"\n ? \"assistant-complete-on-request-opened\"\n : \"assistant-complete-on-user-input-requested\",\n finalDeltaCommandTag:\n event.type === \"request.opened\"\n ? \"assistant-delta-finalize-on-request-opened\"\n : \"assistant-delta-finalize-on-user-input-requested\",\n hasProjectedMessage:\n detailedThread !== null &&\n hasAssistantMessageForTurn(detailedThread.messages, pauseForUserTurnId, {\n streamingOnly: true,\n }),\n flushedMessageIds,\n });\n }", - "checksum": "e91fb7e11d5c75dc5a834b75c111d8805a0342d7807a62d1977cd0542a51508c" + "code": " const turnId = toTurnId(event.turnId);\n const assistantMessageId = yield* getOrCreateAssistantMessageId({\n threadId: thread.id,\n event,\n ...(turnId ? { turnId } : {}),\n });\n if (turnId) {\n yield* rememberAssistantMessageId(thread.id, turnId, assistantMessageId);\n }\n\n const assistantDeliveryMode: AssistantDeliveryMode = yield* Effect.map(\n serverSettingsService.getSettings,\n (settings) => (settings.enableLegacyTokenStreaming ? \"streaming\" : \"buffered\"),\n );\n if (assistantDeliveryMode === \"buffered\") {\n const spillChunk = yield* appendBufferedAssistantText(assistantMessageId, assistantDelta);\n if (spillChunk.length > 0) {\n yield* orchestrationEngine.dispatch({\n type: \"thread.message.assistant.delta\",\n commandId: yield* providerCommandId(event, \"assistant-delta-buffer-spill\"),\n threadId: thread.id,\n messageId: assistantMessageId,\n delta: spillChunk,\n ...(turnId ? { turnId } : {}),\n createdAt: now,\n });\n }\n } else {\n yield* orchestrationEngine.dispatch({\n type: \"thread.message.assistant.delta\",\n commandId: yield* providerCommandId(event, \"assistant-delta\"),\n threadId: thread.id,\n messageId: assistantMessageId,\n delta: assistantDelta,\n ...(turnId ? { turnId } : {}),\n createdAt: now,\n });\n }\n }\n\n const pauseForUserTurnId =\n event.type === \"request.opened\" ||\n (event.type === \"user-input.requested\" && event.payload.responseMode !== \"message\")\n ? toTurnId(event.turnId)\n : undefined;\n if (pauseForUserTurnId) {\n const hasProjectedMessage = yield* projectionThreadMessages.hasAssistantMessageForTurn({\n threadId: thread.id,\n turnId: pauseForUserTurnId,\n streamingOnly: true,\n });\n const assistantDeliveryMode: AssistantDeliveryMode = yield* Effect.map(\n serverSettingsService.getSettings,\n (settings) => (settings.enableLegacyTokenStreaming ? \"streaming\" : \"buffered\"),\n );\n const flushedMessageIds =\n assistantDeliveryMode === \"buffered\"\n ? yield* flushBufferedAssistantMessagesForTurn({\n event,\n threadId: thread.id,\n turnId: pauseForUserTurnId,\n createdAt: now,\n commandTag:\n event.type === \"request.opened\"\n ? \"assistant-delta-flush-on-request-opened\"\n : \"assistant-delta-flush-on-user-input-requested\",\n })\n : new Set();\n yield* finalizeActiveAssistantSegmentForTurn({\n event,\n threadId: thread.id,\n turnId: pauseForUserTurnId,\n createdAt: now,\n commandTag:\n event.type === \"request.opened\"\n ? \"assistant-complete-on-request-opened\"\n : \"assistant-complete-on-user-input-requested\",\n finalDeltaCommandTag:\n event.type === \"request.opened\"\n ? \"assistant-delta-finalize-on-request-opened\"\n : \"assistant-delta-finalize-on-user-input-requested\",\n hasProjectedMessage,\n flushedMessageIds,\n });\n }\n\n if (proposedPlanDelta && proposedPlanDelta.length > 0) {\n const planId = proposedPlanIdFromEvent(event, thread.id);\n yield* appendBufferedProposedPlan(planId, proposedPlanDelta, now);\n }\n\n const assistantCompletion =", + "checksum": "69c9e3d7671d8a571330991c136585888ae4d14b28559911e83a997563f583c0" }, "turn-lifecycle-interrupt": { "id": "turn-lifecycle-interrupt", @@ -936,8 +936,8 @@ "end": 1058, "language": "typescript", "label": "Durable turn interrupt intent", - "code": " case \"thread.turn.interrupt\": {\n yield* requireThread({\n readModel,\n command,\n threadId: command.threadId,\n });\n return {\n ...(yield* withEventBase({\n aggregateKind: \"thread\",\n aggregateId: command.threadId,\n occurredAt: command.createdAt,\n commandId: command.commandId,\n })),\n type: \"thread.turn-interrupt-requested\",\n payload: {\n threadId: command.threadId,\n ...(command.turnId !== undefined ? { turnId: command.turnId } : {}),\n createdAt: command.createdAt,\n },\n };", - "checksum": "78ab7a9c85d8c101f2136c38a88c3cef91b00adf35fb3731e56f8537cced4278" + "code": " type: \"thread.pull-request-linked\",\n payload: {\n threadId: command.threadId,\n link:\n existing !== undefined\n ? { ...existing, url: command.url, source: command.source }\n : {\n ...key,\n url: command.url,\n source: command.source,\n linkedAt: occurredAt,\n snapshot: null,\n stack: null,\n },\n updatedAt: occurredAt,\n },\n };\n }\n\n case \"thread.pull-request.unlink\": {", + "checksum": "67c8a5ebc15928ef1797e0cda34248ca7ac2daf96c7980b4d8fc5dc0fcede4cb" }, "turn-lifecycle-checkpoint-completion": { "id": "turn-lifecycle-checkpoint-completion", @@ -946,8 +946,8 @@ "end": 458, "language": "typescript", "label": "Post-turn checkpoint capture and placeholder replacement", - "code": " function* (event: Extract) {\n const turnId = toTurnId(event.turnId);\n if (!turnId) {\n return;\n }\n\n const thread = yield* resolveThreadDetail(event.threadId);\n if (!thread) {\n return;\n }\n\n // When a primary turn is active, only that turn may produce completion checkpoints.\n if (thread.session?.activeTurnId && !sameId(thread.session.activeTurnId, turnId)) {\n return;\n }\n\n // Only skip if a real (non-placeholder) checkpoint already exists for this turn.\n // ProviderRuntimeIngestion may insert placeholder entries with status \"missing\"\n // before this reactor runs; those must not prevent real git capture.\n if (\n thread.checkpoints.some(\n (checkpoint) => checkpoint.turnId === turnId && checkpoint.status !== \"missing\",\n )\n ) {\n return;\n }\n\n const projects = yield* resolveThreadProjects(thread.projectId);\n const checkpointCwd = yield* resolveCheckpointCwd({\n threadId: thread.id,\n thread,\n projects,\n preferSessionRuntime: true,\n });\n if (!checkpointCwd) {\n return;\n }\n\n // If a placeholder checkpoint exists for this turn, reuse its turn count\n // instead of incrementing past it.\n const existingPlaceholder = thread.checkpoints.find(\n (checkpoint) => checkpoint.turnId === turnId && checkpoint.status === \"missing\",\n );\n const currentTurnCount = thread.checkpoints.reduce(\n (maxTurnCount, checkpoint) => Math.max(maxTurnCount, checkpoint.checkpointTurnCount),\n 0,\n );\n const nextTurnCount = existingPlaceholder\n ? existingPlaceholder.checkpointTurnCount\n : currentTurnCount + 1;\n\n yield* captureAndDispatchCheckpoint({\n threadId: thread.id,\n turnId,\n thread,\n cwd: checkpointCwd,\n turnCount: nextTurnCount,\n status: checkpointStatusFromRuntime(event.payload.state),\n assistantMessageId: undefined,\n createdAt: event.createdAt,\n });\n },\n );\n\n // Captures a real git checkpoint when a placeholder checkpoint (status \"missing\")\n // is detected via a domain event. This replaces the placeholder with a real\n // git-ref-based checkpoint.\n //\n // ProviderRuntimeIngestion creates placeholder checkpoints on turn.diff.updated\n // events from the Codex runtime. This handler fires when the corresponding\n // domain event arrives, allowing the reactor to capture the actual filesystem\n // state into a git ref and dispatch a replacement checkpoint.\n const captureCheckpointFromPlaceholder = Effect.fn(\"captureCheckpointFromPlaceholder\")(function* (\n event: Extract,\n ) {\n const { threadId, turnId, checkpointTurnCount, status } = event.payload;\n\n // Only replace placeholders; skip events from our own real captures.\n if (status !== \"missing\") {\n return;\n }\n\n const thread = yield* resolveThreadDetail(threadId);\n if (!thread) {\n yield* Effect.logWarning(\"checkpoint capture from placeholder skipped: thread not found\", {\n threadId,\n });\n return;\n }\n\n // If a real checkpoint already exists for this turn, skip.\n if (\n thread.checkpoints.some(\n (checkpoint) => checkpoint.turnId === turnId && checkpoint.status !== \"missing\",\n )\n ) {\n yield* Effect.logDebug(\n \"checkpoint capture from placeholder skipped: real checkpoint already exists\",\n { threadId, turnId },\n );\n return;\n }\n", - "checksum": "e341d79b8fcb4969bed1f9f6a7ad0b5d032de516eabaa59661d2f8ce698bd3b7" + "code": " });\n });\n\n // Capture the files left by a completed or interrupted turn.\n const captureCheckpointFromTurnCompletion = Effect.fn(\"captureCheckpointFromTurnCompletion\")(\n function* (event: Extract) {\n const turnId = toTurnId(event.turnId);\n if (!turnId) {\n return;\n }\n\n const thread = yield* resolveThreadDetail(event.threadId);\n if (!thread) {\n return;\n }\n\n // When a primary turn is active, only that turn may produce completion checkpoints.\n if (thread.session?.activeTurnId && !sameId(thread.session.activeTurnId, turnId)) {\n return;\n }\n\n // Only skip if a real (non-placeholder) checkpoint already exists for this turn.\n // ProviderRuntimeIngestion may insert placeholder entries with status \"missing\"\n // before this reactor runs; those must not prevent real git capture.\n if (\n thread.checkpoints.some(\n (checkpoint) => checkpoint.turnId === turnId && checkpoint.status !== \"missing\",\n )\n ) {\n return;\n }\n\n const projects = yield* resolveThreadProjects(thread.projectId);\n const checkpointCwd = yield* resolveCheckpointCwd({\n threadId: thread.id,\n thread,\n projects,\n preferSessionRuntime: true,\n });\n if (!checkpointCwd) {\n return;\n }\n\n // If a placeholder checkpoint exists for this turn, reuse its turn count\n // instead of incrementing past it.\n const existingPlaceholder = thread.checkpoints.find(\n (checkpoint) => checkpoint.turnId === turnId && checkpoint.status === \"missing\",\n );\n const currentTurnCount = thread.checkpoints.reduce(\n (maxTurnCount, checkpoint) => Math.max(maxTurnCount, checkpoint.checkpointTurnCount),\n 0,\n );\n const nextTurnCount = existingPlaceholder\n ? existingPlaceholder.checkpointTurnCount\n : currentTurnCount + 1;\n\n yield* captureAndDispatchCheckpoint({\n threadId: thread.id,\n turnId,\n thread,\n cwd: checkpointCwd,\n turnCount: nextTurnCount,\n status:\n event.type === \"turn.aborted\"\n ? \"ready\"\n : checkpointStatusFromRuntime(event.payload.state),\n assistantMessageId: existingPlaceholder?.assistantMessageId ?? undefined,\n createdAt: event.createdAt,\n });\n },\n );\n\n const ensurePreTurnBaselineFromTurnStart = Effect.fn(\"ensurePreTurnBaselineFromTurnStart\")(\n function* (event: Extract) {\n const turnId = toTurnId(event.turnId);\n if (!turnId) {\n return;\n }\n\n const thread = yield* resolveThreadDetail(event.threadId);\n if (!thread) {\n return;\n }\n\n const projects = yield* resolveThreadProjects(thread.projectId);\n const checkpointCwd = yield* resolveCheckpointCwd({\n threadId: thread.id,\n thread,\n projects,\n preferSessionRuntime: false,\n });\n if (!checkpointCwd) {\n return;\n }\n\n const currentTurnCount = thread.checkpoints.reduce(\n (maxTurnCount, checkpoint) => Math.max(maxTurnCount, checkpoint.checkpointTurnCount),\n 0,\n );\n const baselineCheckpointRef = checkpointRefForThreadTurn(thread.id, currentTurnCount);\n const baselineExists = yield* checkpointStore.hasCheckpointRef({\n cwd: checkpointCwd,\n checkpointRef: baselineCheckpointRef,", + "checksum": "d4730990fa62d77018aaeb518b3fc6b8e69a0a2300305549dd17b4f932e1841f" }, "permission-runtime-mode-contract": { "id": "permission-runtime-mode-contract", @@ -956,8 +956,8 @@ "end": 143, "language": "typescript", "label": "Canonical runtime modes and approval decisions", - "code": "export const RuntimeMode = Schema.Literals([\n \"approval-required\",\n \"auto-accept-edits\",\n \"auto\",\n \"full-access\",\n]);\nexport type RuntimeMode = typeof RuntimeMode.Type;\nexport const DEFAULT_RUNTIME_MODE: RuntimeMode = \"full-access\";\nexport const ProviderInteractionMode = Schema.Literals([\"default\", \"plan\"]);\nexport type ProviderInteractionMode = typeof ProviderInteractionMode.Type;\nexport const DEFAULT_PROVIDER_INTERACTION_MODE: ProviderInteractionMode = \"default\";\nexport const ProviderRequestKind = Schema.Literals([\"command\", \"file-read\", \"file-change\"]);\nexport type ProviderRequestKind = typeof ProviderRequestKind.Type;\nexport const AssistantDeliveryMode = Schema.Literals([\"buffered\", \"streaming\"]);\nexport type AssistantDeliveryMode = typeof AssistantDeliveryMode.Type;\nexport const ProviderApprovalDecision = Schema.Literals([\n \"accept\",\n \"acceptForSession\",\n \"decline\",\n \"cancel\",\n]);\nexport type ProviderApprovalDecision = typeof ProviderApprovalDecision.Type;\nexport const ProviderUserInputAnswers = Schema.Record(Schema.String, Schema.Unknown);\nexport type ProviderUserInputAnswers = typeof ProviderUserInputAnswers.Type;", - "checksum": "2ec2b0f6d38c3c832ef4494cbe97a899fe4f4b83fe808ef16a1356580e935479" + "code": " return Effect.succeed(base as typeof ModelSelectionSource.Encoded);\n },\n }),\n ),\n);\nexport type ModelSelection = typeof ModelSelection.Type;\n\nexport const RuntimeMode = Schema.Literals([\n \"approval-required\",\n \"auto-accept-edits\",\n \"auto\",\n \"full-access\",\n]);\nexport type RuntimeMode = typeof RuntimeMode.Type;\nexport const DEFAULT_RUNTIME_MODE: RuntimeMode = \"full-access\";\nexport const ProviderInteractionMode = Schema.Literals([\"default\", \"plan\"]);\nexport type ProviderInteractionMode = typeof ProviderInteractionMode.Type;\nexport const DEFAULT_PROVIDER_INTERACTION_MODE: ProviderInteractionMode = \"default\";\nexport const ProviderRequestKind = Schema.Literals([\n \"command\",\n \"file-read\",\n \"file-change\",\n \"mcp-elicitation\",\n]);", + "checksum": "01c4763381fc18523f326bf1484bdf1fc4a08667f36891612dba0f930d1f4a45" }, "permission-codex-mode-map": { "id": "permission-codex-mode-map", @@ -966,8 +966,8 @@ "end": 340, "language": "typescript", "label": "T3 runtime modes mapped into Codex approval and sandbox settings", - "code": "function runtimeModeToThreadConfig(input: RuntimeMode): {\n readonly approvalPolicy: EffectCodexSchema.V2ThreadStartParams__AskForApproval;\n readonly sandbox: EffectCodexSchema.V2ThreadStartParams__SandboxMode;\n // Always explicit: omitting the field on resume keeps the thread's previous\n // reviewer, which would leave auto_review sticky after switching modes.\n readonly approvalsReviewer: EffectCodexSchema.V2ThreadStartParams__ApprovalsReviewer;\n} {\n switch (input) {\n case \"approval-required\":\n return {\n approvalPolicy: \"untrusted\",\n sandbox: \"read-only\",\n approvalsReviewer: \"user\",\n };\n case \"auto-accept-edits\":\n return {\n approvalPolicy: \"on-request\",\n sandbox: \"workspace-write\",\n approvalsReviewer: \"user\",\n };\n case \"auto\":\n return {\n approvalPolicy: \"on-request\",\n sandbox: \"workspace-write\",\n approvalsReviewer: \"auto_review\",\n };\n case \"full-access\":\n default:\n return {\n approvalPolicy: \"never\",\n sandbox: \"danger-full-access\",\n approvalsReviewer: \"user\",\n };\n }\n}\n\nfunction buildThreadStartParams(input: {\n readonly cwd: string;\n readonly runtimeMode: RuntimeMode;\n readonly model: string | undefined;\n readonly serviceTier: CodexServiceTier | undefined;\n}): EffectCodexSchema.V2ThreadStartParams {\n const config = runtimeModeToThreadConfig(input.runtimeMode);\n return {\n cwd: input.cwd,\n approvalPolicy: config.approvalPolicy,\n sandbox: config.sandbox,\n approvalsReviewer: config.approvalsReviewer,\n ...(input.model ? { model: input.model } : {}),\n ...(input.serviceTier ? { serviceTier: input.serviceTier } : {}),\n };\n}\n\nfunction runtimeModeToTurnSandboxPolicy(\n input: RuntimeMode,\n): EffectCodexSchema.V2TurnStartParams__SandboxPolicy {\n switch (input) {\n case \"approval-required\":\n return {\n type: \"readOnly\",\n };\n case \"auto-accept-edits\":\n case \"auto\":\n return {\n type: \"workspaceWrite\",\n };\n case \"full-access\":\n default:\n return {\n type: \"dangerFullAccess\",\n };\n }\n}", - "checksum": "bf3d7a104fb45d37eee289512fb96dc72046d830c24c5eda45fdd42346b519d9" + "code": " },\n) {\n override get message(): string {\n return `Invalid Codex user input answers for question '${this.questionId}'`;\n }\n}\n\nexport class CodexSessionRuntimeThreadIdMissingError extends Schema.TaggedError()(\n \"CodexSessionRuntimeThreadIdMissingError\",\n {\n threadId: Schema.String,\n },\n) {\n override get message(): string {\n return `Codex session is missing a provider thread id for ${this.threadId}`;\n }\n}\n\ninterface PendingApproval {\n readonly requestId: ApprovalRequestId;\n readonly jsonRpcId: string;\n readonly requestKind: ProviderRequestKind;\n readonly turnId: TurnId | undefined;\n readonly itemId: ProviderItemId | undefined;\n readonly decision: Deferred.Deferred;\n}\n\ninterface ApprovalCorrelation {\n readonly requestId: ApprovalRequestId;\n readonly requestKind: ProviderRequestKind;\n readonly turnId: TurnId | undefined;\n readonly itemId: ProviderItemId | undefined;\n}\n\ninterface PendingUserInput {\n readonly requestId: ApprovalRequestId;\n readonly turnId: TurnId | undefined;\n readonly itemId: ProviderItemId | undefined;\n readonly answers: Deferred.Deferred;\n}\n\ntype McpElicitationPersistenceDecision = Extract<\n ProviderApprovalDecision,\n \"acceptForSession\" | \"acceptAlways\"\n>;\n\nfunction mcpElicitationPersistenceDecision(\n value: string,\n): McpElicitationPersistenceDecision | null {\n const normalized = value.toLowerCase();\n if (normalized.includes(\"session\")) return \"acceptForSession\";\n if (\n normalized.includes(\"always\") ||\n normalized.includes(\"permanent\") ||\n normalized.includes(\"forever\") ||\n normalized.includes(\"persistent\")\n ) {\n return \"acceptAlways\";\n }\n return null;\n}\n\nfunction mcpElicitationFormFields(payload: EffectCodexSchema.McpServerElicitationRequestParams) {\n if (payload.mode === \"url\" || !isMcpElicitationForm(payload.requestedSchema)) {\n return undefined;\n }\n return payload.requestedSchema;\n}\n\nfunction mcpElicitationFieldOptions(field: typeof McpElicitationFormField.Type) {\n if (field.oneOf) {\n return field.oneOf.map((option) => ({ value: option.const, label: option.title }));\n }", + "checksum": "849989c22875b77def11886a09057a057fd2b7eb9f4bfb0ded3a4b3f2b712410" }, "permission-request-contract": { "id": "permission-request-contract", @@ -976,8 +976,8 @@ "end": 469, "language": "typescript", "label": "Canonical approval and structured-input request payloads", - "code": "const RequestOpenedPayload = Schema.Struct({\n requestType: CanonicalRequestType,\n detail: Schema.optional(TrimmedNonEmptyStringSchema),\n args: Schema.optional(Schema.Unknown),\n});\nexport type RequestOpenedPayload = typeof RequestOpenedPayload.Type;\n\nconst RequestResolvedPayload = Schema.Struct({\n requestType: CanonicalRequestType,\n decision: Schema.optional(TrimmedNonEmptyStringSchema),\n resolution: Schema.optional(Schema.Unknown),\n});\nexport type RequestResolvedPayload = typeof RequestResolvedPayload.Type;\n\nconst UserInputQuestionOption = Schema.Struct({\n label: TrimmedNonEmptyStringSchema,\n description: TrimmedNonEmptyStringSchema,\n});\nexport type UserInputQuestionOption = typeof UserInputQuestionOption.Type;\n\nexport const UserInputQuestion = Schema.Struct({\n id: TrimmedNonEmptyStringSchema,\n header: TrimmedNonEmptyStringSchema,\n question: TrimmedNonEmptyStringSchema,\n options: Schema.Array(UserInputQuestionOption),\n multiSelect: Schema.optional(Schema.Boolean).pipe(\n Schema.withConstructorDefault(Effect.succeed(false)),\n ),\n});\nexport type UserInputQuestion = typeof UserInputQuestion.Type;\n\nconst UserInputRequestedPayload = Schema.Struct({\n questions: Schema.Array(UserInputQuestion),\n});\nexport type UserInputRequestedPayload = typeof UserInputRequestedPayload.Type;\n\nconst UserInputResolvedPayload = Schema.Struct({\n answers: UnknownRecordSchema,\n});\nexport type UserInputResolvedPayload = typeof UserInputResolvedPayload.Type;", - "checksum": "6b1a92d91a1044987815a33034c9d57cf408dde2c10b3b0d1a518d7c63094277" + "code": " delta: Schema.String,\n});\nexport type TurnProposedDeltaPayload = typeof TurnProposedDeltaPayload.Type;\n\nconst TurnProposedCompletedPayload = Schema.Struct({\n planMarkdown: TrimmedNonEmptyStringSchema,\n});\nexport type TurnProposedCompletedPayload = typeof TurnProposedCompletedPayload.Type;\n\nconst TurnDiffUpdatedPayload = Schema.Struct({\n unifiedDiff: Schema.String,\n});\nexport type TurnDiffUpdatedPayload = typeof TurnDiffUpdatedPayload.Type;\n\nexport const ToolActivitySurface = Schema.Literals([\"browser\", \"computer\"]);\nexport type ToolActivitySurface = typeof ToolActivitySurface.Type;\n\nexport const ToolActivityNativeAppReference = Schema.Union([\n Schema.TaggedStruct(\"app-id\", {\n appId: TrimmedNonEmptyStringSchema.check(\n Schema.isMaxLength(512),\n Schema.isPattern(/^[A-Za-z0-9._-]+$/u),\n ),\n }),\n Schema.TaggedStruct(\"display-name\", {\n displayName: TrimmedNonEmptyStringSchema.check(Schema.isMaxLength(160)),\n }),\n]);\nexport type ToolActivityNativeAppReference = typeof ToolActivityNativeAppReference.Type;\n\nexport const ToolActivityIcon = Schema.Union([\n Schema.TaggedStruct(\"website\", {\n pageUrl: TrimmedNonEmptyStringSchema.check(Schema.isMaxLength(4096)),\n faviconUrl: Schema.optional(TrimmedNonEmptyStringSchema.check(Schema.isMaxLength(4096))),\n faviconUrlDark: Schema.optional(TrimmedNonEmptyStringSchema.check(Schema.isMaxLength(4096))),\n }),\n Schema.TaggedStruct(\"native-app\", {\n app: ToolActivityNativeAppReference,\n }),\n Schema.TaggedStruct(\"themed-logo\", {", + "checksum": "c229d8f73ed642582148eabb2645d7f0c4deb848084938651199c30cbeebf2e7" }, "work-task-usage-contract": { "id": "work-task-usage-contract", @@ -986,8 +986,8 @@ "end": 543, "language": "typescript", "label": "Typed per-task usage and agent-versus-background classification", - "code": "/**\n * Typed per-task usage rollup. Field names match the orchestration-v2 subagent\n * usage vocabulary (#4779) so the eventual migration is a rename, not a remap.\n * Claude reports per-activation deltas; Codex reports cumulative totals — the\n * merge strategy is provider-specific and lives in client-runtime.\n */\nexport const RuntimeTaskUsage = Schema.Struct({\n totalTokens: NonNegativeInt,\n inputTokens: Schema.optional(NonNegativeInt),\n cachedInputTokens: Schema.optional(NonNegativeInt),\n outputTokens: Schema.optional(NonNegativeInt),\n reasoningOutputTokens: Schema.optional(NonNegativeInt),\n toolUses: Schema.optional(NonNegativeInt),\n durationMs: Schema.optional(NonNegativeInt),\n});\nexport type RuntimeTaskUsage = typeof RuntimeTaskUsage.Type;\n\nexport const TaskWorkflowPhase = Schema.Struct({\n index: NonNegativeInt,\n title: TrimmedNonEmptyStringSchema,\n});\nexport type TaskWorkflowPhase = typeof TaskWorkflowPhase.Type;\n\nexport const TaskRunHandles = Schema.Struct({\n runId: Schema.optional(TrimmedNonEmptyStringSchema),\n scriptPath: Schema.optional(TrimmedNonEmptyStringSchema),\n transcriptDir: Schema.optional(TrimmedNonEmptyStringSchema),\n /** Only http/https URLs may be stored here — sanitized at the adapter. */\n sessionUrl: Schema.optional(TrimmedNonEmptyStringSchema),\n});\nexport type TaskRunHandles = typeof TaskRunHandles.Type;\n\n/**\n * Watch-loop task types: Monitor-tool tasks plus background shells (a shell\n * that outlives its turn is in practice a watch loop). Canonical single copy —\n * the server liveness registry, ingestion's agentKind stamp, and the client\n * fold's legacy fallback all classify with these sets.\n */\nexport const MONITOR_TASK_TYPES: ReadonlySet = new Set([\n \"monitor\",\n \"monitor_mcp\",\n \"local_bash\",\n \"shell\",\n]);\n/** Task types that are neither agents nor watch loops (plan-mode bookkeeping). */\nexport const INERT_TASK_TYPES: ReadonlySet = new Set([\"plan\", \"dream\"]);\n\n/**\n * Agent-vs-background classification, stamped by ingestion as `agentKind` so\n * persisted rows are self-describing. A deliberate denylist: the SDK's\n * agent-flavored type names drift (subagent, local_agent, local_workflow, …)\n * and an allowlist silently dropped real subagents when \"local_agent\"\n * appeared. A task launched from inside a subagent (agentId set) is\n * agent-internal background work UNLESS it is itself agent-flavored — a\n * nested agent can outlive its parent and stays in the roster.\n */\nexport function classifyTaskAgentKind(input: {\n readonly taskType?: string | undefined;\n readonly agentId?: string | undefined;\n}): \"agent\" | \"background\" {\n const { taskType, agentId } = input;\n const nonAgentType =\n taskType !== undefined && (MONITOR_TASK_TYPES.has(taskType) || INERT_TASK_TYPES.has(taskType));\n if (agentId !== undefined && agentId.trim().length > 0) {\n return taskType === undefined || nonAgentType ? \"background\" : \"agent\";\n }\n return nonAgentType ? \"background\" : \"agent\";\n}\n\n/**\n * Optional agent-identity linkage carried on every task lifecycle payload.\n * Repeated on progress and terminal rows (not just start) so client folds can\n * reconstruct an agent even when its start row aged out of activity retention.", - "checksum": "9c7794660442c1d9a47d1553b18d8e593f52d0f6f4ded9f377c395421336aab5" + "code": " logoUrlDark: Schema.optional(TrimmedNonEmptyStringSchema.check(Schema.isMaxLength(4096))),\n }),\n]);\nexport type ToolActivityIcon = typeof ToolActivityIcon.Type;\n\nexport const ToolActivitySource = Schema.Struct({\n key: TrimmedNonEmptyStringSchema.check(Schema.isMaxLength(512)),\n name: TrimmedNonEmptyStringSchema.check(Schema.isMaxLength(160)),\n kind: Schema.Literals([\"browser\", \"computer\", \"integration\"]),\n icon: Schema.optional(ToolActivityIcon),\n});\nexport type ToolActivitySource = typeof ToolActivitySource.Type;\n\nexport const ItemLifecyclePayload = Schema.Struct({\n itemType: CanonicalItemType,\n status: Schema.optional(RuntimeItemStatus),\n title: Schema.optional(TrimmedNonEmptyStringSchema),\n detail: Schema.optional(TrimmedNonEmptyStringSchema),\n toolSurface: Schema.optional(ToolActivitySurface),\n toolIcon: Schema.optional(ToolActivityIcon),\n toolSource: Schema.optional(ToolActivitySource),\n data: Schema.optional(Schema.Unknown),\n /**\n * Owning agent when this item ran inside a subagent (resolved from the\n * SDK's parent_tool_use_id). Clients re-home attributed items out of the\n * main timeline and into the owning agent's Agents-surface row.\n */\n agentId: Schema.optional(TrimmedNonEmptyStringSchema),\n parentToolUseId: Schema.optional(TrimmedNonEmptyStringSchema),\n});\nexport type ItemLifecyclePayload = typeof ItemLifecyclePayload.Type;\n\nconst ContentDeltaPayload = Schema.Struct({\n streamKind: RuntimeContentStreamKind,\n delta: Schema.String,\n contentIndex: Schema.optional(Schema.Int),\n summaryIndex: Schema.optional(Schema.Int),\n});\nexport type ContentDeltaPayload = typeof ContentDeltaPayload.Type;\n\nconst RequestOpenedPayload = Schema.Struct({\n requestType: CanonicalRequestType,\n detail: Schema.optional(TrimmedNonEmptyStringSchema),\n appName: Schema.optional(TrimmedNonEmptyStringSchema),\n options: Schema.optional(Schema.Array(ProviderApprovalOption)),\n args: Schema.optional(Schema.Unknown),\n});\nexport type RequestOpenedPayload = typeof RequestOpenedPayload.Type;\n\nconst RequestResolvedPayload = Schema.Struct({\n requestType: CanonicalRequestType,\n decision: Schema.optional(TrimmedNonEmptyStringSchema),\n resolution: Schema.optional(Schema.Unknown),\n});\nexport type RequestResolvedPayload = typeof RequestResolvedPayload.Type;\n\nconst UserInputQuestionOption = Schema.Struct({\n label: TrimmedNonEmptyStringSchema,\n description: Schema.String,\n value: Schema.optional(Schema.String),\n});\nexport type UserInputQuestionOption = typeof UserInputQuestionOption.Type;\n\nexport const UserInputQuestion = Schema.Struct({\n id: TrimmedNonEmptyStringSchema,\n header: TrimmedNonEmptyStringSchema,\n question: TrimmedNonEmptyStringSchema,\n options: Schema.Array(UserInputQuestionOption),\n allowCustomAnswer: Schema.optional(Schema.Boolean),\n multiSelect: Schema.optional(Schema.Boolean).pipe(\n Schema.withConstructorDefault(Effect.succeed(false)),\n ),\n});", + "checksum": "eeb106d9c479a469e60cd0912255db786660bbef06281fb99b7273ca5ccb1e2e" }, "work-task-latest-activities": { "id": "work-task-latest-activities", @@ -996,8 +996,8 @@ "end": 646, "language": "typescript", "label": "Separate stable task progress and usage activity snapshots", - "code": " const linkage = taskLinkageActivityFields(event.payload as Record);\n // Usage and activity are independent latest-state streams. Keeping them\n // under separate stable ids prevents a command/reasoning update from\n // replacing the last known token count (and prevents a usage-only tick\n // from blanking the last meaningful activity).\n const identityLinkage = { ...linkage };\n delete identityLinkage.typedUsage;\n delete identityLinkage.status;\n delete identityLinkage.error;\n const title =\n event.payload.description.trim().length > 0\n ? { title: truncateDetail(event.payload.description, 120) }\n : {};\n const hasProgressState =\n event.payload.typedUsage === undefined ||\n event.payload.summary !== undefined ||\n event.payload.lastToolName !== undefined ||\n event.payload.status !== undefined ||\n event.payload.error !== undefined;\n return [\n ...(hasProgressState\n ? [\n {\n // Stable per-task id: activity is \"latest state\", not\n // history, so each meaningful tick replaces the last. This\n // bounds a large fleet to one activity row per task.\n id: EventId.make(`task-progress:${event.threadId}:${event.payload.taskId}`),\n createdAt: event.createdAt,\n tone: \"info\" as const,\n kind: \"task.progress\" as const,\n summary:\n event.payload.description.trim().length > 0\n ? truncateDetail(event.payload.description, 120)\n : \"Reasoning update\",\n payload: {\n taskId: event.payload.taskId,\n ...title,\n detail: truncateDetail(event.payload.summary ?? event.payload.description),\n ...(event.payload.summary\n ? { summary: truncateDetail(event.payload.summary) }\n : {}),\n ...(event.payload.lastToolName\n ? { lastToolName: event.payload.lastToolName }\n : {}),\n ...(event.payload.status ? { status: event.payload.status } : {}),\n ...(event.payload.error ? { error: event.payload.error } : {}),\n ...(event.payload.usage !== undefined ? { usage: event.payload.usage } : {}),\n ...identityLinkage,\n },\n turnId: toTurnId(event.turnId) ?? null,\n ...maybeSequence,\n },\n ]\n : []),\n ...(event.payload.typedUsage !== undefined\n ? [\n {\n id: EventId.make(`task-usage:${event.threadId}:${event.payload.taskId}`),\n createdAt: event.createdAt,\n tone: \"info\" as const,\n kind: \"task.progress\" as const,\n summary: \"Task usage updated\",\n payload: {\n taskId: event.payload.taskId,\n ...title,\n ...identityLinkage,\n usageSnapshot: true,\n typedUsage: event.payload.typedUsage,\n },\n turnId: toTurnId(event.turnId) ?? null,\n ...maybeSequence,\n },\n ]\n : []),\n ];\n }\n\n case \"task.updated\": {", - "checksum": "11d184eb4842e6907a766a04a86a1b355065eb1ae64b2891b58d24d836acd812" + "code": " const identityLinkage = { ...linkage };\n delete identityLinkage.typedUsage;\n delete identityLinkage.status;\n delete identityLinkage.error;\n const title =\n event.payload.description.trim().length > 0\n ? { title: truncateDetail(event.payload.description, 120) }\n : {};\n const hasProgressState =\n event.payload.typedUsage === undefined ||\n event.payload.summary !== undefined ||\n event.payload.lastToolName !== undefined ||\n event.payload.status !== undefined ||\n event.payload.error !== undefined;\n return [\n ...(hasProgressState\n ? [\n {\n // Stable per-task id: activity is \"latest state\", not\n // history, so each meaningful tick replaces the last. This\n // bounds a large fleet to one activity row per task.\n id: EventId.make(`task-progress:${event.threadId}:${event.payload.taskId}`),\n createdAt: event.createdAt,\n tone: \"info\" as const,\n kind: \"task.progress\" as const,\n summary:\n event.payload.description.trim().length > 0\n ? truncateDetail(event.payload.description, 120)\n : \"Reasoning update\",\n payload: {\n taskId: event.payload.taskId,\n ...title,\n detail: truncateDetail(event.payload.summary ?? event.payload.description),\n ...(event.payload.summary\n ? { summary: truncateDetail(event.payload.summary) }\n : {}),\n ...(event.payload.lastToolName\n ? { lastToolName: event.payload.lastToolName }\n : {}),\n ...(event.payload.status ? { status: event.payload.status } : {}),\n ...(event.payload.error ? { error: event.payload.error } : {}),\n ...(event.payload.usage !== undefined ? { usage: event.payload.usage } : {}),\n ...identityLinkage,\n },\n turnId: toTurnId(event.turnId) ?? null,\n ...maybeSequence,\n },\n ]\n : []),\n ...(event.payload.typedUsage !== undefined\n ? [\n {\n id: EventId.make(`task-usage:${event.threadId}:${event.payload.taskId}`),\n createdAt: event.createdAt,\n tone: \"info\" as const,\n kind: \"task.progress\" as const,\n summary: \"Task usage updated\",\n payload: {\n taskId: event.payload.taskId,\n ...title,\n ...identityLinkage,\n usageSnapshot: true,\n typedUsage: event.payload.typedUsage,\n },\n turnId: toTurnId(event.turnId) ?? null,\n ...maybeSequence,\n },\n ]\n : []),\n ];\n }\n\n case \"task.updated\": {\n return [\n {\n id: event.eventId,\n createdAt: event.createdAt,\n tone: event.payload.status === \"failed\" ? \"error\" : \"info\",", + "checksum": "9bfc2b9f894795a0e9337468cbb63819c0ddc9357e7d52d53bb5022a2a9e0d70" }, "work-subagent-usage-merge": { "id": "work-subagent-usage-merge", @@ -1016,8 +1016,8 @@ "end": 110, "language": "typescript", "label": "Persisted provider-instance binding and opaque continuation state", - "code": "export const ProviderSessionRuntime = Schema.Struct({\n threadId: ThreadId,\n providerName: Schema.String,\n /**\n * User-defined routing key for the configured provider instance that\n * owns this session. Nullable only at the storage/migration boundary:\n * rows persisted before the driver/instance split carry only\n * `providerName`. Repository consumers must materialize a concrete\n * instance id before routing.\n */\n providerInstanceId: Schema.NullOr(ProviderInstanceId),\n adapterKey: Schema.String,\n runtimeMode: RuntimeMode,\n status: ProviderSessionRuntimeStatus,\n lastSeenAt: IsoDateTime,\n resumeCursor: Schema.NullOr(Schema.Unknown),\n runtimePayload: Schema.NullOr(Schema.Unknown),\n});\nexport type ProviderSessionRuntime = typeof ProviderSessionRuntime.Type;\n\nexport const GetProviderSessionRuntimeInput = Schema.Struct({ threadId: ThreadId });\nexport type GetProviderSessionRuntimeInput = typeof GetProviderSessionRuntimeInput.Type;\n\nexport const DeleteProviderSessionRuntimeInput = Schema.Struct({ threadId: ThreadId });\nexport type DeleteProviderSessionRuntimeInput = typeof DeleteProviderSessionRuntimeInput.Type;\n\n/**\n * ProviderSessionRuntimeRepository - Service tag for provider runtime persistence.\n */\nexport class ProviderSessionRuntimeRepository extends Context.Service<\n ProviderSessionRuntimeRepository,\n {\n /**\n * Insert or replace a provider runtime row.\n *\n * Upserts by canonical `threadId`, including JSON payload/cursor fields.\n */\n readonly upsert: (\n runtime: ProviderSessionRuntime,\n ) => Effect.Effect;\n\n /**\n * Read provider runtime state by canonical thread id.\n */\n readonly getByThreadId: (\n input: GetProviderSessionRuntimeInput,\n ) => Effect.Effect<\n Option.Option,\n ProviderSessionRuntimeRepositoryError\n >;\n\n /**\n * List all provider runtime rows.\n *\n * Returned in ascending last-seen order.\n */\n readonly list: () => Effect.Effect<\n ReadonlyArray,\n ProviderSessionRuntimeRepositoryError\n >;\n\n /**\n * Delete provider runtime state by canonical thread id.\n */\n readonly deleteByThreadId: (\n input: DeleteProviderSessionRuntimeInput,\n ) => Effect.Effect;\n }\n>()(\"t3/persistence/ProviderSessionRuntime/ProviderSessionRuntimeRepository\") {}\n\nconst ProviderSessionRuntimeDbRowSchema = ProviderSessionRuntime.mapFields(\n Struct.assign({\n resumeCursor: Schema.NullOr(Schema.fromJsonString(Schema.Unknown)),\n runtimePayload: Schema.NullOr(Schema.fromJsonString(Schema.Unknown)),\n }),\n);", - "checksum": "b92dc6c01460eee6a905a51d0d848df3b28fd4410a9403789699bc459815cadd" + "code": "\nexport const ProviderSessionRuntime = Schema.Struct({\n threadId: ThreadId,\n providerName: Schema.String,\n /**\n * User-defined routing key for the configured provider instance that\n * owns this session. Nullable only at the storage/migration boundary:\n * rows persisted before the driver/instance split carry only\n * `providerName`. Repository consumers must materialize a concrete\n * instance id before routing.\n */\n providerInstanceId: Schema.NullOr(ProviderInstanceId),\n adapterKey: Schema.String,\n runtimeMode: RuntimeMode,\n status: ProviderSessionRuntimeStatus,\n lastSeenAt: IsoDateTime,\n resumeCursor: Schema.NullOr(Schema.Unknown),\n runtimePayload: Schema.NullOr(Schema.Unknown),\n});\nexport type ProviderSessionRuntime = typeof ProviderSessionRuntime.Type;\n\nexport const GetProviderSessionRuntimeInput = Schema.Struct({ threadId: ThreadId });\nexport type GetProviderSessionRuntimeInput = typeof GetProviderSessionRuntimeInput.Type;\n\nexport const DeleteProviderSessionRuntimeInput = Schema.Struct({ threadId: ThreadId });\nexport type DeleteProviderSessionRuntimeInput = typeof DeleteProviderSessionRuntimeInput.Type;\n\nexport const RecordImportedTranscriptInput = Schema.Struct({\n threadId: ThreadId,\n source: AgentSessionImportSource,\n});\nexport type RecordImportedTranscriptInput = typeof RecordImportedTranscriptInput.Type;\n\nexport interface ProviderSessionRuntimeUpsertOptions {\n readonly onConflict?: \"update\" | \"ignore\";\n}\n\n/**\n * ProviderSessionRuntimeRepository - Service tag for provider runtime persistence.\n */\nexport class ProviderSessionRuntimeRepository extends Context.Service<\n ProviderSessionRuntimeRepository,\n {\n /**\n * Insert or replace a provider runtime row.\n *\n * Upserts by canonical `threadId`, retaining imported transcript records\n * from the current database row.\n */\n readonly upsert: (\n runtime: ProviderSessionRuntime,\n options?: ProviderSessionRuntimeUpsertOptions,\n ) => Effect.Effect;\n\n /** Record one source file without replacing the current session state. */\n readonly recordImportedTranscript: (\n input: RecordImportedTranscriptInput,\n ) => Effect.Effect;\n\n /**\n * Read provider runtime state by canonical thread id.\n */\n readonly getByThreadId: (\n input: GetProviderSessionRuntimeInput,\n ) => Effect.Effect<\n Option.Option,\n ProviderSessionRuntimeRepositoryError\n >;\n\n /**\n * List all provider runtime rows.\n *\n * Returned in ascending last-seen order.\n */\n readonly list: () => Effect.Effect<\n ReadonlyArray,", + "checksum": "baeb91f02bdfdd5a6a1ea9acdf96df9cce2742385ac9ae2ff760ae68b8453499" }, "context-memory-compaction-ingestion": { "id": "context-memory-compaction-ingestion", @@ -1026,8 +1026,8 @@ "end": 763, "language": "typescript", "label": "Provider compaction observation becomes durable thread activity", - "code": " case \"thread.state.changed\": {\n if (event.payload.state !== \"compacted\") {\n return [];\n }\n\n return [\n {\n id: event.eventId,\n createdAt: event.createdAt,\n tone: \"info\",\n kind: \"context-compaction\",\n summary: \"Context compacted\",\n payload: {\n state: event.payload.state,\n ...(event.payload.detail !== undefined ? { detail: event.payload.detail } : {}),\n },\n turnId: toTurnId(event.turnId) ?? null,\n ...maybeSequence,\n },\n ];", - "checksum": "84f0ec54be712d09ef6dc181bcd36318a26e36de8def7182a8377454dd21fcdd" + "code": " const beforeTokens = event.payload.beforeTokens;\n const afterTokens = event.payload.afterTokens;\n const summary =\n beforeTokens !== undefined && afterTokens !== undefined\n ? `Compacted context ${formatTokens(beforeTokens)} → ${formatTokens(afterTokens)} tokens`\n : \"Context compacted\";\n return [\n {\n id: event.eventId,\n createdAt: event.createdAt,\n tone: \"info\",\n kind: \"context-compaction\",\n summary,\n payload: {\n state: event.payload.state,\n ...(beforeTokens !== undefined ? { beforeTokens } : {}),\n ...(afterTokens !== undefined ? { afterTokens } : {}),\n ...(event.requestId !== undefined ? { requestId: event.requestId } : {}),\n ...(event.payload.detail !== undefined ? { detail: event.payload.detail } : {}),\n },", + "checksum": "91dc6a551b3d0215b581c6cf80e0352dd6371b702abdda4dd85b3c766c9941f8" }, "context-window-reducer-retention": { "id": "context-window-reducer-retention", @@ -1036,8 +1036,8 @@ "end": 592, "language": "typescript", "label": "Latest resolvable context-window snapshot retained per turn id", - "code": " // ── Activities ──────────────────────────────────────────────────\n case \"thread.activity-appended\": {\n const activity = event.payload.activity;\n // A resolvable context-window update supersedes earlier resolvable ones\n // for the same turn: consumers only read the latest value (walking the\n // array backwards), and providers stream these updates continuously, so\n // retaining the history grows the thread by thousands of rows over a\n // long session. Mirrors the server-side snapshot rule in\n // dropStaleContextWindowActivities; retention stays per turn so a\n // thread.reverted that discards turns can still resolve a value from\n // the turns that survive.\n const supersedesContextWindow = isResolvableContextWindowActivity(activity);\n const activities = pipe(\n thread.activities,\n Arr.filter(\n (entry) =>\n entry.id !== activity.id &&\n !(\n supersedesContextWindow &&\n entry.turnId === activity.turnId &&\n isResolvableContextWindowActivity(entry)\n ),\n ),\n Arr.append(activity),\n Arr.sort(activityOrder),\n );\n\n return {\n kind: \"updated\",\n thread: { ...thread, activities, updatedAt: event.occurredAt },\n };", - "checksum": "ae9a3e3858741dd9a80e70b34a5ab36dca52539d93e1b199f9572b7113f6c995" + "code": " const checkpoint: OrchestrationCheckpointSummary = {\n turnId: event.payload.turnId,\n checkpointTurnCount: event.payload.checkpointTurnCount,\n checkpointRef: event.payload.checkpointRef,\n status: event.payload.status,\n files: event.payload.files,\n assistantMessageId: event.payload.assistantMessageId,\n completedAt: event.payload.completedAt,\n };\n\n const existing = thread.checkpoints.find((entry) => entry.turnId === checkpoint.turnId);\n // Don't overwrite a non-missing checkpoint with a missing one.\n if (existing && existing.status !== \"missing\" && checkpoint.status === \"missing\") {\n return { kind: \"unchanged\" };\n }\n\n const checkpoints = pipe(\n thread.checkpoints,\n Arr.filter((entry) => entry.turnId !== checkpoint.turnId),\n Arr.append(checkpoint),\n Arr.sort(checkpointOrder),\n );\n\n // Mid-turn diff updates produce placeholder checkpoints; record the\n // checkpoint, but don't settle a turn its session is still running.\n const diffTurnStillRunning =\n thread.session?.status === \"running\" &&\n thread.session.activeTurnId === event.payload.turnId;\n const latestTurn =\n !diffTurnStillRunning &&\n (thread.latestTurn === null || thread.latestTurn.turnId === event.payload.turnId)", + "checksum": "a240aeafa413d382634dced3b6cdcd4286fbc4dd9669b8da74c1e69d73fd5cba" }, "workbench-terminal-attach-order": { "id": "workbench-terminal-attach-order", @@ -1046,8 +1046,8 @@ "end": 2402, "language": "typescript", "label": "Terminal attach buffers races, emits a snapshot, then delivers live events", - "code": " const attachStream: TerminalManager[\"Service\"][\"attachStream\"] = (input, listener) => {\n let unsubscribe: (() => void) | null = null;\n\n return Effect.gen(function* () {\n const bufferedEvents: TerminalEvent[] = [];\n let deliverLive = false;\n\n unsubscribe = yield* subscribe((event) => {\n if (event.threadId !== input.threadId || event.terminalId !== input.terminalId) {\n return Effect.void;\n }\n\n if (!deliverLive) {\n bufferedEvents.push(event);\n return Effect.void;\n }\n\n const attachEvent = terminalEventToAttachEvent(event);\n return attachEvent ? listener(attachEvent) : Effect.void;\n });\n\n const initialSnapshot = yield* openOrAttachForStream(input);\n\n yield* listener({\n type: \"snapshot\",\n snapshot: initialSnapshot,\n });\n\n for (const event of bufferedEvents) {\n if (isDuplicateAttachSnapshotEvent(event, initialSnapshot)) {\n continue;\n }\n\n const attachEvent = terminalEventToAttachEvent(event);\n if (attachEvent) {\n yield* listener(attachEvent);\n }\n }\n\n deliverLive = true;\n return () => {\n unsubscribe?.();\n unsubscribe = null;\n };\n }).pipe(\n Effect.catchCause((cause) =>\n Effect.flatMap(\n Effect.sync(() => {\n unsubscribe?.();\n unsubscribe = null;\n }),\n () => Effect.failCause(cause),", - "checksum": "b271e39c3ccce7f5ba8015d4dc72f7ca64041d9d6ec6d4c91bdbbba668800ba9" + "code": "\n if (deleteHistoryOnClose) {\n yield* deleteHistory(threadId, terminalId);\n }\n });\n\n const pollSubprocessActivity = Effect.fn(\"terminal.pollSubprocessActivity\")(function* () {\n const state = yield* readManagerState;\n const runningSessions = [...state.sessions.values()].filter(\n (session): session is TerminalSessionState & { pid: number } =>\n session.status === \"running\" && Number.isInteger(session.pid),\n );\n\n if (runningSessions.length === 0) {\n return true;\n }\n\n const inspectorOption = yield* acquireSubprocessInspector.pipe(\n Effect.map(Option.some),\n Effect.catch((reason) =>\n Effect.logWarning(\"failed to snapshot processes for terminal subprocess polling\", {\n reason,\n }).pipe(\n Effect.as(\n Option.none<{\n readonly inspector: TerminalSubprocessInspector;\n readonly snapshotSucceeded: boolean;\n }>(),\n ),\n ),\n ),\n );\n\n if (Option.isNone(inspectorOption)) {\n return false;\n }\n\n const { inspector: subprocessInspector, snapshotSucceeded } = inspectorOption.value;\n\n const checkSubprocessActivity = Effect.fn(\"terminal.checkSubprocessActivity\")(function* (\n session: TerminalSessionState & { pid: number },\n ) {\n const terminalPid = session.pid;\n const inspectResult = yield* subprocessInspector(terminalPid).pipe(\n Effect.map(Option.some),\n Effect.catch((reason) =>\n Effect.logWarning(\"failed to check terminal subprocess activity\", {\n threadId: session.threadId,\n terminalId: session.terminalId,\n terminalPid,\n reason,\n }).pipe(Effect.as(Option.none())),", + "checksum": "714116f67d80764dd9082c10b235a9aeaff799d9c90a5115bbf5edba22dffe3a" }, "workbench-asset-token-verification": { "id": "workbench-asset-token-verification", @@ -1056,8 +1056,8 @@ "end": 451, "language": "typescript", "label": "Asset URL signing followed by signature and expiry verification", - "code": " cause,\n }),\n ),\n );\n if (claims.kind === \"project-favicon\" || claims.kind === \"project-favicon-external\") {\n const issuedAt = yield* Clock.currentTimeMillis;\n expiresAt =\n (Math.floor(issuedAt / PROJECT_FAVICON_TOKEN_BUCKET_MS) + 2) *\n PROJECT_FAVICON_TOKEN_BUCKET_MS;\n claims = { ...claims, expiresAt };\n }\n const encodedPayload = base64UrlEncode(encodeAssetClaims(claims));\n const token = `${encodedPayload}.${signPayload(encodedPayload, signingSecret)}`;\n return {\n relativeUrl: `${ASSET_ROUTE_PREFIX}/${token}/${encodeURIComponent(fileName)}`,\n expiresAt,\n ...(sourcePath !== undefined ? { sourcePath } : {}),\n };\n});\n\nexport const resolveAsset = Effect.fn(\"AssetAccess.resolveAsset\")(function* (\n token: string,\n relativePath: string,\n) {\n const [encodedPayload, signature] = token.split(\".\");\n if (!encodedPayload || !signature) return null;\n\n const secretStore = yield* ServerSecretStore.ServerSecretStore;\n const signingSecret = yield* secretStore.getOrCreateRandom(SIGNING_SECRET_NAME, 32).pipe(\n Effect.tapError((cause) => Effect.logError(\"Failed to load the asset signing key.\", { cause })),\n Effect.orElseSucceed(() => null),\n );\n if (!signingSecret) return null;\n if (!timingSafeEqualBase64Url(signature, signPayload(encodedPayload, signingSecret))) return null;\n\n const claims = decodeClaims(encodedPayload);\n if (!claims || claims.expiresAt <= (yield* Clock.currentTimeMillis)) return null;\n\n if (claims.kind === \"attachment\") {\n const config = yield* ServerConfig.ServerConfig;\n const attachmentPath = resolveAttachmentPathById({\n attachmentsDir: config.attachmentsDir,", - "checksum": "27cc3e4c869c07078758c87260ae1f162edb77abdb33fd820bf5a612faef5e3a" + "code": " : {\n version: 1,\n kind: \"workspace-file\",\n workspaceRoot: canonicalWorkspaceRoot,\n baseRelativePath: path.dirname(resolved.relativePath),\n expiresAt,\n };\n fileName = path.basename(resolved.relativePath);\n break;\n }\n case \"attachment\": {\n const config = yield* ServerConfig.ServerConfig;\n const attachmentPath = resolveAttachmentPathById({\n attachmentsDir: config.attachmentsDir,\n attachmentId: input.resource.attachmentId,\n });\n if (!attachmentPath) {\n return yield* new AssetAttachmentNotFoundError({\n resource: input.resource,\n });\n }\n // Generic files carry their extension inside the attachment id (that\n // shape resolves the on-disk path); images do not. Videos and images\n // render inline. Other generic files download, unless a document viewer\n // asked for inline and the stored extension is one a browser can show.\n const extension = parseAttachmentFileExtension(input.resource.attachmentId);\n const isGenericFile = extension !== null;\n const videoMimeType = input.resource.mimeType?.split(\";\", 1)[0]?.trim() ?? \"\";\n const isVideo = INLINE_VIDEO_MIME_TYPE_PATTERN.test(videoMimeType);\n const inlineDocumentMimeType =\n input.resource.disposition === \"inline\" &&\n extension !== null &&\n INLINE_DOCUMENT_EXTENSIONS.has(extension)\n ? INLINE_DOCUMENT_MIME_TYPES[extension]\n : undefined;\n if (!isGenericFile) {\n imageDimensions = yield* readImageDimensionsFromHeader(attachmentPath);\n }\n claims = {\n version: 1,\n kind: \"attachment\",\n attachmentId: input.resource.attachmentId,", + "checksum": "1bf176ab744b17701522c27f444cb24d0412016ff62951c48098ea22a33c376f" }, "workbench-mcp-credential-issue": { "id": "workbench-mcp-credential-issue", @@ -1066,8 +1066,8 @@ "end": 168, "language": "typescript", "label": "Random MCP bearer credential and registry-generated scoped session identity", - "code": " const next = new Map(\n Array.from(records).filter(\n ([, record]) => timestamp - record.lastAliveAt <= livenessWindowMs,\n ),\n );\n return next.size === records.size ? records : next;\n };\n\n const issue: McpSessionRegistryShape[\"issue\"] = Effect.fn(\"McpSessionRegistry.issue\")(\n function* (request) {\n const issuedAt = yield* currentTimeMillis;\n const providerSessionId = yield* crypto.randomUUIDv4.pipe(Effect.orDie);\n const rawToken = yield* crypto.randomBytes(32).pipe(Effect.map(tokenFromBytes), Effect.orDie);\n const tokenHash = yield* hashToken(rawToken);\n const scope: McpInvocationContext.McpInvocationScope = {\n environmentId,\n threadId: ThreadId.make(request.threadId),\n providerSessionId,\n providerInstanceId: ProviderInstanceId.make(request.providerInstanceId),\n capabilities: new Set([\"preview\"]),\n issuedAt,\n };\n yield* SynchronizedRef.update(state, ({ records }) => {\n const next = new Map(pruneDead(records, issuedAt));\n next.set(tokenHash, { tokenHash, scope, lastAliveAt: issuedAt });\n return { records: next };\n });\n return {\n config: {\n environmentId,\n threadId: scope.threadId,\n providerSessionId,\n providerInstanceId: scope.providerInstanceId,\n endpoint,\n authorizationHeader: `Bearer ${rawToken}`,\n },\n };\n },\n );\n\n const resolve: McpSessionRegistryShape[\"resolve\"] = Effect.fn(\"McpSessionRegistry.resolve\")(\n function* (rawToken) {\n if (rawToken.length === 0) return undefined;\n const tokenHash = yield* hashToken(rawToken);\n const timestamp = yield* currentTimeMillis;\n return yield* SynchronizedRef.modify(state, ({ records }) => {\n const current = pruneDead(records, timestamp);\n const record = current.get(tokenHash);\n if (!record) return [undefined, { records: current }] as const;\n const next = new Map(current);\n next.set(tokenHash, { ...record, lastAliveAt: timestamp });\n return [record.scope, { records: next }] as const;\n });\n },\n );\n\n const touch: McpSessionRegistryShape[\"touch\"] = Effect.fn(\"McpSessionRegistry.touch\")(", - "checksum": "1c72aab293661e234f0a570d76f7c90b77c75b4982796ae2258b258060022cb0" + "code": " const pruneDead = (records: ReadonlyMap, timestamp: number) => {\n const next = new Map(\n Array.from(records).filter(\n ([, record]) => timestamp - record.lastAliveAt <= livenessWindowMs,\n ),\n );\n return next.size === records.size ? records : next;\n };\n\n const issue: McpSessionRegistryShape[\"issue\"] = Effect.fn(\"McpSessionRegistry.issue\")(\n function* (request) {\n const issuedAt = yield* currentTimeMillis;\n const providerSessionId = yield* crypto.randomUUIDv4.pipe(Effect.orDie);\n const rawToken = yield* crypto.randomBytes(32).pipe(Effect.map(tokenFromBytes), Effect.orDie);\n const tokenHash = yield* hashToken(rawToken);\n const scope: McpInvocationContext.McpInvocationScope = {\n environmentId,\n threadId: ThreadId.make(request.threadId),\n providerSessionId,\n providerInstanceId: ProviderInstanceId.make(request.providerInstanceId),\n capabilities: new Set([\n \"pull-requests\",\n ...request.capabilities,\n ]),\n issuedAt,\n };\n yield* SynchronizedRef.update(state, ({ records }) => {\n const next = new Map(pruneDead(records, issuedAt));\n next.set(tokenHash, { tokenHash, scope, lastAliveAt: issuedAt });\n return { records: next };\n });\n return {\n config: {\n environmentId,\n threadId: scope.threadId,\n providerSessionId,\n providerInstanceId: scope.providerInstanceId,\n endpoint,\n authorizationHeader: `Bearer ${rawToken}`,\n capabilities: scope.capabilities,\n },\n };\n },\n );\n\n const resolve: McpSessionRegistryShape[\"resolve\"] = Effect.fn(\"McpSessionRegistry.resolve\")(\n function* (rawToken) {\n if (rawToken.length === 0) return undefined;\n const tokenHash = yield* hashToken(rawToken);\n const timestamp = yield* currentTimeMillis;\n return yield* SynchronizedRef.modify(state, ({ records }) => {\n const current = pruneDead(records, timestamp);\n const record = current.get(tokenHash);\n if (!record) return [undefined, { records: current }] as const;\n const next = new Map(current);\n next.set(tokenHash, { ...record, lastAliveAt: timestamp });\n return [record.scope, { records: next }] as const;", + "checksum": "b961ffb8b6984b36ff86dbd0b033fdf882ea83c1f70fe83ab57e488e8eccf4a8" }, "workbench-pr-write-gates": { "id": "workbench-pr-write-gates", @@ -1076,8 +1076,8 @@ "end": 1367, "language": "typescript", "label": "Pull-request host capability checks followed by fresh viewer authorization", - "code": " const runAction: PullRequestService[\"Service\"][\"runAction\"] = (input) =>\n requireProject(input).pipe(\n Effect.flatMap((project): Effect.Effect => {\n // The surface hides what a host cannot do, and this refuses it as well: a request that\n // reached here anyway must not be handed to a provider that never claimed the action.\n if (!project.api.capabilities.actions.includes(input.action)) {\n return Effect.fail(\n new PullRequestOperationError({\n operation: \"runAction\",\n detail: `This host cannot ${input.action} a change request.`,\n }),\n );\n }\n // A strategy the host does not offer must be refused rather than passed on: every\n // provider maps an unrecognised method to its own default, so asking Azure DevOps to\n // rebase would quietly merge instead of failing.\n if (\n input.mergeMethod !== undefined &&\n !project.api.capabilities.mergeMethods.includes(input.mergeMethod)\n ) {\n return Effect.fail(\n new PullRequestOperationError({\n operation: \"runAction\",\n detail: `This host cannot merge with the ${input.mergeMethod} strategy.`,\n }),\n );\n }\n // The same for the way a stale branch is brought up to date: a host that only merges\n // must not be asked to rebase and left to pick something else.\n if (\n input.updateMethod !== undefined &&\n !(project.api.capabilities.updateMethods ?? []).includes(input.updateMethod)\n ) {\n return Effect.fail(\n new PullRequestOperationError({\n operation: \"runAction\",\n detail: `This host cannot update a branch by ${input.updateMethod}.`,\n }),\n );\n }\n // What the host can do and what this account may ask of it are two questions, and both\n // have to say yes. The second is asked last, because it costs a request and the checks\n // above do not.\n return viewerPermissionsOf(project, input, \"runAction\").pipe(\n Effect.flatMap((viewer): Effect.Effect => {\n if (!viewer.actions.includes(input.action)) {\n return Effect.fail(\n new PullRequestOperationError({\n operation: \"runAction\",\n detail: ACTION_ACCESS_REFUSALS[input.action],\n }),\n );\n }\n if (\n input.updateMethod !== undefined &&\n !(viewer.updateMethods ?? []).includes(input.updateMethod)\n ) {\n return Effect.fail(\n new PullRequestOperationError({\n operation: \"runAction\",\n detail: ACTION_ACCESS_REFUSALS[\"update-branch\"],\n }),\n );\n }\n return project.api\n .runAction({\n cwd: project.project.workspaceRoot,\n repository: project.repository,", - "checksum": "a76b545059b2edbfff26945859bf1c0085707d87b6d1d2ee395be300619adc6a" + "code": " Effect.map((changeRequest): PullRequestSummary => ({\n provider: project.api.kind,\n projectId: project.project.id,\n repository: project.repository,\n number: changeRequest.number,\n title: changeRequest.title,\n url: changeRequest.url,\n state: changeRequest.state,\n headBranch: changeRequest.headBranch,\n baseBranch: changeRequest.baseBranch,\n closedAt: changeRequest.closedAt ?? null,\n mergedAt: changeRequest.mergedAt ?? null,\n updatedAt: changeRequest.updatedAt,\n ...(changeRequest.isDraft === undefined ? {} : { isDraft: changeRequest.isDraft }),\n ...(changeRequest.author === undefined ? {} : { author: changeRequest.author }),\n ...(changeRequest.additions === undefined\n ? {}\n : { additions: changeRequest.additions }),\n ...(changeRequest.deletions === undefined\n ? {}\n : { deletions: changeRequest.deletions }),\n ...(changeRequest.changedFiles === undefined\n ? {}\n : { changedFiles: changeRequest.changedFiles }),\n ...(changeRequest.reviewDecision === undefined\n ? {}\n : { reviewDecision: changeRequest.reviewDecision }),\n ...(changeRequest.checksState === undefined\n ? {}\n : { checksState: changeRequest.checksState }),\n ...(changeRequest.mergeability === undefined\n ? {}\n : { mergeability: changeRequest.mergeability }),\n })),\n );\n }),\n );\n\n const stackUncached: PullRequestService[\"Service\"][\"stack\"] = (input, options) =>\n requireProject(input).pipe(\n Effect.flatMap((project) => {\n const read = project.api.getChangeRequestStack;\n if (read === undefined) return Effect.succeed(null);\n return read({\n cwd: project.project.workspaceRoot,\n repository: project.repository,\n host: project.host,\n number: input.number,\n includeDetails: options?.includeDetails !== false,\n }).pipe(\n Effect.mapError(toPullRequestError(\"stack\")),\n Effect.map((stack): PullRequestStack | null =>\n stack === null\n ? null\n : {\n id: stack.id,\n number: stack.number,\n url: stack.url,\n base: stack.base,\n layers: stack.layers.map((layer) => ({\n ...layer,\n number: layer.number,\n headBranch: layer.headBranch,\n state: layer.state,\n })),\n },\n ),\n );", + "checksum": "7dbba0fbc6ec7d3ef77e53411b052195310b643a05e25abf6d73a1509fcdeaca" }, "project-script-import-flow": { "id": "project-script-import-flow", @@ -1096,8 +1096,8 @@ "end": 61, "language": "tsx", "label": "Electron-aware history and authenticated React entry", - "code": "import React from \"react\";\nimport ReactDOM from \"react-dom/client\";\nimport { ClerkProvider } from \"@clerk/react\";\nimport { passkeys } from \"@clerk/electron/passkeys\";\nimport { ClerkProvider as ElectronClerkProvider } from \"@clerk/electron/react\";\nimport { createHashHistory, createBrowserHistory } from \"@tanstack/react-router\";\n\nimport \"./index.css\";\n\nimport { isElectron } from \"./env\";\nimport { ManagedRelayAuthProvider } from \"./cloud/managedAuth\";\nimport { hasCloudPublicConfig } from \"./cloud/publicConfig\";\nimport { getRouter } from \"./router\";\nimport {\n syncDocumentElectronPlatformClasses,\n syncDocumentWindowControlsOverlayClass,\n} from \"./lib/windowControlsOverlay\";\nimport { AppRoot } from \"./AppRoot\";\nimport { clerkAppearance } from \"./components/clerk/clerkAppearance\";\n\n// Electron loads the app from a file-backed shell, so hash history avoids path resolution issues.\nconst history = isElectron ? createHashHistory() : createBrowserHistory();\n\nconst router = getRouter(history);\n\nif (isElectron) {\n syncDocumentElectronPlatformClasses(navigator.platform);\n syncDocumentWindowControlsOverlayClass();\n}\n\nconst clerkPublishableKey = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY as string | undefined;\n\n// First Clerk UI build containing https://github.com/clerk/javascript/pull/9500.\nconst electronClerkUI = {\n __internal_clerkUIVersion: \"1.30.5-canary.v20260819050620\",\n};\n\nconst app = ;\n\nReactDOM.createRoot(document.getElementById(\"root\") as HTMLElement).render(\n \n {clerkPublishableKey && hasCloudPublicConfig() ? (\n isElectron ? (\n \n {app}\n \n ) : (\n \n {app}\n \n )\n ) : (\n app\n )}\n ,\n);", - "checksum": "469fcbadd4accb57b28f26730ae794e728586908360a345a42ebe3d8dd708f4d" + "code": "import React from \"react\";\nimport ReactDOM from \"react-dom/client\";\nimport { createHashHistory, createBrowserHistory } from \"@tanstack/react-router\";\n\nimport \"./index.css\";\n\nimport { isElectron } from \"./env\";\nimport { hasCloudPublicConfig } from \"./cloud/publicConfig\";\nimport { getRouter } from \"./router\";\nimport {\n syncDocumentElectronPlatformClasses,\n syncDocumentWindowControlsOverlayClass,\n} from \"./lib/windowControlsOverlay\";\nimport { AppRoot } from \"./AppRoot\";\nimport { clearChunkReloadGuard, reloadOnceForChunkLoadError } from \"./lib/chunkReloadGuard\";\n\n// Electron loads the app from a file-backed shell, so hash history avoids path resolution issues.\nconst history = isElectron ? createHashHistory() : createBrowserHistory();\n\nconst router = getRouter(history);\n\nif (isElectron) {\n syncDocumentElectronPlatformClasses(navigator.platform);\n syncDocumentWindowControlsOverlayClass();\n}\n\nconst clerkPublishableKey = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY as string | undefined;\n\n// A failed split-chunk fetch usually means the hashed assets went stale under\n// a deploy; one guarded reload picks up the fresh index.html.\nlet chunkLoadFailed = false;\nlet reloadScheduled = false;\nwindow.addEventListener(\"vite:preloadError\", (event) => {\n chunkLoadFailed = true;\n if (reloadOnceForChunkLoadError()) {\n reloadScheduled = true;\n event.preventDefault();\n }\n});\n\nconst app = ;\n\n// Managed auth is cloud-only, and the Electron Clerk provider bundles the full\n// clerk-js runtime. Loading only the selected runtime as a split chunk keeps\n// every Clerk byte out of the startup graph for local-mode users, and keeps\n// the bundled clerk-js out of the browser build entirely.\nconst managedAuthShellModule =\n clerkPublishableKey && hasCloudPublicConfig()\n ? isElectron\n ? import(\"./components/clerk/ElectronManagedAuthShell\")\n : import(\"./components/clerk/BrowserManagedAuthShell\")\n : null;\n\n// The index.html boot splash lives inside #root, and React's first commit\n// clears it. Resolve everything that first commit needs, the selected\n// managed-auth runtime and the initial route's split chunks, before\n// rendering, so the splash holds until real UI paints instead of dropping to\n// a blank window while chunks download.\nexport const startup = Promise.all([\n managedAuthShellModule?.then((module) => module.default) ?? null,\n router.load(),", + "checksum": "7ce8df0ec8a5014ed58937a20a9360804e3a46a4be53c63c589f521133d23c65" }, "web-app-root-providers": { "id": "web-app-root-providers", @@ -1116,8 +1116,8 @@ "end": 20, "language": "typescript", "label": "Generated route-tree router construction", - "code": "import { createRouter, RouterHistory } from \"@tanstack/react-router\";\n\nimport { routeTree } from \"./routeTree.gen\";\n\nexport function getRouter(history: RouterHistory) {\n return createRouter({\n routeTree,\n history,\n context: {},\n });\n}\n\nexport type AppRouter = ReturnType;\n\ndeclare module \"@tanstack/react-router\" {\n interface Register {\n router: AppRouter;\n }\n}\n", - "checksum": "0327fd93597b643fc4e5addbe87d4d74fa8a7310fca94e256569285cf8836979" + "code": "import { createRouter, RouterHistory } from \"@tanstack/react-router\";\n\nimport { routeTree } from \"./routeTree.gen\";\n\nexport function getRouter(history: RouterHistory) {\n return createRouter({\n routeTree,\n history,\n context: {},\n // Route components are split chunks (autoCodeSplitting in vite.config);\n // fetching them on hover/focus intent hides the load from the first\n // settings or pull-request navigation.\n defaultPreload: \"intent\",\n });\n}\n\nexport type AppRouter = ReturnType;\n\ndeclare module \"@tanstack/react-router\" {\n interface Register {", + "checksum": "cfb3a1459cfff80d1dd7bad1ea5ba6afb1af820ab4727d7bb3ad2cae4f728ea6" }, "web-environment-atom-catalog": { "id": "web-environment-atom-catalog", @@ -1146,8 +1146,8 @@ "end": 648, "language": "tsx", "label": "Keyed virtualized message timeline configuration", - "code": " if (rows.length === 0 && !isWorking) {\n if (hideEmptyPlaceholder) {\n return null;\n }\n return (\n
\n

Send a message to start the conversation.

\n
\n );\n }\n\n return (\n \n \n
\n \n ref={listRef}\n data={rows}\n keyExtractor={keyExtractor}\n getItemType={getItemType}\n renderItem={renderItem}\n estimatedItemSize={90}\n initialScrollAtEnd\n {...(anchoredEndSpace ? { anchoredEndSpace } : {})}\n contentInsetEndAdjustment={contentInsetEndAdjustment}\n maintainScrollAtEnd={\n anchoredEndSpace || !liveFollowEnabled || disclosureToggleSettling\n ? false\n : TIMELINE_MAINTAIN_SCROLL_AT_END\n }\n maintainVisibleContentPosition={maintainVisibleContentPosition}\n onScroll={handleScroll}\n className={cn(\n \"scrollbar-gutter-both h-full min-h-0 overflow-x-hidden overscroll-y-contain px-3 [overflow-anchor:none] sm:px-5\",\n topFadeEnabled && \"topbar-scroll-fade\",\n )}\n ListHeaderComponent={\n loadEarlier !== null ? (\n \n ) : topFadeEnabled ? (\n TIMELINE_LIST_FADE_HEADER\n ) : (\n TIMELINE_LIST_HEADER\n )\n }\n ListFooterComponent={TIMELINE_LIST_FOOTER}\n />\n {\n onManualNavigation();\n void listRef.current?.scrollToIndex({\n index: item.rowIndex,\n animated: true,\n viewOffset: 24,\n });\n }}\n />\n
\n
\n
\n );\n});\n\nfunction keyExtractor(item: MessagesTimelineRow) {\n return item.id;\n}\n\nfunction getItemType(item: MessagesTimelineRow) {\n return item.kind === \"message\" ? `message:${item.message.role}` : item.kind;\n}\n", - "checksum": "ad64d1e19d070fdd16b99a02269619f65b85df48b309607f10b58fb156a6832e" + "code": " expandedTurnIds,\n expandedWorkGroupIds,\n isWorking,\n activeTurnStartedAt,\n turnDiffSummaries,\n supportsConversationRollback,\n ]);\n const rows = useStableRows(rawRows);\n const minimapItems = useMemo(() => deriveTimelineMinimapItems(rows), [rows]);\n const [timelineViewportElement, setTimelineViewportElement] = useState(\n null,\n );\n const {\n target: readyCitationRequest,\n positioning: citationPositioning,\n onListLoad: onCitationListLoad,\n alwaysRender: citationAlwaysRender,\n } = useAssistantCitationTarget({\n request: citationRequest,\n entries: timelineEntries,\n rows,\n listRef,\n viewport: timelineViewportElement,\n historyLoading: citationHistoryLoading,\n loadEarlier,\n onExpandTurn: expandCitedTurn,\n onManualNavigation,\n });\n const [minimapHasPersistentGutter, setMinimapHasPersistentGutter] = useState(false);\n const [minimapHitStripWidth, setMinimapHitStripWidth] = useState(0);\n const [minimapCurrentIndex, setMinimapCurrentIndex] = useState(null);\n const handleAnchorReady = useCallback(\n (info: { anchorIndex: number | undefined }) => {\n if (anchorMessageId !== null && info.anchorIndex !== undefined) {\n onAnchorReady(anchorMessageId, info.anchorIndex);\n }\n },\n [anchorMessageId, onAnchorReady],\n );\n const anchoredEndSpace = useMemo(() => {\n const config = resolveChatListAnchoredEndSpace(\n rows,\n anchorMessageId,\n (row) => (row.kind === \"message\" && row.message.role === \"user\" ? row.message.id : null),\n { anchorOffset: CHAT_TIMELINE_ANCHOR_OFFSET },\n );\n return config ? { ...config, onReady: handleAnchorReady } : undefined;\n }, [anchorMessageId, handleAnchorReady, rows]);\n const timelineListFooter = useMemo(\n () => ,\n [anchoredEndSpace, contentInsetEndAdjustment],\n );\n\n const measureContentOverflow = useCallback(\n () =>\n timelineContentOverflowsViewport(listRef.current?.getState?.(), {\n composerInset: contentInsetEndAdjustment,\n anchorOffset: CHAT_TIMELINE_ANCHOR_OFFSET,\n }),\n [contentInsetEndAdjustment, listRef],\n );\n // LegendList lays rows out from layout effects, so a read on the next frame\n // sees the settled positions. One frame is shared across bursts of size\n // changes.\n const contentOverflowFrameRef = useRef(null);\n const cancelContentOverflowFrame = useCallback(() => {\n if (contentOverflowFrameRef.current !== null) {\n cancelAnimationFrame(contentOverflowFrameRef.current);\n contentOverflowFrameRef.current = null;\n }\n }, []);\n const reportContentOverflow = useCallback(() => {\n if (!onContentOverflowChange || contentOverflowFrameRef.current !== null) return;\n contentOverflowFrameRef.current = requestAnimationFrame(() => {\n contentOverflowFrameRef.current = null;\n onContentOverflowChange(measureContentOverflow());\n });\n }, [measureContentOverflow, onContentOverflowChange]);\n useEffect(() => cancelContentOverflowFrame, [cancelContentOverflowFrame]);", + "checksum": "0878401a52ac2f43176a657c1a0cdd05d2e86b2e9fa0b964188acb42a20393db" }, "web-timeline-row-structural-sharing": { "id": "web-timeline-row-structural-sharing", @@ -1156,8 +1156,8 @@ "end": 2113, "language": "tsx", "label": "Timeline row structural sharing for virtualized memo boundaries", - "code": "// so LegendList (and React) can skip re-rendering unchanged items.\n// ---------------------------------------------------------------------------\n\n/** Returns a structurally-shared copy of `rows`: for each row whose content\n * hasn't changed since last call, the previous object reference is reused. */\nfunction useStableRows(rows: MessagesTimelineRow[]): MessagesTimelineRow[] {\n const prevState = useRef({\n byId: new Map(),\n result: [],\n });\n\n return useMemo(() => {\n const nextState = computeStableMessagesTimelineRows(rows, prevState.current);\n prevState.current = nextState;\n return nextState.result;\n }, [rows]);\n}\n\n// ---------------------------------------------------------------------------", - "checksum": "74026e988767cb072249b769001abbf35b59542f6245e824e2fefdb2fbbe8ffc" + "code": " \n );\n}\n\nconst failedToolIconClassName = \"text-tool-error-icon/40\";\n\n/** Image icons and the gradient computer-use mark cannot take a currentColor\n * tint, so failed rows using them get a trailing x instead. */\nfunction toolIconAcceptsTint(\n iconName: WorkEntryIconName,\n toolIcon: ToolActivityIcon | undefined,\n): boolean {\n return toolIcon === undefined && iconName !== \"computer\";\n}\n\nfunction LiveActivityRow({\n label,\n iconName,\n toolIcon,", + "checksum": "5b040e86888d07b3b1561d91700b4d9e9903dfd75bfeaa0c8226fb7ad2f62e35" }, "web-sidebar-row-locality": { "id": "web-sidebar-row-locality", @@ -1166,8 +1166,8 @@ "end": 2252, "language": "tsx", "label": "Sidebar streaming row locality and callback references", - "code": " SNOOZED_SHELF_EXPANDED_KEY,\n false,\n Schema.Boolean,\n );\n const toggleSnoozedShelf = useCallback(\n () => setSnoozedShelfExpanded((value) => !value),\n [setSnoozedShelfExpanded],\n );\n const visibleSnoozedThreads = useMemo(() => {\n if (snoozedShelfExpanded) return snoozedThreads;\n // The open thread must never vanish behind the collapsed shelf: a\n // snoozed thread reached by route (deep link, open before snoozing\n // elsewhere) keeps its row — with highlight and wake affordance — same\n // exception the settled tail's \"Show more\" makes.\n if (routeThreadKey === null) return [];\n const routeThread = snoozedThreads.find(\n (thread) =>\n scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id)) === routeThreadKey,\n );\n return routeThread === undefined ? [] : [routeThread];\n }, [routeThreadKey, snoozedShelfExpanded, snoozedThreads]);\n\n const orderedThreads = useMemo(\n () => [...pinnedThreads, ...activeThreads, ...visibleSnoozedThreads, ...renderedSettledThreads],\n [pinnedThreads, activeThreads, visibleSnoozedThreads, renderedSettledThreads],\n );\n const orderedThreadKeys = useMemo(\n () =>\n orderedThreads.map((thread) =>\n scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id)),\n ),\n [orderedThreads],\n );\n // Rows call back into the click handler without carrying the ordered list as\n // a prop — a fresh array identity per shell update would defeat every row's\n // memoization. The ref keeps shift-range-select working against the list as\n // rendered at click time.\n const orderedThreadKeysRef = useRef(orderedThreadKeys);\n orderedThreadKeysRef.current = orderedThreadKeys;\n const threadByKey = useMemo(\n () =>\n new Map(\n orderedThreads.map(\n (thread) =>\n [scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id)), thread] as const,\n ),\n ),\n [orderedThreads],\n );\n // Handlers read these through refs: depending on per-update Map/Set\n // identities would give every row a fresh callback prop on each shell\n // event and defeat row memoization during streaming.\n const threadByKeyRef = useRef(threadByKey);", - "checksum": "0ca7fefcf227ef98a2738f7ce0001d6889c5b1e1e7a76701cdaab7ad1d3ed2ae" + "code": " onError: (error) => {\n toastManager.add(\n stackedThreadToast({\n type: \"error\",\n title: \"Failed to copy thread ID\",\n description: error instanceof Error ? error.message : \"An error occurred.\",\n }),\n );\n },\n });\n const newThreadContext = useHandleNewThread();\n const openAddProjectCommandPalette = useCallback(\n () => openCommandPalette({ open: \"add-project\" }),\n [],\n );\n const { environments } = useEnvironments();\n const primaryEnvironmentId = usePrimaryEnvironmentId();\n const clearSelection = useThreadSelectionStore((s) => s.clearSelection);\n const setSelectionAnchor = useThreadSelectionStore((s) => s.setAnchor);\n const toggleThreadSelection = useThreadSelectionStore((s) => s.toggleThread);\n const rangeSelectTo = useThreadSelectionStore((s) => s.rangeSelectTo);\n const markThreadUnread = useUiStateStore((s) => s.markThreadUnread);\n const markThreadVisited = useUiStateStore((s) => s.markThreadVisited);\n const acknowledgeWoke = useCallback(\n (threadRef: ScopedThreadRef, visitedAt: string) => {\n markThreadVisited(scopedThreadKey(threadRef), visitedAt);\n },\n [markThreadVisited],\n );\n const routeTarget = useParams({\n strict: false,\n select: (params) => resolveThreadRouteTarget(params),\n });\n const routeDraftThread = useComposerDraftStore((store) =>\n routeTarget?.kind === \"draft\" ? store.getDraftSession(routeTarget.draftId) : null,\n );\n const routeThreadRef = useMemo(\n () => resolveActiveThreadRouteRef(routeTarget, routeDraftThread),\n [routeDraftThread, routeTarget],\n );\n const routeThreadKey = routeThreadRef ? scopedThreadKey(routeThreadRef) : null;\n const routeTargetRef = useRef(routeTarget);\n routeTargetRef.current = routeTarget;\n // Post-settle navigation validates against the CURRENT route, not the one\n // captured when the settle started: if the user navigated elsewhere while\n // the command was in flight, completing it must not yank them away.\n const routeThreadKeyRef = useRef(routeThreadKey);\n routeThreadKeyRef.current = routeThreadKey;\n\n const environmentLabelById = useMemo(\n () =>\n new Map(\n environments.map((environment) => [environment.environmentId, environment.label] as const),", + "checksum": "d1766b548ad92872d1a73fed0d4e1a96beef22036f510747c8d8780625b90c28" }, "web-client-tracing-export": { "id": "web-client-tracing-export", @@ -1176,8 +1176,8 @@ "end": 96, "language": "typescript", "label": "Primary-environment OTLP client tracing lifecycle", - "code": "\nconst DEFAULT_EXPORT_INTERVAL_MS = 1_000;\nconst CLIENT_TRACING_RESOURCE = {\n serviceName: \"t3-web\",\n attributes: {\n \"service.runtime\": \"t3-web\",\n \"service.mode\": isElectron ? \"electron\" : \"browser\",\n \"service.version\": APP_VERSION,\n },\n} as const;\n\nconst delegateRuntimeLayer = Layer.mergeAll(\n primaryEnvironmentHttpLayer,\n OtlpExporter.layerFlusher,\n OtlpSerialization.layerJson,\n Layer.succeed(HttpClient.TracerDisabledWhen, () => true),\n);\n\nlet activeDelegate: Tracer.Tracer | null = null;\nlet activeRuntime: ManagedRuntime.ManagedRuntime | null = null;\nlet activeScope: Scope.Closeable | null = null;\nlet activeConfigKey: string | null = null;\nlet configurationGeneration = 0;\nlet pendingConfiguration = Promise.resolve();\n\nexport interface ClientTracingConfig {\n readonly exportIntervalMs?: number;\n}\n\nexport const ClientTracingLive = Layer.succeed(\n Tracer.Tracer,\n Tracer.make({\n span(options) {\n return activeDelegate?.span(options) ?? new Tracer.NativeSpan(options);\n },\n }),\n);\n\nexport function configureClientTracing(config: ClientTracingConfig = {}): Promise {\n if (config.exportIntervalMs === undefined && activeConfigKey !== null) {\n return pendingConfiguration;\n }\n pendingConfiguration = pendingConfiguration.finally(() => applyClientTracingConfig(config));\n return pendingConfiguration;\n}\n\nasync function applyClientTracingConfig(config: ClientTracingConfig): Promise {\n const otlpTracesUrl = resolvePrimaryEnvironmentHttpUrl(\"/api/observability/v1/traces\");\n const exportIntervalMs = Math.max(10, config.exportIntervalMs ?? DEFAULT_EXPORT_INTERVAL_MS);\n const nextConfigKey = `${otlpTracesUrl}|${exportIntervalMs}`;\n\n if (activeConfigKey === nextConfigKey && activeDelegate !== null) {\n return;\n }\n\n activeConfigKey = nextConfigKey;\n const generation = ++configurationGeneration;\n\n const previousRuntime = activeRuntime;\n const previousScope = activeScope;\n\n activeDelegate = null;\n activeRuntime = null;\n activeScope = null;\n\n await disposeTracerRuntime(previousRuntime, previousScope);\n\n const runtime = ManagedRuntime.make(delegateRuntimeLayer);\n const scope = runtime.runSync(Scope.make());\n\n const delegateResult = await settleAsyncResult(() =>\n runtime.runPromiseExit(\n Scope.provide(scope)(\n OtlpTracer.make({\n url: otlpTracesUrl,\n exportInterval: `${exportIntervalMs} millis`,\n resource: CLIENT_TRACING_RESOURCE,\n }),\n ),\n ),\n );\n if (delegateResult._tag === \"Failure\") {", - "checksum": "ea79ca0af49221763e5a8a63f3964a422be6f57b8bf234cea9115c51800954d5" + "code": "\nconst DEFAULT_EXPORT_INTERVAL_MS = 1_000;\nconst CLIENT_TRACING_RESOURCE = {\n serviceName: \"t3-web\",\n attributes: {\n \"service.runtime\": \"t3-web\",\n \"service.mode\": isElectron ? \"electron\" : \"browser\",\n \"service.version\": APP_VERSION,\n },\n} as const;\n\nconst delegateRuntimeLayer = Layer.mergeAll(\n primaryEnvironmentHttpLayer,\n OtlpExporter.layerFlusher,\n OtlpSerialization.layerJson,\n Layer.succeed(HttpClient.TracerDisabledWhen, () => true),\n);\n\nlet activeDelegate: Tracer.Tracer | null = null;\nlet activeRuntime: ManagedRuntime.ManagedRuntime | null = null;\nlet activeScope: Scope.Closeable | null = null;\nlet activeConfigKey: string | null = null;\nlet configurationGeneration = 0;\nlet pendingConfiguration = Promise.resolve();\n\nexport interface ClientTracingConfig {\n readonly exportIntervalMs?: number;\n}\n\nexport function configureClientTracing(config: ClientTracingConfig = {}): Promise {\n if (config.exportIntervalMs === undefined && activeConfigKey !== null) {\n return pendingConfiguration;\n }\n pendingConfiguration = pendingConfiguration.finally(() => applyClientTracingConfig(config));\n return pendingConfiguration;\n}\n\nasync function applyClientTracingConfig(config: ClientTracingConfig): Promise {\n const otlpTracesUrl = resolvePrimaryEnvironmentHttpUrl(\"/api/observability/v1/traces\");\n const exportIntervalMs = Math.max(10, config.exportIntervalMs ?? DEFAULT_EXPORT_INTERVAL_MS);\n const nextConfigKey = `${otlpTracesUrl}|${exportIntervalMs}`;\n\n if (activeConfigKey === nextConfigKey && activeDelegate !== null) {\n return;\n }\n\n activeConfigKey = nextConfigKey;\n const generation = ++configurationGeneration;\n\n const previousRuntime = activeRuntime;\n const previousScope = activeScope;\n\n activeDelegate = null;\n activeRuntime = null;\n activeScope = null;\n\n await disposeTracerRuntime(previousRuntime, previousScope);\n\n const runtime = ManagedRuntime.make(delegateRuntimeLayer);\n const scope = runtime.runSync(Scope.make());\n\n const delegateResult = await settleAsyncResult(() =>\n runtime.runPromiseExit(\n Scope.provide(scope)(\n OtlpTracer.make({\n url: otlpTracesUrl,\n exportInterval: `${exportIntervalMs} millis`,\n resource: CLIENT_TRACING_RESOURCE,\n }),\n ),\n ),\n );\n if (delegateResult._tag === \"Failure\") {\n await disposeTracerRuntime(runtime, scope);\n\n if (generation === configurationGeneration) {\n const error = squashAtomCommandFailure(delegateResult);\n const tracesUrl = new URL(otlpTracesUrl);\n console.warn(\"Failed to configure client tracing exporter\", {\n scheme: tracesUrl.protocol.replace(/:$/, \"\"),\n host: tracesUrl.hostname,\n port: tracesUrl.port || undefined,", + "checksum": "6177c54d216d21e334847fcf1e10654cb8985fb27e0264d531a115e88d3a6c5f" }, "web-sidebar-optimistic-pinning": { "id": "web-sidebar-optimistic-pinning", @@ -1186,8 +1186,8 @@ "end": 2610, "language": "tsx", "label": "Optimistic pin-order override and canonical surrender rules", - "code": " // and holding it would launder a stale order into later drags.\n const pinnedDndSensors = useSensors(\n useSensor(PointerSensor, { activationConstraint: { distance: 6 } }),\n );\n const [optimisticPinnedOrder, setOptimisticPinnedOrder] = useState<{\n readonly order: readonly string[];\n /** pinOrderKey per thread as of the drop — the baseline that tells a\n concurrent client's write apart from one of our own landing. */\n readonly keysAtDrop: ReadonlyMap;\n /** The keys this drop writes (one per planned assignment). The\n override holds until all of them appear in canonical state. */\n readonly assignedKeys: ReadonlyMap;\n } | null>(null);\n const orderedPinnedThreads = useMemo(() => {\n if (optimisticPinnedOrder === null) return pinnedThreads;\n return orderItemsByPreferredIds({\n items: pinnedThreads,\n preferredIds: optimisticPinnedOrder.order,\n getId: (thread) => scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id)),\n });\n }, [optimisticPinnedOrder, pinnedThreads]);\n useEffect(() => {\n if (optimisticPinnedOrder === null) return;\n const canonical = pinnedThreads.filter((thread) =>\n reorderablePinnedKeys.has(scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id))),\n );\n const canonicalKeys = canonical.map((thread) =>\n scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id)),\n );\n // The override represents one drop against one snapshot of the world.\n // Release it when the world moves on: membership changed (pin/unpin/\n // snooze/wake — the override can't say where members it never saw\n // belong), a key changed to something we did NOT write (a concurrent\n // client's reorder that must win), every key we wrote has landed, or\n // canonical already matches. Releasing on the FIRST landed key instead\n // of the last exposes the half-written order mid-materialization and\n // the block visibly reshuffles once per write.\n const membershipChanged =\n canonicalKeys.length !== optimisticPinnedOrder.order.length ||\n canonicalKeys.some((key) => !optimisticPinnedOrder.order.includes(key));\n const foreignKeyLanded = canonical.some((thread, index) => {\n const threadKey = canonicalKeys[index]!;\n const currentKey = thread.pinOrderKey ?? null;\n if (currentKey === optimisticPinnedOrder.keysAtDrop.get(threadKey)) return false;\n return currentKey !== optimisticPinnedOrder.assignedKeys.get(threadKey);\n });\n const currentKeyByThreadKey = new Map(\n canonical.map((thread, index) => [canonicalKeys[index]!, thread.pinOrderKey ?? null]),\n );\n const allAssignmentsLanded = [...optimisticPinnedOrder.assignedKeys].every(\n ([threadKey, orderKey]) => currentKeyByThreadKey.get(threadKey) === orderKey,\n );\n const orderConfirmed =\n !membershipChanged &&\n canonicalKeys.every((key, index) => key === optimisticPinnedOrder.order[index]);\n if (membershipChanged || foreignKeyLanded || allAssignmentsLanded || orderConfirmed) {\n setOptimisticPinnedOrder(null);", - "checksum": "587d2e08f3d5f4a8193054fcd9c8afa66b3c80d57456718f2adcdee4ffa71836" + "code": " ? settled\n : active\n ).push(\n optimisticDrop.clearsSnooze\n ? projected\n : { ...projected, snoozedAt: thread.snoozedAt, snoozedUntil: thread.snoozedUntil },\n );\n } else if (supportsSnooze && effectiveSnoozed(thread, { now: preciseNow })) {\n // Snooze outranks settlement and pinning until the thread wakes.\n snoozed.push(thread);\n } else if (supportsSettlement && thread.settledOverride === \"settled\") {\n settled.push(thread);\n } else if (thread.pinnedAt != null) {\n pinned.push(thread);\n } else {\n active.push(thread);\n }\n }\n // One shared rule on every platform (see sortPinnedThreadsByOrderKey):\n // user-arranged keys first, keyless threads in creation order below.\n // Server capability only gates DRAGGING — it must not influence the\n // sort, or mixed-version fleets would render different pinned orders on\n // web and mobile from the same data.\n const sortedPinned = sortPinnedThreadsForSidebar(pinned);\n const sortedActive = sortThreadsForSidebar(active);\n return {\n pinnedThreads:\n optimisticDrop?.section !== \"pinned\" || optimisticDrop.order === null\n ? sortedPinned\n : orderItemsByPreferredIds({\n items: sortedPinned,\n preferredIds: optimisticDrop.order,\n getId: (thread) => scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id)),\n }),\n draggableThreadKeys: draggable,\n activeReorderableThreadKeys: activeReorderable,\n activeThreads:\n optimisticDrop?.section !== \"active\" || optimisticDrop.order === null\n ? sortedActive\n : orderItemsByPreferredIds({\n items: sortedActive,\n preferredIds: optimisticDrop.order,\n getId: (thread) => scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id)),\n }),\n // Soonest wake first: \"what comes back next\" is the shelf's question.\n snoozedThreads: snoozed.toSorted(\n (left, right) =>\n firstValidTimestampMs(left.snoozedUntil ?? null) -\n firstValidTimestampMs(right.snoozedUntil ?? null),\n ),\n settledThreads: sortSettledThreadsForSidebar(settled),\n snoozeNow: preciseNow,\n };\n }, [nowMinute, optimisticDrop, scopedProjectKeys, serverConfigs, snoozeWakeTick, threads]);\n\n const threadSearchInputRef = useRef(null);\n const [threadSearchQuery, setThreadSearchQuery] = useState(\"\");", + "checksum": "ef261da338e3ca6c665c034e0ea65b870e8af370e422fc53ec07340db4859030" }, "mobile-app-root-composition": { "id": "mobile-app-root-composition", @@ -1196,8 +1196,8 @@ "end": 106, "language": "tsx", "label": "Mobile root Atom, provider, navigation, and native host composition", - "code": "export default function App() {\n return (\n \n \n \n \n \n \n \n );\n}\n\nfunction AppContent() {\n const { themeAppearance } = useAppearancePreferences();\n const statusBarBg = useThemeColor(\"--color-status-bar\");\n const navigationTheme = useMobileNavigationTheme(themeAppearance);\n\n return (\n <>\n \n \n \n \n \n {/* The navigation theme drives the NATIVE header appearance: native-stack\n forwards `dark` as the nav bar's overrideUserInterfaceStyle. Without\n this, React Navigation defaults to its light theme and every native\n header (glass buttons, title, materials) is forced light even when\n the system is in dark mode. */}\n {/* Blur target for Android dropdown backdrops — see appBlurTarget.ts. */}\n \n \n \n \n \n \n {/* Anchored-menu overlays render here — in-window, so the\n keyboard stays up while a dropdown is open. */}\n \n \n \n ", - "checksum": "46115b33d72272b987a9b97658d848d7e5fd73e991f885c285a6c2ff369725fd" + "code": "export default function App() {\n return (\n \n \n \n \n \n \n \n );\n}\n\nfunction AppContent() {\n const { themeAppearance } = useAppearancePreferences();\n const navigationTheme = useMobileNavigationTheme();\n\n return (\n <>\n \n \n \n \n \n {/* The navigation theme drives the NATIVE header appearance: native-stack\n forwards `dark` as the nav bar's overrideUserInterfaceStyle. Without\n this, React Navigation defaults to its light theme and every native\n header (glass buttons, title, materials) is forced light even when\n the system is in dark mode. */}\n {/* Blur target for Android dropdown backdrops — see appBlurTarget.ts. */}\n \n \n \n \n \n \n \n {/* Anchored-menu overlays render here — in-window, so the\n keyboard stays up while a dropdown is open. */}\n \n \n \n \n ", + "checksum": "b62dccc1b4fd85c152372535ac9a31d83b58a3b23d32c541678bbb4214b226ad" }, "mobile-connection-runtime-layer": { "id": "mobile-connection-runtime-layer", @@ -1206,8 +1206,8 @@ "end": 45, "language": "typescript", "label": "Shared connection and snapshot layers with mobile platform services", - "code": "import { Connection } from \"@t3tools/client-runtime/connection\";\nimport { shellSnapshotLoaderLayer } from \"@t3tools/client-runtime/state/shell\";\nimport { threadSnapshotLoaderLayer } from \"@t3tools/client-runtime/state/threads\";\nimport * as Layer from \"effect/Layer\";\nimport { Atom } from \"effect/unstable/reactivity\";\n\nimport { runtimeContextLayer } from \"../lib/runtime\";\nimport {\n mobileBackgroundActivityObserverLayer,\n mobileBackgroundActivityReporterLayer,\n} from \"./background-activity\";\nimport { connectionPlatformLayer } from \"./platform\";\n\nconst providedConnectionPlatformLayer = connectionPlatformLayer.pipe(\n Layer.provide(runtimeContextLayer),\n);\n\nconst snapshotLoaderLayer = Layer.merge(threadSnapshotLoaderLayer, shellSnapshotLoaderLayer);\n\ntype ConnectionLayerSource =\n | typeof Connection.layer\n | typeof snapshotLoaderLayer\n | typeof runtimeContextLayer\n | typeof connectionPlatformLayer\n | typeof mobileBackgroundActivityObserverLayer\n | typeof mobileBackgroundActivityReporterLayer;\n\nconst providedClientConnectionLayer = Layer.merge(Connection.layer, snapshotLoaderLayer).pipe(\n Layer.provideMerge(\n Layer.mergeAll(\n runtimeContextLayer,\n providedConnectionPlatformLayer,\n mobileBackgroundActivityObserverLayer,\n ),\n ),\n);\n\nconst connectionLayer = mobileBackgroundActivityReporterLayer.pipe(\n Layer.provideMerge(providedClientConnectionLayer),\n);\n\nexport const connectionAtomRuntime: Atom.AtomRuntime<\n Layer.Success,\n Layer.Error\n> = Atom.runtime(connectionLayer);", - "checksum": "b65580b503a79da712a1398f7ef6988f434ec21193c7223eca5edbf597857db4" + "code": "import { Connection } from \"@t3tools/client-runtime/connection\";\nimport { shellSnapshotLoaderLayer } from \"@t3tools/client-runtime/state/shell\";\nimport { threadSnapshotLoaderLayer } from \"@t3tools/client-runtime/state/threads\";\nimport * as Layer from \"effect/Layer\";\nimport { Atom } from \"effect/unstable/reactivity\";\n\nimport type { FoundationHotModule } from \"../lib/foundation-fast-refresh\";\nimport { hotSwappableAtomRuntime } from \"../lib/hot-swappable-atom-runtime\";\nimport { runtimeContextLayer } from \"../lib/runtime\";\nimport { appAtomRegistry } from \"../state/atom-registry\";\nimport {\n mobileBackgroundActivityObserverLayer,\n mobileBackgroundActivityReporterLayer,\n} from \"./background-activity\";\nimport { connectionPlatformLayer } from \"./platform\";\n\ndeclare const module: { readonly hot?: FoundationHotModule } | undefined;\n\nconst providedConnectionPlatformLayer = connectionPlatformLayer.pipe(\n Layer.provide(runtimeContextLayer),\n);\n\nconst snapshotLoaderLayer = Layer.merge(threadSnapshotLoaderLayer, shellSnapshotLoaderLayer);\n\ntype ConnectionLayerSource =\n | typeof Connection.layer\n | typeof snapshotLoaderLayer\n | typeof runtimeContextLayer\n | typeof connectionPlatformLayer\n | typeof mobileBackgroundActivityObserverLayer\n | typeof mobileBackgroundActivityReporterLayer;\n\nconst providedClientConnectionLayer = snapshotLoaderLayer.pipe(\n Layer.provideMerge(\n Connection.layerWithOptions({ usageLimitSources: true, usageLimitsCommand: true }),\n ),\n Layer.provideMerge(\n Layer.mergeAll(\n runtimeContextLayer,\n providedConnectionPlatformLayer,\n mobileBackgroundActivityObserverLayer,\n ),\n ),\n);\n", + "checksum": "1b091e795ecfe1a35db98a547dd458eb3a45b2606e620801d340835843f3a1f8" }, "mobile-persistence-schema": { "id": "mobile-persistence-schema", @@ -1216,8 +1216,8 @@ "end": 275, "language": "typescript", "label": "SQLite client cache and preference schema with versioned migration", - "code": " yield* Effect.tryPromise({\n try: async () => {\n await database.execAsync(\"PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON;\");\n const schema = await database.getFirstAsync<{ readonly user_version: number }>(\n \"PRAGMA user_version\",\n );\n await database.withExclusiveTransactionAsync(async (transaction) => {\n await transaction.execAsync(`\n CREATE TABLE IF NOT EXISTS client_cache (\n environment_id TEXT NOT NULL,\n kind TEXT NOT NULL,\n cache_key TEXT NOT NULL,\n schema_version INTEGER NOT NULL,\n payload TEXT NOT NULL,\n updated_at INTEGER NOT NULL,\n PRIMARY KEY (environment_id, kind, cache_key)\n ) WITHOUT ROWID;\n\n CREATE INDEX IF NOT EXISTS client_cache_environment_updated\n ON client_cache (environment_id, updated_at DESC);\n\n CREATE TABLE IF NOT EXISTS client_preferences (\n singleton INTEGER PRIMARY KEY NOT NULL CHECK (singleton = 1),\n payload TEXT NOT NULL,\n updated_at INTEGER NOT NULL\n );\n `);\n });\n if ((schema?.user_version ?? 0) < DATABASE_SCHEMA_VERSION) {\n const migrated = await migrateLegacyFileCaches(database);\n if (migrated) {\n await database.execAsync(`PRAGMA user_version = ${DATABASE_SCHEMA_VERSION};`);\n }\n }", - "checksum": "4c4f4137e909027a14ac1ec6e1724cfb765961790354b87747351df46910ecb0" + "code": " Effect.tryPromise({\n try: async () => {\n const SQLite = await import(\"expo-sqlite\");\n return SQLite.openDatabaseAsync(DATABASE_NAME);\n },\n catch: databaseError(\"open\"),\n }),\n (openDatabase) => Effect.promise(() => openDatabase.closeAsync()).pipe(Effect.ignore),\n );\n\n yield* Effect.tryPromise({\n try: async () => {\n await database.execAsync(\"PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON;\");\n const schema = await database.getFirstAsync<{ readonly user_version: number }>(\n \"PRAGMA user_version\",\n );\n await database.withExclusiveTransactionAsync(async (transaction) => {\n await transaction.execAsync(`\n CREATE TABLE IF NOT EXISTS client_cache (\n environment_id TEXT NOT NULL,\n kind TEXT NOT NULL,\n cache_key TEXT NOT NULL,\n schema_version INTEGER NOT NULL,\n payload TEXT NOT NULL,\n updated_at INTEGER NOT NULL,\n PRIMARY KEY (environment_id, kind, cache_key)\n ) WITHOUT ROWID;\n\n CREATE INDEX IF NOT EXISTS client_cache_environment_updated\n ON client_cache (environment_id, updated_at DESC);\n\n CREATE TABLE IF NOT EXISTS client_preferences (\n singleton INTEGER PRIMARY KEY NOT NULL CHECK (singleton = 1),\n payload TEXT NOT NULL,", + "checksum": "b910bd34c836a4ead9b25b6ac3997fa371da81af3ff686b76d677475b30a7435" }, "mobile-draft-update-flush": { "id": "mobile-draft-update-flush", @@ -1226,8 +1226,8 @@ "end": 229, "language": "typescript", "label": "Draft persistence flush before JavaScript runtime teardown", - "code": "/**\n * Lands any debounced or in-flight draft write before the JS runtime is torn\n * down (app update restart), so the freshest draft state survives it. A write\n * failure propagates so the caller can decide whether the restart may proceed.\n */\nexport async function flushComposerDrafts(): Promise {\n // An edit during an awaited write schedules another debounced write, so\n // keep landing snapshots until no debounce is pending after a queue drain.\n do {\n while (persistTimer !== null) {\n clearTimeout(persistTimer);\n persistTimer = null;\n await persistenceQueue.run(() =>\n writePersistedComposerDrafts(appAtomRegistry.get(composerDraftsAtom)),\n );\n }\n await persistenceQueue.run(() => Promise.resolve());\n } while (persistTimer !== null);", - "checksum": "eae8cd0dee12f66549b90f8f01a1eca248189e6b8a1c0c5d7af1bc9ea281c576" + "code": " draft.modelSelection === undefined &&\n draft.runtimeMode === undefined &&\n draft.interactionMode === undefined &&\n draft.workspaceSelection === undefined\n );\n}\n\n/**\n * Writes a draft back, dropping it once empty. A new-task draft keeps its\n * entry while the composer is bound to it (the project stamp is what the\n * composer binds to); the persist sweep still leaves empty ones off disk.\n */\nfunction withComposerDraft(\n current: Record,\n draftKey: string,\n draft: ComposerDraft,\n): Record {\n if (isEmptyDraft(draft) && draft.project === undefined) {", + "checksum": "47eeb81dc71024a95486b2b4aa006679401091452efb7d195507dc4451de96f2" }, "mobile-outbox-optimistic-durability": { "id": "mobile-outbox-optimistic-durability", @@ -1236,8 +1236,8 @@ "end": 124, "language": "typescript", "label": "Optimistic enqueue, durable rollback, and pre-delivery confirmation", - "code": " // The queued atom drives the composer's immediate \"queued\" feedback, so it\n // is published synchronously; the durable write happens behind it and rolls\n // the message back out if it fails (durability only matters for crash\n // recovery, not for the in-session queue).\n const enqueue = (message: QueuedThreadMessage): Promise => {\n setMessages([\n ...currentMessages().filter((candidate) => candidate.messageId !== message.messageId),\n message,\n ]);\n return serialize(async () => {\n try {\n await options.storage.write(message);\n } catch (cause) {\n // Roll back by reference, not messageId: a retry enqueue with the same\n // id may have optimistically replaced this attempt while the write was\n // in flight, and its entry must survive this attempt's failure.\n setMessages(currentMessages().filter((candidate) => candidate !== message));\n throw new ThreadOutboxManagerError({\n operation: \"enqueue\",\n environmentId: message.environmentId,\n threadId: message.threadId,\n messageId: message.messageId,\n cause,\n });\n }\n });\n };\n\n // Resolves once all pending mutations (including any in-flight enqueue\n // write) have settled, reporting whether the message is still queued. The\n // drain awaits this before dispatching so a message whose durable write\n // later fails can never have been delivered first.\n const confirmQueued = (message: QueuedThreadMessage): Promise =>\n serialize(async () => currentMessages().some((candidate) => candidate === message));", - "checksum": "616baa8c3ce65855265d73b8db622400fa95ca5b49951b6aa112a214a55dc21c" + "code": " if (recovered.length > 0) setMessages([...recovered, ...current]);\n if (result.errors.length > 0) {\n throw new AggregateError(result.errors, \"Some queued messages could not be read.\");\n }\n return true;\n }).catch((cause) => {\n loadPromise = null;\n warn(\n \"[thread-outbox] failed to load persisted messages\",\n new ThreadOutboxManagerError({\n operation: \"load\",\n environmentId: null,\n threadId: null,\n messageId: null,\n cause,\n }),\n );\n return false;\n });\n return loadPromise;\n };\n\n // The queued atom drives the composer's immediate \"queued\" feedback, so it\n // is published synchronously; the durable write happens behind it and rolls\n // the message back out if it fails (durability only matters for crash\n // recovery, not for the in-session queue).\n const enqueue = (message: QueuedThreadMessage): Promise => {\n bumpRevision(message.messageId);\n setMessages([\n ...currentMessages().filter((candidate) => candidate.messageId !== message.messageId),\n message,\n ]);\n return serialize(async () => {\n try {", + "checksum": "5275ba22e3cec4e5f76620c81b8f2b231da025eb0a9be8157320f5f8a6af053a" }, "mobile-outbox-delivery-policy": { "id": "mobile-outbox-delivery-policy", @@ -1246,8 +1246,8 @@ "end": 226, "language": "typescript", "label": "Outbox existence guards, capped backoff, and failure classification", - "code": "export function threadOutboxRetryDelayMs(attempt: number): number {\n return Math.min(1_000 * 2 ** Math.max(0, attempt - 1), THREAD_OUTBOX_MAX_RETRY_DELAY_MS);\n}\n\nexport type ThreadOutboxDeliveryAction = \"wait\" | \"remove\" | \"send\";\n\nexport function resolveThreadOutboxDeliveryAction(input: {\n readonly isCreation: boolean;\n readonly threadExists: boolean;\n readonly shellStatus: EnvironmentShellStatus;\n readonly environmentConnected: boolean;\n readonly threadBusy: boolean;\n}): ThreadOutboxDeliveryAction {\n if (input.isCreation) {\n // A pending task creates its thread on delivery. If the thread already\n // exists the creation command went through and only cleanup remains.\n if (input.threadExists) {\n return \"remove\";\n }\n // Wait for the shell to be live before sending: until the thread list has\n // synchronized, a previously delivered creation whose cleanup failed would\n // look missing and get re-issued, duplicating the thread.\n return input.environmentConnected && input.shellStatus === \"live\" ? \"send\" : \"wait\";\n }\n if (!input.threadExists) {\n return input.shellStatus === \"live\" ? \"remove\" : \"wait\";\n }\n return input.environmentConnected ? \"send\" : \"wait\";\n}\n\n/**\n * A queued creation can only be dispatched once its payload would pass server\n * validation; incomplete payloads stay pending until the user edits them.\n */\nexport function isQueuedThreadCreationSendable(message: QueuedThreadMessage): boolean {\n if (!message.creation) {\n return false;\n }\n if (message.text.trim().length === 0 || message.modelSelection === undefined) {\n return false;\n }\n return message.creation.workspaceMode !== \"worktree\" || Boolean(message.creation.branch);\n}\n\nfunction errorMessage(error: unknown): string | null {\n if (error instanceof Error) {\n return error.message;\n }\n if (typeof error === \"object\" && error !== null && \"message\" in error) {\n return typeof error.message === \"string\" ? error.message : null;\n }\n return typeof error === \"string\" ? error : null;\n}\n\nexport function shouldRetryThreadOutboxDelivery(error: unknown): boolean {\n if (\n typeof error === \"object\" &&\n error !== null &&\n \"_tag\" in error &&\n error._tag === \"ConnectionTransientError\"\n ) {\n return true;\n }\n return isTransportConnectionErrorMessage(errorMessage(error));\n}\n\nexport type ThreadOutboxCommandStage = \"settings-sync\" | \"start-turn\";\nexport type ThreadOutboxFailureAction = \"retry\" | \"discard\";\n\nexport function resolveThreadOutboxFailureAction(input: {\n readonly stage: ThreadOutboxCommandStage;\n readonly error: unknown;\n readonly interrupted: boolean;\n}): ThreadOutboxFailureAction {\n if (\n input.stage === \"settings-sync\" ||\n input.interrupted ||\n shouldRetryThreadOutboxDelivery(input.error)\n ) {\n return \"retry\";\n }\n return \"discard\";", - "checksum": "d2969b0b428827e9c62ee69aaa4353ae90a097d523872d145dc0650edd68d6cf" + "code": " const threadKey = scopedThreadKey(message.environmentId, message.threadId);\n (grouped[threadKey] ??= []).push(message);\n }\n for (const queue of Object.values(grouped)) {\n queue.sort((left, right) => left.createdAt.localeCompare(right.createdAt));\n }\n return grouped;\n}\n\nexport function flattenQueuedThreadMessages(\n queues: Record>,\n): ReadonlyArray {\n return Object.values(queues).flat();\n}\n\nexport function threadOutboxRetryDelayMs(attempt: number): number {\n return Math.min(1_000 * 2 ** Math.max(0, attempt - 1), THREAD_OUTBOX_MAX_RETRY_DELAY_MS);\n}\n\nexport type ThreadOutboxDeliveryAction = \"wait\" | \"remove\" | \"send\";\n\nexport function resolveThreadOutboxDeliveryAction(input: {\n readonly isCreation: boolean;\n readonly threadExists: boolean;\n readonly shellStatus: EnvironmentShellStatus;\n readonly environmentConnected: boolean;\n readonly threadBusy: boolean;\n}): ThreadOutboxDeliveryAction {\n if (input.isCreation) {\n // A pending task creates its thread on delivery. If the thread already\n // exists the creation command went through and only cleanup remains.\n if (input.threadExists) {\n return \"remove\";\n }\n // Wait for the shell to be live before sending: until the thread list has\n // synchronized, a previously delivered creation whose cleanup failed would\n // look missing and get re-issued, duplicating the thread.\n return input.environmentConnected && input.shellStatus === \"live\" ? \"send\" : \"wait\";\n }\n if (!input.threadExists) {\n return input.shellStatus === \"live\" ? \"remove\" : \"wait\";\n }\n return input.environmentConnected ? \"send\" : \"wait\";\n}\n\nexport type ThreadOutboxDispatchStep =\n | { readonly step: \"wait\" }\n | { readonly step: \"remove\" }\n | { readonly step: \"retry\" }\n | { readonly step: \"restore\"; readonly reason: string }\n | { readonly step: \"send\" };\n\n/**\n * Wait for provider and file capabilities before sending. Cleanup does not\n * need config: a creation whose thread exists, or a message whose thread is\n * gone, can still be removed while config loads.\n */\nexport function resolveThreadOutboxDispatchStep(input: {\n readonly deliveryAction: ThreadOutboxDeliveryAction;\n readonly fileAttachments: ReadonlyArray<{ readonly name: string; readonly sizeBytes: number }>;\n /** Null while the environment's server config has not synced yet. */\n readonly serverConfig: { readonly maxFileUploadBytes: number | undefined } | null;\n}): ThreadOutboxDispatchStep {\n if (input.deliveryAction !== \"send\") {\n return { step: input.deliveryAction };\n }\n if (input.serverConfig === null) {\n return { step: \"retry\" };\n }\n if (input.fileAttachments.length === 0) {\n return { step: \"send\" };\n }\n const maxBytes = input.serverConfig.maxFileUploadBytes;\n if (maxBytes === undefined) {\n return { step: \"restore\", reason: \"This server does not support file attachments.\" };\n }\n const effectiveMaxBytes = clampFileAttachmentUploadBytes(maxBytes);\n const oversized = input.fileAttachments.find(\n (attachment) => attachment.sizeBytes > effectiveMaxBytes,\n );\n return oversized\n ? { step: \"restore\", reason: fileAttachmentTooLargeMessage(oversized.name, effectiveMaxBytes) }", + "checksum": "4be61750e76e852cc913a8ff8338a961e33b79e53a843eb3462a6a83043132c5" }, "mobile-share-commit-boundary": { "id": "mobile-share-commit-boundary", @@ -1256,8 +1256,8 @@ "end": 124, "language": "typescript", "label": "Durable share inbox write before native handoff acknowledgement", - "code": "/**\n * Serializes every durable inbox mutation. This prevents a stale storage load\n * or a foreground refresh from restoring an item after it has been consumed.\n */\nexport class IncomingShareInbox {\n private readonly operations = new SerializedAsyncQueue();\n\n constructor(private readonly dependencies: IncomingShareInboxDependencies) {}\n\n private runExclusive(operation: () => Promise): Promise {\n return this.operations.run(operation);\n }\n\n private clearNativePayloads(): void {\n try {\n this.dependencies.clearPayloads();\n } catch (error) {\n this.dependencies.onClearError?.(error);\n }\n }\n\n private async cleanup(operation: () => Promise): Promise {\n try {\n await operation();\n } catch (error) {\n this.dependencies.onCleanupError?.(error);\n }\n }\n\n refresh(options: { readonly ingestNative: boolean }): Promise> {\n return this.runExclusive(async () => {\n const loaded = await this.dependencies.loadDrafts();\n const persisted = sortAndDedupeIncomingShares(loaded);\n if (!options.ingestNative) {\n return persisted;\n }\n\n const payloads = this.dependencies.getPayloads();\n if (payloads.length === 0) {\n return persisted;\n }\n\n // A share extension payload remains available until the containing app\n // acknowledges it. Use a content-derived id so a crash after the durable\n // write but before acknowledgement reuses the same inbox item.\n const shareId = await this.dependencies.idForPayloads(payloads);\n if (loaded.some((draft) => draft.id === shareId)) {\n if (this.dependencies.cleanupReplayedPayloads) {\n await this.cleanup(() => this.dependencies.cleanupReplayedPayloads!(payloads));\n }\n this.clearNativePayloads();\n return persisted;\n }\n\n const built = await this.dependencies.buildDraft({\n payloads,\n id: shareId,\n createdAt: this.dependencies.now(),\n });\n const { draft } = built;\n if (!hasIncomingShareContent(draft)) {\n // Unsupported native payloads cannot become actionable on retry and\n // would otherwise reopen the project picker on every foreground.\n await this.cleanup(built.cleanup);\n this.clearNativePayloads();\n throw new Error(\n draft.warnings[0] ?? \"The shared content is not supported by the composer.\",\n );\n }\n\n // The durable inbox write is the transaction boundary. Never clear the\n // native handoff first: a process termination must leave one recoverable\n // copy on one side of the boundary.\n await this.dependencies.writeDraft(draft);\n await this.cleanup(built.cleanup);\n this.clearNativePayloads();\n return sortAndDedupeIncomingShares([draft, ...persisted]);\n });\n }", - "checksum": "0677f4b5f578d272ee74ec62dbb82f10bfbf9f0a1e7fb08a7aebd371b20e891e" + "code": "\n/**\n * Serializes every durable inbox mutation. This prevents a stale storage load\n * or a foreground refresh from restoring an item after it has been consumed.\n */\nexport class IncomingShareInbox {\n private readonly operations = new SerializedAsyncQueue();\n\n constructor(private readonly dependencies: IncomingShareInboxDependencies) {}\n\n private runExclusive(operation: () => Promise): Promise {\n return this.operations.run(operation);\n }\n\n private clearNativePayloads(): void {\n try {\n this.dependencies.clearPayloads();\n } catch (error) {\n this.dependencies.onClearError?.(error);\n }\n }\n\n private async cleanup(operation: () => Promise): Promise {\n try {\n await operation();\n } catch (error) {\n this.dependencies.onCleanupError?.(error);\n }\n }\n\n refresh(options: { readonly ingestNative: boolean }): Promise> {\n return this.runExclusive(async () => {\n const loaded = await this.dependencies.loadDrafts();\n const persisted = sortAndDedupeIncomingShares(loaded);\n if (!options.ingestNative) {\n return persisted;\n }\n\n const payloads = this.dependencies.getPayloads();\n if (payloads.length === 0) {\n return persisted;\n }\n\n // A share extension payload remains available until the containing app\n // acknowledges it. Use a content-derived id so a crash after the durable\n // write but before acknowledgement reuses the same inbox item.\n const shareId = await this.dependencies.idForPayloads(payloads);\n if (loaded.some((draft) => draft.id === shareId)) {\n if (this.dependencies.cleanupReplayedPayloads) {\n await this.cleanup(() => this.dependencies.cleanupReplayedPayloads!(payloads));\n }\n this.clearNativePayloads();\n return persisted;\n }\n\n const built = await this.dependencies.buildDraft({\n payloads,\n id: shareId,\n createdAt: this.dependencies.now(),\n });\n const { draft } = built;\n if (!hasIncomingShareContent(draft)) {\n // Unsupported native payloads cannot become actionable on retry and\n // would otherwise reopen the project picker on every foreground.\n await this.cleanup(built.cleanup);\n this.clearNativePayloads();\n throw new Error(\n draft.warnings[0] ?? \"The shared content is not supported by the composer.\",\n );\n }\n\n // The durable inbox write is the transaction boundary. Never clear the\n // native handoff first: a process termination must leave one recoverable\n // copy on one side of the boundary.\n try {\n await this.dependencies.writeDraft(draft);\n } catch (error) {\n if (built.rollback) {\n await this.cleanup(built.rollback);", + "checksum": "c24ebfa0545fbd3e559e3f5a871ea29495e13a405ac16e4bac61e0fe398fbde2" }, "mobile-native-surface-probe": { "id": "mobile-native-surface-probe", @@ -1286,8 +1286,8 @@ "end": 103, "language": "typescript", "label": "Curated renderer bridge for local, SSH, exposure, WSL, and file operations", - "code": "contextBridge.exposeInMainWorld(\"desktopBridge\", {\n getAppBranding: () => {\n const result = ipcRenderer.sendSync(IpcChannels.GET_APP_BRANDING_CHANNEL);\n if (typeof result !== \"object\" || result === null) {\n return null;\n }\n return result as ReturnType;\n },\n getSystemLocale: () => {\n const result = ipcRenderer.sendSync(IpcChannels.GET_SYSTEM_LOCALE_CHANNEL);\n return typeof result === \"string\" ? result : null;\n },\n getLocalEnvironmentBootstraps: () => {\n const result = ipcRenderer.sendSync(IpcChannels.GET_LOCAL_ENVIRONMENT_BOOTSTRAPS_CHANNEL);\n if (!Array.isArray(result)) {\n return [];\n }\n return result as ReturnType;\n },\n getLocalEnvironmentBearerToken: () =>\n ipcRenderer.invoke(IpcChannels.GET_LOCAL_ENVIRONMENT_BEARER_TOKEN_CHANNEL),\n getClientSettings: () => ipcRenderer.invoke(IpcChannels.GET_CLIENT_SETTINGS_CHANNEL),\n setClientSettings: (settings) =>\n ipcRenderer.invoke(IpcChannels.SET_CLIENT_SETTINGS_CHANNEL, settings),\n getConnectionCatalog: () => ipcRenderer.invoke(IpcChannels.GET_CONNECTION_CATALOG_CHANNEL),\n setConnectionCatalog: (catalog) =>\n ipcRenderer.invoke(IpcChannels.SET_CONNECTION_CATALOG_CHANNEL, catalog),\n clearConnectionCatalog: () => ipcRenderer.invoke(IpcChannels.CLEAR_CONNECTION_CATALOG_CHANNEL),\n discoverSshHosts: () => ipcRenderer.invoke(IpcChannels.DISCOVER_SSH_HOSTS_CHANNEL),\n ensureSshEnvironment: async (target, options) =>\n unwrapEnsureSshEnvironmentResult(\n await ipcRenderer.invoke(IpcChannels.ENSURE_SSH_ENVIRONMENT_CHANNEL, {\n target,\n ...(options === undefined ? {} : { options }),\n }),\n ),\n disconnectSshEnvironment: (target) =>\n ipcRenderer.invoke(IpcChannels.DISCONNECT_SSH_ENVIRONMENT_CHANNEL, target),\n fetchSshEnvironmentDescriptor: (httpBaseUrl) =>\n ipcRenderer.invoke(IpcChannels.FETCH_SSH_ENVIRONMENT_DESCRIPTOR_CHANNEL, { httpBaseUrl }),\n bootstrapSshBearerSession: (httpBaseUrl, credential) =>\n ipcRenderer.invoke(IpcChannels.BOOTSTRAP_SSH_BEARER_SESSION_CHANNEL, {\n httpBaseUrl,\n credential,\n }),\n fetchSshSessionState: (httpBaseUrl, bearerToken) =>\n ipcRenderer.invoke(IpcChannels.FETCH_SSH_SESSION_STATE_CHANNEL, { httpBaseUrl, bearerToken }),\n issueSshWebSocketTicket: (httpBaseUrl, bearerToken) =>\n ipcRenderer.invoke(IpcChannels.ISSUE_SSH_WEBSOCKET_TOKEN_CHANNEL, { httpBaseUrl, bearerToken }),\n onSshPasswordPrompt: (listener) => {\n const wrappedListener = (_event: Electron.IpcRendererEvent, request: unknown) => {\n if (typeof request !== \"object\" || request === null) return;\n listener(request as Parameters[0]);\n };\n\n ipcRenderer.on(IpcChannels.SSH_PASSWORD_PROMPT_CHANNEL, wrappedListener);\n return () => {\n ipcRenderer.removeListener(IpcChannels.SSH_PASSWORD_PROMPT_CHANNEL, wrappedListener);\n };\n },\n resolveSshPasswordPrompt: (requestId, password) =>\n ipcRenderer.invoke(IpcChannels.RESOLVE_SSH_PASSWORD_PROMPT_CHANNEL, { requestId, password }),\n getServerExposureState: () => ipcRenderer.invoke(IpcChannels.GET_SERVER_EXPOSURE_STATE_CHANNEL),\n setServerExposureMode: (mode) =>\n ipcRenderer.invoke(IpcChannels.SET_SERVER_EXPOSURE_MODE_CHANNEL, mode),\n setTailscaleServeEnabled: (input) =>\n ipcRenderer.invoke(IpcChannels.SET_TAILSCALE_SERVE_ENABLED_CHANNEL, input),\n getAdvertisedEndpoints: () => ipcRenderer.invoke(IpcChannels.GET_ADVERTISED_ENDPOINTS_CHANNEL),\n getWslState: () => ipcRenderer.invoke(IpcChannels.GET_WSL_STATE_CHANNEL),\n setWslBackendEnabled: (enabled) =>\n ipcRenderer.invoke(IpcChannels.SET_WSL_BACKEND_ENABLED_CHANNEL, enabled),\n setWslDistro: (distro) => ipcRenderer.invoke(IpcChannels.SET_WSL_DISTRO_CHANNEL, distro),\n setWslOnly: (enabled) => ipcRenderer.invoke(IpcChannels.SET_WSL_ONLY_CHANNEL, enabled),\n pickFolder: (options) => ipcRenderer.invoke(IpcChannels.PICK_FOLDER_CHANNEL, options),", - "checksum": "f1c239b62a4b38550da29b44ca8086edd3b2f76df4483787b6628674fe980daf" + "code": "exposeClerkBridge({ passkeys: true });\n\n// oxlint-disable-next-line t3code/no-global-process-runtime -- Electron exposes the client platform in its sandboxed preload process.\nconst clientPlatform = process.platform;\n\nfunction unwrapEnsureSshEnvironmentResult(result: unknown) {\n if (\n typeof result === \"object\" &&\n result !== null &&\n \"type\" in result &&\n result.type === IpcChannels.SSH_PASSWORD_PROMPT_CANCELLED_RESULT\n ) {\n const message =\n \"message\" in result && typeof result.message === \"string\"\n ? result.message\n : \"SSH authentication cancelled.\";\n throw new Error(message);\n }\n return result as Awaited>;\n}\n\ncontextBridge.exposeInMainWorld(\"desktopBridge\", {\n getAppBranding: () => {\n const result = ipcRenderer.sendSync(IpcChannels.GET_APP_BRANDING_CHANNEL);\n if (typeof result !== \"object\" || result === null) {\n return null;\n }\n return result as ReturnType;\n },\n getClientPlatform: () => clientPlatform,\n getSystemLocale: () => {\n const result = ipcRenderer.sendSync(IpcChannels.GET_SYSTEM_LOCALE_CHANNEL);\n return typeof result === \"string\" ? result : null;\n },\n getLocalEnvironmentBootstraps: () => {\n const result = ipcRenderer.sendSync(IpcChannels.GET_LOCAL_ENVIRONMENT_BOOTSTRAPS_CHANNEL);\n if (!Array.isArray(result)) {\n return [];\n }\n return result as ReturnType;\n },\n getLocalEnvironmentBearerToken: () =>\n ipcRenderer.invoke(IpcChannels.GET_LOCAL_ENVIRONMENT_BEARER_TOKEN_CHANNEL),\n getClientSettings: () => ipcRenderer.invoke(IpcChannels.GET_CLIENT_SETTINGS_CHANNEL),\n setClientSettings: (settings) =>\n ipcRenderer.invoke(IpcChannels.SET_CLIENT_SETTINGS_CHANNEL, settings),\n requestSnapShotPermissions: (includeAccessibility) =>\n ipcRenderer.invoke(IpcChannels.REQUEST_SNAP_SHOT_PERMISSIONS_CHANNEL, includeAccessibility),\n getSnapShotState: () => ipcRenderer.invoke(IpcChannels.GET_SNAP_SHOT_STATE_CHANNEL),\n setupSnapShot: (action) => ipcRenderer.invoke(IpcChannels.SETUP_SNAP_SHOT_CHANNEL, action),\n previewSnapShotConfig: (request) =>\n ipcRenderer.invoke(IpcChannels.PREVIEW_SNAP_SHOT_CONFIG_CHANNEL, request),\n applySnapShotConfig: (id) => ipcRenderer.invoke(IpcChannels.APPLY_SNAP_SHOT_CONFIG_CHANNEL, id),\n checkSnapShotShortcut: (shortcut) =>\n ipcRenderer.invoke(IpcChannels.CHECK_SNAP_SHOT_SHORTCUT_CHANNEL, shortcut),\n setSnapShotShortcutSuppressed: (suppressed) =>\n ipcRenderer.invoke(IpcChannels.SET_SNAP_SHOT_SHORTCUT_SUPPRESSED_CHANNEL, suppressed),\n listPendingSnapShots: () => ipcRenderer.invoke(IpcChannels.LIST_PENDING_SNAP_SHOTS_CHANNEL),\n readSnapShot: (id) => ipcRenderer.invoke(IpcChannels.READ_SNAP_SHOT_CHANNEL, id),\n setSnapShotAnimationDestination: (destination) =>\n ipcRenderer.invoke(IpcChannels.SET_SNAP_SHOT_ANIMATION_DESTINATION_CHANNEL, destination),\n dismissSnapShotAnimation: (id) =>\n ipcRenderer.invoke(IpcChannels.DISMISS_SNAP_SHOT_ANIMATION_CHANNEL, id),\n acknowledgeSnapShot: (id) => ipcRenderer.invoke(IpcChannels.ACKNOWLEDGE_SNAP_SHOT_CHANNEL, id),\n getConnectionCatalog: () => ipcRenderer.invoke(IpcChannels.GET_CONNECTION_CATALOG_CHANNEL),\n setConnectionCatalog: (catalog) =>\n ipcRenderer.invoke(IpcChannels.SET_CONNECTION_CATALOG_CHANNEL, catalog),\n clearConnectionCatalog: () => ipcRenderer.invoke(IpcChannels.CLEAR_CONNECTION_CATALOG_CHANNEL),\n discoverSshHosts: () => ipcRenderer.invoke(IpcChannels.DISCOVER_SSH_HOSTS_CHANNEL),\n resolveSshHost: (alias) => ipcRenderer.invoke(IpcChannels.RESOLVE_SSH_HOST_CHANNEL, alias),\n ensureSshEnvironment: async (target, options) =>\n unwrapEnsureSshEnvironmentResult(\n await ipcRenderer.invoke(IpcChannels.ENSURE_SSH_ENVIRONMENT_CHANNEL, {\n target,", + "checksum": "0cef1f818387cb2b2c8888e1292996f499cb6cec0980706413caff0eff89f6c1" }, "desktop-bootstrap-order": { "id": "desktop-bootstrap-order", @@ -1296,8 +1296,8 @@ "end": 218, "language": "typescript", "label": "Endpoint selection, desktop protocol, IPC, primary start, and parallel WSL reconciliation", - "code": "const bootstrap = Effect.gen(function* () {\n const pool = yield* DesktopBackendPool.DesktopBackendPool;\n const primaryBackend = yield* pool.primary;\n const state = yield* DesktopState.DesktopState;\n const environment = yield* DesktopEnvironment.DesktopEnvironment;\n const desktopSettings = yield* DesktopAppSettings.DesktopAppSettings;\n const serverExposure = yield* DesktopServerExposure.DesktopServerExposure;\n const wslBackend = yield* DesktopWslBackend.DesktopWslBackend;\n const desktopWindow = yield* DesktopWindow.DesktopWindow;\n yield* logBootstrapInfo(\"bootstrap start\");\n\n if (environment.isDevelopment && Option.isNone(environment.configuredBackendPort)) {\n return yield* new DesktopDevelopmentBackendPortRequiredError();\n }\n\n const backendPortSelection = yield* resolveDesktopBackendPort(environment.configuredBackendPort);\n const backendPort = backendPortSelection.port;\n yield* logBootstrapInfo(\n backendPortSelection.selectedByScan\n ? \"selected backend port via sequential scan\"\n : \"using configured backend port\",\n {\n port: backendPort,\n ...(backendPortSelection.selectedByScan ? { startPort: DEFAULT_DESKTOP_BACKEND_PORT } : {}),\n },\n );\n\n const settings = yield* desktopSettings.get;\n if (settings.serverExposureMode !== environment.defaultDesktopSettings.serverExposureMode) {\n yield* logBootstrapInfo(\"bootstrap restoring persisted server exposure mode\", {\n mode: settings.serverExposureMode,\n });\n }\n const serverExposureState = yield* serverExposure.configureFromSettings({ port: backendPort });\n const backendConfig = yield* serverExposure.backendConfig;\n const electronProtocol = yield* ElectronProtocol.ElectronProtocol;\n const rendererTarget = environment.isDevelopment\n ? Option.getOrThrow(environment.devServerUrl)\n : backendConfig.httpBaseUrl;\n yield* electronProtocol.registerDesktopProtocol({\n scheme: ElectronProtocol.getDesktopScheme(environment.isDevelopment),\n targetOrigin: rendererTarget,\n backendOrigin: backendConfig.httpBaseUrl,\n clerkFrontendApiHostname: DesktopClerk.desktopClerkFrontendApiHostname,\n });\n yield* logBootstrapInfo(\"bootstrap resolved backend endpoint\", {\n baseUrl: backendConfig.httpBaseUrl.href,\n });\n if (serverExposureState.endpointUrl) {\n yield* logBootstrapInfo(\"bootstrap enabled network access\", {\n endpointUrl: serverExposureState.endpointUrl,\n });\n } else if (settings.serverExposureMode === \"network-accessible\") {\n yield* logBootstrapWarning(\n \"bootstrap fell back to local-only because no advertised network host was available\",\n );\n }\n\n yield* installDesktopIpcHandlers();\n yield* logBootstrapInfo(\"bootstrap ipc handlers registered\");\n\n if (!(yield* Ref.get(state.quitting))) {\n // In wsl-only mode the renderer is served by the WSL backend, which can be\n // slow to cold-boot — show a \"Connecting to WSL\" splash immediately so the\n // app feels responsive instead of presenting no window until WSL is ready.\n // (Dual mode opens fast off the Windows primary, so no splash there.)\n if (settings.wslOnly === true && settings.wslBackendEnabled === true) {\n yield* desktopWindow.showConnectingSplash;\n }\n yield* primaryBackend.start;\n yield* logBootstrapInfo(\"bootstrap backend start requested\");\n // Bring up the WSL backend if the user previously enabled it. The\n // primary is already starting; reconcile fires off the WSL register\n // in parallel rather than blocking primary readiness on a possibly\n // slow first wsl.exe spawn.\n yield* Effect.forkScoped(wslBackend.reconcile);\n }", - "checksum": "3382a448d70b84c155daa8b98ea9f42a42702152d451cb49be7ba0608c948342" + "code": "const fatalStartupCause = (stage: string, cause: Cause.Cause) =>\n handleFatalStartupError(stage, Cause.pretty(cause)).pipe(Effect.andThen(Effect.failCause(cause)));\n\nconst bootstrap = Effect.gen(function* () {\n const pool = yield* DesktopBackendPool.DesktopBackendPool;\n const primaryBackend = yield* pool.primary;\n const state = yield* DesktopState.DesktopState;\n const environment = yield* DesktopEnvironment.DesktopEnvironment;\n const desktopSettings = yield* DesktopAppSettings.DesktopAppSettings;\n const serverExposure = yield* DesktopServerExposure.DesktopServerExposure;\n const wslBackend = yield* DesktopWslBackend.DesktopWslBackend;\n const desktopWindow = yield* DesktopWindow.DesktopWindow;\n const snapShot = yield* DesktopSnapShot.DesktopSnapShot;\n const appActivation = yield* DesktopAppActivation.DesktopAppActivation;\n yield* logBootstrapInfo(\"bootstrap start\");\n\n if (environment.isDevelopment && Option.isNone(environment.configuredBackendPort)) {\n return yield* new DesktopDevelopmentBackendPortRequiredError();\n }\n\n const backendPortSelection = yield* resolveDesktopBackendPort(environment.configuredBackendPort);\n const backendPort = backendPortSelection.port;\n yield* logBootstrapInfo(\n backendPortSelection.selectedByScan\n ? \"selected backend port via sequential scan\"\n : \"using configured backend port\",\n {\n port: backendPort,\n ...(backendPortSelection.selectedByScan ? { startPort: DEFAULT_DESKTOP_BACKEND_PORT } : {}),\n },\n );\n\n const settings = yield* desktopSettings.get;\n if (settings.serverExposureMode !== environment.defaultDesktopSettings.serverExposureMode) {\n yield* logBootstrapInfo(\"bootstrap restoring persisted server exposure mode\", {\n mode: settings.serverExposureMode,\n });\n }\n const serverExposureState = yield* serverExposure.configureFromSettings({ port: backendPort });\n const backendConfig = yield* serverExposure.backendConfig;\n const electronProtocol = yield* ElectronProtocol.ElectronProtocol;\n const rendererTarget = environment.isDevelopment\n ? Option.getOrThrow(environment.devServerUrl)\n : backendConfig.httpBaseUrl;\n yield* electronProtocol.registerDesktopProtocol({\n scheme: ElectronProtocol.getDesktopScheme(environment.isDevelopment),\n targetOrigin: rendererTarget,\n backendOrigin: backendConfig.httpBaseUrl,\n clerkFrontendApiHostname: DesktopClerk.desktopClerkFrontendApiHostname,\n });\n yield* logBootstrapInfo(\"bootstrap resolved backend endpoint\", {\n baseUrl: backendConfig.httpBaseUrl.href,\n });\n if (serverExposureState.endpointUrl) {\n yield* logBootstrapInfo(\"bootstrap enabled network access\", {\n endpointUrl: serverExposureState.endpointUrl,\n });\n } else if (\n settings.serverExposureMode === \"network-accessible\" &&\n serverExposureState.mode === \"local-only\"\n ) {\n yield* logBootstrapWarning(\n \"bootstrap fell back to local-only because no advertised network host was available\",\n );\n }\n yield* snapShot.initialize;\n\n yield* installDesktopIpcHandlers();\n yield* logBootstrapInfo(\"bootstrap ipc handlers registered\");\n\n if (!(yield* Ref.get(state.quitting))) {\n // In wsl-only mode the renderer is served by the WSL backend, which can be\n // slow to cold-boot — show a \"Connecting to WSL\" splash immediately so the\n // app feels responsive instead of presenting no window until WSL is ready.\n // (Dual mode opens fast off the Windows primary, so no splash there.)\n if (settings.wslOnly === true && settings.wslBackendEnabled === true) {\n yield* desktopWindow.showConnectingSplash;", + "checksum": "b0062735d26efe3c55dfd793bfa384d8160239d153c656944e9dd827819e8da8" }, "desktop-primary-platform-choice": { "id": "desktop-primary-platform-choice", @@ -1306,8 +1306,8 @@ "end": 728, "language": "typescript", "label": "Host-local primary label with the Windows-only WSL override", - "code": " // Single source of truth for what the primary actually runs as. Both\n // the start-config dispatch and the renderer-facing label derive from\n // this, so they can't disagree — e.g. the label reading \"WSL\" while the\n // config silently fell back to Windows because WSL is unavailable.\n // Dispatch happens at resolve time so toggling wsl-only between restarts\n // is picked up on the next start cycle (the pool's primary instance is\n // created once at layer init, but configResolve fires on each restart).\n const describePrimary = Effect.gen(function* () {\n const persistedSettings = yield* settings.get;\n const wslRequested = persistedSettings.wslOnly && persistedSettings.wslBackendEnabled;\n // Only honor wsl-only when WSL is actually usable. If the user\n // persisted wsl-only but WSL has since become unavailable (wsl.exe\n // removed, no distro), fall back to the Windows primary instead of\n // looping forever on preflight failures: the Connections backend\n // control is hidden while WSL is unavailable, so a stuck WSL primary\n // would otherwise leave no in-app way back to Windows.\n const useWsl = wslRequested && (yield* wslEnvironment.isAvailable);\n return { useWsl, wslRequested, distro: persistedSettings.wslDistro };\n });\n\n return DesktopBackendConfiguration.of({\n resolvePrimary: Effect.gen(function* () {\n const { useWsl, wslRequested } = yield* describePrimary;\n if (useWsl) {\n return yield* buildWslPrimaryConfig;\n }\n if (wslRequested) {\n yield* Effect.logWarning(\n \"WSL-only backend requested but WSL is unavailable; starting the Windows primary instead.\",\n );\n }\n return yield* buildWindowsPrimaryConfig;\n }).pipe(Effect.withSpan(\"desktop.backendConfiguration.resolvePrimary\")),\n resolvePrimaryLabel: Effect.gen(function* () {\n const { useWsl, distro } = yield* describePrimary;\n if (!useWsl) {\n return environment.platform === \"win32\" ? \"Windows\" : \"Local environment\";\n }\n return distro ? `WSL (${distro})` : \"WSL\";\n }).pipe(Effect.withSpan(\"desktop.backendConfiguration.resolvePrimaryLabel\")),", - "checksum": "58261db624128a02030dd25fb009fc7f141e0d8e67cf9b57cfab3735e468550d" + "code": " // slashes get translated unpredictably depending on flags), and the\n // packaged build leaves devServerUrl as None anyway.\n const devUrlArgs = Option.match(environment.devServerUrl, {\n onNone: () => [] as ReadonlyArray,\n onSome: (url) => [\"--dev-url\", url.href],\n });\n\n if (preflight._tag === \"Failed\") {\n const retryLimit =\n preflight.retryLimit ?? (preflight.fatal ? undefined : WSL_TRANSIENT_PREFLIGHT_RETRY_LIMIT);\n return {\n ...baseConfig,\n args: [...distroArgs, \"--\", \"node\", \"--version\"],\n preflightFailure: Option.some({\n reason: preflight.reason,\n fatal: preflight.fatal,\n ...(retryLimit === undefined ? {} : { retryLimit }),\n }),\n } satisfies DesktopBackendManager.DesktopBackendStartConfig;\n }\n\n // The WSL server spawns commands its providers reference by name — `npm`/`npx`\n // for provider updates, and the installed CLIs themselves (e.g. `codex`). Those\n // live in the resolved Node's bin dir, which `wsl.exe -- node` does NOT put on\n // the process PATH, so `npm install -g ...` fails with NotFound. Pass the\n // user PATH entries captured by the login-shell preflight. Every dynamic\n // value is a separate argv entry under `wsl.exe --exec`; no shell command is\n // involved, so Windows cannot mangle nested quotes and stdin remains reserved\n // for the bootstrap envelope.\n const lastSlash = preflight.nodePath.lastIndexOf(\"/\");\n const nodeBinDir = lastSlash > 0 ? preflight.nodePath.slice(0, lastSlash) : \"/usr/bin\";\n const launchPath = `${nodeBinDir}:${WSL_SERVER_SYSTEM_PATH}:${preflight.resolvedPath}`;\n\n return {\n ...baseConfig,\n args: [\n ...distroArgs,\n \"--exec\",\n \"env\",\n `PATH=${launchPath}`,", + "checksum": "ffe006aef260382264b931a635a7aa86d09d832ad8aa3951acf1a7f359052cea" }, "desktop-pool-scope-lifecycle": { "id": "desktop-pool-scope-lifecycle", @@ -1316,8 +1316,8 @@ "end": 419, "language": "typescript", "label": "Per-instance child scopes and primary-protected unregister lifecycle", - "code": " return Effect.fail(\n new DesktopBackendPoolInstanceAlreadyRegisteredError({ id: spec.id }),\n );\n }\n if (existing?._tag === \"Closing\") {\n return Effect.succeed([\n { _tag: \"Wait\", done: existing.done } as const,\n current,\n ] as const);\n }\n return Effect.gen(function* () {\n // Provide the captured factory services first, then the child scope\n // last so instance finalizers are owned by the unregisterable scope.\n const instanceScope = yield* Scope.fork(layerScope, \"sequential\");\n const instance = yield* DesktopBackendManager.makeBackendInstance(spec).pipe(\n Effect.provide(factoryContext),\n Scope.provide(instanceScope),\n );\n const next = new Map(current);\n next.set(spec.id, {\n _tag: \"Active\",\n instance,\n scope: Option.some(instanceScope),\n });\n return [\n { _tag: \"Registered\", instance } as const,\n next as ReadonlyMap,\n ] as const;\n });\n },\n ).pipe(\n Effect.flatMap((result) =>\n result._tag === \"Registered\"\n ? Effect.succeed(result.instance)\n : Deferred.await(result.done).pipe(Effect.andThen(register(spec))),\n ),\n ),\n );\n\n const unregister: DesktopBackendPool[\"Service\"][\"unregister\"] = (id) =>\n Effect.gen(function* () {\n if (id === DesktopBackendManager.PRIMARY_INSTANCE_ID) {\n return yield* new DesktopBackendPoolCannotUnregisterPrimaryError();\n }\n const done = yield* Deferred.make();\n const action = yield* SynchronizedRef.modifyEffect(\n instancesRef,\n (\n current,\n ): Effect.Effect<\n readonly [UnregisterAction, ReadonlyMap]\n > => {\n const entry = current.get(id);\n if (entry === undefined) {\n return Effect.succeed([{ _tag: \"Absent\" } as const, current] as const);\n }\n if (entry._tag === \"Closing\") {\n return Effect.succeed([\n { _tag: \"Wait\", done: entry.done } as const,\n current,\n ] as const);\n }\n const next = new Map(current);\n next.set(id, { _tag: \"Closing\", done });\n return Effect.succeed([\n { _tag: \"Close\", entry } as const,\n next as ReadonlyMap,\n ] as const);\n },\n );\n\n if (action._tag === \"Absent\") return;\n if (action._tag === \"Wait\") {\n yield* Deferred.await(action.done);\n return;\n }\n\n const finish = SynchronizedRef.modifyEffect(instancesRef, (current) => {\n const closing = current.get(id);\n if (closing?._tag !== \"Closing\" || closing.done !== done) {\n return Effect.succeed([undefined, current] as const);\n }\n const next = new Map(current);\n next.delete(id);\n return Effect.succeed([\n undefined,\n next as ReadonlyMap,\n ] as const);\n }).pipe(Effect.andThen(Deferred.succeed(done, undefined)), Effect.asVoid);\n yield* Option.match(action.entry.scope, {\n onNone: () => Effect.void,\n onSome: (scope) => Scope.close(scope, Exit.void).pipe(Effect.ignore),\n }).pipe(Effect.ensuring(finish));", - "checksum": "6cc62e74d59a260508a4457eaed647f2bcf1ff9b77a41fd4d41276af7f22aab0" + "code": " const existing = current.get(spec.id);\n if (existing?._tag === \"Active\") {\n return Effect.fail(\n new DesktopBackendPoolInstanceAlreadyRegisteredError({ id: spec.id }),\n );\n }\n if (existing?._tag === \"Closing\") {\n return Effect.succeed([\n { _tag: \"Wait\", done: existing.done } as const,\n current,\n ] as const);\n }\n return Effect.gen(function* () {\n // Provide the captured factory services first, then the child scope\n // last so instance finalizers are owned by the unregisterable scope.\n const instanceScope = yield* Scope.fork(layerScope, \"sequential\");\n const instance = yield* DesktopBackendManager.makeBackendInstance(spec).pipe(\n Effect.provide(factoryContext),\n Scope.provide(instanceScope),\n );\n const next = new Map(current);\n next.set(spec.id, {\n _tag: \"Active\",\n instance,\n scope: Option.some(instanceScope),\n });\n return [\n { _tag: \"Registered\", instance } as const,\n next as ReadonlyMap,\n ] as const;\n });\n },\n ).pipe(\n Effect.flatMap((result) =>\n result._tag === \"Registered\"\n ? Effect.succeed(result.instance)\n : Deferred.await(result.done).pipe(Effect.andThen(register(spec))),\n ),\n ),\n );\n\n const unregister: DesktopBackendPool[\"Service\"][\"unregister\"] = (id) =>\n Effect.gen(function* () {\n if (id === DesktopBackendManager.PRIMARY_INSTANCE_ID) {\n return yield* new DesktopBackendPoolCannotUnregisterPrimaryError();\n }\n const done = yield* Deferred.make();\n const action = yield* SynchronizedRef.modifyEffect(\n instancesRef,\n (\n current,\n ): Effect.Effect<\n readonly [UnregisterAction, ReadonlyMap]\n > => {\n const entry = current.get(id);\n if (entry === undefined) {\n return Effect.succeed([{ _tag: \"Absent\" } as const, current] as const);\n }\n if (entry._tag === \"Closing\") {\n return Effect.succeed([\n { _tag: \"Wait\", done: entry.done } as const,\n current,\n ] as const);\n }\n const next = new Map(current);\n next.set(id, { _tag: \"Closing\", done });\n return Effect.succeed([\n { _tag: \"Close\", entry } as const,\n next as ReadonlyMap,\n ] as const);\n },\n );\n\n if (action._tag === \"Absent\") return;\n if (action._tag === \"Wait\") {\n yield* Deferred.await(action.done);\n return;\n }\n\n const finish = SynchronizedRef.modifyEffect(instancesRef, (current) => {\n const closing = current.get(id);\n if (closing?._tag !== \"Closing\" || closing.done !== done) {\n return Effect.succeed([undefined, current] as const);\n }\n const next = new Map(current);\n next.delete(id);\n return Effect.succeed([\n undefined,\n next as ReadonlyMap,\n ] as const);\n }).pipe(Effect.andThen(Deferred.succeed(done, undefined)), Effect.asVoid);\n yield* Option.match(action.entry.scope, {\n onNone: () => Effect.void,", + "checksum": "2374167dedba1d98522af2ea5a85b360916c59cf2804a092fb9cc0a668977ebc" }, "desktop-process-channels": { "id": "desktop-process-channels", @@ -1326,8 +1326,8 @@ "end": 526, "language": "typescript", "label": "Bootstrap, telemetry, diagnostics-control, and WSL stdin process channels", - "code": " const onOutput = options.onOutput ?? (() => Effect.void);\n const bootstrapStream = Stream.encodeText(Stream.make(`${bootstrapJson}\\n`));\n const additionalFds: Record<`fd${number}`, ChildProcess.AdditionalFdConfig> = {};\n if (options.bootstrapDelivery === \"fd3\") {\n additionalFds.fd3 = {\n type: \"input\",\n stream: bootstrapStream,\n };\n if (options.bootstrap.desktopTelemetryFd !== undefined) {\n additionalFds[`fd${options.bootstrap.desktopTelemetryFd}`] = {\n type: \"input\",\n stream: options.desktopTelemetryStream,\n };\n }\n if (options.bootstrap.desktopTelemetryControlFd !== undefined) {\n additionalFds[`fd${options.bootstrap.desktopTelemetryControlFd}`] = {\n type: \"output\",\n };\n }\n }\n const command = ChildProcess.make(options.executablePath, options.args, {\n cwd: options.cwd,\n env: options.env,\n extendEnv: options.extendEnv,\n // In Electron main, process.execPath points to the Electron binary.\n // Run the child in Node mode so this backend process does not become a GUI app instance.\n stdin: options.bootstrapDelivery === \"stdin\" ? bootstrapStream : \"ignore\",\n stdout: options.captureOutput ? \"pipe\" : \"inherit\",\n stderr: options.captureOutput ? \"pipe\" : \"inherit\",\n killSignal: \"SIGTERM\",\n forceKillAfter: DEFAULT_BACKEND_TERMINATE_GRACE,\n // wsl.exe drops additional file descriptors when forwarding to the Linux\n // side, so the WSL spawn path delivers the bootstrap envelope via stdin\n // (`--bootstrap-fd 0`) instead.\n ...(options.bootstrapDelivery === \"fd3\" ? { additionalFds } : {}),\n });\n\n const handle = yield* spawner.spawn(command).pipe(\n Effect.mapError(\n (cause) =>\n new BackendProcessSpawnError({\n executablePath: options.executablePath,\n entryPath: options.entryPath,\n cwd: options.cwd,\n httpBaseUrl: options.httpBaseUrl,\n cause,\n }),\n ),\n );\n const outputFibers: Array> = [];\n\n yield* options.onStarted?.(handle.pid) ?? Effect.void;\n if (\n options.bootstrap.desktopTelemetryControlFd !== undefined &&\n options.onDesktopTelemetryControl !== undefined\n ) {\n const controlFd = options.bootstrap.desktopTelemetryControlFd;\n const handleControl = options.onDesktopTelemetryControl;\n yield* handle.getOutputFd(controlFd).pipe(\n Stream.decodeText(),\n Stream.splitLines,\n Stream.filter((line) => line.trim().length > 0),\n Stream.runForEach((line) =>\n decodeDesktopTelemetryControlLine(line).pipe(\n Effect.flatMap(handleControl),\n Effect.catchCause((cause) =>\n logBackendProcessWarning(\"ignored invalid desktop telemetry control message\", {\n fd: controlFd,\n cause: Cause.pretty(cause),\n }),\n ),\n ),\n ),\n Effect.catchCause((cause) =>\n logBackendProcessWarning(\"desktop telemetry control stream stopped\", {\n fd: controlFd,", - "checksum": "9a38b54eef1d5d1bb2e4a11a70cac83f681b4d8369b8d44c0548c7efd93119de" + "code": " httpBaseUrl: options.httpBaseUrl,\n cause,\n }),\n ),\n );\n const onOutput = options.onOutput ?? (() => Effect.void);\n const bootstrapStream = Stream.encodeText(Stream.make(`${bootstrapJson}\\n`));\n const additionalFds: Record<`fd${number}`, ChildProcess.AdditionalFdConfig> = {};\n if (options.bootstrapDelivery === \"fd3\") {\n additionalFds.fd3 = {\n type: \"input\",\n stream: bootstrapStream,\n };\n if (options.bootstrap.desktopTelemetryFd !== undefined) {\n additionalFds[`fd${options.bootstrap.desktopTelemetryFd}`] = {\n type: \"input\",\n stream: options.desktopTelemetryStream,\n };\n }\n if (options.bootstrap.desktopTelemetryControlFd !== undefined) {\n additionalFds[`fd${options.bootstrap.desktopTelemetryControlFd}`] = {\n type: \"output\",\n };\n }\n }\n const command = ChildProcess.make(options.executablePath, options.args, {\n cwd: options.cwd,\n env: options.env,\n extendEnv: options.extendEnv,\n // In Electron main, process.execPath points to the Electron binary.\n // Run the child in Node mode so this backend process does not become a GUI app instance.\n stdin: options.bootstrapDelivery === \"stdin\" ? bootstrapStream : \"ignore\",\n stdout: options.captureOutput ? \"pipe\" : \"inherit\",\n stderr: options.captureOutput ? \"pipe\" : \"inherit\",\n killSignal: \"SIGTERM\",\n forceKillAfter: DEFAULT_BACKEND_TERMINATE_GRACE,\n // wsl.exe drops additional file descriptors when forwarding to the Linux\n // side, so the WSL spawn path delivers the bootstrap envelope via stdin\n // (`--bootstrap-fd 0`) instead.\n ...(options.bootstrapDelivery === \"fd3\" ? { additionalFds } : {}),\n });\n\n const handle = yield* spawner.spawn(command).pipe(\n Effect.mapError(\n (cause) =>\n new BackendProcessSpawnError({\n executablePath: options.executablePath,\n entryPath: options.entryPath,\n cwd: options.cwd,\n httpBaseUrl: options.httpBaseUrl,\n cause,\n }),\n ),\n );\n const outputFibers: Array> = [];\n\n yield* options.onStarted?.(handle.pid) ?? Effect.void;\n if (\n options.bootstrap.desktopTelemetryControlFd !== undefined &&\n options.onDesktopTelemetryControl !== undefined\n ) {\n const controlFd = options.bootstrap.desktopTelemetryControlFd;\n const handleControl = options.onDesktopTelemetryControl;\n yield* handle.getOutputFd(controlFd).pipe(\n Stream.decodeText(),\n Stream.splitLines,\n Stream.filter((line) => line.trim().length > 0),\n Stream.runForEach((line) =>\n decodeDesktopTelemetryControlLine(line).pipe(\n Effect.flatMap(handleControl),\n Effect.catchCause((cause) =>\n logBackendProcessWarning(\"ignored invalid desktop telemetry control message\", {\n fd: controlFd,\n cause: Cause.pretty(cause),\n }),\n ),", + "checksum": "e3b16d8007954feb7afe64936cd0da3f1f0fa49b83bdedbd7e06fb91aeba3f54" }, "desktop-http-readiness-loop": { "id": "desktop-http-readiness-loop", @@ -1336,8 +1336,8 @@ "end": 591, "language": "typescript", "label": "Repeated HTTP readiness budgets while the local child remains alive", - "code": " // Probe readiness in a loop while the backend process is still alive\n // instead of giving up after the first budget. A slow cold boot (the\n // WSL bundle loading across /mnt/c, or a first launch right after an\n // update) can exceed the initial readiness budget while the backend is\n // about to come up moments later; a one-shot probe left the app stuck\n // on \"Connecting to WSL…\" forever even though the backend kept running\n // and became healthy. Each round gets a fresh budget, and the forked\n // loop is torn down with the run scope once the child exits.\n const probeReadiness = Effect.fn(\"desktop.backendProcess.probeReadiness\")(() =>\n waitForHttpReady({\n executablePath: options.executablePath,\n entryPath: options.entryPath,\n cwd: options.cwd,\n httpBaseUrl: options.httpBaseUrl,\n timeout: options.readinessTimeout ?? DEFAULT_BACKEND_READINESS_TIMEOUT,\n }).pipe(\n Effect.flatMap(() => options.onReady?.() ?? Effect.void),\n Effect.as(true),\n Effect.catchTags({\n BackendReadinessTimeoutError: (error) =>\n (options.onReadinessFailure?.(error) ?? Effect.void).pipe(Effect.as(false)),\n }),\n ),\n );\n\n yield* probeReadiness().pipe(Effect.repeat({ while: (ready) => !ready }), Effect.forkScoped);", - "checksum": "cb8c41817f08f82de4211db178fdc26c0f9686235c75cd46ff75bf53e8c0f476" + "code": " onOutput,\n onOutputFailure,\n ).pipe(Effect.forkScoped),\n );\n }\n // Probe readiness in a loop while the backend process is still alive\n // instead of giving up after the first budget. A slow cold boot (the\n // WSL bundle loading across /mnt/c, or a first launch right after an\n // update) can exceed the initial readiness budget while the backend is\n // about to come up moments later; a one-shot probe left the app stuck\n // on \"Connecting to WSL…\" forever even though the backend kept running\n // and became healthy. Each round gets a fresh budget, and the forked\n // loop is torn down with the run scope once the child exits.\n const probeReadiness = Effect.fn(\"desktop.backendProcess.probeReadiness\")(() =>\n waitForHttpReady({\n executablePath: options.executablePath,\n entryPath: options.entryPath,\n cwd: options.cwd,\n httpBaseUrl: options.httpBaseUrl,\n timeout: options.readinessTimeout ?? DEFAULT_BACKEND_READINESS_TIMEOUT,\n }).pipe(\n Effect.flatMap(() => options.onReady?.() ?? Effect.void),\n Effect.as(true),\n Effect.catchTags({\n BackendReadinessTimeoutError: (error) =>\n (options.onReadinessFailure?.(error) ?? Effect.void).pipe(Effect.as(false)),", + "checksum": "84a7181bbbca0512fa8a08a0149589cc9815a7e9bd08702812501228e1fa7400" }, "desktop-updater-quit-boundary": { "id": "desktop-updater-quit-boundary", @@ -1346,8 +1346,8 @@ "end": 227, "language": "typescript", "label": "Updater-controlled quit bypasses the ordinary before-quit hold", - "code": " let updaterQuitAllowed = false;\n yield* electronTheme.onUpdated(() => {\n void runEffect(\n desktopWindow.syncAppearance.pipe(Effect.withSpan(\"desktop.lifecycle.themeUpdated\")),\n );\n });\n yield* electronApp.onBeforeQuitForUpdate(() => {\n // Electron's updater owns the remaining quit/install/relaunch sequence.\n // Cancelling the following app \"before-quit\" event breaks that sequence,\n // most visibly on macOS where the native updater performs the relaunch.\n updaterQuitAllowed = true;\n void runEffect(\n logLifecycleInfo(\"allowing updater-controlled quit\").pipe(\n Effect.withSpan(\"desktop.lifecycle.beforeQuitForUpdate\"),\n ),\n );\n });\n yield* electronApp.on(\"before-quit\", (event: Electron.Event) => {\n handleBeforeQuit(\n event,\n runEffect,\n () => quitAllowed || updaterQuitAllowed,\n () => {\n quitAllowed = true;\n },\n );\n });\n yield* electronApp.on(\"activate\", () => {\n void runEffect(\n Effect.gen(function* () {", - "checksum": "cc836843d91d37f40a435053ead939e6c9ce439e4fd7860cf814e0497261be6f" + "code": " const runEffect = Effect.runPromiseWith(context);\n let quitAllowed = false;\n let updaterQuitAllowed = false;\n yield* electronTheme.onUpdated(() => {\n void runEffect(\n desktopWindow.syncAppearance.pipe(Effect.withSpan(\"desktop.lifecycle.themeUpdated\")),\n );\n });\n yield* electronApp.onBeforeQuitForUpdate(() => {\n // Electron's updater owns the remaining quit/install/relaunch sequence.\n // Cancelling the following app \"before-quit\" event breaks that sequence,\n // most visibly on macOS where the native updater performs the relaunch.\n updaterQuitAllowed = true;\n // This event is synchronous and the updater's quit proceeds as soon as\n // the listener returns, so a forked destroyAll would race the quit\n // and windows could still be open when the process exits (visible on\n // macOS). Destroy them inline.\n Effect.runSyncWith(context)(\n electronWindow.destroyAll.pipe(\n Effect.andThen(logLifecycleInfo(\"allowing updater-controlled quit\")),\n Effect.catchCause((cause) =>\n logLifecycleError(\"failed to destroy windows before updater quit\", { cause }),\n ),\n Effect.withSpan(\"desktop.lifecycle.beforeQuitForUpdate\"),\n ),\n );\n });\n yield* electronApp.on(\"before-quit\", (event: Electron.Event) => {\n handleBeforeQuit(\n event,", + "checksum": "049fa13e784e1a19292abc2d037386a610446d18f15e143ba7b5132ffbbf2ee6" }, "desktop-all-instance-shutdown": { "id": "desktop-all-instance-shutdown", @@ -1356,8 +1356,8 @@ "end": 315, "language": "typescript", "label": "Application finalizer gracefully stops every registered local backend", - "code": " const runId = yield* makeDesktopRunId;\n yield* Effect.annotateLogsScoped({ scope: \"desktop\", runId });\n yield* Effect.annotateCurrentSpan({ scope: \"desktop\", runId });\n\n const shutdown = yield* DesktopShutdown.DesktopShutdown;\n\n yield* Effect.addFinalizer(() =>\n Effect.gen(function* () {\n const pool = yield* DesktopBackendPool.DesktopBackendPool;\n // Stop every backend in the pool, not just the primary. The\n // electronApp.quit() path can race ahead of the layer-scope\n // cascade, so leaving the WSL instance for its parent scope\n // finalizer means it gets hard-killed by the OS instead of\n // receiving SIGTERM + grace. Stops run concurrently.\n const instances = yield* pool.list;\n yield* Effect.forEach(instances, (instance) => instance.stop(), {\n concurrency: \"unbounded\",\n });\n }).pipe(Effect.ensuring(shutdown.markComplete)),\n );\n", - "checksum": "cd58d3b5122f8a81ad2f1f2f27e43a2e7c188f6e8a354df6ce67585070d496a9" + "code": " yield* logStartupInfo(\"safe storage ready\", {\n backend: Option.getOrElse(selectedBackend, () => \"unknown\"),\n });\n }\n yield* appIdentity.configure;\n yield* applicationMenu.configure;\n yield* updates.configure;\n yield* DesktopRemoteUpdates.listen;\n yield* linuxUrlHandler.register;\n yield* bootstrap.pipe(Effect.catchCause((cause) => fatalStartupCause(\"bootstrap\", cause)));\n}).pipe(Effect.withSpan(\"desktop.startup\"));\n\nconst scopedProgram = Effect.scoped(\n Effect.gen(function* () {\n const runId = yield* makeDesktopRunId;\n yield* Effect.annotateLogsScoped({ scope: \"desktop\", runId });\n yield* Effect.annotateCurrentSpan({ scope: \"desktop\", runId });\n\n const shutdown = yield* DesktopShutdown.DesktopShutdown;\n\n yield* Effect.addFinalizer(() =>", + "checksum": "bdc461ecaad10816c4c84d65141aabd9e34c67a492e5157c64f432ab21b8e4a4" }, "distribution-cli-contract": { "id": "distribution-cli-contract", @@ -1366,8 +1366,8 @@ "end": 22, "language": "json", "label": "The t3 npm executable and published dist payload", - "code": "{\n \"name\": \"t3\",\n \"version\": \"0.0.33\",\n \"license\": \"MIT\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"https://github.com/pingdotgg/t3code\",\n \"directory\": \"apps/server\"\n },\n \"bin\": {\n \"t3\": \"./dist/bin.mjs\"\n },\n \"files\": [\n \"dist\"\n ],\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"node --watch src/bin.ts\",\n \"build:bundle\": \"vp pack && vp pack src/service-launcher.ts --out-dir dist --no-clean\",\n \"start\": \"node dist/bin.mjs\",\n \"typecheck\": \"tsgo --noEmit\",\n \"test\": \"vp test run\"", - "checksum": "d17e2567fc16fd9ca3b5640c87befcc259679127fb33e96434ef58249f1288f3" + "code": "{\n \"name\": \"t3\",\n \"version\": \"0.0.40\",\n \"license\": \"MIT\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"https://github.com/pingdotgg/t3code\",\n \"directory\": \"apps/server\"\n },\n \"bin\": {\n \"t3\": \"./dist/bin.mjs\"\n },\n \"files\": [\n \"dist\"\n ],\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"node --watch src/bin.ts\",\n \"build:bundle\": \"vp pack && vp pack src/service-launcher.ts --out-dir dist --no-clean\",\n \"start\": \"node dist/bin.mjs\",\n \"typecheck\": \"tsc --noEmit\",\n \"test\": \"vp test run\"", + "checksum": "ff0b0d77151e58daf9c4c49aff99adf1fadb95548e4cc4a1122158ba1bdecd83" }, "distribution-cli-bundles-web": { "id": "distribution-cli-bundles-web", @@ -1386,8 +1386,8 @@ "end": 2107, "language": "typescript", "label": "Desktop artifact naming, packed resources, update feed, and macOS target", - "code": " const buildConfig: Record = {\n appId: DESKTOP_APP_ID,\n productName: resolveDesktopProductName(version),\n artifactName: \"T3-Code-${version}-${arch}.${ext}\",\n electronLanguages: [...DESKTOP_ELECTRON_LANGUAGES],\n files: [...DESKTOP_FILE_EXCLUSIONS],\n directories: {\n buildResources: \"apps/desktop/resources\",\n },\n // All platforms keep app.asar fully packed; electron-builder's default\n // smart unpack extracts native libraries, which loaders find in\n // app.asar.unpacked. Windows additionally ships the server tree as the\n // hand-packed server.asar sidecar (see WINDOWS_SERVER_ASAR_RESOURCE).\n extraResources: [\n ...DESKTOP_EXTRA_RESOURCES,\n ...(platform === \"win\" ? WINDOWS_SERVER_EXTRA_RESOURCES : []),\n ],\n };\n const updateChannel = resolveDesktopUpdateChannel(version);\n const publishConfig = yield* resolveGitHubPublishConfig(updateChannel);\n if (publishConfig) {\n buildConfig.publish = [publishConfig];\n } else if (mockUpdates) {\n buildConfig.publish = [\n {\n provider: \"generic\",\n url: resolveMockUpdateServerUrl(mockUpdateServerPort),\n },\n ];\n }\n\n if (platform === \"mac\") {\n buildConfig.mac = {\n target: target === \"dmg\" ? [target, \"zip\"] : [target],\n icon: \"icon.icns\",\n category: \"public.app-category.developer-tools\",\n protocols: [\n {\n name: \"T3 Code\",\n schemes: [\"t3code\", \"t3code-dev\"],\n },\n ],\n ...(macPasskeySigning\n ? {\n entitlements: macPasskeySigning.entitlementsPath,\n provisioningProfile: macPasskeySigning.provisioningProfilePath,\n }\n : {}),\n };", - "checksum": "d86868659c92e31e0c84c37ffc43eb5d126a3dfdaa6d14e97fa9cc076312ff89" + "code": " }\n });\n\n yield* restoreRelativeSymlinks(source, destination);\n },\n);\n\nconst verifyPackagedBundleIsSelfContained = Effect.fn(\"verifyPackagedBundleIsSelfContained\")(\n function* (input: { readonly asarPath: string; readonly verbose: boolean }) {\n const fs = yield* FileSystem.FileSystem;\n const path = yield* Path.Path;\n\n const probeRoot = yield* fs.makeTempDirectoryScoped({\n prefix: \"t3code-bundle-selfcheck-\",\n });\n const extractedApp = path.join(probeRoot, \"extracted\");\n const probeApp = path.join(probeRoot, \"app\");\n yield* Effect.try({\n try: () => extractAll(input.asarPath, extractedApp),\n catch: (cause) =>\n new BundleNotSelfContainedError({\n exitCode: -1,\n output: `Could not extract ${input.asarPath} for the bundle self-containment check: ${String(cause)}`,\n }),\n });\n // Keep the existing symlink isolation guard even though the sidecar stage\n // is hoisted and should be physical. A future package-manager layout change\n // must not let the probe resolve through the build tree.\n yield* copyDirectoryPreservingSymlinks(extractedApp, probeApp);\n\n // Guard the guard: if anything above the probe provides a node_modules, a\n // missing dependency would resolve there and the check would pass while the\n // packaged tree is broken.\n for (const candidate of ancestorNodeModulesPaths(probeApp, path.sep)) {\n if (yield* fs.exists(candidate).pipe(Effect.orElseSucceed(() => false))) {\n return yield* new BundleNotSelfContainedError({\n exitCode: -1,\n output: `Refusing to report success: ${candidate} is visible from the probe directory, so bare imports could resolve outside the packaged tree. Remove or rename it, or point TMPDIR somewhere without one.`,\n });\n }\n }\n\n const entryPoint = path.join(probeApp, \"apps/server/dist/bin.mjs\");\n if (!(yield* fs.exists(entryPoint).pipe(Effect.orElseSucceed(() => false)))) {\n return yield* new BundleNotSelfContainedError({\n exitCode: -1,\n output: `Expected the server entry at ${entryPoint}.`,\n });\n }", + "checksum": "85de88acdfef7f3b3492da21423ba36d56e83dd3d158f376d01f454555135087" }, "distribution-desktop-linux-windows-targets": { "id": "distribution-desktop-linux-windows-targets", @@ -1396,8 +1396,8 @@ "end": 2174, "language": "typescript", "label": "Linux and Windows desktop target configuration", - "code": " if (platform === \"linux\") {\n buildConfig.linux = {\n target: [target],\n executableName: \"t3code\",\n icon: \"icons\",\n category: \"Development\",\n // electron-builder turns these into MimeType=x-scheme-handler/;\n // in the .desktop entry (Exec already gets %U), so browsers can hand\n // t3code:// OAuth callbacks to the app.\n protocols: [\n {\n name: \"T3 Code\",\n schemes: [\"t3code\", \"t3code-dev\"],\n },\n ],\n desktop: {\n entry: {\n StartupWMClass: \"t3code\",\n },\n },\n };\n }\n\n if (platform === \"win\") {\n buildConfig.npmRebuild = false;\n // Keep blockmap-based differential downloads enabled while changing the\n // installed file topology. The optimization is in the payload shape, not\n // in trading update bandwidth for install speed.\n buildConfig.nsis = { differentialPackage: true };\n const winConfig: Record = {\n target: [target],\n icon: \"icon.ico\",\n // Resource editing applies the product metadata and icon independently\n // of code signing. Disabling it for local unsigned builds leaves the\n // packaged executable with Electron's stock icon.\n signAndEditExecutable: true,\n };\n if (signed) {\n winConfig.azureSignOptions = yield* AzureTrustedSigningOptionsConfig;\n }\n buildConfig.win = winConfig;\n }", - "checksum": "86c6ea4e376e5cce7355d8e2c5e0471d8cfbb17be0975ca8ad5233c0ad82f39d" + "code": " {\n label: \"server sidecar self-containment check (node bin.mjs --version)\",\n verbose: input.verbose,\n },\n ).pipe(\n // Printing a version should be immediate. A regression that blocks (on\n // stdin, a port, a lock) would otherwise hang release CI until the job\n // times out with nothing useful in the log.\n Effect.timeout(BUNDLE_SELF_CHECK_TIMEOUT),\n Effect.catchTags({\n TimeoutError: () =>\n Effect.fail(\n new BundleNotSelfContainedError({\n exitCode: -1,\n output: `The packaged bundle did not print its version within ${Duration.toSeconds(BUNDLE_SELF_CHECK_TIMEOUT)}s; it is hanging rather than failing to resolve.`,\n }),\n ),\n BuildCommandFailedError: (error) =>\n Effect.fail(\n new BundleNotSelfContainedError({\n exitCode: error.exitCode,\n output: `${error.stderrTail ?? \"\"}${error.stdoutTail ?? \"\"}`.trim(),\n }),\n ),\n }),\n );\n },\n);\n\nexport const stageLinuxCaptureHelper = Effect.fn(\"stageLinuxCaptureHelper\")(function* (input: {\n readonly backend: \"kde\" | \"hyprland\";\n readonly repoRoot: string;\n readonly stageResourcesDir: string;\n readonly arch: typeof BuildArch.Type;\n readonly verbose: boolean;\n}) {\n const fs = yield* FileSystem.FileSystem;\n const path = yield* Path.Path;\n const [rustTarget] = resolveResourceMonitorRustTargets(\"linux\", input.arch);\n // Release CI restores these binaries from a cache keyed on the crate sources and\n // skips the Rust toolchain on a hit, so the build must be skippable too.\n const reuseHelpers = yield* Config.boolean(\"T3CODE_DESKTOP_REUSE_LINUX_CAPTURE_HELPERS\").pipe(", + "checksum": "72d73898e34878019a4f5ef835a4f7eb7f5d7724879626a56d3245c4d272bcb6" }, "distribution-release-matrix": { "id": "distribution-release-matrix", @@ -1406,8 +1406,8 @@ "end": 397, "language": "yaml", "label": "The four desktop artifacts actually shipped by release CI", - "code": " build:\n name: Build ${{ matrix.label }}\n # build_wsl_node_pty stays in `needs` so it runs first and its artifact is\n # available to download, but only the Windows matrix entry consumes it. We\n # therefore gate the job on preflight + relay (must succeed) WITHOUT requiring\n # build_wsl_node_pty, so a failed Linux prebuild doesn't skip the macOS/Linux\n # builds. `!cancelled()` (not `!failure()`) lets the job run even when\n # build_wsl_node_pty failed; the Windows-only download step below then fails\n # that single platform if the prebuild is missing.\n needs: [preflight, relay_public_config, build_wsl_node_pty]\n if: ${{ !cancelled() && needs.preflight.result == 'success' && needs.relay_public_config.result == 'success' }}\n runs-on: ${{ matrix.runner }}\n timeout-minutes: 30\n env:\n T3CODE_CLERK_PUBLISHABLE_KEY: ${{ needs.relay_public_config.outputs.clerk_publishable_key }}\n T3CODE_CLERK_JWT_TEMPLATE: ${{ needs.relay_public_config.outputs.clerk_jwt_template }}\n T3CODE_CLERK_CLI_OAUTH_CLIENT_ID: ${{ needs.relay_public_config.outputs.clerk_cli_oauth_client_id }}\n T3CODE_RELAY_URL: ${{ needs.relay_public_config.outputs.relay_url }}\n strategy:\n fail-fast: false\n matrix:\n include:\n - label: macOS arm64\n runner: blacksmith-12vcpu-macos-26\n platform: mac\n target: dmg\n arch: arm64\n rust_target: aarch64-apple-darwin\n resource_key: darwin-arm64\n - label: macOS x64\n runner: blacksmith-12vcpu-macos-26\n platform: mac\n target: dmg\n arch: x64\n rust_target: x86_64-apple-darwin\n resource_key: darwin-x64\n - label: Linux x64\n runner: blacksmith-32vcpu-ubuntu-2404\n platform: linux\n target: AppImage\n arch: x64\n rust_target: x86_64-unknown-linux-gnu\n resource_key: linux-x64\n - label: Windows x64\n runner: blacksmith-32vcpu-windows-2025\n platform: win\n target: nsis\n arch: x64\n rust_target: x86_64-pc-windows-msvc\n resource_key: win32-x64\n # - label: Windows arm64\n # runner: windows-11-arm\n # platform: win\n # target: nsis\n # arch: arm64\n steps:", - "checksum": "e7a92ff379e65dc8c67d6b2cbe3d5f834a4ff4a6a48fca09f395ac33d93a6dde" + "code": " runs-on: blacksmith-8vcpu-ubuntu-2404\n timeout-minutes: 15\n steps:\n - name: Checkout\n uses: actions/checkout@v6\n with:\n ref: ${{ needs.resolve_commit.outputs.ref }}\n sparse-checkout: |\n /*\n !/.repos/\n sparse-checkout-cone-mode: false\n\n - name: Setup Vite+\n uses: voidzero-dev/setup-vp@v1\n with:\n node-version-file: package.json\n cache: true\n run-install: |\n args:\n - --filter=t3...\n\n - name: Build node-pty linux-x64 prebuild\n shell: bash\n run: |\n set -euo pipefail\n # Resolve node-pty from apps/server (where it's a dependency) and build\n # its native binary from source for Linux. node-addon-api resolves from\n # node-pty's own dependency tree, so node-gyp has everything it needs.\n pty_pkg=\"$(node -e \"console.log(require.resolve('node-pty/package.json', { paths: ['$GITHUB_WORKSPACE/apps/server'] }))\")\"\n pty_dir=\"$(dirname \"$pty_pkg\")\"\n ( cd \"$pty_dir\" && npx --yes node-gyp rebuild )\n mkdir -p wsl-prebuild\n cp \"$pty_dir/build/Release/pty.node\" wsl-prebuild/pty.node\n file wsl-prebuild/pty.node\n\n - name: Upload node-pty linux-x64 prebuild\n uses: actions/upload-artifact@v7\n with:\n name: wsl-node-pty-x64\n path: wsl-prebuild/pty.node\n if-no-files-found: error\n\n build:\n name: Build ${{ matrix.label }}\n # build_wsl_node_pty stays in `needs` so it runs first and its artifact is\n # available to download, but only the Windows matrix entry consumes it. We\n # therefore gate the job on preflight + relay (must succeed) WITHOUT requiring\n # build_wsl_node_pty, so a failed Linux prebuild doesn't skip the macOS/Linux\n # builds. `!cancelled()` (not `!failure()`) lets the job run even when\n # build_wsl_node_pty failed; the Windows-only download step below then fails\n # that single platform if the prebuild is missing.\n needs: [preflight, relay_public_config, build_wsl_node_pty]\n if: ${{ !cancelled() && needs.preflight.result == 'success' && needs.relay_public_config.result == 'success' }}\n runs-on: ${{ matrix.runner }}\n timeout-minutes: 30\n env:", + "checksum": "ea2ed73a3027b4ad6df579c8a5fe42eb7e1ec1efe71c2229b795a8cfee758f17" }, "distribution-cli-before-release": { "id": "distribution-cli-before-release", @@ -1416,8 +1416,8 @@ "end": 791, "language": "yaml", "label": "Exact CLI publication gates GitHub Release publication", - "code": " publish_cli:\n name: Publish CLI to npm\n needs: [preflight, relay_public_config, quality, build]\n if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.relay_public_config.result == 'success' && needs.quality.result == 'success' && needs.build.result == 'success' }}\n runs-on: ubuntu-24.04 # blacksmith-8vcpu-ubuntu-2404\n timeout-minutes: 10\n permissions:\n contents: read\n id-token: write\n env:\n T3CODE_CLERK_PUBLISHABLE_KEY: ${{ needs.relay_public_config.outputs.clerk_publishable_key }}\n T3CODE_CLERK_JWT_TEMPLATE: ${{ needs.relay_public_config.outputs.clerk_jwt_template }}\n T3CODE_CLERK_CLI_OAUTH_CLIENT_ID: ${{ needs.relay_public_config.outputs.clerk_cli_oauth_client_id }}\n T3CODE_RELAY_URL: ${{ needs.relay_public_config.outputs.relay_url }}\n steps:\n - name: Checkout\n uses: actions/checkout@v6\n with:\n ref: ${{ needs.preflight.outputs.ref }}\n sparse-checkout: |\n /*\n !/.repos/\n sparse-checkout-cone-mode: false\n\n - name: Setup Vite+\n uses: voidzero-dev/setup-vp@v1\n with:\n node-version-file: package.json\n cache: true\n run-install: |\n args:\n - --filter=t3...\n - --filter=@t3tools/web...\n - --filter=@t3tools/scripts...\n\n - name: Download relay client tracing config\n uses: actions/download-artifact@v8\n with:\n name: relay-client-tracing-config\n path: ${{ runner.temp }}/relay-client-tracing\n\n - name: Load relay client tracing config\n shell: bash\n run: |\n config_path=\"$RUNNER_TEMP/relay-client-tracing/relay-client-tracing.env\"\n tracing_token=\"$(sed -n 's/^T3CODE_RELAY_CLIENT_OTLP_TRACES_TOKEN=//p' \"$config_path\")\"\n echo \"::add-mask::$tracing_token\"\n cat \"$config_path\" >> \"$GITHUB_ENV\"\n\n - name: Align package versions to release version\n run: node scripts/update-release-package-versions.ts \"${{ needs.preflight.outputs.version }}\"\n\n - name: Build web package\n run: vp run --filter @t3tools/web build\n\n - name: Build CLI package\n run: vp run --filter t3 build\n\n - name: Download resource monitors\n uses: actions/download-artifact@v8\n with:\n pattern: resource-monitor-*\n path: ${{ runner.temp }}/resource-monitors\n\n - name: Bundle resource monitors into CLI package\n shell: bash\n run: |\n set -euo pipefail\n for artifact_dir in \"$RUNNER_TEMP\"/resource-monitors/resource-monitor-*; do\n resource_key=\"${artifact_dir##*/resource-monitor-}\"\n target_dir=\"apps/server/dist/resource-monitor/${resource_key}\"\n mkdir -p \"$target_dir\"\n cp \"$artifact_dir\"/t3-resource-monitor* \"$target_dir/\"\n chmod +x \"$target_dir\"/t3-resource-monitor 2>/dev/null || true\n done\n\n - name: Publish CLI package\n run: node apps/server/scripts/cli.ts publish --tag \"${{ needs.preflight.outputs.cli_dist_tag }}\" --app-version \"${{ needs.preflight.outputs.version }}\" --verbose\n\n release:\n name: Publish GitHub Release\n needs: [preflight, build, publish_cli]\n if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.build.result == 'success' && needs.publish_cli.result == 'success' }}", - "checksum": "08174e0cfbd9d1a218a3690cdd7f77f0de1d49b01cf5289c2ef9097b5c164af6" + "code": " fi\n\n vp run dist:desktop:artifact \"${args[@]}\"\n\n - name: Collect release assets\n shell: bash\n run: |\n set -euo pipefail\n mkdir -p release-publish\n\n shopt -s nullglob\n for pattern in \\\n \"release/*.dmg\" \\\n \"release/*.zip\" \\\n \"release/*.AppImage\" \\\n \"release/*.exe\" \\\n \"release/*.blockmap\" \\\n \"release/*.yml\"; do\n for file in $pattern; do\n cp \"$file\" release-publish/\n done\n done\n\n if [[ \"${{ matrix.platform }}\" == \"mac\" && \"${{ matrix.arch }}\" != \"arm64\" ]]; then\n shopt -s nullglob\n for manifest in release-publish/*-mac.yml; do\n mv \"$manifest\" \"${manifest%.yml}-${{ matrix.arch }}.yml\"\n done\n fi\n\n # Enable if Windows arm64 builds are enabled.\n # Windows updater metadata is channel-specific (for example\n # \"latest.yml\" or \"nightly.yml\"). Suffix each per-arch copy so the\n # release job can merge matching arm64/x64 manifests back into one\n # canonical manifest per channel.\n # if [[ \"${{ matrix.platform }}\" == \"win\" ]]; then\n # shopt -s nullglob\n # for manifest in release-publish/*.yml; do\n # mv \"$manifest\" \"${manifest%.yml}-win-${{ matrix.arch }}.yml\"\n # done\n # fi\n\n - name: Collect resource monitor\n shell: bash\n run: |\n set -euo pipefail\n binary_name=\"t3-resource-monitor\"\n if [[ \"${{ matrix.platform }}\" == \"win\" ]]; then\n binary_name=\"${binary_name}.exe\"\n fi\n source_path=\"native/resource-monitor/target/${{ matrix.rust_target }}/release/${binary_name}\"\n target_dir=\"resource-monitor-publish/${{ matrix.resource_key }}\"\n mkdir -p \"$target_dir\"\n cp \"$source_path\" \"$target_dir/$binary_name\"\n\n - name: Upload build artifacts\n uses: actions/upload-artifact@v7\n with:\n name: desktop-${{ matrix.platform }}-${{ matrix.arch }}\n path: release-publish/*\n if-no-files-found: error\n\n - name: Upload resource monitor\n uses: actions/upload-artifact@v7\n with:\n name: resource-monitor-${{ matrix.resource_key }}\n path: resource-monitor-publish/${{ matrix.resource_key }}/*\n if-no-files-found: error\n\n publish_cli:\n name: Publish CLI to npm\n needs: [preflight, relay_public_config, quality, build]\n if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.relay_public_config.result == 'success' && needs.quality.result == 'success' && needs.build.result == 'success' }}\n runs-on: ubuntu-24.04 # blacksmith-8vcpu-ubuntu-2404\n timeout-minutes: 10\n permissions:\n contents: read\n id-token: write\n env:\n T3CODE_CLERK_PUBLISHABLE_KEY: ${{ needs.relay_public_config.outputs.clerk_publishable_key }}\n T3CODE_CLERK_JWT_TEMPLATE: ${{ needs.relay_public_config.outputs.clerk_jwt_template }}\n T3CODE_CLERK_CLI_OAUTH_CLIENT_ID: ${{ needs.relay_public_config.outputs.clerk_cli_oauth_client_id }}\n T3CODE_RELAY_URL: ${{ needs.relay_public_config.outputs.relay_url }}", + "checksum": "d9de8322785cc47bf55fc0f1a35603917e2a196975c79def61bef2f99e1503b5" }, "distribution-mobile-fingerprint-publish": { "id": "distribution-mobile-fingerprint-publish", @@ -1436,8 +1436,8 @@ "end": 132, "language": "typescript", "label": "Marketing download links resolve against latest GitHub Release assets", - "code": "", - "checksum": "2797f24449eb4c2753ee4fc82674f127d1cd273af1aa0b25d2f7cc769b4367d0" + "code": "
\n \n

Linux

\n
\n
\n \n\n \n
\n
\n \n

Mobile

\n
\n \n
\n\n
\n
\n \n

Terminal

", + "checksum": "cd152ea56c962c64e35e9150ac84b9f2e08a2e898f93181705c512b6ba3a0b95" }, "distribution-aur-selection": { "id": "distribution-aur-selection", @@ -1456,8 +1456,8 @@ "end": 155, "language": "bash", "label": "Stable and nightly releases derive different versions, tags, and latest policy", - "code": " - id: release_meta\n name: Resolve release version\n shell: bash\n env:\n DISPATCH_CHANNEL: ${{ github.event.inputs.channel }}\n DISPATCH_VERSION: ${{ github.event.inputs.version }}\n NIGHTLY_DATE: ${{ github.run_started_at }}\n NIGHTLY_SHA: ${{ github.sha }}\n NIGHTLY_RUN_NUMBER: ${{ github.run_number }}\n run: |\n if [[ \"${GITHUB_EVENT_NAME}\" == \"schedule\" || ( \"${GITHUB_EVENT_NAME}\" == \"workflow_dispatch\" && \"${DISPATCH_CHANNEL:-stable}\" == \"nightly\" ) ]]; then\n nightly_date=\"$(date -u -d \"$NIGHTLY_DATE\" +%Y%m%d)\"\n\n node scripts/resolve-nightly-release.ts \\\n --date \"$nightly_date\" \\\n --run-number \"$NIGHTLY_RUN_NUMBER\" \\\n --sha \"$NIGHTLY_SHA\" \\\n --github-output\n\n echo \"release_channel=nightly\" >> \"$GITHUB_OUTPUT\"\n echo \"cli_dist_tag=nightly\" >> \"$GITHUB_OUTPUT\"\n echo \"is_prerelease=true\" >> \"$GITHUB_OUTPUT\"\n echo \"make_latest=false\" >> \"$GITHUB_OUTPUT\"\n else\n if [[ \"${GITHUB_EVENT_NAME}\" == \"workflow_dispatch\" ]]; then\n raw=\"${DISPATCH_VERSION}\"\n if [[ -z \"$raw\" ]]; then\n echo \"workflow_dispatch stable releases require the version input.\" >&2\n exit 1\n fi\n else\n raw=\"${GITHUB_REF_NAME}\"\n fi\n\n version=\"${raw#v}\"\n if [[ ! \"$version\" =~ ^[0-9]+\\.[0-9]+\\.[0-9]+([.-][0-9A-Za-z.-]+)?$ ]]; then\n echo \"Invalid release version: $raw\" >&2\n exit 1\n fi\n\n echo \"release_channel=stable\" >> \"$GITHUB_OUTPUT\"\n echo \"version=$version\" >> \"$GITHUB_OUTPUT\"\n echo \"tag=v$version\" >> \"$GITHUB_OUTPUT\"\n echo \"name=T3 Code v$version\" >> \"$GITHUB_OUTPUT\"\n echo \"cli_dist_tag=latest\" >> \"$GITHUB_OUTPUT\"\n if [[ \"$version\" =~ ^[0-9]+\\.[0-9]+\\.[0-9]+$ ]]; then\n echo \"is_prerelease=false\" >> \"$GITHUB_OUTPUT\"\n echo \"make_latest=true\" >> \"$GITHUB_OUTPUT\"\n else\n echo \"is_prerelease=true\" >> \"$GITHUB_OUTPUT\"\n echo \"make_latest=false\" >> \"$GITHUB_OUTPUT\"\n fi\n fi", - "checksum": "14a9355c1c509245042afe02fcfb4fd155578c5b31615b9440e1ca4a7375c4e7" + "code": " steps:\n - name: Checkout\n uses: actions/checkout@v6\n with:\n ref: ${{ needs.resolve_commit.outputs.ref }}\n fetch-depth: 0\n sparse-checkout: |\n /*\n !/.repos/\n sparse-checkout-cone-mode: false\n\n - name: Setup Vite+\n uses: voidzero-dev/setup-vp@v1\n with:\n node-version-file: package.json\n cache: true\n run-install: true\n env:\n pnpm_config_cache_dir: ${{ runner.temp }}/pnpm-metadata\n\n - id: release_meta\n name: Resolve release version\n shell: bash\n env:\n DISPATCH_CHANNEL: ${{ github.event.inputs.channel }}\n DISPATCH_VERSION: ${{ github.event.inputs.version }}\n NIGHTLY_VERSION: ${{ needs.resolve_commit.outputs.nightly_version }}\n NIGHTLY_DATE: ${{ github.run_started_at }}\n NIGHTLY_SHA: ${{ needs.resolve_commit.outputs.ref }}\n NIGHTLY_RUN_NUMBER: ${{ github.run_number }}\n run: |\n if [[ \"${GITHUB_EVENT_NAME}\" == \"schedule\" || ( \"${GITHUB_EVENT_NAME}\" == \"workflow_dispatch\" && \"${DISPATCH_CHANNEL:-stable}\" == \"nightly\" ) ]]; then\n nightly_date=\"$(date -u -d \"$NIGHTLY_DATE\" +%Y%m%d)\"\n\n node scripts/resolve-nightly-release.ts \\\n --date \"$nightly_date\" \\\n --run-number \"$NIGHTLY_RUN_NUMBER\" \\\n --sha \"$NIGHTLY_SHA\" \\\n --github-output\n\n echo \"release_channel=nightly\" >> \"$GITHUB_OUTPUT\"\n echo \"cli_dist_tag=nightly\" >> \"$GITHUB_OUTPUT\"\n echo \"is_prerelease=true\" >> \"$GITHUB_OUTPUT\"\n echo \"make_latest=false\" >> \"$GITHUB_OUTPUT\"\n else\n if [[ \"${GITHUB_EVENT_NAME}\" == \"workflow_dispatch\" ]]; then\n raw=\"${DISPATCH_VERSION:-$NIGHTLY_VERSION}\"\n if [[ -z \"$raw\" ]]; then\n echo \"workflow_dispatch stable releases need a version input or a published nightly.\" >&2\n exit 1\n fi\n else\n raw=\"${GITHUB_REF_NAME}\"", + "checksum": "8aac1e2b1d3a8b2f8c4ac97bf4961e421a1d238f851d69c1dbbeb4d395a13a7b" }, "release-exact-version-invariant": { "id": "release-exact-version-invariant", @@ -1466,8 +1466,8 @@ "end": 187, "language": "markdown", "label": "npm exact-version publication precedes every released client", - "code": "## Server self-update release invariant\n\nConnected servers update to the client's exact version, not to an npm dist-tag. Every released\ndesktop or hosted client version must therefore have a matching `t3@` package available on\nnpm before users can receive that client.\n\nThe workflow enforces this ordering:\n\n1. `publish_cli` publishes the exact stable or nightly version to npm.\n2. `release` depends on `publish_cli` before exposing desktop artifacts in GitHub Releases.\n3. `deploy_web` depends on `release` before moving the hosted channel to the new client.\n\nPreserve these dependencies when changing the release graph. Publishing a client first would leave\nthe **Update server** action targeting a package version that does not exist yet.", - "checksum": "22eb4423a30d49b23f50117b70459bc87aacfafc9f766b8fe75d0083df4377f8" + "code": " deployment, so `app.t3.codes` points at a deployment containing the router\n rules in `apps/web/vercel.ts`. Future stable releases keep this alias current.\n\n## Nightly builds\n\n- Workflow: `.github/workflows/release.yml`\n- Triggers:\n - scheduled check every 30 minutes\n - manual `workflow_dispatch` with `channel=nightly`\n- Automatic nightlies require new commits and at least six hours since the last nightly was published, including manual nightlies.\n- Manual nightlies bypass the time and change checks. Nightly runs remain serialized. Scheduled runs wait for an active nightly to finish, then check the publication gap before building.\n- Runs the same desktop quality gates and artifact matrix as the tagged release flow.\n- Publishes a GitHub prerelease only:\n - current tag format: `vX.Y.Z-nightly.YYYYMMDD.`", + "checksum": "c35ffbdb391f4bccf16d8ec8671959dfe2a3bc81f793ec45e16ccfab5305b8a5" }, "desktop-update-check-download": { "id": "desktop-update-check-download", @@ -1476,8 +1476,8 @@ "end": 458, "language": "typescript", "label": "Desktop channel selection, update checks, and user-triggered download", - "code": " const applyAutoUpdaterChannel = Effect.fn(\"desktop.updates.applyAutoUpdaterChannel\")(function* (\n channel: DesktopUpdateChannel,\n ) {\n yield* Effect.annotateCurrentSpan({ channel });\n const allowsPrerelease = channel === \"nightly\";\n yield* electronUpdater.setChannel(channel);\n yield* electronUpdater.setAllowPrerelease(allowsPrerelease);\n yield* electronUpdater.setAllowDowngrade(allowsPrerelease);\n yield* electronUpdater.setFullChangelog(allowsPrerelease);\n yield* logUpdaterInfo(\"using update channel\", {\n channel,\n allowPrerelease: allowsPrerelease,\n allowDowngrade: allowsPrerelease,\n fullChangelog: allowsPrerelease,\n });\n });\n\n const shouldEnableAutoUpdates = resolveDisabledReason.pipe(Effect.map(Option.isNone));\n\n const checkForUpdates = Effect.fn(\"desktop.updates.checkForUpdates\")(function* (\n reason: string,\n actionReservation: \"acquire\" | \"held\" = \"acquire\",\n ) {\n yield* Effect.annotateCurrentSpan({ reason });\n if (yield* Ref.get(desktopState.quitting)) return false;\n if (!(yield* Ref.get(updaterConfiguredRef))) return false;\n\n const state = yield* Ref.get(updateStateRef);\n if (state.status === \"downloading\") {\n yield* logUpdaterInfo(\"skipping update check while update is active\", {\n reason,\n status: state.status,\n });\n return false;\n }\n\n if (actionReservation === \"acquire\" && !(yield* tryStartUpdateAction(\"check\"))) return false;\n\n const check = Effect.gen(function* () {\n const checkedAt = yield* currentIsoTimestamp;\n yield* setState(reduceDesktopUpdateStateOnCheckStart(state, checkedAt));\n yield* logUpdaterInfo(\"checking for updates\", { reason });\n\n return yield* electronUpdater.checkForUpdates.pipe(\n Effect.as(true),\n Effect.catchTags({\n ElectronUpdaterCheckForUpdatesError: Effect.fn(\n \"desktop.updates.handleCheckForUpdatesFailure\",\n )(function* (error) {\n const failedAt = yield* currentIsoTimestamp;\n yield* updateState((current) =>\n reduceDesktopUpdateStateOnCheckFailure(current, error.message, failedAt),\n );\n yield* logUpdaterError(error.message, {\n errorTag: error._tag,\n channel: error.channel,\n });\n return true;\n }),\n }),\n );\n });\n\n return yield* actionReservation === \"held\"\n ? check\n : check.pipe(Effect.ensuring(finishUpdateAction(\"check\")));\n });\n\n const downloadAvailableUpdate = Effect.gen(function* () {\n const state = yield* Ref.get(updateStateRef);\n if (!(yield* Ref.get(updaterConfiguredRef)) || state.status !== \"available\") {\n return { accepted: false, completed: false };\n }\n\n if (!(yield* tryStartUpdateAction(\"download\"))) {\n return { accepted: false, completed: false };\n }\n\n return yield* Effect.gen(function* () {\n yield* setState(reduceDesktopUpdateStateOnDownloadStart(state));\n yield* electronUpdater.setDisableDifferentialDownload(\n isArm64HostRunningIntelBuild(environment.runtimeInfo),\n );\n yield* logUpdaterInfo(\"downloading update\");\n yield* electronUpdater.downloadUpdate;\n return { accepted: true, completed: true };\n }).pipe(\n Effect.catchTags({\n ElectronUpdaterDownloadUpdateError: Effect.fn(\"desktop.updates.handleDownloadFailure\")(\n function* (error) {\n yield* updateState((current) =>\n reduceDesktopUpdateStateOnDownloadFailure(current, error.message),\n );\n yield* logUpdaterError(error.message, {\n errorTag: error._tag,\n channel: error.channel,\n });\n return { accepted: true, completed: false };\n },\n ),\n }),\n Effect.onInterrupt(() =>\n updateState((current) => (current.status === \"downloading\" ? state : current)).pipe(\n Effect.asVoid,\n ),\n ),\n Effect.catchCause((cause) => {\n if (Cause.hasInterruptsOnly(cause)) {\n return Effect.failCause(cause);\n }\n const error = new DesktopUpdateUnexpectedActionError({ action: \"download\", cause });\n return Effect.gen(function* () {\n yield* updateState((current) =>\n reduceDesktopUpdateStateOnDownloadFailure(current, error.message),\n );\n yield* logUpdaterError(error.message, {\n errorTag: error._tag,\n action: error.action,\n });\n return { accepted: true, completed: false };\n });\n }),\n Effect.ensuring(finishUpdateAction(\"download\")),\n );\n }).pipe(Effect.withSpan(\"desktop.updates.downloadAvailableUpdate\"));", - "checksum": "446f64f027ab1d89316bef75ed8f588b90ca57499d539bc7843dbab3994a2393" + "code": " const hasUpdateFeedConfig = Ref.get(appUpdateYmlConfigRef).pipe(\n Effect.map((appUpdateYmlConfig) => Option.isSome(appUpdateYmlConfig) || config.mockUpdates),\n );\n\n const resolveDisabledReason = Effect.gen(function* () {\n const hasFeedConfig = yield* hasUpdateFeedConfig;\n return Option.fromNullishOr(\n getAutoUpdateDisabledReason({\n isDevelopment: environment.isDevelopment,\n isPackaged: environment.isPackaged,\n platform: environment.platform,\n appImage: Option.getOrUndefined(config.appImagePath),\n disabledByEnv: config.disableAutoUpdate,\n hasUpdateFeedConfig: hasFeedConfig,\n }),\n );\n });\n\n const activeUpdateAction = Ref.get(activeUpdateActionRef);\n\n const tryStartUpdateAction = (action: UpdateAction): Effect.Effect =>\n Ref.modify(activeUpdateActionRef, (activeAction) =>\n Option.isSome(activeAction) ? [false, activeAction] : [true, Option.some(action)],\n );\n\n const tryStartChannelChange = Ref.modify(activeUpdateActionRef, (activeAction) =>\n Option.isSome(activeAction)\n ? [activeAction, activeAction]\n : [Option.none(), Option.some(\"channel\")],\n );\n\n const finishUpdateAction = (action: UpdateAction): Effect.Effect =>\n Ref.modify(activeUpdateActionRef, (activeAction) => {\n const finished = Option.isSome(activeAction) && activeAction.value === action;\n return [finished, finished ? Option.none() : activeAction] as const;\n }).pipe(\n Effect.flatMap((finished) =>\n finished ? PubSub.publish(finishedUpdateActions, action).pipe(Effect.asVoid) : Effect.void,\n ),\n );\n\n const applyAutoUpdaterChannel = Effect.fn(\"desktop.updates.applyAutoUpdaterChannel\")(function* (\n channel: DesktopUpdateChannel,\n ) {\n yield* Effect.annotateCurrentSpan({ channel });\n const allowsPrerelease = channel === \"nightly\";\n yield* electronUpdater.setChannel(channel);\n yield* electronUpdater.setAllowPrerelease(allowsPrerelease);\n yield* electronUpdater.setAllowDowngrade(allowsPrerelease);\n yield* electronUpdater.setFullChangelog(allowsPrerelease);\n yield* logUpdaterInfo(\"using update channel\", {\n channel,\n allowPrerelease: allowsPrerelease,\n allowDowngrade: allowsPrerelease,\n fullChangelog: allowsPrerelease,\n });\n });\n\n const shouldEnableAutoUpdates = resolveDisabledReason.pipe(Effect.map(Option.isNone));\n\n const checkForUpdates = Effect.fn(\"desktop.updates.checkForUpdates\")(function* (\n reason: string,\n actionReservation: \"acquire\" | \"held\" = \"acquire\",\n ) {\n yield* Effect.annotateCurrentSpan({ reason });\n if (yield* Ref.get(desktopState.quitting)) return false;\n if (!(yield* Ref.get(updaterConfiguredRef))) return false;\n\n const state = yield* Ref.get(updateStateRef);\n if (state.status === \"downloading\") {\n yield* logUpdaterInfo(\"skipping update check while update is active\", {\n reason,\n status: state.status,\n });\n return false;\n }\n\n if (actionReservation === \"acquire\" && !(yield* tryStartUpdateAction(\"check\"))) return false;\n\n const check = Effect.gen(function* () {\n const checkedAt = yield* currentIsoTimestamp;\n yield* setState(reduceDesktopUpdateStateOnCheckStart(state, checkedAt));\n yield* logUpdaterInfo(\"checking for updates\", { reason });\n\n return yield* electronUpdater.checkForUpdates.pipe(\n Effect.as(true),\n Effect.catchTags({\n ElectronUpdaterCheckForUpdatesError: Effect.fn(\n \"desktop.updates.handleCheckForUpdatesFailure\",\n )(function* (error) {\n const failedAt = yield* currentIsoTimestamp;\n yield* updateState((current) =>\n reduceDesktopUpdateStateOnCheckFailure(current, error.message, failedAt),\n );\n yield* logUpdaterError(error.message, {\n errorTag: error._tag,\n channel: error.channel,\n });\n return true;\n }),\n }),\n );\n });\n\n return yield* actionReservation === \"held\"\n ? check\n : check.pipe(\n Effect.onInterrupt(() => setState(state)),\n Effect.ensuring(finishUpdateAction(\"check\")),\n );\n });\n\n const downloadAvailableUpdate = Effect.gen(function* () {\n const state = yield* Ref.get(updateStateRef);\n if (!(yield* Ref.get(updaterConfiguredRef)) || state.status !== \"available\") {\n return { accepted: false, completed: false };\n }\n\n if (!(yield* tryStartUpdateAction(\"download\"))) {\n return { accepted: false, completed: false };\n }\n\n return yield* Effect.gen(function* () {\n yield* setState(reduceDesktopUpdateStateOnDownloadStart(state));\n yield* electronUpdater.setDisableDifferentialDownload(", + "checksum": "fa9aca9c2f281bf8bde3c52cc0bf148bdc9039c547070339a4e509ec6af60925" }, "desktop-update-install-boundary": { "id": "desktop-update-install-boundary", @@ -1486,8 +1486,8 @@ "end": 575, "language": "typescript", "label": "Desktop install stops every backend before updater-controlled restart", - "code": " const installDownloadedUpdate = Effect.gen(function* () {\n const state = yield* Ref.get(updateStateRef);\n const hasInstallableDownload =\n state.downloadedVersion !== null &&\n (state.status === \"downloaded\" ||\n (state.status === \"error\" &&\n (state.errorContext === null || state.errorContext === \"install\")));\n if (\n (yield* Ref.get(desktopState.quitting)) ||\n !(yield* Ref.get(updaterConfiguredRef)) ||\n !hasInstallableDownload\n ) {\n return { accepted: false, completed: false };\n }\n\n if (!(yield* tryStartUpdateAction(\"install\"))) {\n return { accepted: false, completed: false };\n }\n\n yield* Ref.set(desktopState.quitting, true);\n\n return yield* Effect.gen(function* () {\n // Stop every backend in the pool, not just the primary. With\n // parallel WSL + Windows backends, leaving the WSL instance up\n // means quitAndInstall's app.quit() exits before the pool's\n // scope cascade has a chance to run its stop finalizer, so the\n // WSL child gets hard-killed by the OS instead of receiving\n // SIGTERM + grace. Stops run concurrently with the same 5s\n // budget the primary had on its own.\n const instances = yield* pool.list;\n yield* Effect.forEach(\n instances,\n (instance) => instance.stop({ timeout: Duration.seconds(5) }),\n { concurrency: \"unbounded\" },\n );\n yield* electronWindow.destroyAll;\n yield* electronUpdater.quitAndInstall({\n isSilent: true,\n isForceRunAfter: true,\n });\n return { accepted: true, completed: false };\n }).pipe(\n Effect.catchTags({\n ElectronUpdaterQuitAndInstallError: Effect.fn(\"desktop.updates.handleInstallFailure\")(\n function* (error) {\n yield* resetInstallAction;\n yield* updateState((current) =>\n reduceDesktopUpdateStateOnInstallFailure(current, error.message),\n );\n yield* logUpdaterError(error.message, {\n errorTag: error._tag,\n channel: error.channel,\n isSilent: error.isSilent,\n isForceRunAfter: error.isForceRunAfter,\n });\n return { accepted: true, completed: false };\n },\n ),\n }),\n Effect.onInterrupt(() => resetInstallAction),\n Effect.catchCause((cause) =>\n Effect.gen(function* () {\n if (Cause.hasInterruptsOnly(cause)) {\n return yield* Effect.failCause(cause);\n }\n yield* resetInstallAction;\n const error = new DesktopUpdateUnexpectedActionError({ action: \"install\", cause });\n yield* updateState((current) =>\n reduceDesktopUpdateStateOnInstallFailure(current, error.message),\n );\n yield* logUpdaterError(error.message, {\n errorTag: error._tag,\n action: error.action,\n });\n return { accepted: true, completed: false };\n }),\n ),\n );\n }).pipe(Effect.withSpan(\"desktop.updates.installDownloadedUpdate\"));\n\n const startUpdatePollers: Effect.Effect = Effect.gen(function* () {\n yield* Effect.sleep(AUTO_UPDATE_STARTUP_DELAY).pipe(\n Effect.andThen(checkForUpdates(\"startup\")),\n Effect.catchCause((cause) => {\n if (Cause.hasInterruptsOnly(cause)) {\n return Effect.void;\n }\n const error = new DesktopUpdatePollerError({ poller: \"startup\", cause });\n return logUpdaterError(error.message, {\n errorTag: error._tag,\n poller: error.poller,\n });\n }),\n Effect.forkScoped,\n );\n yield* Effect.sleep(AUTO_UPDATE_POLL_INTERVAL).pipe(\n Effect.andThen(checkForUpdates(\"poll\")),\n Effect.forever,\n Effect.catchCause((cause) => {\n if (Cause.hasInterruptsOnly(cause)) {\n return Effect.void;\n }\n const error = new DesktopUpdatePollerError({ poller: \"poll\", cause });\n return logUpdaterError(error.message, {\n errorTag: error._tag,\n poller: error.poller,\n });\n }),\n Effect.forkScoped,\n );\n }).pipe(Effect.withSpan(\"desktop.updates.startPollers\"));", - "checksum": "c192a6561381eb1d602b20151a6d027ae2bbbf6a34ec901d4b37a3f1421d8e95" + "code": " Effect.catchTags({\n ElectronUpdaterDownloadUpdateError: Effect.fn(\"desktop.updates.handleDownloadFailure\")(\n function* (error) {\n yield* updateState((current) =>\n reduceDesktopUpdateStateOnDownloadFailure(current, error.message),\n );\n yield* logUpdaterError(error.message, {\n errorTag: error._tag,\n channel: error.channel,\n });\n return { accepted: true, completed: false };\n },\n ),\n }),\n Effect.onInterrupt(() =>\n updateState((current) => (current.status === \"downloading\" ? state : current)).pipe(\n Effect.asVoid,\n ),\n ),\n Effect.catchCause((cause) => {\n if (Cause.hasInterruptsOnly(cause)) {\n return Effect.failCause(cause);\n }\n const error = new DesktopUpdateUnexpectedActionError({ action: \"download\", cause });\n return Effect.gen(function* () {\n yield* updateState((current) =>\n reduceDesktopUpdateStateOnDownloadFailure(current, error.message),\n );\n yield* logUpdaterError(error.message, {\n errorTag: error._tag,\n action: error.action,\n });\n return { accepted: true, completed: false };\n });\n }),\n Effect.ensuring(finishUpdateAction(\"download\")),\n );\n }).pipe(Effect.withSpan(\"desktop.updates.downloadAvailableUpdate\"));\n\n const resetInstallAction = Effect.all(\n [finishUpdateAction(\"install\"), Ref.set(desktopState.quitting, false)],\n { discard: true },\n );\n\n const recoverFailedInstall = Effect.fn(\"desktop.updates.recoverFailedInstall\")(function* (\n message: string,\n ) {\n const ownsRecovery = yield* Ref.modify(activeUpdateActionRef, (activeAction) =>\n Option.isSome(activeAction) && activeAction.value === \"install\"\n ? ([true, Option.some(\"install-recovery\")] as const)\n : ([false, activeAction] as const),\n );\n if (!ownsRecovery) return;\n\n yield* Ref.set(desktopState.quitting, false);\n yield* Effect.gen(function* () {\n const instances = yield* pool.list;\n const restartExit = yield* Effect.forEach(instances, (instance) => instance.start, {\n concurrency: \"unbounded\",\n discard: true,\n }).pipe(Effect.exit);\n yield* updateState((current) => reduceDesktopUpdateStateOnInstallFailure(current, message));\n if (Exit.isFailure(restartExit)) {\n yield* logUpdaterError(\"Desktop update install recovery could not restart every backend.\");\n }\n }).pipe(\n Effect.catchCause(() =>\n logUpdaterError(\"Desktop update install recovery failed unexpectedly.\"),\n ),\n Effect.ensuring(finishUpdateAction(\"install-recovery\")),\n );\n });\n\n const installDownloadedUpdate = (expectedVersion?: string) =>\n Effect.scoped(\n Effect.gen(function* () {\n const actionCompletions = yield* PubSub.subscribe(finishedUpdateActions);\n let admission: \"admitted\" | \"refused\" | \"wait-for-check\" = \"wait-for-check\";\n while (admission === \"wait-for-check\") {\n admission = yield* stateMutex.withPermits(1)(\n Effect.gen(function* () {\n const state = yield* Ref.get(updateStateRef);\n const activeAction = yield* Ref.get(activeUpdateActionRef);\n const hasExpectedDownload =\n state.downloadedVersion !== null &&\n (expectedVersion === undefined || state.downloadedVersion === expectedVersion);\n if (\n (yield* Ref.get(desktopState.quitting)) ||\n !(yield* Ref.get(updaterConfiguredRef)) ||\n !hasExpectedDownload\n ) {\n return \"refused\" as const;\n }\n if (Option.isSome(activeAction)) {\n return activeAction.value === \"check\" && expectedVersion !== undefined\n ? (\"wait-for-check\" as const)\n : (\"refused\" as const);\n }\n const hasInstallableDownload =\n state.status === \"downloaded\" ||\n (state.status === \"error\" &&\n (state.errorContext === null || state.errorContext === \"install\"));\n if (!hasInstallableDownload) return \"refused\" as const;\n return (yield* tryStartUpdateAction(\"install\"))\n ? (\"admitted\" as const)\n : (\"refused\" as const);\n }),\n );\n if (admission === \"wait-for-check\") {\n const finishedAction = yield* PubSub.take(actionCompletions).pipe(\n Effect.timeoutOption(PREPARED_INSTALL_CHECK_WAIT),", + "checksum": "8d886fdb8fdc99dc286c5ef1d42c07c61e68c4846db321fbae53906899b6eef3" }, "server-update-exact-preflight": { "id": "server-update-exact-preflight", @@ -1496,8 +1496,8 @@ "end": 169, "language": "typescript", "label": "Server self-update accepts only an exact version that passes staged preflight", - "code": " const update: ServerSelfUpdate[\"Service\"][\"update\"] = Effect.fn(\n \"cloud.server_self_update.update\",\n )(function* (input, reportProgress = () => Effect.void) {\n if (capability === \"desktop-managed\") {\n return yield* failWith(\n \"This server is managed by the T3 Code desktop app on its machine; update the desktop app to update it.\",\n );\n }\n if (capability === null) {\n return yield* failWith(\n \"Remote updates require the T3 Code background service. Run `t3 service install` on the server machine.\",\n );\n }\n\n const targetVersion = input.targetVersion.trim();\n if (!isExactServiceVersion(targetVersion)) {\n return yield* failWith(`'${targetVersion}' is not an exact t3 version.`);\n }\n if (yield* Ref.getAndSet(inFlight, true)) {\n return yield* failWith(\"A server update is already in progress.\");\n }\n\n return yield* Effect.gen(function* () {\n yield* reportProgress(\"downloading\");\n const paths = yield* ensurePinnedRuntimeInstalled({\n baseDir: serverConfig.baseDir,\n version: targetVersion,\n fs,\n path,\n runner,\n validate: (runtime) =>\n runner\n .run({\n command: execPath,\n args: [\n runtime.entryPath,\n \"__service-preflight\",\n \"--database-path\",\n serverConfig.dbPath,\n \"--launcher-protocol\",\n String(SERVICE_LAUNCHER_PROTOCOL),\n ],\n timeout: PREFLIGHT_TIMEOUT,\n })\n .pipe(\n Effect.mapError(\n (cause) =>\n new PinnedRuntimeInstallError({\n step: \"running the staged service preflight\",\n cause,\n }),\n ),\n Effect.flatMap(\n (\n result,\n ): Effect.Effect<\n void,\n PinnedRuntimeInstallError | PinnedRuntimePreflightBlockedError\n > => {\n if (result.code !== 0) {\n return Effect.fail(\n new PinnedRuntimeInstallError({\n step: \"running the staged service preflight\",\n exitCode: Number(result.code),\n stdoutLength: result.stdout.length,\n stderrLength: result.stderr.length,\n }),\n );\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(result.stdout.trim());\n } catch (cause) {\n return Effect.fail(\n new PinnedRuntimeInstallError({\n step: \"decoding the staged service preflight\",\n cause,\n }),\n );\n }\n const preflight = decodeServicePreflightResult(parsed);\n if (preflight === undefined || preflight.version !== targetVersion) {\n return Effect.fail(\n new PinnedRuntimeInstallError({\n step: \"verifying the staged service preflight\",\n }),\n );\n }\n return preflight.status === \"ready\"\n ? Effect.void\n : Effect.fail(\n new PinnedRuntimePreflightBlockedError({\n version: targetVersion,\n reason: preflight.reason,\n }),\n );\n },\n ),\n ),\n }).pipe(\n Effect.mapError((error) =>\n error._tag === \"PinnedRuntimePreflightBlockedError\"\n ? failWith(error.reason, error)\n : failWith(`Could not prepare t3@${targetVersion}.`, error),\n ),\n );", - "checksum": "2024b45fe781d2feca6c8eb96b8b543c146198449303cc50ed7d838efa3e3ab3" + "code": " readonly prepare: Effect.Effect, ServerSelfUpdateError>;\n readonly clear: (\n threadIds: ReadonlyArray,\n ) => Effect.Effect;\n}) {\n const desktopContinuationTokens = yield* Ref.make(HashSet.empty());\n const clearOnError = (\n effect: Effect.Effect,\n threadIds: () => ReadonlyArray,\n handoffAccepted: () => boolean,\n ): Effect.Effect =>\n effect.pipe(\n Effect.catchCause((cause) =>\n (handoffAccepted() && Cause.hasInterruptsOnly(cause)\n ? Effect.void\n : input.clear(threadIds())\n ).pipe(Effect.andThen(Effect.failCause(cause))),\n ),\n );\n\n const update: ServerSelfUpdate[\"Service\"][\"update\"] = (\n request,\n reportProgress = () => Effect.void,\n ) => {\n let prepared = false;\n let handoffAccepted = false;\n let continuationThreadIds: ReadonlyArray = [];\n return clearOnError(\n input.selfUpdate\n .update(\n request,\n (stage) =>\n (request.continueRunningThreads === true &&\n input.mode !== \"desktop\" &&\n stage === \"installing\" &&\n !prepared\n ? input.prepare.pipe(\n Effect.tap((threadIds) =>\n Effect.sync(() => {\n prepared = true;\n continuationThreadIds = threadIds;\n }),\n ),\n Effect.asVoid,\n )\n : Effect.void\n ).pipe(Effect.andThen(reportProgress(stage))),\n () =>\n Effect.sync(() => {\n handoffAccepted = true;\n }),\n )\n .pipe(\n Effect.tap((result) => {\n if (\n result.method === \"desktop-app\" &&\n result.desktopUpdateToken !== undefined &&\n request.continueRunningThreads === true\n ) {\n return Ref.update(desktopContinuationTokens, HashSet.add(result.desktopUpdateToken));\n }\n return Effect.void;\n }),\n ),\n () => continuationThreadIds,\n () => handoffAccepted,\n );\n };\n\n return ServerSelfUpdate.of({\n update,\n commitDesktopUpdate: (requestId) =>\n Effect.gen(function* () {\n const shouldContinue = yield* Ref.modify(desktopContinuationTokens, (tokens) => [\n HashSet.has(tokens, requestId),\n HashSet.remove(tokens, requestId),\n ]);\n let handoffAccepted = false;\n let continuationThreadIds: ReadonlyArray = [];\n return yield* clearOnError(\n Effect.gen(function* () {\n continuationThreadIds = shouldContinue ? yield* input.prepare : [];\n return yield* input.selfUpdate.commitDesktopUpdate(requestId, () =>\n Effect.sync(() => {\n handoffAccepted = true;\n }),\n );\n }),\n () => continuationThreadIds,\n () => handoffAccepted,\n ).pipe(\n Effect.catchCause((cause) =>\n (shouldContinue && !handoffAccepted\n ? Ref.update(desktopContinuationTokens, HashSet.add(requestId))\n : Effect.void\n ).pipe(Effect.andThen(Effect.failCause(cause))),\n ),\n );\n }),\n });\n});\n\nexport const make = Effect.fn(\"cloud.server_self_update.make\")(function* () {\n const serverConfig = yield* ServerConfig.ServerConfig;\n const desktopAppUpdate = yield* DesktopAppUpdate.DesktopAppUpdate;\n const launcher = yield* ServiceLauncherClient.ServiceLauncherClient;", + "checksum": "9a192b7758f29803e0afa10bffc9c33a4bed075ee390544f1c4c66fc7da90250" }, "server-update-launcher-handoff": { "id": "server-update-launcher-handoff", @@ -1506,8 +1506,8 @@ "end": 191, "language": "typescript", "label": "Prepared server runtime is handed to the stable launcher by update id", - "code": " yield* reportProgress(\"installing\");\n const updateId = yield* launcher\n .requestUpdate({ targetVersion, dbPath: serverConfig.dbPath })\n .pipe(\n Effect.mapError((error) =>\n failWith(\n error._tag === \"ServiceLauncherRejectedError\"\n ? error.reason\n : \"Could not ask the service launcher to activate the prepared update.\",\n error,\n ),\n ),\n );\n\n yield* Effect.logInfo(\"Server update prepared; handing off to the service launcher.\", {\n updateId,\n targetVersion,\n runtimePath: paths.entryPath,\n });\n return { targetVersion, method: \"boot-service\" as const, updateId };\n }).pipe(Effect.onError(() => Ref.set(inFlight, false)));", - "checksum": "ce552ec0f0f18d4a12f7a90044b4492a886c4e35c1044f3bedf158442e4ec62b" + "code": " const fs = yield* FileSystem.FileSystem;\n const path = yield* Path.Path;\n const execPath = yield* HostProcessExecutablePath;\n const inFlight = yield* Ref.make(false);\n\n const capability: ServerSelfUpdateCapability | null =\n serverConfig.mode === \"desktop\" ? \"desktop-managed\" : launcher.managed ? \"boot-service\" : null;\n const failWith = (reason: string, cause?: unknown) =>\n cause === undefined\n ? new ServerSelfUpdateError({ reason })\n : new ServerSelfUpdateError({ reason, cause });\n\n const update: ServerSelfUpdate[\"Service\"][\"update\"] = Effect.fn(\n \"cloud.server_self_update.update\",\n )(function* (input, reportProgress = () => Effect.void, onHandoffAccepted = () => Effect.void) {\n if (capability === \"desktop-managed\") {\n // input.targetVersion is meaningless here: the desktop app's own\n // update feed decides what it downloads, and the result carries what\n // it actually got.\n if (desktopAppUpdate.available) {\n return yield* desktopAppUpdate.run(reportProgress);", + "checksum": "72eb2ef4a9cbf72d7bdeed1f4dd67fa502a265df975b89535d43e11867906505" }, "mobile-ota-fingerprint-policy": { "id": "mobile-ota-fingerprint-policy", @@ -1516,8 +1516,8 @@ "end": 180, "language": "typescript", "label": "Mobile runtime compatibility is keyed by native fingerprint", - "code": "const config: ExpoConfig = {\n name: variant.appName,\n slug: \"t3-code\",\n platforms: [\"ios\", \"android\"],\n scheme: variant.scheme,\n version: \"1.0.4\",\n runtimeVersion: {\n // Fingerprint (not appVersion) so an OTA only reaches binaries whose native\n // project — native deps, config plugins, AND patches/ — matches the update.\n // With appVersion, every 0.1.0 build shares a runtime version, so a JS update\n // could land on a binary missing the native changes it needs and crash.\n policy: process.env.MOBILE_VERSION_POLICY ?? \"fingerprint\",\n },\n orientation: \"portrait\",\n icon: variant.assets.appIcon,\n userInterfaceStyle: \"automatic\",\n updates: {\n enabled: true,\n url: \"https://u.expo.dev/d763fcb8-d37c-41ea-a773-b54a0ab4a454\",\n checkAutomatically: \"ON_LOAD\",\n fallbackToCacheTimeout: 0,\n },", - "checksum": "8fca9ce8ca5fe7a6a1fa27d63f6bef643e0df47b68108ea3c1bbb3d29227b3a2" + "code": " },\n },\n android: {\n enabled: true,\n singleShareMimeTypes: [\"*/*\"],\n multipleShareMimeTypes: [\"*/*\"],\n },\n },\n];\n\n// These aliases match the fonts' PostScript names on iOS. Register the same\n// names on Android so React Native and the native composer use one set of\n// family names without waiting for runtime font loading.\n\nconst config: ExpoConfig = {\n name: variant.appName,\n slug: \"t3-code\",\n platforms: [\"ios\", \"android\"],\n scheme: variant.scheme,\n version: \"1.1.1\",\n runtimeVersion: {\n // Development manifests resolve on every launch, so avoid fingerprint's", + "checksum": "afa129f0b5ab92f8adcae337b5fdcda466fa3b090c12a5cd0a69020f03a80b2a" }, "analytics-posthog-controls": { "id": "analytics-posthog-controls", @@ -1526,8 +1526,8 @@ "end": 44, "language": "typescript", "label": "PostHog destination, opt-out, and bounded buffer configuration", - "code": "const TelemetryEnvConfig = Config.all({\n posthogKey: Config.string(\"T3CODE_POSTHOG_KEY\").pipe(\n Config.withDefault(\"phc_XOWci4oZP4VvLiEyrFqkFjP4CZn55mjYYBMREK5Wd6m\"),\n ),\n posthogHost: Config.string(\"T3CODE_POSTHOG_HOST\").pipe(\n Config.withDefault(\"https://us.i.posthog.com\"),\n ),\n enabled: Config.boolean(\"T3CODE_TELEMETRY_ENABLED\").pipe(Config.withDefault(true)),\n flushBatchSize: Config.number(\"T3CODE_TELEMETRY_FLUSH_BATCH_SIZE\").pipe(Config.withDefault(20)),\n maxBufferedEvents: Config.number(\"T3CODE_TELEMETRY_MAX_BUFFERED_EVENTS\").pipe(\n Config.withDefault(1_000),\n ),\n wslDistroName: Config.string(\"WSL_DISTRO_NAME\").pipe(Config.option),\n});", - "checksum": "f3c3ac2e572cef41e21529c9b74a5142da3d04ae2b8f5b948096e8eb497141c9" + "code": "\nconst TelemetryEnvConfig = Config.all({\n posthogKey: Config.string(\"T3CODE_POSTHOG_KEY\").pipe(\n Config.withDefault(\"phc_XOWci4oZP4VvLiEyrFqkFjP4CZn55mjYYBMREK5Wd6m\"),\n ),\n posthogHost: Config.string(\"T3CODE_POSTHOG_HOST\").pipe(\n Config.withDefault(\"https://us.i.posthog.com\"),\n ),\n enabled: Config.boolean(\"T3CODE_TELEMETRY_ENABLED\").pipe(Config.withDefault(true)),\n flushBatchSize: Config.number(\"T3CODE_TELEMETRY_FLUSH_BATCH_SIZE\").pipe(Config.withDefault(20)),\n maxBufferedEvents: Config.number(\"T3CODE_TELEMETRY_MAX_BUFFERED_EVENTS\").pipe(\n Config.withDefault(1_000),\n ),\n wslDistroName: Config.string(\"WSL_DISTRO_NAME\").pipe(Config.option),", + "checksum": "2303d01dd7d7b65ecb1c74c963c53e6f33b988abad062f8dc771d4351ed48e15" }, "analytics-posthog-boundary": { "id": "analytics-posthog-boundary", @@ -1536,8 +1536,8 @@ "end": 134, "language": "typescript", "label": "Installation-scoped analytics payload and PostHog batch boundary", - "code": "export const make = Effect.gen(function* () {\n const telemetryConfig = yield* TelemetryEnvConfig;\n const httpClient = yield* HttpClient.HttpClient;\n const serverConfig = yield* ServerConfig.ServerConfig;\n const identifier = yield* getTelemetryIdentifier;\n const bufferRef = yield* Ref.make>([]);\n const clientType = serverConfig.mode === \"desktop\" ? \"desktop-app\" : \"cli-web-client\";\n const hostPlatform = yield* HostProcessPlatform;\n const hostArchitecture = yield* HostProcessArchitecture;\n\n const enqueueBufferedEvent = (event: string, properties?: Readonly>) =>\n Effect.flatMap(DateTime.now, (now) =>\n Ref.modify(bufferRef, (current) => {\n const appended = [\n ...current,\n {\n event,\n ...(properties ? { properties } : {}),\n capturedAt: DateTime.formatIso(now),\n } satisfies BufferedAnalyticsEvent,\n ];\n\n const next =\n appended.length > telemetryConfig.maxBufferedEvents\n ? appended.slice(appended.length - telemetryConfig.maxBufferedEvents)\n : appended;\n\n return [\n {\n size: next.length,\n dropped: next.length !== appended.length,\n } as const,\n next,\n ] as const;\n }),\n );\n\n const sendBatch = Effect.fn(\"AnalyticsService.sendBatch\")(function* (\n events: ReadonlyArray,\n ) {\n if (!telemetryConfig.enabled || !identifier) return;\n\n const payload = {\n api_key: telemetryConfig.posthogKey,\n batch: events.map((event) => ({\n event: event.event,\n distinct_id: identifier,\n properties: {\n ...event.properties,\n $process_person_profile: false,\n platform: hostPlatform,\n wsl: Option.getOrUndefined(telemetryConfig.wslDistroName),\n arch: hostArchitecture,\n t3CodeVersion: packageJson.version,\n clientType,\n },\n timestamp: event.capturedAt,\n })),\n };\n\n yield* HttpClientRequest.post(`${telemetryConfig.posthogHost}/batch/`).pipe(\n HttpClientRequest.bodyJson(payload),\n Effect.flatMap(httpClient.execute),\n Effect.flatMap(HttpClientResponse.filterStatusOk),\n );\n });", - "checksum": "80a1888ab4ce1d23e1388a370d7d82c5313eeb46c325eb8d74c5609a55b8a82a" + "code": "\nfunction serverOsFromNodePlatform(platform: string): ClientOs {\n switch (platform) {\n case \"darwin\":\n return \"macOS\";\n case \"win32\":\n return \"Windows\";\n case \"linux\":\n return \"Linux\";\n case \"android\":\n return \"Android\";\n default:\n return \"other\";\n }\n}\n\n/** @public Service construction is part of the canonical Effect module API. */\nexport const make = Effect.gen(function* () {\n const telemetryConfig = yield* TelemetryEnvConfig;\n const httpClient = yield* HttpClient.HttpClient;\n const serverConfig = yield* ServerConfig.ServerConfig;\n const identifier = yield* getTelemetryIdentifier;\n const bufferRef = yield* Ref.make>([]);\n const clientType = serverConfig.mode === \"desktop\" ? \"desktop-app\" : \"cli-web-client\";\n const hostPlatform = yield* HostProcessPlatform;\n const hostArchitecture = yield* HostProcessArchitecture;\n\n const enqueueBufferedEvent = (event: string, properties?: Readonly>) =>\n Effect.flatMap(DateTime.now, (now) =>\n Ref.modify(bufferRef, (current) => {\n const appended = [\n ...current,\n {\n event,\n ...(properties ? { properties } : {}),\n capturedAt: DateTime.formatIso(now),\n } satisfies BufferedAnalyticsEvent,\n ];\n\n const next =\n appended.length > telemetryConfig.maxBufferedEvents\n ? appended.slice(appended.length - telemetryConfig.maxBufferedEvents)\n : appended;\n\n return [\n {\n size: next.length,\n dropped: next.length !== appended.length,\n } as const,\n next,\n ] as const;\n }),\n );\n\n const sendBatch = Effect.fn(\"AnalyticsService.sendBatch\")(function* (\n events: ReadonlyArray,\n ) {\n if (!telemetryConfig.enabled || !identifier) return;\n\n const payload = {\n api_key: telemetryConfig.posthogKey,\n batch: events.map((event) => ({\n event: event.event,\n distinct_id: identifier,\n properties: {\n ...event.properties,", + "checksum": "37fdc32332b00c42b38dab213b9bdf0df1fe3f36ecd254715674a98a6141559c" }, "analytics-identity-ladder": { "id": "analytics-identity-ladder", @@ -1566,18 +1566,18 @@ "end": 199, "language": "typescript", "label": "Authenticated browser traces land locally before optional OTLP forwarding", - "code": "export const otlpTracesProxyRouteLayer = HttpRouter.add(\n \"POST\",\n OTLP_TRACES_PROXY_PATH,\n Effect.gen(function* () {\n yield* authenticateRawRouteWithScope(AuthOrchestrationOperateScope);\n const request = yield* HttpServerRequest.HttpServerRequest;\n const config = yield* ServerConfig.ServerConfig;\n const otlpTracesUrl = config.otlpTracesUrl;\n const browserTraceCollector = yield* BrowserTraceCollector.BrowserTraceCollector;\n const httpClient = yield* HttpClient.HttpClient;\n const bodyJson = cast(yield* request.json);\n\n yield* Effect.try({\n try: () => decodeOtlpTraceRecords(bodyJson),\n catch: (cause) => new DecodeOtlpTraceRecordsError({ cause, bodyJson }),\n }).pipe(\n Effect.flatMap((records) => browserTraceCollector.record(records)),\n Effect.catch((cause) =>\n Effect.logWarning(\"Failed to decode browser OTLP traces\", {\n cause,\n bodyJson,\n }),\n ),\n );\n\n if (otlpTracesUrl === undefined) {\n return HttpServerResponse.empty({ status: 204 });\n }\n\n return yield* httpClient\n .post(otlpTracesUrl, {\n body: HttpBody.jsonUnsafe(bodyJson),\n })\n .pipe(\n Effect.flatMap(HttpClientResponse.filterStatusOk),\n Effect.as(HttpServerResponse.empty({ status: 204 })),\n Effect.tapError((cause) =>\n Effect.logWarning(\"Failed to export browser OTLP traces\", {\n cause,\n otlpTracesUrl,\n }),\n ),\n Effect.orElseSucceed(() =>\n HttpServerResponse.text(\"Trace export failed.\", { status: 502 }),\n ),\n );\n }).pipe(\n Effect.catchTags({\n EnvironmentAuthInvalidError: HttpServerRespondable.toResponse,\n EnvironmentInternalError: HttpServerRespondable.toResponse,\n EnvironmentScopeRequiredError: HttpServerRespondable.toResponse,\n }),\n ),\n);", - "checksum": "220e35097ba87ab35826ac3da655a6551055a457378aba8aaa77c21f843990c0" + "code": " const end = first === null || last === null || last >= size ? size - 1n : last;\n if (!Number.isSafeInteger(Number(start)) || !Number.isSafeInteger(Number(end))) {\n return { _tag: \"Unsatisfiable\" as const };\n }\n return {\n _tag: \"Range\" as const,\n offset: start,\n bytesToRead: end - start + 1n,\n contentRange: `bytes ${start}-${end}/${size}`,\n };\n}\n\nexport const assetFileResponse = Effect.fn(\"assetFileResponse\")(function* (\n asset: {\n readonly path: string;\n readonly download?: boolean;\n readonly fileName?: string;\n readonly mimeType?: string;\n readonly file?: OpenMediaFile;\n },\n rangeHeader?: string,\n ifRangeHeader?: string,\n method: \"GET\" | \"HEAD\" = \"GET\",\n) {\n const headers = assetResponseHeaders(asset.path, asset);\n const mediaFile = asset.file;\n const mediaInfo = mediaFile ? yield* statMediaFile(asset.path, mediaFile) : undefined;\n const isVideo = headers[\"Content-Type\"]?.toLowerCase().startsWith(\"video/\") === true;\n if (mediaFile && isVideo) {\n // Host videos can change in place. Do not invite conditional range requests\n // with validators that cannot establish byte-for-byte identity.\n headers[\"Cache-Control\"] = \"private, no-store\";\n }\n let status = 200;\n let offset = 0n;\n let bytesToRead: bigint | undefined;\n if (isVideo) {\n headers[\"Accept-Ranges\"] = \"bytes\";\n // If-Range requires a matching validator. A full response is safe when we cannot validate it.\n if (method === \"GET\" && rangeHeader && ifRangeHeader === undefined) {\n const fs = yield* FileSystem.FileSystem;\n const info = mediaInfo ?? (yield* fs.stat(asset.path));\n const range = assetByteRange(rangeHeader, info.size);\n if (range?._tag === \"Unsatisfiable\") {\n return HttpServerResponse.empty({\n status: 416,\n headers: { ...headers, \"Content-Range\": `bytes */${info.size}` },\n });\n }\n if (range?._tag === \"Range\") {\n status = 206;\n offset = range.offset;\n bytesToRead = range.bytesToRead;\n headers[\"Content-Range\"] = range.contentRange;", + "checksum": "282fbae0aab341fc849b293ed391443a83bcf88ed890ecd3a420557b9fae2091" }, "resource-telemetry-demand-history": { "id": "resource-telemetry-demand-history", "path": "docs/internals/resource-telemetry.md", - "start": 129, - "end": 170, + "start": 9, + "end": 50, "language": "markdown", "label": "Bounded native history and diagnostics-driven telemetry streaming", - "code": "### Native history and streaming\n\nEvery native sample is appended to a one-hour in-memory ring bounded to 3,600\nsnapshots, 20,000 retained process rows, and 64 MiB of retained history bytes.\nHistory stays in the sidecar until a `readHistory` request and is returned in\nbounded chunks. The first bound reached wins, so high process counts or large\nprocess names and command lines shorten the effective history window.\n\nPeriodic snapshot streaming is disabled by default. The server enables it only\nwhile at least one diagnostics subscription is retained. `sampleNow` remains\navailable for explicit refreshes and identity validation.\n\nThe server adjusts native sampling without restarting the sidecar:\n\n- suspended, locked, low-power, or serious/critical thermal state: 15 seconds;\n- battery: 5 seconds;\n- normal AC: 1 second;\n- unknown or stale power: 5 seconds in the background and 1 second while live\n diagnostics is open.\n\n### Sampling limits\n\nThis is counter sampling, not syscall tracing.\n\n- A process that starts and exits entirely between samples may not be observed.\n- Cumulative CPU and I/O counters still provide accurate deltas for processes\n that survive across samples.\n- Exact file paths, individual write syscalls, ETW events, eBPF events, and\n Endpoint Security events are outside this implementation.\n\nThose deeper tracing systems can be added later as opt-in diagnostic modes\nwithout changing the public `ResourceTelemetry` model.\n\n## I/O semantics\n\nThe monitor preserves platform semantics instead of presenting all counters as\nequivalent:\n\n- Unix-like platforms report storage I/O counters exposed by `sysinfo`.\n- Windows reports all process I/O bytes, not only disk bytes.\n- Operating-system caches can prevent logical application reads or writes from\n appearing as physical storage bytes.", - "checksum": "5bc90ad0498b5f6e9b3dfc618b9d6f42d05938c26dfb3dcdc8b98d01446d89f7" + "code": "## Collection cost\n\nThe native child owns sampling and bounded in-memory history. The server requests\ncontinuous snapshots only while diagnostics has live subscribers and fetches\nhistory on demand. Consuming host power for background scheduling must not retain\nlive diagnostics. There is no telemetry database or recurring shell-probe fallback.\n\nHistory has independent bounds for age, snapshot count, process rows, and retained\nbytes. A count limit alone cannot bound memory when command lines vary in size.\nLarge process trees therefore shorten the available history window. Linux task\nenumeration is disabled because walking every `/proc//task/` directory\nmakes sampling itself expensive.\n\nElectron power updates travel over private inherited pipes, independent of the\nrenderer connection. Power events and slow heartbeats continue with diagnostics\nclosed; `app.getAppMetrics()` runs only on live demand. The receiver's stale deadline\nmust exceed the slowest configured heartbeat plus scheduling grace, or intentional\nidle polling makes background policy oscillate between constrained and\nunconstrained states. Headless servers leave unavailable power data unknown.\n\n## Measurement traps\n\n- Process identity includes start time because operating systems reuse PIDs.\n Electron and native start times have different precision, so merging allows a\n small tolerance. Process signaling rechecks the native identity with a fresh\n sample.\n- Snapshot sequence numbers belong to a monitor generation. Comparing them across\n restarts would discard the new monitor's samples until its sequence caught up.\n- Sampling can miss a process that starts and exits between samples. Cumulative\n counters still yield deltas for processes observed across samples.\n- Windows process I/O includes more than disk traffic. Unix counters report storage\n I/O, which can differ from logical application reads and writes because of OS\n caching. Keep instrumented logical I/O separate from these counters.\n- Group totals accumulate observed deltas since telemetry started. Per-process\n cumulative counters cover the operating system's lifetime for that process.\n- Historical replay uses native samples without current Electron CPU or memory\n metrics. Merging the latest Electron values would overwrite the past.\n\nA WSL backend needs a Linux monitor even though Electron runs on Windows. Windows\ndesktop packages currently supply only the Windows executable, so native process\ntelemetry for the WSL backend is unavailable. The inherited Electron power feed\nstill works.", + "checksum": "dfcfc862a5d21e0e58a4c6613cfa3e0bf940b23a21d01f0c4b65cd7f4c57b2bb" }, "connect-oauth-hosted-handoff": { "id": "connect-oauth-hosted-handoff", @@ -1596,8 +1596,8 @@ "end": 323, "language": "typescript", "label": "CLI PKCE, state validation, and authorization-code exchange", - "code": "const makePkceRequest = Effect.gen(function* () {\n const crypto = yield* Crypto.Crypto;\n const verifier = Encoding.encodeBase64Url(yield* crypto.randomBytes(32));\n const challenge = Encoding.encodeBase64Url(\n yield* crypto.digest(\"SHA-256\", new TextEncoder().encode(verifier)),\n );\n const state = Encoding.encodeBase64Url(yield* crypto.randomBytes(16));\n return { verifier, challenge, state };\n});\n\nexport interface OutOfBandOAuthPromptInput {\n readonly authorizeUrl: string;\n readonly validate: (value: string) => Effect.Effect;\n}\n\n/**\n * Out-of-band OAuth for machines without a local browser (SSH). The user\n * opens the hosted /connect URL elsewhere, signs in, and enters the displayed\n * code in this terminal. The PKCE verifier never leaves this process, so the\n * authorization code is useless to an observer, and the state bundled into\n * the blob preserves the loopback flow's CSRF check.\n */\nexport const outOfBandOAuthLogin = Effect.fn(\"cloud.cli_token.out_of_band_oauth_login\")(function* <\n E,\n R,\n>(promptForCode: (input: OutOfBandOAuthPromptInput) => Effect.Effect) {\n const metadata = yield* cloudCliOAuthConfig;\n const hostedAppUrl = yield* hostedAppUrlConfig;\n const { verifier, challenge, state } = yield* makePkceRequest;\n\n const authorizationCode = yield* promptForCode({\n authorizeUrl: buildConnectAuthorizeRequestUrl({ hostedAppUrl, state, challenge }),\n validate: (value) => {\n const checked = checkConnectAuthCode(value, state);\n return typeof checked === \"string\" ? Effect.fail(checked) : Effect.succeed(value);\n },\n }).pipe(\n // Clerk authorization codes expire on this horizon anyway; matching the\n // loopback flow's timeout turns an abandoned prompt into a clear error.\n Effect.timeout(CLOUD_CLI_OAUTH_CALLBACK_TIMEOUT),\n Effect.catchTag(\"TimeoutError\", (cause) =>\n Effect.fail(new CloudCliAuthorizationTimeoutError({ cause })),\n ),\n );\n // promptForCode is caller-supplied, so re-check the returned value rather\n // than trusting that the prompt ran validate.\n const authCode = checkConnectAuthCode(authorizationCode, state);\n if (typeof authCode === \"string\") {\n return yield* new CloudCliAuthorizationError({ cause: authCode });\n }\n\n return yield* exchangeToken(metadata, {\n grant_type: \"authorization_code\",\n code: authCode.code,\n redirect_uri: connectCallbackUrl(hostedAppUrl),\n client_id: metadata.clientId,\n code_verifier: verifier,\n });", - "checksum": "0c0afa4526b464e06018e6ceb78ba3bbcfd588ced7303dbd9b515a9a5f7cedd8" + "code": "\nconst makePkceRequest = Effect.gen(function* () {\n const crypto = yield* Crypto.Crypto;\n const verifier = Encoding.encodeBase64Url(yield* crypto.randomBytes(32));\n const challenge = Encoding.encodeBase64Url(\n yield* crypto.digest(\"SHA-256\", new TextEncoder().encode(verifier)),\n );\n const state = Encoding.encodeBase64Url(yield* crypto.randomBytes(16));\n return { verifier, challenge, state };\n});\n\nexport interface OutOfBandOAuthPromptInput {\n readonly authorizeUrl: string;\n readonly validate: (value: string) => Effect.Effect;\n}\n\n/**\n * Out-of-band OAuth for machines without a local browser (SSH). The user\n * opens the hosted /connect URL elsewhere, signs in, and enters the displayed\n * code in this terminal. The PKCE verifier never leaves this process, so the\n * authorization code is useless to an observer, and the state bundled into\n * the blob preserves the loopback flow's CSRF check.\n */\nexport const outOfBandOAuthLogin = Effect.fn(\"cloud.cli_token.out_of_band_oauth_login\")(function* <\n E,\n R,\n>(promptForCode: (input: OutOfBandOAuthPromptInput) => Effect.Effect) {\n const metadata = yield* cloudCliOAuthConfig;\n const hostedAppUrl = yield* hostedAppUrlConfig;\n const { verifier, challenge, state } = yield* makePkceRequest;\n\n const authorizationCode = yield* promptForCode({\n authorizeUrl: buildConnectAuthorizeRequestUrl({ hostedAppUrl, state, challenge }),\n validate: (value) => {\n const checked = checkConnectAuthCode(value, state);\n return typeof checked === \"string\" ? Effect.fail(checked) : Effect.succeed(value);\n },\n }).pipe(\n // Clerk authorization codes expire on this horizon anyway; matching the\n // loopback flow's timeout turns an abandoned prompt into a clear error.\n Effect.timeout(CLOUD_CLI_OAUTH_CALLBACK_TIMEOUT),\n Effect.catchTag(\"TimeoutError\", (cause) =>\n Effect.fail(new CloudCliAuthorizationTimeoutError({ cause })),\n ),\n );\n // promptForCode is caller-supplied, so re-check the returned value rather\n // than trusting that the prompt ran validate.\n const authCode = checkConnectAuthCode(authorizationCode, state);\n if (typeof authCode === \"string\") {\n return yield* new CloudCliAuthorizationError({ cause: authCode });\n }\n\n return yield* exchangeToken(metadata, {\n grant_type: \"authorization_code\",\n code: authCode.code,\n redirect_uri: connectCallbackUrl(hostedAppUrl),\n client_id: metadata.clientId,\n code_verifier: verifier,", + "checksum": "7f0efb747bf87f31c6a32bbb856ec6851885bdf0784d438813a826ce626606c6" }, "connect-browser-dpop-key": { "id": "connect-browser-dpop-key", @@ -1612,8 +1612,8 @@ "connect-environment-link-handshake": { "id": "connect-environment-link-handshake", "path": "apps/web/src/cloud/linkEnvironment.ts", - "start": 402, - "end": 493, + "start": 256, + "end": 347, "language": "typescript", "label": "Client-side relay challenge, environment proof, link, and runtime setup", "code": "export function linkPrimaryEnvironmentToCloud(input: {\n readonly target: CloudLinkTarget;\n readonly clerkToken: string;\n readonly mode?: CloudLinkMode;\n}): Effect.Effect<\n void,\n CloudEnvironmentLinkError,\n EnvironmentRegistry | HttpClient.HttpClient | ManagedRelay.ManagedRelayClient\n> {\n return Effect.gen(function* () {\n const configuredRelayUrl = relayUrl();\n if (!configuredRelayUrl) {\n return yield* new CloudEnvironmentLinkError({\n message: \"T3CODE_RELAY_URL is not configured.\",\n });\n }\n const managedTunnelsEnabled = (input.mode ?? \"managed\") === \"managed\";\n const providerKind = managedTunnelsEnabled\n ? MANAGED_ENDPOINT_PROVIDER_KIND\n : PUBLISH_ONLY_PROVIDER_KIND;\n const relayClient = yield* ManagedRelay.ManagedRelayClient;\n const environmentClient = yield* makeEnvironmentHttpApiClient(input.target.httpBaseUrl);\n if (managedTunnelsEnabled) {\n yield* ensureRelayClientAvailable(EnvironmentId.make(input.target.environmentId));\n }\n\n const challenge = yield* relayClient\n .createEnvironmentLinkChallenge({\n clerkToken: input.clerkToken,\n payload: {\n notificationsEnabled: true,\n liveActivitiesEnabled: true,\n managedTunnelsEnabled,\n },\n })\n .pipe(\n Effect.mapError(\n decodedRelayClientError(\n `${configuredRelayUrl}/v1/client/environment-link-challenges failed`,\n ),\n ),\n );\n const proof = yield* environmentClient.connect\n .linkProof({\n headers: {},\n payload: {\n challenge: challenge.challenge,\n relayIssuer: configuredRelayUrl,\n endpoint: {\n httpBaseUrl: input.target.httpBaseUrl,\n wsBaseUrl: input.target.wsBaseUrl,\n providerKind,\n },\n origin: endpointOrigin(input.target.httpBaseUrl),\n },\n })\n .pipe(Effect.mapError(environmentApiError(\"Could not obtain environment link proof.\")));\n const link = yield* relayClient\n .linkEnvironment({\n clerkToken: input.clerkToken,\n payload: {\n proof,\n notificationsEnabled: true,\n liveActivitiesEnabled: true,\n managedTunnelsEnabled,\n },\n })\n .pipe(\n Effect.mapError(\n decodedRelayClientError(`${configuredRelayUrl}/v1/client/environment-links failed`),\n ),\n );\n yield* ensureLinkedEnvironmentMatches({\n expectedEnvironmentId: input.target.environmentId,\n expectedProviderKind: providerKind,\n link,\n });\n\n yield* environmentClient.connect\n .relayConfig({\n headers: {},\n payload: {\n relayUrl: configuredRelayUrl,\n relayIssuer: link.relayIssuer,\n cloudUserId: link.cloudUserId,\n environmentCredential: link.environmentCredential,\n cloudMintPublicKey: link.cloudMintPublicKey,\n endpointRuntime: link.endpointRuntime,\n },\n })\n .pipe(Effect.mapError(environmentApiError(\"Could not configure environment relay access.\")));\n }).pipe(Effect.provide(primaryEnvironmentHttpLayer));", @@ -1646,8 +1646,8 @@ "end": 287, "language": "typescript", "label": "Environment-owned managed tunnel child supervision", - "code": " reconcileConfig = Effect.fn(\"CloudManagedEndpointRuntime.reconcileConfig\")(function* (config) {\n if (!config || config.providerKind !== \"cloudflare_tunnel\") {\n yield* stopActive;\n return config\n ? { status: \"unsupported\", providerKind: config.providerKind }\n : { status: \"disabled\" };\n }\n\n const nextConfigKey = runtimeConfigKey(config);\n const active = yield* Ref.get(activeRef);\n if (active?.configKey === nextConfigKey) {\n const isRunning = yield* active.child.isRunning.pipe(Effect.orElseSucceed(() => false));\n if (isRunning) {\n return {\n status: \"running\",\n providerKind: \"cloudflare_tunnel\",\n pid: Number(active.child.pid),\n ...(active.config.tunnelId ? { tunnelId: active.config.tunnelId } : {}),\n ...(active.config.tunnelName ? { tunnelName: active.config.tunnelName } : {}),\n } satisfies CloudManagedEndpointRuntimeStatus;\n }\n }\n\n yield* stopActive;\n\n const executable = yield* relayClient.resolve;\n if (executable.status !== \"available\") {\n return {\n status: \"failed\",\n providerKind: \"cloudflare_tunnel\",\n reason:\n executable.status === \"unsupported\"\n ? `Relay client is unsupported on ${executable.platform}-${executable.arch}.`\n : \"The relay client is not installed.\",\n ...(config.tunnelId ? { tunnelId: config.tunnelId } : {}),\n ...(config.tunnelName ? { tunnelName: config.tunnelName } : {}),\n } satisfies CloudManagedEndpointRuntimeStatus;\n }\n\n const connectorScope = yield* Scope.make(\"sequential\");\n const child = yield* spawner\n .spawn(\n ChildProcess.make(executable.executablePath, [\"tunnel\", \"run\"], {\n detached: false,\n env: {\n ...process.env,\n TUNNEL_TOKEN: config.connectorToken,\n },\n shell: false,\n stderr: \"pipe\",\n stdout: \"pipe\",\n }),\n )\n .pipe(\n Effect.provideService(Scope.Scope, connectorScope),\n Effect.tap((child) =>\n Effect.logInfo(\"Relay client process started; waiting for tunnel connection\", {\n pid: Number(child.pid),\n tunnelId: config.tunnelId,\n tunnelName: config.tunnelName,\n }),\n ),\n Effect.catch((cause) =>\n Effect.logWarning(\"Failed to start relay client\", {\n cause,\n tunnelId: config.tunnelId,\n tunnelName: config.tunnelName,\n }).pipe(\n Effect.andThen(Scope.close(connectorScope, Exit.void).pipe(Effect.ignore)),\n Effect.as({\n status: \"failed\",\n providerKind: \"cloudflare_tunnel\",\n reason: String(cause),\n ...(config.tunnelId ? { tunnelId: config.tunnelId } : {}),\n ...(config.tunnelName ? { tunnelName: config.tunnelName } : {}),\n } satisfies CloudManagedEndpointRuntimeStatus),\n ),\n ),\n );\n\n if (\"status\" in child && child.status === \"failed\") {\n return child;\n }\n\n if (!(\"status\" in child)) {\n const connector = {\n child,\n scope: connectorScope,\n configKey: nextConfigKey,\n config,\n } satisfies ActiveConnector;\n yield* Ref.set(activeRef, connector);\n yield* Effect.forkIn(observeConnectorOutput(connector), connectorScope);\n yield* Effect.forkIn(superviseConnector(connector), connectorScope);\n return {\n status: \"running\",\n providerKind: \"cloudflare_tunnel\",\n pid: Number(child.pid),\n ...(config.tunnelId ? { tunnelId: config.tunnelId } : {}),\n ...(config.tunnelName ? { tunnelName: config.tunnelName } : {}),\n } satisfies CloudManagedEndpointRuntimeStatus;", - "checksum": "15a5736f8e3481d7117acb0331cf0d9969a2bf86bf2755734a1f3af6036df8b2" + "code": " pid: Number(connector.child.pid),\n ...(Result.isSuccess(result)\n ? { exitCode: Number(result.success) }\n : { cause: result.failure }),\n tunnelId: connector.config.tunnelId,\n tunnelName: connector.config.tunnelName,\n });\n yield* reconcileConfig(desiredConfig);\n }),\n );\n }).pipe(\n Effect.catchCause((cause) => Effect.logWarning(\"Relay client supervisor failed\", { cause })),\n );\n\n const observeConnectorOutput = (connector: ActiveConnector) =>\n connector.child.all.pipe(\n Stream.decodeText(),\n Stream.splitLines,\n Stream.map((line) => line.trim()),\n Stream.filter((line) => line.length > 0),\n Stream.runForEach((line) => {\n const output = line.replaceAll(connector.config.connectorToken, \"\");\n const attributes = {\n pid: Number(connector.child.pid),\n tunnelId: connector.config.tunnelId,\n tunnelName: connector.config.tunnelName,\n output,\n };\n switch (classifyRelayClientOutput(line)) {\n case \"connected\":\n return Effect.logInfo(\"Relay client tunnel connection registered\", attributes);\n case \"warning\":\n return Effect.logWarning(\"Relay client reported a transport warning\", attributes);\n case \"debug\":\n return Effect.logDebug(\"Relay client output\", attributes);\n }\n }),\n Effect.catchCause((cause) =>\n Effect.logWarning(\"Relay client output observer failed\", {\n cause,\n pid: Number(connector.child.pid),\n tunnelId: connector.config.tunnelId,\n tunnelName: connector.config.tunnelName,\n }),\n ),\n );\n\n reconcileConfig = Effect.fn(\"CloudManagedEndpointRuntime.reconcileConfig\")(function* (config) {\n if (!config || config.providerKind !== \"cloudflare_tunnel\") {\n yield* stopActive;\n return config\n ? { status: \"unsupported\", providerKind: config.providerKind }\n : { status: \"disabled\" };\n }\n\n const nextConfigKey = runtimeConfigKey(config);\n const active = yield* Ref.get(activeRef);\n if (active?.configKey === nextConfigKey) {\n const isRunning = yield* active.child.isRunning.pipe(Effect.orElseSucceed(() => false));\n if (isRunning) {\n return {\n status: \"running\",\n providerKind: \"cloudflare_tunnel\",\n pid: Number(active.child.pid),\n ...(active.config.tunnelId ? { tunnelId: active.config.tunnelId } : {}),\n ...(active.config.tunnelName ? { tunnelName: active.config.tunnelName } : {}),\n } satisfies CloudManagedEndpointRuntimeStatus;\n }\n }\n\n yield* stopActive;\n\n const executable = yield* relayClient.resolve;\n if (executable.status !== \"available\") {\n return {\n status: \"failed\",\n providerKind: \"cloudflare_tunnel\",\n reason:\n executable.status === \"unsupported\"\n ? `Relay client is unsupported on ${executable.platform}-${executable.arch}.`\n : \"The relay client is not installed.\",\n ...(config.tunnelId ? { tunnelId: config.tunnelId } : {}),\n ...(config.tunnelName ? { tunnelName: config.tunnelName } : {}),\n } satisfies CloudManagedEndpointRuntimeStatus;\n }\n\n const connectorScope = yield* Scope.make(\"sequential\");\n const child = yield* spawner\n .spawn(\n ChildProcess.make(executable.executablePath, [\"tunnel\", \"run\"], {\n detached: false,\n env: {\n ...process.env,\n TUNNEL_TOKEN: config.connectorToken,\n },\n shell: false,\n stderr: \"pipe\",\n stdout: \"pipe\",\n }),\n )\n .pipe(", + "checksum": "cc55c3361e052261263c22865a112449d0777d84e61d5b31d56bc386079f2c73" }, "connect-relay-environment-mint": { "id": "connect-relay-environment-mint", @@ -1666,8 +1666,8 @@ "end": 175, "language": "typescript", "label": "DPoP signature, target, token hash, and time validation", - "code": "import { p256 } from \"@noble/curves/nist\";\nimport { sha256 } from \"@noble/hashes/sha2\";\nimport * as Encoding from \"effect/Encoding\";\nimport * as Option from \"effect/Option\";\nimport * as Result from \"effect/Result\";\nimport * as Schema from \"effect/Schema\";\n\nimport { DpopPublicJwk as DpopPublicJwkSchema, normalizeDpopHtu } from \"./dpopCommon.ts\";\nimport type { DpopPublicJwk as DpopPublicJwkType } from \"./dpopCommon.ts\";\nimport { stableStringify } from \"./relaySigning.ts\";\n\nconst DPOP_TYP = \"dpop+jwt\";\nconst DPOP_ALG = \"ES256\";\nconst DEFAULT_MAX_AGE_SECONDS = 300;\n\nexport const DpopPublicJwk = DpopPublicJwkSchema;\nexport type DpopPublicJwk = DpopPublicJwkType;\nexport { normalizeDpopHtu };\n\nconst DpopJwtHeaderPublicJwk = Schema.Struct({\n ...DpopPublicJwkSchema.fields,\n d: Schema.optionalKey(Schema.Never),\n});\n\nconst DpopJwtHeaderJson = Schema.fromJsonString(\n Schema.Struct({\n typ: Schema.Literal(DPOP_TYP),\n alg: Schema.Literal(DPOP_ALG),\n jwk: DpopJwtHeaderPublicJwk,\n }),\n);\nconst decodeDpopJwtHeaderJson = Schema.decodeUnknownOption(DpopJwtHeaderJson);\n\nconst DpopJwtPayloadJson = Schema.fromJsonString(\n Schema.Struct({\n htm: Schema.String.check(Schema.isNonEmpty()),\n htu: Schema.String.check(Schema.isNonEmpty()),\n jti: Schema.String.check(Schema.isNonEmpty()),\n iat: Schema.Int,\n ath: Schema.optionalKey(Schema.String),\n }),\n);\nconst decodeDpopJwtPayloadJson = Schema.decodeUnknownOption(DpopJwtPayloadJson);\n\nexport type DpopVerificationResult =\n | {\n readonly ok: true;\n readonly thumbprint: string;\n readonly jti: string;\n readonly iat: number;\n }\n | {\n readonly ok: false;\n readonly reason: string;\n };\n\nfunction base64UrlToBytes(value: string): Uint8Array {\n return Result.getOrThrow(Encoding.decodeBase64Url(value));\n}\n\nfunction decodeBase64UrlDpopJwtHeader(value: string) {\n return decodeDpopJwtHeaderJson(Result.getOrThrow(Encoding.decodeBase64UrlString(value)));\n}\n\nfunction decodeBase64UrlDpopJwtPayload(value: string) {\n return decodeDpopJwtPayloadJson(Result.getOrThrow(Encoding.decodeBase64UrlString(value)));\n}\n\nfunction dpopThumbprintInput(jwk: DpopPublicJwkType): string {\n return stableStringify({\n crv: jwk.crv,\n kty: jwk.kty,\n x: jwk.x,\n y: jwk.y,\n });\n}\n\nexport function computeDpopJwkThumbprint(jwk: DpopPublicJwkType): string {\n return Encoding.encodeBase64Url(sha256(new TextEncoder().encode(dpopThumbprintInput(jwk))));\n}\n\nexport function computeDpopAccessTokenHash(accessToken: string): string {\n return Encoding.encodeBase64Url(sha256(new TextEncoder().encode(accessToken)));\n}\n\nfunction publicKeyBytesFromJwk(jwk: DpopPublicJwkType): Uint8Array {\n const x = base64UrlToBytes(jwk.x);\n const y = base64UrlToBytes(jwk.y);\n if (x.length !== 32 || y.length !== 32) {\n throw new Error(\"Invalid P-256 public key coordinate length.\");\n }\n const publicKey = new Uint8Array(65);\n publicKey[0] = 0x04;\n publicKey.set(x, 1);\n publicKey.set(y, 33);\n return publicKey;\n}\n\nexport function verifyDpopProof(input: {\n readonly proof: string | null | undefined;\n readonly method: string;\n readonly url: string;\n readonly nowEpochSeconds: number;\n readonly expectedThumbprint?: string;\n readonly expectedAccessToken?: string;\n readonly maxAgeSeconds?: number;\n}): DpopVerificationResult {\n if (!input.proof?.trim()) {\n return { ok: false, reason: \"Missing DPoP proof.\" };\n }\n\n const parts = input.proof.split(\".\");\n if (parts.length !== 3 || !parts[0] || !parts[1] || !parts[2]) {\n return { ok: false, reason: \"Invalid DPoP compact JWT.\" };\n }\n\n try {\n const header = decodeBase64UrlDpopJwtHeader(parts[0]);\n const payload = decodeBase64UrlDpopJwtPayload(parts[1]);\n if (Option.isNone(header)) {\n return { ok: false, reason: \"Invalid DPoP JWT header.\" };\n }\n if (Option.isNone(payload)) {\n return { ok: false, reason: \"Invalid DPoP JWT payload.\" };\n }\n\n const thumbprint = computeDpopJwkThumbprint(header.value.jwk);\n if (input.expectedThumbprint && thumbprint !== input.expectedThumbprint) {\n return { ok: false, reason: \"DPoP key thumbprint mismatch.\" };\n }\n if (payload.value.htm.toUpperCase() !== input.method.toUpperCase()) {\n return { ok: false, reason: \"DPoP method mismatch.\" };\n }\n const normalizedHtu = normalizeDpopHtu(input.url);\n if (normalizedHtu === null || payload.value.htu !== normalizedHtu) {\n return { ok: false, reason: \"DPoP URL mismatch.\" };\n }\n if (input.expectedAccessToken) {\n const expectedAth = computeDpopAccessTokenHash(input.expectedAccessToken);\n if (payload.value.ath !== expectedAth) {\n return { ok: false, reason: \"DPoP access token hash mismatch.\" };\n }\n }\n\n const maxAgeSeconds = input.maxAgeSeconds ?? DEFAULT_MAX_AGE_SECONDS;\n if (\n payload.value.iat > input.nowEpochSeconds + 5 ||\n input.nowEpochSeconds - payload.value.iat > maxAgeSeconds\n ) {\n return { ok: false, reason: \"DPoP proof is outside the allowed time window.\" };\n }\n\n const signature = base64UrlToBytes(parts[2]);\n const signatureInputHash = sha256(new TextEncoder().encode(`${parts[0]}.${parts[1]}`));\n const verified = p256.verify(\n signature,\n signatureInputHash,\n publicKeyBytesFromJwk(header.value.jwk),\n {\n prehash: false,\n format: \"compact\",\n },\n );\n return verified\n ? {\n ok: true,\n thumbprint,\n jti: payload.value.jti,\n iat: payload.value.iat,\n }\n : { ok: false, reason: \"Invalid DPoP signature.\" };\n } catch {\n return { ok: false, reason: \"Invalid DPoP proof.\" };\n }\n}", - "checksum": "a201cbe8739a6da8f782df3f52c70e27b8ff61b5dcdf9de50075955966c8bf43" + "code": "import { p256 } from \"@noble/curves/nist\";\nimport { sha256 } from \"@noble/hashes/sha2\";\nimport * as Encoding from \"effect/Encoding\";\nimport * as Option from \"effect/Option\";\nimport * as Result from \"effect/Result\";\nimport * as Schema from \"effect/Schema\";\n\nimport { DpopPublicJwk as DpopPublicJwkSchema, normalizeDpopHtu } from \"./dpopCommon.ts\";\nimport type { DpopPublicJwk as DpopPublicJwkType } from \"./dpopCommon.ts\";\nimport { stableStringify } from \"./relaySigning.ts\";\n\nconst DPOP_TYP = \"dpop+jwt\";\nconst DPOP_ALG = \"ES256\";\nconst DEFAULT_MAX_AGE_SECONDS = 300;\n\nexport const DpopPublicJwk = DpopPublicJwkSchema;\nexport type DpopPublicJwk = DpopPublicJwkType;\nexport { normalizeDpopHtu };\n\nexport const DpopVerificationFailureCode = Schema.Literals([\n \"missing_proof\",\n \"malformed_proof\",\n \"key_mismatch\",\n \"method_mismatch\",\n \"url_mismatch\",\n \"access_token_hash_mismatch\",\n \"time_window\",\n \"invalid_signature\",\n \"invalid_proof\",\n]);\nexport type DpopVerificationFailureCode = typeof DpopVerificationFailureCode.Type;\n\nconst DpopJwtHeaderPublicJwk = Schema.Struct({\n ...DpopPublicJwkSchema.fields,\n d: Schema.optionalKey(Schema.Never),\n});\n\nconst DpopJwtHeaderJson = Schema.fromJsonString(\n Schema.Struct({\n typ: Schema.Literal(DPOP_TYP),\n alg: Schema.Literal(DPOP_ALG),\n jwk: DpopJwtHeaderPublicJwk,\n }),\n);\nconst decodeDpopJwtHeaderJson = Schema.decodeUnknownOption(DpopJwtHeaderJson);\n\nconst DpopJwtPayloadJson = Schema.fromJsonString(\n Schema.Struct({\n htm: Schema.String.check(Schema.isNonEmpty()),\n htu: Schema.String.check(Schema.isNonEmpty()),\n jti: Schema.String.check(Schema.isNonEmpty()),\n iat: Schema.Int,\n ath: Schema.optionalKey(Schema.String),\n }),\n);\nconst decodeDpopJwtPayloadJson = Schema.decodeUnknownOption(DpopJwtPayloadJson);\n\nexport type DpopVerificationResult =\n | {\n readonly ok: true;\n readonly thumbprint: string;\n readonly jti: string;\n readonly iat: number;\n }\n | {\n readonly ok: false;\n readonly code: DpopVerificationFailureCode;\n readonly reason: string;\n };\n\nfunction base64UrlToBytes(value: string): Uint8Array {\n return Result.getOrThrow(Encoding.decodeBase64Url(value));\n}\n\nfunction decodeBase64UrlDpopJwtHeader(value: string) {\n return decodeDpopJwtHeaderJson(Result.getOrThrow(Encoding.decodeBase64UrlString(value)));\n}\n\nfunction decodeBase64UrlDpopJwtPayload(value: string) {\n return decodeDpopJwtPayloadJson(Result.getOrThrow(Encoding.decodeBase64UrlString(value)));\n}\n\nfunction dpopThumbprintInput(jwk: DpopPublicJwkType): string {\n return stableStringify({\n crv: jwk.crv,\n kty: jwk.kty,\n x: jwk.x,\n y: jwk.y,\n });\n}\n\nexport function computeDpopJwkThumbprint(jwk: DpopPublicJwkType): string {\n return Encoding.encodeBase64Url(sha256(new TextEncoder().encode(dpopThumbprintInput(jwk))));\n}\n\nexport function computeDpopAccessTokenHash(accessToken: string): string {\n return Encoding.encodeBase64Url(sha256(new TextEncoder().encode(accessToken)));\n}\n\nfunction publicKeyBytesFromJwk(jwk: DpopPublicJwkType): Uint8Array {\n const x = base64UrlToBytes(jwk.x);\n const y = base64UrlToBytes(jwk.y);\n if (x.length !== 32 || y.length !== 32) {\n throw new Error(\"Invalid P-256 public key coordinate length.\");\n }\n const publicKey = new Uint8Array(65);\n publicKey[0] = 0x04;\n publicKey.set(x, 1);\n publicKey.set(y, 33);\n return publicKey;\n}\n\nexport function verifyDpopProof(input: {\n readonly proof: string | null | undefined;\n readonly method: string;\n readonly url: string;\n readonly nowEpochSeconds: number;\n readonly expectedThumbprint?: string;\n readonly expectedAccessToken?: string;\n readonly maxAgeSeconds?: number;\n}): DpopVerificationResult {\n if (!input.proof?.trim()) {\n return { ok: false, code: \"missing_proof\", reason: \"Missing DPoP proof.\" };\n }\n\n const parts = input.proof.split(\".\");\n if (parts.length !== 3 || !parts[0] || !parts[1] || !parts[2]) {\n return { ok: false, code: \"malformed_proof\", reason: \"Invalid DPoP compact JWT.\" };\n }\n\n try {\n const header = decodeBase64UrlDpopJwtHeader(parts[0]);\n const payload = decodeBase64UrlDpopJwtPayload(parts[1]);\n if (Option.isNone(header)) {\n return { ok: false, code: \"malformed_proof\", reason: \"Invalid DPoP JWT header.\" };\n }\n if (Option.isNone(payload)) {\n return { ok: false, code: \"malformed_proof\", reason: \"Invalid DPoP JWT payload.\" };\n }\n\n const thumbprint = computeDpopJwkThumbprint(header.value.jwk);\n if (input.expectedThumbprint && thumbprint !== input.expectedThumbprint) {\n return { ok: false, code: \"key_mismatch\", reason: \"DPoP key thumbprint mismatch.\" };\n }\n if (payload.value.htm.toUpperCase() !== input.method.toUpperCase()) {\n return { ok: false, code: \"method_mismatch\", reason: \"DPoP method mismatch.\" };\n }\n const normalizedHtu = normalizeDpopHtu(input.url);\n if (normalizedHtu === null || payload.value.htu !== normalizedHtu) {\n return { ok: false, code: \"url_mismatch\", reason: \"DPoP URL mismatch.\" };\n }\n if (input.expectedAccessToken) {\n const expectedAth = computeDpopAccessTokenHash(input.expectedAccessToken);\n if (payload.value.ath !== expectedAth) {\n return {\n ok: false,\n code: \"access_token_hash_mismatch\",\n reason: \"DPoP access token hash mismatch.\",\n };\n }\n }\n\n const signature = base64UrlToBytes(parts[2]);\n const signatureInputHash = sha256(new TextEncoder().encode(`${parts[0]}.${parts[1]}`));\n const verified = p256.verify(\n signature,\n signatureInputHash,\n publicKeyBytesFromJwk(header.value.jwk),\n {\n prehash: false,\n format: \"compact\",\n },\n );\n if (!verified) {\n return { ok: false, code: \"invalid_signature\", reason: \"Invalid DPoP signature.\" };", + "checksum": "fdb47cd19783c81c6ba7d74e753e66bef383d978288970014630dfe88d540c11" }, "managed-relay-dpop-authorization": { "id": "managed-relay-dpop-authorization", @@ -1676,8 +1676,8 @@ "end": 299, "language": "typescript", "label": "Managed relay bootstrap becomes a distinct DPoP-bound environment session", - "code": " const authorizeDpop = Effect.fn(\"clientRuntime.connection.remote.authorizeDpop\")(\n function* (input: {\n readonly expectedEnvironmentId: Parameters<\n RemoteEnvironmentAuthorization[\"Service\"][\"authorizeDpop\"]\n >[0][\"expectedEnvironmentId\"];\n readonly obtainBootstrap: Parameters<\n RemoteEnvironmentAuthorization[\"Service\"][\"authorizeDpop\"]\n >[0][\"obtainBootstrap\"];\n }) {\n const thumbprint = yield* signer.thumbprint.pipe(\n Effect.mapError(\n () =>\n new ConnectionBlockedError({\n reason: \"configuration\",\n detail: \"Could not load the environment authorization key.\",\n }),\n ),\n Effect.withSpan(\"environment.authorization.dpopKey.resolve\"),\n );\n const now = yield* Clock.currentTimeMillis;\n const cached = yield* tokenStore\n .get(input.expectedEnvironmentId)\n .pipe(Effect.withSpan(\"environment.authorization.accessToken.cache\"));\n if (\n Option.isSome(cached) &&\n cached.value.environmentId === input.expectedEnvironmentId &&\n cached.value.dpopThumbprint === thumbprint &&\n cached.value.expiresAtEpochMs > now + TOKEN_EXPIRY_SAFETY_MARGIN_MS\n ) {\n yield* Effect.annotateCurrentSpan({\n \"connection.remote_token_cache\": \"hit\",\n });\n const cachedSocket = yield* createDpopSocketUrl(\n cached.value,\n CACHED_ENDPOINT_SOCKET_TIMEOUT_MS,\n ).pipe(Effect.result);\n if (Result.isSuccess(cachedSocket)) {\n return {\n environmentId: cached.value.environmentId,\n label: cached.value.label,\n httpBaseUrl: cached.value.endpoint.httpBaseUrl,\n socketUrl: cachedSocket.success,\n httpAuthorization: {\n _tag: \"Dpop\" as const,\n accessToken: cached.value.accessToken,\n },\n };\n }\n if (cachedSocket.failure._tag === \"ConnectionBlockedError\") {\n return yield* mapDpopSocketError(cachedSocket.failure);\n }\n yield* tokenStore\n .remove(input.expectedEnvironmentId)\n .pipe(Effect.withSpan(\"environment.authorization.accessToken.remove\"));\n }\n\n yield* Effect.annotateCurrentSpan({\n \"connection.remote_token_cache\": \"miss\",\n });\n const bootstrap = yield* input.obtainBootstrap;\n const descriptor = yield* fetchDescriptor(bootstrap.endpoint.httpBaseUrl).pipe(\n Effect.provideService(HttpClient.HttpClient, httpClient),\n Effect.withSpan(\"environment.authorization.descriptor\"),\n );\n if (descriptor.environmentId !== input.expectedEnvironmentId) {\n return yield* environmentMismatchError({\n expected: input.expectedEnvironmentId,\n actual: descriptor.environmentId,\n });\n }\n const bootstrapProof = yield* signer\n .createProof({\n method: \"POST\",\n url: environmentEndpointUrl(bootstrap.endpoint.httpBaseUrl, \"/oauth/token\"),\n })\n .pipe(\n Effect.mapError(\n () =>\n new ConnectionBlockedError({\n reason: \"configuration\",\n detail: \"Could not create the environment authorization proof.\",\n }),\n ),\n );\n const access = yield* exchangeRemoteDpopAccessToken({\n httpBaseUrl: bootstrap.endpoint.httpBaseUrl,\n credential: bootstrap.credential,\n dpopProof: bootstrapProof,\n scopes: presentation.scopes,\n clientMetadata: presentation.metadata,\n }).pipe(\n Effect.mapError(mapRemoteEnvironmentError),\n Effect.provideService(HttpClient.HttpClient, httpClient),\n Effect.withSpan(\"environment.authorization.accessToken.exchange\"),\n );\n const issuedAt = yield* Clock.currentTimeMillis;\n const token = new TokenStore.RemoteDpopAccessToken({\n environmentId: descriptor.environmentId,\n label: descriptor.label,\n endpoint: bootstrap.endpoint,\n accessToken: access.access_token,\n expiresAtEpochMs: issuedAt + access.expires_in * 1_000,\n dpopThumbprint: thumbprint,\n });\n const socketUrl = yield* createDpopSocketUrl(token).pipe(Effect.mapError(mapDpopSocketError));\n yield* tokenStore\n .put(token)\n .pipe(Effect.withSpan(\"environment.authorization.accessToken.persist\"));\n return {\n environmentId: descriptor.environmentId,\n label: descriptor.label,\n httpBaseUrl: bootstrap.endpoint.httpBaseUrl,\n socketUrl,\n httpAuthorization: {\n _tag: \"Dpop\" as const,\n accessToken: token.accessToken,\n },\n };\n },\n );", - "checksum": "6815918aa0e64771f373da90aa947033e4b180c8a4d9c6308afdacebd6880dbb" + "code": " Effect.provideService(HttpClient.HttpClient, httpClient),\n );\n return {\n environmentId: descriptor.environmentId,\n label: descriptor.label,\n httpBaseUrl: input.httpBaseUrl,\n socketUrl,\n httpAuthorization: {\n _tag: \"Bearer\" as const,\n token: input.bearerToken,\n },\n };\n },\n );\n\n const createDpopSocketUrl = Effect.fn(\"clientRuntime.connection.remote.createDpopSocketUrl\")(\n function* (token: TokenStore.RemoteDpopAccessToken, timeoutMs?: number) {\n const ticketProof = yield* signer\n .createProof({\n method: \"POST\",\n url: environmentEndpointUrl(token.endpoint.httpBaseUrl, \"/api/auth/websocket-ticket\"),\n accessToken: token.accessToken,\n })\n .pipe(\n Effect.mapError(\n () =>\n new ConnectionBlockedError({\n reason: \"configuration\",\n detail: \"Could not create the websocket authorization proof.\",\n }),\n ),\n );\n return yield* resolveRemoteDpopWebSocketConnectionUrl({\n wsBaseUrl: token.endpoint.wsBaseUrl,\n httpBaseUrl: token.endpoint.httpBaseUrl,\n accessToken: token.accessToken,\n dpopProof: ticketProof,\n clientMetadata: presentation.metadata,\n connectionMethod: \"relay\",\n ...(timeoutMs === undefined ? {} : { timeoutMs }),\n }).pipe(Effect.provideService(HttpClient.HttpClient, httpClient));\n },\n );\n\n const sessionChanged = () =>\n new ConnectionBlockedError({\n reason: \"authentication\",\n detail: \"Your cloud sign-in changed. Sign in again to authorize the environment.\",\n });\n\n const assertSession = Effect.fnUntraced(function* (\n identity: ClientCapabilities.CloudSessionIdentity,\n ) {\n const current = yield* cloudSession.identity;\n if (Option.isNone(current) || current.value !== identity) {\n return yield* sessionChanged();\n }\n });\n\n // Called under tokenLock so a late rejection cannot remove a newer credential.\n const removeRejectedToken = Effect.fnUntraced(function* (\n environmentId: EnvironmentId,\n accessToken: string,\n ) {\n const cached = yield* tokenStore.get(environmentId);\n if (Option.isSome(cached) && cached.value.accessToken === accessToken) {\n yield* tokenStore.remove(environmentId);\n tokenOwners.delete(environmentId);\n }\n });\n\n const obtainBootstrap = Effect.fn(\"relay.connection.bootstrap.obtain\")(function* (\n environmentId: EnvironmentId,\n identity: ClientCapabilities.CloudSessionIdentity,\n ) {\n yield* assertSession(identity);\n const clerkToken = yield* cloudSession.clerkToken.pipe(\n Effect.withSpan(\"relay.connection.cloudSessionToken.resolve\"),\n );\n const deviceId = yield* deviceIdentity.deviceId.pipe(\n Effect.withSpan(\"relay.connection.deviceIdentity.resolve\"),\n );\n yield* assertSession(identity);\n const connected = yield* relay\n .connectEnvironment({\n clerkToken,\n scopes: [RelayEnvironmentConnectScope],\n environmentId,\n ...(Option.isSome(deviceId) ? { deviceId: deviceId.value } : {}),\n })\n .pipe(Effect.mapError(mapManagedRelayError));\n if (connected.environmentId !== environmentId) {\n return yield* environmentMismatchError({\n expected: environmentId,\n actual: connected.environmentId,\n });\n }\n yield* assertSession(identity);\n return connected;\n });\n\n const exchangeDpopToken = Effect.fn(\"clientRuntime.connection.remote.exchangeDpopToken\")(\n function* (\n environmentId: EnvironmentId,\n thumbprint: string,\n identity: ClientCapabilities.CloudSessionIdentity,\n ) {\n const bootstrap = yield* obtainBootstrap(environmentId, identity);\n const descriptor = yield* fetchDescriptor(bootstrap.endpoint.httpBaseUrl, \"relay\").pipe(\n Effect.provideService(HttpClient.HttpClient, httpClient),\n Effect.withSpan(\"environment.authorization.descriptor\"),\n );\n if (descriptor.environmentId !== environmentId) {\n return yield* environmentMismatchError({\n expected: environmentId,\n actual: descriptor.environmentId,\n });\n }\n const bootstrapProof = yield* signer\n .createProof({", + "checksum": "7363bdcf93ea7b4df9a7ad535f31a94b1a99a61b284e19afce49d499d0101105" }, "reconnect-environment-registry": { "id": "reconnect-environment-registry", @@ -1686,8 +1686,8 @@ "end": 289, "language": "typescript", "label": "Environment-scoped runtime registry and lease replacement", - "code": " const entries =\n yield* SubscriptionRef.make>(initialEntries);\n const networkStatus = yield* SubscriptionRef.make(yield* connectivity.status);\n const serviceScopes = yield* SubscriptionRef.make<\n ReadonlyMap\n >(new Map());\n const platformEnvironmentIds = yield* Ref.make>(new Set());\n const persistedTargetsByEnvironment = yield* Ref.make<\n ReadonlyMap\n >(new Map(persistedTargets.map((target) => [target.environmentId, target])));\n interface LeaseLock {\n readonly semaphore: Semaphore.Semaphore;\n readonly users: number;\n }\n\n const leaseLocks = yield* Ref.make>(new Map());\n const leaseLocksGuard = yield* Semaphore.make(1);\n const started = yield* Ref.make(false);\n\n const withLeaseLock = (\n environmentId: EnvironmentId,\n effect: Effect.Effect,\n ): Effect.Effect =>\n Effect.acquireUseRelease(\n leaseLocksGuard.withPermits(1)(\n Effect.gen(function* () {\n const current = yield* Ref.get(leaseLocks);\n const existing = current.get(environmentId);\n if (existing !== undefined) {\n yield* Ref.set(\n leaseLocks,\n new Map(current).set(environmentId, {\n semaphore: existing.semaphore,\n users: existing.users + 1,\n }),\n );\n return existing.semaphore;\n }\n const semaphore = yield* Semaphore.make(1);\n yield* Ref.set(leaseLocks, new Map(current).set(environmentId, { semaphore, users: 1 }));\n return semaphore;\n }),\n ),\n (semaphore) => semaphore.withPermits(1)(effect),\n (semaphore) =>\n leaseLocksGuard.withPermits(1)(\n Ref.update(leaseLocks, (current) => {\n const existing = current.get(environmentId);\n if (existing === undefined || existing.semaphore !== semaphore) {\n return current;\n }\n const next = new Map(current);\n if (existing.users === 1) {\n next.delete(environmentId);\n } else {\n next.set(environmentId, {\n semaphore,\n users: existing.users - 1,\n });\n }\n return next;\n }),\n ),\n ).pipe(Effect.withSpan(\"EnvironmentRegistry.withLeaseLock\"));\n\n const getEntry = Effect.fn(\"EnvironmentRegistry.getEntry\")(function* (\n environmentId: EnvironmentId,\n ) {\n const entry = (yield* SubscriptionRef.get(entries)).get(environmentId);\n if (entry === undefined) {\n return yield* new EnvironmentNotRegisteredError({\n environmentId,\n });\n }\n return entry;\n });\n\n const closeServiceScope = Effect.fn(\"EnvironmentRegistry.closeServiceScope\")(function* (\n environmentId: EnvironmentId,\n ) {\n const current = yield* SubscriptionRef.get(serviceScopes);\n const lease = current.get(environmentId);\n if (lease === undefined) {\n return;\n }\n const next = new Map(current);\n next.delete(environmentId);\n yield* SubscriptionRef.set(serviceScopes, next);\n yield* Scope.close(lease.scope, Exit.void);\n });\n\n const createServiceScope = Effect.fn(\"EnvironmentRegistry.createServiceScope\")(\n (entry: ConnectionCatalogEntry) =>\n Effect.uninterruptible(\n Effect.gen(function* () {\n const environmentId = entry.target.environmentId;\n const scope = yield* Scope.make();\n const supervisor = yield* EnvironmentSupervisor.make(entry, {\n initiallyDesired: false,\n }).pipe(\n Effect.provideService(Connectivity.Connectivity, connectivity),\n Effect.provideService(ConnectionDriver.ConnectionDriver, driver),\n Effect.provideService(ConnectionWakeups.ConnectionWakeups, wakeups),\n Scope.provide(scope),\n Effect.onError(() => Scope.close(scope, Exit.void)),\n );\n yield* supervisor.connect;\n yield* SubscriptionRef.update(serviceScopes, (current) => {\n const next = new Map(current);\n next.set(environmentId, { entry, supervisor, scope });\n return next;\n });\n return supervisor;\n }),\n ),\n );\n\n const acquireSupervisor = Effect.fn(\"EnvironmentRegistry.acquireSupervisor\")(function* (\n environmentId: EnvironmentId,\n ) {\n return yield* withLeaseLock(\n environmentId,\n Effect.gen(function* () {\n const entry = yield* getEntry(environmentId);\n const existing = (yield* SubscriptionRef.get(serviceScopes)).get(environmentId);\n if (existing !== undefined) {\n if (Equal.equals(existing.entry, entry)) {\n return existing.supervisor;\n }\n yield* closeServiceScope(environmentId);\n }\n return yield* createServiceScope(entry);\n }),\n );", - "checksum": "12b74a7bf02c25c818e09218075f7ada2c45fbc08ff74ec48531c05611264fd3" + "code": " ),\n );\n const entries =\n yield* SubscriptionRef.make>(initialEntries);\n const networkStatus = yield* SubscriptionRef.make(yield* connectivity.status);\n const serviceScopes = yield* SubscriptionRef.make<\n ReadonlyMap\n >(new Map());\n const platformEnvironmentIds = yield* Ref.make>(new Set());\n const persistedTargetsByEnvironment = yield* Ref.make<\n ReadonlyMap\n >(new Map(persistedTargets.map((target) => [target.environmentId, target])));\n interface LeaseLock {\n readonly semaphore: Semaphore.Semaphore;\n readonly users: number;\n }\n\n const leaseLocks = yield* Ref.make>(new Map());\n const leaseLocksGuard = yield* Semaphore.make(1);\n const started = yield* Ref.make(false);\n\n const withLeaseLock = (\n environmentId: EnvironmentId,\n effect: Effect.Effect,\n ): Effect.Effect =>\n Effect.acquireUseRelease(\n leaseLocksGuard.withPermits(1)(\n Effect.gen(function* () {\n const current = yield* Ref.get(leaseLocks);\n const existing = current.get(environmentId);\n if (existing !== undefined) {\n yield* Ref.set(\n leaseLocks,\n new Map(current).set(environmentId, {\n semaphore: existing.semaphore,\n users: existing.users + 1,\n }),\n );\n return existing.semaphore;\n }\n const semaphore = yield* Semaphore.make(1);\n yield* Ref.set(leaseLocks, new Map(current).set(environmentId, { semaphore, users: 1 }));\n return semaphore;\n }),\n ),\n (semaphore) => semaphore.withPermits(1)(effect),\n (semaphore) =>\n leaseLocksGuard.withPermits(1)(\n Ref.update(leaseLocks, (current) => {\n const existing = current.get(environmentId);\n if (existing === undefined || existing.semaphore !== semaphore) {\n return current;\n }\n const next = new Map(current);\n if (existing.users === 1) {\n next.delete(environmentId);\n } else {\n next.set(environmentId, {\n semaphore,\n users: existing.users - 1,\n });\n }\n return next;\n }),\n ),\n ).pipe(Effect.withSpan(\"EnvironmentRegistry.withLeaseLock\"));\n\n const getEntry = Effect.fn(\"EnvironmentRegistry.getEntry\")(function* (\n environmentId: EnvironmentId,\n ) {\n const entry = (yield* SubscriptionRef.get(entries)).get(environmentId);\n if (entry === undefined) {\n return yield* new EnvironmentNotRegisteredError({\n environmentId,\n });\n }\n return entry;\n });\n\n const closeServiceScope = Effect.fn(\"EnvironmentRegistry.closeServiceScope\")(function* (\n environmentId: EnvironmentId,\n ) {\n const current = yield* SubscriptionRef.get(serviceScopes);\n const lease = current.get(environmentId);\n if (lease === undefined) {\n return;\n }\n const next = new Map(current);\n next.delete(environmentId);\n yield* SubscriptionRef.set(serviceScopes, next);\n yield* Scope.close(lease.scope, Exit.void);\n });\n\n const createServiceScope = Effect.fn(\"EnvironmentRegistry.createServiceScope\")(\n (entry: ConnectionCatalogEntry) =>\n Effect.uninterruptible(\n Effect.gen(function* () {\n const environmentId = entry.target.environmentId;\n const scope = yield* Scope.fork(registryScope);\n const supervisor = yield* EnvironmentSupervisor.make(entry, {\n initiallyDesired: false,\n }).pipe(\n Effect.provideService(Connectivity.Connectivity, connectivity),\n Effect.provideService(ConnectionDriver.ConnectionDriver, driver),\n Effect.provideService(ConnectionWakeups.ConnectionWakeups, wakeups),\n Scope.provide(scope),\n Effect.onError(() => Scope.close(scope, Exit.void)),\n );\n yield* supervisor.connect;\n yield* SubscriptionRef.update(serviceScopes, (current) => {\n const next = new Map(current);\n next.set(environmentId, { entry, supervisor, scope });\n return next;\n });\n return supervisor;\n }),\n ),\n );\n\n const acquireSupervisor = Effect.fn(\"EnvironmentRegistry.acquireSupervisor\")(function* (\n environmentId: EnvironmentId,\n ) {\n return yield* withLeaseLock(\n environmentId,\n Effect.gen(function* () {\n const entry = yield* getEntry(environmentId);\n const existing = (yield* SubscriptionRef.get(serviceScopes)).get(environmentId);\n if (existing !== undefined) {\n if (Equal.equals(existing.entry, entry)) {\n return existing.supervisor;\n }\n yield* closeServiceScope(environmentId);\n }\n return yield* createServiceScope(entry);", + "checksum": "dd61efe5f6940f0e1950ecb8da6481b2cd08e765a2a5aad3539f9cdb8944bbf5" }, "reconnect-scoped-environment-refs": { "id": "reconnect-scoped-environment-refs", @@ -1696,8 +1696,8 @@ "end": 130, "language": "typescript", "label": "Environment descriptors and scoped project and thread references", - "code": "/** How a server can replace itself with another version when asked over RPC.\n New servers only advertise the stable launcher-backed \"boot-service\" path;\n \"respawn\" remains decodable for compatibility with older servers. */\nexport const ServerSelfUpdateMethod = Schema.Literals([\"boot-service\", \"respawn\"]);\nexport type ServerSelfUpdateMethod = typeof ServerSelfUpdateMethod.Type;\n\n/** What update path a client should offer for a server: one of the RPC\n self-update methods above, or \"desktop-managed\" when the backend's\n version belongs to the T3 Code desktop app supervising it — updating the\n app on that machine is the only way to update the server. */\nexport const ServerSelfUpdateCapability = Schema.Literals([\n \"boot-service\",\n \"respawn\",\n \"desktop-managed\",\n]);\nexport type ServerSelfUpdateCapability = typeof ServerSelfUpdateCapability.Type;\n\nexport const ExecutionEnvironmentCapabilities = Schema.Struct({\n repositoryIdentity: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),\n connectionProbe: Schema.optionalKey(Schema.Boolean),\n /** Server exposes the pull-request list, detail, activity, diff, and mutation APIs. Absent on\n servers from before the pull-request workspace shipped, so clients must not probe them. */\n pullRequests: Schema.optionalKey(Schema.Boolean),\n /** Server understands thread.settle / thread.unsettle commands. Absent on\n pre-settlement servers, so clients treat missing as unsupported and\n never send the commands under version skew. */\n threadSettlement: Schema.optionalKey(Schema.Boolean),\n /** Server understands thread.snooze / thread.unsnooze commands. Same\n version-skew contract as threadSettlement. */\n threadSnooze: Schema.optionalKey(Schema.Boolean),\n /** Server understands thread.pin / thread.unpin commands. Same\n version-skew contract as threadSettlement. */\n threadPinning: Schema.optionalKey(Schema.Boolean),\n /** Server understands thread.pin.reorder (and orderKey on thread.pin).\n Same version-skew contract as threadSettlement. */\n threadPinReorder: Schema.optionalKey(Schema.Boolean),\n /** Server understands regenerateTitle on thread.meta.update. Absent on\n older servers, so clients hide the action instead of sending it. */\n threadTitleRegeneration: Schema.optionalKey(Schema.Boolean),\n /** The update path clients should offer for this server. Absent on\n servers that must be relaunched manually (dev checkouts, Windows\n foreground runs, pre-update servers). */\n serverSelfUpdate: Schema.optionalKey(ServerSelfUpdateCapability),\n /** Server can stream self-update progress before acknowledging the\n restart. Clients fall back to server.updateServer when absent. */\n serverSelfUpdateProgress: Schema.optionalKey(Schema.Boolean),\n /** Agent-activity publishes (push notifications and Live Activities)\n currently leave this environment: the publish opt-in is enabled and the\n relay link credentials exist. Clients skip seeding a Live Activity when\n this is false — no update would ever repaint it. Absent on older\n servers, which may still publish, so only an explicit false skips. */\n agentActivityPublishing: Schema.optionalKey(Schema.Boolean),\n});\nexport type ExecutionEnvironmentCapabilities = typeof ExecutionEnvironmentCapabilities.Type;\n\nexport const ExecutionEnvironmentDescriptor = Schema.Struct({\n environmentId: EnvironmentId,\n label: TrimmedNonEmptyString,\n platform: ExecutionEnvironmentPlatform,\n serverVersion: TrimmedNonEmptyString,\n capabilities: ExecutionEnvironmentCapabilities,\n});\nexport type ExecutionEnvironmentDescriptor = typeof ExecutionEnvironmentDescriptor.Type;\n\nexport const EnvironmentConnectionState = Schema.Literals([\n \"connecting\",\n \"connected\",\n \"disconnected\",\n \"error\",\n]);\nexport type EnvironmentConnectionState = typeof EnvironmentConnectionState.Type;\n\nexport const RepositoryIdentityLocator = Schema.Struct({\n source: Schema.Literal(\"git-remote\"),\n remoteName: TrimmedNonEmptyString,\n remoteUrl: TrimmedNonEmptyString,\n});\nexport type RepositoryIdentityLocator = typeof RepositoryIdentityLocator.Type;\n\nexport const RepositoryIdentity = Schema.Struct({\n canonicalKey: TrimmedNonEmptyString,\n locator: RepositoryIdentityLocator,\n rootPath: Schema.optionalKey(TrimmedNonEmptyString),\n displayName: Schema.optionalKey(TrimmedNonEmptyString),\n provider: Schema.optionalKey(TrimmedNonEmptyString),\n owner: Schema.optionalKey(TrimmedNonEmptyString),\n name: Schema.optionalKey(TrimmedNonEmptyString),\n});\nexport type RepositoryIdentity = typeof RepositoryIdentity.Type;\n\nexport const ScopedProjectRef = Schema.Struct({\n environmentId: EnvironmentId,\n projectId: ProjectId,\n});\nexport type ScopedProjectRef = typeof ScopedProjectRef.Type;\n\nexport const ScopedThreadRef = Schema.Struct({\n environmentId: EnvironmentId,\n threadId: ThreadId,\n});", - "checksum": "8adc3eeb2048647115e388b4fc57aea27ff94e4eeb613ad4a0bd5727d0c63d7e" + "code": " \"linux\",\n \"desktop\",\n \"laptop\",\n \"mac-mini\",\n \"mac-studio\",\n] as const;\nexport const EnvironmentMachineKind = Schema.Literals(ENVIRONMENT_MACHINE_KINDS);\nexport type EnvironmentMachineKind = typeof EnvironmentMachineKind.Type;\nexport const isEnvironmentMachineKind = Schema.is(EnvironmentMachineKind);\n\nexport const ExecutionEnvironmentPlatform = Schema.Struct({\n os: ExecutionEnvironmentPlatformOs,\n arch: ExecutionEnvironmentPlatformArch,\n /** Hardware shape detected at startup. Absent when the host gives no usable\n signal (containers, Windows, unknown DMI), on servers that predate it, or\n when a newer server names a kind this build cannot draw. */\n machine: ForwardCompatibleOptional(EnvironmentMachineKind),\n});\n\n/**\n * Where a new thread runs: the project's current checkout (\"local\") or a\n * fresh git worktree (\"worktree\"). Lives here (not settings.ts) so\n * orchestration contracts can reference it without an import cycle.\n */\nexport const ThreadEnvMode = Schema.Literals([\"local\", \"worktree\"]);\nexport type ThreadEnvMode = typeof ThreadEnvMode.Type;\nexport type ExecutionEnvironmentPlatform = typeof ExecutionEnvironmentPlatform.Type;\n\n/** How a server can replace itself with another version when asked over RPC.\n New servers only advertise the stable launcher-backed \"boot-service\" path;\n \"respawn\" remains decodable for compatibility with older servers.\n \"desktop-app\" means the supervising desktop app updated and relaunched\n itself, bringing the server back with it. */\nexport const ServerSelfUpdateMethod = Schema.Literals([\"boot-service\", \"respawn\", \"desktop-app\"]);\nexport type ServerSelfUpdateMethod = typeof ServerSelfUpdateMethod.Type;\n\n/** What update path a client should offer for a server: one of the RPC\n self-update methods above, or \"desktop-managed\" when the backend's\n version belongs to the T3 Code desktop app supervising it — updating the\n app on that machine is the only way to update the server. */\nexport const ServerSelfUpdateCapability = Schema.Literals([\n \"boot-service\",\n \"respawn\",\n \"desktop-managed\",\n]);\nexport type ServerSelfUpdateCapability = typeof ServerSelfUpdateCapability.Type;\n\nexport const ExecutionEnvironmentCapabilities = Schema.Struct({\n repositoryIdentity: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),\n connectionProbe: Schema.optionalKey(Schema.Boolean),\n /** Missing on older servers, which still accept inline image attachments. */\n attachmentUploads: Schema.optionalKey(Schema.Boolean),\n /** Uploaded files may accompany question answers. */\n questionAttachments: Schema.optionalKey(Schema.Boolean),\n /** Missing on servers that only accept image attachments. */\n fileAttachments: Schema.optionalKey(\n Schema.Struct({\n maxUploadBytes: Schema.Int.check(Schema.isGreaterThanOrEqualTo(1)),\n }),\n ),\n /** Server exposes the pull-request list, detail, activity, diff, and mutation APIs. Absent on\n servers from before the pull-request workspace shipped, so clients must not probe them. */\n pullRequests: Schema.optionalKey(Schema.Boolean),\n /** Server understands thread.settle / thread.unsettle commands. Absent on\n pre-settlement servers, so clients treat missing as unsupported and\n never send the commands under version skew. */\n threadSettlement: Schema.optionalKey(Schema.Boolean),\n /** Server evaluates merge and inactivity settlement without a client. */\n threadAutoSettlement: Schema.optionalKey(Schema.Boolean),\n /** Server persists the opt-in for continuing interrupted threads after restarts. */\n threadRestartContinuation: Schema.optionalKey(Schema.Boolean),\n /** Server understands thread.snooze / thread.unsnooze commands. Same\n version-skew contract as threadSettlement. */\n threadSnooze: Schema.optionalKey(Schema.Boolean),\n /** Server streams themes an environment publishes. Absent on servers from\n before environment themes shipped, which never emit the events -- so a\n client reconnecting to one must drop published themes rather than keep\n showing a set nothing will ever update. */\n environmentThemes: Schema.optionalKey(Schema.Boolean),\n /** Server streams quota from configured usage-limit sources. Same\n version-skew contract as environmentThemes. */\n usageLimitSources: Schema.optionalKey(Schema.Boolean),\n /** Server persists custom model rates and applies them to usage summaries. */\n usagePriceOverrides: Schema.optionalKey(Schema.Boolean),\n /** Server understands thread.pin / thread.unpin commands. Same\n version-skew contract as threadSettlement. */\n threadPinning: Schema.optionalKey(Schema.Boolean),\n /** Server understands thread.pin.reorder (and orderKey on thread.pin).\n Same version-skew contract as threadSettlement. */\n threadPinReorder: Schema.optionalKey(Schema.Boolean),\n /** Server persists manual Active order through thread.active.reorder. */\n threadActiveReorder: Schema.optionalKey(Schema.Boolean),\n /** Server understands regenerateTitle on thread.meta.update. Absent on\n older servers, so clients hide the action instead of sending it. */\n threadTitleRegeneration: Schema.optionalKey(Schema.Boolean),\n /** Server supports legacy linkedPullRequest updates through thread.meta.update.\n Independent of threadPullRequests; servers supporting both advertise both. */\n threadPullRequestLinking: Schema.optionalKey(Schema.Boolean),\n /** Server understands thread.pull-request.link / .unlink, exposes `pullRequests` on\n threads, and routes PullRequestRef.host across projects on the same host. Same", + "checksum": "0e0e8732b8ddbe0ac3213821dde46fe78bc54888621daf25b38524cc9d1ae72f" }, "reconnect-supervisor-active-lease": { "id": "reconnect-supervisor-active-lease", @@ -1706,8 +1706,8 @@ "end": 615, "language": "typescript", "label": "Active prepared connection and RPC session lease lifecycle", - "code": " const runAttempt = Effect.fnUntraced(function* (\n attempt: number,\n generation: number,\n lastFailure: ConnectionAttemptError | null,\n pendingRetry: Option.Option,\n ) {\n yield* SubscriptionRef.set(prepared, Option.none());\n const establishment = yield* Effect.raceAllFirst([\n exitUnlessInterrupted(\n establishTracedConnection(attempt, generation, lastFailure, pendingRetry),\n ).pipe(\n Effect.map(\n (exit): EstablishmentEvent => ({\n _tag: \"Completed\",\n exit,\n }),\n ),\n ),\n waitForEstablishmentInterrupt().pipe(\n Effect.map(\n (resetRetry): EstablishmentEvent => ({\n _tag: \"Interrupted\",\n resetRetry,\n }),\n ),\n ),\n Effect.sleep(CONNECTION_ESTABLISHMENT_TIMEOUT).pipe(\n Effect.as({ _tag: \"TimedOut\" }),\n ),\n ]);\n\n if (establishment._tag === \"Interrupted\") {\n return {\n _tag: \"Interrupted\",\n established: false,\n stable: false,\n resetRetry: establishment.resetRetry,\n } satisfies AttemptOutcome;\n }\n if (establishment._tag === \"TimedOut\") {\n return {\n _tag: \"Failure\",\n established: false,\n stable: false,\n failure: {\n error: new ConnectionTransientError({\n reason: \"timeout\",\n detail: `${target.label} did not respond during connection setup.`,\n }),\n attemptSpan: Option.none(),\n },\n } satisfies AttemptOutcome;\n }\n if (Exit.isFailure(establishment.exit)) {\n const isUnexpectedDefect =\n !Cause.hasInterruptsOnly(establishment.exit.cause) &&\n !establishment.exit.cause.reasons.some(Cause.isFailReason);\n const outcome = failureFromExit(target, establishment.exit, false, false);\n if (isUnexpectedDefect) {\n const defect = establishment.exit.cause.reasons.find(Cause.isDieReason)?.defect;\n yield* Effect.logError(\"Connection attempt failed with an unexpected defect.\").pipe(\n Effect.annotateLogs({\n \"environment.id\": target.environmentId,\n \"environment.label\": target.label,\n \"cause.reason_count\": establishment.exit.cause.reasons.length,\n ...safeErrorLogAttributes(defect),\n }),\n );\n }\n return outcome;\n }\n\n const active = establishment.exit.value;\n const currentIntent = yield* Ref.get(intent);\n if (!currentIntent.desired || currentIntent.network === \"offline\") {\n return {\n _tag: \"Interrupted\",\n established: false,\n stable: false,\n resetRetry: false,\n } satisfies AttemptOutcome;\n }\n\n const connectedAt = yield* Clock.currentTimeMillis;\n yield* SubscriptionRef.set(prepared, Option.some(active.lease.prepared));\n yield* SubscriptionRef.set(session, Option.some(active.lease.session));\n yield* setState({\n desired: true,\n network: currentIntent.network,\n phase: \"connected\",\n stage: null,\n attempt,\n generation,\n lastFailure: null,\n retryAt: null,\n });\n\n const connectedExit = yield* Effect.raceFirst(\n active.lease.session.closed.pipe(\n Effect.mapError(\n (error): TracedAttemptFailure => ({\n error,\n attemptSpan: active.attemptSpan,\n }),\n ),\n ),\n monitorConnectedLease(active.lease).pipe(\n Effect.mapError(\n (error): TracedAttemptFailure => ({\n error,\n attemptSpan: active.attemptSpan,\n }),\n ),\n ),\n ).pipe(exitUnlessInterrupted);\n const connectedForMs = (yield* Clock.currentTimeMillis) - connectedAt;\n if (Exit.isSuccess(connectedExit)) {\n return {\n _tag: \"Interrupted\",\n established: true,\n stable: connectedForMs >= BACKOFF_RESET_AFTER_MS,\n resetRetry: connectedExit.value,\n } satisfies AttemptOutcome;\n }\n return failureFromExit(target, connectedExit, true, connectedForMs >= BACKOFF_RESET_AFTER_MS);\n }, Effect.ensuring(clearLease));", - "checksum": "8c24ef212e1baf658eabc67c2a00996ebc7dae1354bb2d441368ff58e4b90f15" + "code": " attempt: number,\n generation: number,\n lastFailure: ConnectionAttemptError | null,\n pendingRetry: Option.Option,\n ) {\n yield* SubscriptionRef.set(prepared, Option.none());\n const establishment = yield* Effect.raceAllFirst([\n exitUnlessInterrupted(\n establishTracedConnection(attempt, generation, lastFailure, pendingRetry),\n ).pipe(\n Effect.map((exit): EstablishmentEvent => ({\n _tag: \"Completed\",\n exit,\n })),\n ),\n waitForEstablishmentInterrupt().pipe(\n Effect.map((resetRetry): EstablishmentEvent => ({\n _tag: \"Interrupted\",\n resetRetry,\n })),\n ),\n Effect.sleep(CONNECTION_ESTABLISHMENT_TIMEOUT).pipe(\n Effect.as({ _tag: \"TimedOut\" }),\n ),\n ]);\n\n if (establishment._tag === \"Interrupted\") {\n return {\n _tag: \"Interrupted\",\n established: false,\n stable: false,\n resetRetry: establishment.resetRetry,\n } satisfies AttemptOutcome;\n }\n if (establishment._tag === \"TimedOut\") {\n return {\n _tag: \"Failure\",\n established: false,\n stable: false,\n failure: {\n error: new ConnectionTransientError({\n reason: \"timeout\",\n detail: setupTimeoutDetail,\n }),\n attemptSpan: Option.none(),\n },\n } satisfies AttemptOutcome;\n }\n if (Exit.isFailure(establishment.exit)) {\n const isUnexpectedDefect =\n !Cause.hasInterruptsOnly(establishment.exit.cause) &&\n !establishment.exit.cause.reasons.some(Cause.isFailReason);\n const outcome = failureFromExit(target, establishment.exit, false, false);\n if (isUnexpectedDefect) {\n const defect = establishment.exit.cause.reasons.find(Cause.isDieReason)?.defect;\n yield* Effect.logError(\"Connection attempt failed with an unexpected defect.\").pipe(\n Effect.annotateLogs({\n \"environment.id\": target.environmentId,\n \"environment.label\": target.label,\n \"cause.reason_count\": establishment.exit.cause.reasons.length,\n ...safeErrorLogAttributes(defect),\n }),\n );\n }\n return outcome;\n }\n\n const active = establishment.exit.value;\n const currentIntent = yield* Ref.get(intent);\n if (!currentIntent.desired || currentIntent.network === \"offline\") {\n return {\n _tag: \"Interrupted\",\n established: false,\n stable: false,\n resetRetry: false,\n } satisfies AttemptOutcome;\n }\n\n const connectedAt = yield* Clock.currentTimeMillis;\n yield* SubscriptionRef.set(prepared, Option.some(active.lease.prepared));\n yield* SubscriptionRef.set(session, Option.some(active.lease.session));\n yield* setState({\n desired: true,\n network: currentIntent.network,\n phase: \"connected\",\n stage: null,\n attempt,\n generation,\n lastFailure: null,\n retryAt: null,\n });\n\n const connectedExit = yield* Effect.raceFirst(\n active.lease.session.closed.pipe(\n Effect.mapError((error): TracedAttemptFailure => ({\n error,\n attemptSpan: active.attemptSpan,\n })),\n ),\n monitorConnectedLease(active.lease).pipe(\n Effect.mapError((error): TracedAttemptFailure => ({\n error,\n attemptSpan: active.attemptSpan,\n })),\n ),\n ).pipe(exitUnlessInterrupted);\n const connectedForMs = (yield* Clock.currentTimeMillis) - connectedAt;\n if (Exit.isSuccess(connectedExit)) {\n return {\n _tag: \"Interrupted\",\n established: true,\n stable: connectedForMs >= BACKOFF_RESET_AFTER_MS,\n resetRetry: connectedExit.value,\n } satisfies AttemptOutcome;\n }\n return failureFromExit(target, connectedExit, true, connectedForMs >= BACKOFF_RESET_AFTER_MS);\n }, Effect.ensuring(clearLease));\n\n const waitForRetrySignal = Effect.fnUntraced(function* (delayMs: number) {\n return yield* Effect.raceFirst(\n Effect.sleep(delayMs).pipe(Effect.as(false)),\n Effect.gen(function* () {\n for (;;) {\n const next = yield* Queue.take(signals);\n switch (next._tag) {\n case \"Wakeup\":", + "checksum": "de49ad3c0f52273f4ce40c5f9bb81a737ff400b096c12befa35e69504119306a" }, "reconnect-supervisor-generation-loop": { "id": "reconnect-supervisor-generation-loop", @@ -1716,8 +1716,8 @@ "end": 755, "language": "typescript", "label": "Environment supervisor generation, offline, retry, and backoff loop", - "code": " const run = Effect.fnUntraced(function* () {\n let failureCount = 0;\n let generation = 0;\n let latestFailure: ConnectionAttemptError | null = null;\n let pendingRetry = Option.none();\n const resetRetryLadder = () => {\n failureCount = 0;\n pendingRetry = Option.none();\n };\n\n for (;;) {\n if (yield* Ref.getAndSet(resetRetryState, false)) {\n failureCount = 0;\n latestFailure = null;\n pendingRetry = Option.none();\n }\n const currentIntent = yield* Ref.get(intent);\n if (!currentIntent.desired) {\n resetRetryLadder();\n latestFailure = null;\n yield* clearLease;\n yield* setState(availableState(currentIntent, generation));\n yield* waitForSignal;\n continue;\n }\n if (currentIntent.network === \"offline\") {\n yield* clearLease;\n yield* setState(offlineState(currentIntent, generation, failureCount + 1, latestFailure));\n const applicationActivated = yield* waitForSignal;\n if (applicationActivated) {\n resetRetryLadder();\n }\n continue;\n }\n\n const attempt = failureCount + 1;\n const nextGeneration = generation + 1;\n const outcome: AttemptOutcome = yield* Effect.scoped(\n runAttempt(attempt, nextGeneration, latestFailure, pendingRetry),\n );\n // Consumed on every iteration so a stale marker can never leak into a\n // later, unrelated failure.\n const failedWakeProbe = yield* Ref.getAndSet(wakeProbeFailed, false);\n if (outcome.established) {\n generation = nextGeneration;\n if (outcome.stable) {\n resetRetryLadder();\n latestFailure = null;\n }\n }\n if (outcome._tag === \"Interrupted\") {\n if (outcome.resetRetry) {\n resetRetryLadder();\n }\n continue;\n }\n\n const attemptSpan: Option.Option = outcome.failure.attemptSpan;\n const error: ConnectionAttemptError = outcome.failure.error;\n latestFailure = error;\n if (error._tag === \"ConnectionBlockedError\") {\n const blockedIntent = yield* Ref.get(intent);\n yield* setState({\n desired: blockedIntent.desired,\n network: blockedIntent.network,\n phase: \"blocked\",\n stage: null,\n attempt,\n generation,\n lastFailure: error,\n retryAt: null,\n });\n const applicationActivated = yield* waitForSignal;\n if (applicationActivated) {\n resetRetryLadder();\n }\n continue;\n }\n\n if (failedWakeProbe) {\n // The wake probe found a dead transport while the user is returning to\n // the app, so reconnect immediately instead of sleeping the first\n // backoff rung. Only this first attempt skips the ladder; if it fails\n // too, normal backoff resumes.\n resetRetryLadder();\n yield* setState(connectingState(yield* Ref.get(intent), generation, 1, error));\n continue;\n }\n\n failureCount += 1;\n const delayMs = retryDelayMs(failureCount - 1);\n pendingRetry = Option.map(attemptSpan, (previousAttempt) => ({\n previousAttempt,\n failureCount,\n delayMs,\n reason: error.reason,\n }));\n const failedIntent = yield* Ref.get(intent);\n yield* setState({\n desired: failedIntent.desired,\n network: failedIntent.network,\n phase: \"backoff\",\n stage: null,\n attempt,\n generation,\n lastFailure: error,\n retryAt: (yield* Clock.currentTimeMillis) + delayMs,\n });\n const applicationActivated = yield* waitForRetrySignal(delayMs);\n if (applicationActivated) {\n resetRetryLadder();\n }\n }", - "checksum": "c05d5d84a1e4a46eb365fbb096528c45b5defa3f110b2d0e7c7a56f19d0e21e5" + "code": "\n for (;;) {\n if (yield* Ref.getAndSet(resetRetryState, false)) {\n failureCount = 0;\n latestFailure = null;\n pendingRetry = Option.none();\n }\n const currentIntent = yield* Ref.get(intent);\n if (!currentIntent.desired) {\n resetRetryLadder();\n latestFailure = null;\n yield* clearLease;\n yield* setState(availableState(currentIntent, generation));\n yield* waitForSignal;\n continue;\n }\n if (currentIntent.network === \"offline\") {\n yield* clearLease;\n yield* setState(offlineState(currentIntent, generation, failureCount + 1, latestFailure));\n const applicationActivated = yield* waitForSignal;\n if (applicationActivated) {\n resetRetryLadder();\n }\n continue;\n }\n\n const attempt = failureCount + 1;\n const nextGeneration = generation + 1;\n const outcome: AttemptOutcome = yield* Effect.scoped(\n runAttempt(attempt, nextGeneration, latestFailure, pendingRetry),\n );\n // Consumed on every iteration so a stale marker can never leak into a\n // later, unrelated failure.\n const failedWakeProbe = yield* Ref.getAndSet(wakeProbeFailed, false);\n if (outcome.established) {\n generation = nextGeneration;\n if (outcome.stable) {\n resetRetryLadder();\n latestFailure = null;\n }\n }\n if (outcome._tag === \"Interrupted\") {\n if (outcome.resetRetry) {\n resetRetryLadder();\n }\n continue;\n }\n\n const attemptSpan: Option.Option = outcome.failure.attemptSpan;\n const error: ConnectionAttemptError = outcome.failure.error;\n latestFailure = error;\n if (error._tag === \"ConnectionBlockedError\") {\n const blockedIntent = yield* Ref.get(intent);\n yield* setState({\n desired: blockedIntent.desired,\n network: blockedIntent.network,\n phase: \"blocked\",\n stage: null,\n attempt,\n generation,\n lastFailure: error,\n retryAt: null,\n });\n const applicationActivated = yield* waitForSignal;\n if (applicationActivated) {\n resetRetryLadder();\n }\n continue;\n }\n\n if (failedWakeProbe) {\n // The wake probe found a dead transport while the user is returning to\n // the app, so reconnect immediately instead of sleeping the first\n // backoff rung. Only this first attempt skips the ladder; if it fails\n // too, normal backoff resumes.\n resetRetryLadder();\n yield* setState(connectingState(yield* Ref.get(intent), generation, 1, error));\n continue;\n }\n\n failureCount += 1;\n const delayMs = retryDelayMs(failureCount - 1);\n pendingRetry = Option.map(attemptSpan, (previousAttempt) => ({\n previousAttempt,\n failureCount,\n delayMs,\n reason: error.reason,\n }));\n const failedIntent = yield* Ref.get(intent);\n yield* setState({\n desired: failedIntent.desired,\n network: failedIntent.network,\n phase: \"backoff\",\n stage: null,\n attempt,\n generation,\n lastFailure: error,\n retryAt: (yield* Clock.currentTimeMillis) + delayMs,\n });\n const applicationActivated = yield* waitForRetrySignal(delayMs);\n if (applicationActivated) {\n resetRetryLadder();\n }\n }\n });\n\n yield* connectivity.changes.pipe(\n Stream.runForEach((network) =>\n Ref.modify(intent, (current) =>\n current.network === network ? [false, current] : ([true, { ...current, network }] as const),\n ).pipe(\n Effect.flatMap((changed) =>\n changed ? signal({ _tag: \"NetworkChanged\", network }) : Effect.void,", + "checksum": "d7299662058f6aa359ea6e6b4c7276513d7668cdcd5950412d63fca368491272" }, "reconnect-shell-authoritative-refresh": { "id": "reconnect-shell-authoritative-refresh", @@ -1726,8 +1726,8 @@ "end": 254, "language": "typescript", "label": "Shell cache, authoritative refresh, cursor, and synchronization boundary", - "code": " const applyItem = Effect.fn(\"EnvironmentShellState.applyItem\")(function* (\n item: OrchestrationShellStreamItem,\n ) {\n if (item.kind === \"synchronized\") {\n yield* Ref.set(awaitingCompletion, false);\n yield* SubscriptionRef.update(state, (current) =>\n Option.isSome(current.snapshot)\n ? { ...current, status: \"live\" as const, error: Option.none() }\n : current,\n );\n return;\n }\n\n const current = yield* SubscriptionRef.get(state);\n const nextSnapshot =\n item.kind === \"snapshot\"\n ? item.snapshot\n : Option.match(current.snapshot, {\n onNone: () => null,\n onSome: (snapshot) =>\n item.sequence > snapshot.snapshotSequence\n ? applyShellStreamEvent(snapshot, item)\n : snapshot,\n });\n if (nextSnapshot === null) {\n return;\n }\n\n const waiting = yield* Ref.get(awaitingCompletion);\n yield* SubscriptionRef.set(state, {\n snapshot: Option.some(nextSnapshot),\n status: waiting ? \"synchronizing\" : \"live\",\n error: Option.none(),\n });\n if (item.kind === \"snapshot\") {\n const session = yield* Ref.get(activeSubscriptionSession);\n if (session !== null) {\n yield* Ref.set(lastAuthoritativeSession, session);\n }\n }\n yield* Queue.offer(persistence, nextSnapshot);\n });\n\n const foregroundResubscriptions = Option.match(wakeups, {\n onNone: () => Stream.never,\n onSome: (service) =>\n service.changes.pipe(Stream.filter(ConnectionWakeups.shouldResubscribeAfterWakeup)),\n });\n\n yield* setSynchronizing;\n yield* Effect.forkScoped(\n subscribeDynamic(\n ORCHESTRATION_WS_METHODS.subscribeShell,\n Effect.fn(\"EnvironmentShellState.makeSubscribeInput\")(function* (session) {\n yield* Ref.set(activeSubscriptionSession, session);\n const supportsCompletionMarker = yield* session.initialConfig.pipe(\n Effect.map((config) => config.shellResumeCompletionMarker === true),\n Effect.orElseSucceed(() => false),\n );\n yield* Ref.set(awaitingCompletion, supportsCompletionMarker);\n yield* setSynchronizing;\n\n // Foreground resubscriptions on the same live session can resume from\n // the in-memory cursor. A new session reloads the authoritative HTTP\n // snapshot so a valid cursor cannot preserve incomplete cached data.\n const hasAuthoritativeSnapshot = (yield* Ref.get(lastAuthoritativeSession)) === session;\n let canResume = hasAuthoritativeSnapshot;\n let current = yield* SubscriptionRef.get(state);\n if (!hasAuthoritativeSnapshot || Option.isNone(current.snapshot)) {\n const prepared = yield* SubscriptionRef.get(supervisor.prepared).pipe(\n Effect.flatMap(\n Option.match({\n onSome: Effect.succeed,\n onNone: () =>\n SubscriptionRef.changes(supervisor.prepared).pipe(\n Stream.filter(Option.isSome),\n Stream.map((value) => value.value),\n Stream.runHead,\n Effect.map(Option.getOrThrow),\n ),\n }),\n ),\n );\n const httpSnapshot = yield* snapshotLoader.load(prepared);\n if (Option.isSome(httpSnapshot)) {\n yield* applyItem({ kind: \"snapshot\", snapshot: httpSnapshot.value });\n canResume = true;\n current = yield* SubscriptionRef.get(state);\n }\n }\n\n // If the authoritative refresh failed, omit the cached cursor so the\n // socket fallback sends a complete snapshot for this new session.\n if (!canResume || Option.isNone(current.snapshot)) {\n return supportsCompletionMarker ? { requestCompletionMarker: true as const } : {};\n }\n if (!supportsCompletionMarker) {\n // Without a completion marker there is no synchronized signal for a\n // resumed subscription, so report live immediately, like threads.\n yield* SubscriptionRef.update(state, (value) => ({\n ...value,\n status: \"live\" as const,\n error: Option.none(),\n }));\n }\n return {\n afterSequence: current.snapshot.value.snapshotSequence,\n ...(supportsCompletionMarker ? { requestCompletionMarker: true as const } : {}),\n };\n }),\n {\n onExpectedFailure: (cause) => setStreamError(Cause.squash(cause)),\n retryExpectedFailureAfter: \"250 millis\",\n resubscribe: foregroundResubscriptions,\n },\n ).pipe(Stream.runForEach(applyItem)),\n );", - "checksum": "1d9311e98f9edc09e36d4c35e425a45c477d9c51739b382efed42b285d41f19d" + "code": " // Apply each received batch with one state write. The RPC client's bounded\n // buffer can split a server chunk, so a bulk action can still need several\n // writes, but each write includes every event in that batch.\n const applyItems = Effect.fn(\"EnvironmentShellState.applyItems\")(function* (\n items: ReadonlyArray,\n ) {\n const initial = yield* SubscriptionRef.get(state);\n let waiting = yield* Ref.get(awaitingCompletion);\n let next = initial;\n let receivedSnapshot = false;\n for (const item of items) {\n if (item.kind === \"synchronized\") {\n waiting = false;\n if (Option.isSome(next.snapshot)) {\n next = { ...next, status: \"live\", error: Option.none() };\n }\n continue;\n }\n const nextSnapshot =\n item.kind === \"snapshot\"\n ? item.snapshot\n : Option.match(next.snapshot, {\n onNone: () => null,\n onSome: (snapshot) =>\n item.sequence > snapshot.snapshotSequence\n ? applyShellStreamEvent(snapshot, item)\n : snapshot,\n });\n if (nextSnapshot === null) continue;\n receivedSnapshot ||= item.kind === \"snapshot\";\n next = {\n snapshot: Option.some(nextSnapshot),\n status: waiting ? \"synchronizing\" : \"live\",\n error: Option.none(),\n };\n }\n yield* Ref.set(awaitingCompletion, waiting);\n if (next === initial) return;\n yield* SubscriptionRef.set(state, next);\n if (receivedSnapshot) {\n const session = yield* Ref.get(activeSubscriptionSession);\n if (session !== null) {\n yield* Ref.set(lastAuthoritativeSession, session);\n }\n }\n if (next.snapshot !== initial.snapshot && Option.isSome(next.snapshot)) {\n yield* Queue.offer(persistence, next.snapshot.value);\n }\n });\n\n const foregroundResubscriptions = Option.match(wakeups, {\n onNone: () => Stream.never,\n onSome: (service) =>\n service.changes.pipe(Stream.filter(ConnectionWakeups.shouldResubscribeAfterWakeup)),\n });\n\n yield* setSynchronizing;\n yield* Effect.forkScoped(\n subscribeDynamic(\n ORCHESTRATION_WS_METHODS.subscribeShell,\n Effect.fn(\"EnvironmentShellState.makeSubscribeInput\")(function* (session) {\n yield* Ref.set(activeSubscriptionSession, session);\n const supportsCompletionMarker = yield* session.initialConfig.pipe(\n Effect.map((config) => config.shellResumeCompletionMarker === true),\n Effect.orElseSucceed(() => false),\n );\n yield* Ref.set(awaitingCompletion, supportsCompletionMarker);\n yield* setSynchronizing;\n\n // Foreground resubscriptions on the same live session can resume from\n // the in-memory cursor. A new session reloads the authoritative HTTP\n // snapshot so a valid cursor cannot preserve incomplete cached data.\n const hasAuthoritativeSnapshot = (yield* Ref.get(lastAuthoritativeSession)) === session;\n let canResume = hasAuthoritativeSnapshot;\n let current = yield* SubscriptionRef.get(state);\n if (!hasAuthoritativeSnapshot || Option.isNone(current.snapshot)) {\n const prepared = yield* SubscriptionRef.get(supervisor.prepared).pipe(\n Effect.flatMap(\n Option.match({\n onSome: Effect.succeed,\n onNone: () =>\n SubscriptionRef.changes(supervisor.prepared).pipe(\n Stream.filter(Option.isSome),\n Stream.map((value) => value.value),\n Stream.runHead,\n Effect.map(Option.getOrThrow),\n ),\n }),\n ),\n );\n const httpSnapshot = yield* snapshotLoader.load(prepared);\n if (Option.isSome(httpSnapshot)) {\n yield* applyItems([{ kind: \"snapshot\", snapshot: httpSnapshot.value }]);\n canResume = true;\n current = yield* SubscriptionRef.get(state);\n }\n }\n\n // If the authoritative refresh failed, omit the cached cursor so the\n // socket fallback sends a complete snapshot for this new session.\n if (!canResume || Option.isNone(current.snapshot)) {\n return supportsCompletionMarker ? { requestCompletionMarker: true as const } : {};\n }\n if (!supportsCompletionMarker) {\n // Without a completion marker there is no synchronized signal for a\n // resumed subscription, so report live immediately, like threads.\n yield* SubscriptionRef.update(state, (value) => ({\n ...value,\n status: \"live\" as const,\n error: Option.none(),\n }));\n }\n return {\n afterSequence: current.snapshot.value.snapshotSequence,\n ...(supportsCompletionMarker ? { requestCompletionMarker: true as const } : {}),\n };\n }),", + "checksum": "1f7df9a13c4a468cc9b0e3c470bf1939ab28002c865add78e657c486f890ff2b" }, "reconnect-thread-snapshot-epoch": { "id": "reconnect-thread-snapshot-epoch", @@ -1736,8 +1736,8 @@ "end": 338, "language": "typescript", "label": "Thread snapshot replacement and stale history epoch guard", - "code": " const setSynchronizing = SubscriptionRef.update(state, (current) =>\n current.status === \"deleted\"\n ? current\n : {\n ...current,\n status: \"synchronizing\" as const,\n error: Option.none(),\n },\n );\n const setReady = SubscriptionRef.update(state, (current) =>\n current.status === \"live\" || current.status === \"deleted\"\n ? current\n : {\n ...current,\n status: \"synchronizing\" as const,\n error: Option.none(),\n },\n );\n const setDisconnected = Effect.gen(function* () {\n yield* Ref.set(awaitingCompletion, false);\n // The capability belongs to the session that advertised it. During a\n // reconnect, a new prepared connection can exist before the new session's\n // config arrives; leaving the old value would let loadOlderTurns send\n // window parameters to a server that may not accept them (review\n // finding). makeSubscribeInput re-sets it from the next session's config.\n yield* Ref.set(paginationSupported, false);\n yield* SubscriptionRef.update(state, (current) => ({\n ...current,\n status: current.status === \"deleted\" ? current.status : statusWithoutLiveData(current.data),\n }));\n });\n const setStreamError = (cause: Cause.Cause) =>\n Ref.set(awaitingCompletion, false).pipe(\n Effect.andThen(\n SubscriptionRef.update(state, (current) => ({\n ...current,\n status:\n current.status === \"deleted\" ? current.status : statusWithoutLiveData(current.data),\n error: Option.some(formatThreadError(cause)),\n })),\n ),\n );\n\n const setThread = Effect.fn(\"EnvironmentThreadState.setThread\")(function* (\n thread: OrchestrationThread,\n // \"keep\" preserves the current page state (live events touch only loaded\n // recent turns); a snapshot or merged page passes its own page state.\n page: Option.Option | \"keep\",\n ) {\n const waiting = yield* Ref.get(awaitingCompletion);\n yield* SubscriptionRef.update(state, (current) => ({\n data: Option.some(thread),\n status: waiting ? (\"synchronizing\" as const) : (\"live\" as const),\n error: Option.none(),\n page: page === \"keep\" ? current.page : page,\n }));\n // Active threads can update many times per second and retain large tool\n // payloads. The server remains the source of truth while a turn is active;\n // persist once it settles so cache encoding stays off the streaming path.\n if (shouldPersistThread(thread)) {\n const snapshotSequence = yield* SubscriptionRef.get(lastSequence);\n const currentPage = yield* SubscriptionRef.get(state).pipe(Effect.map((value) => value.page));\n yield* Queue.offer(persistence, {\n snapshotSequence,\n thread,\n // Persist the window boundary with the window's content so a cache\n // restore can keep paging from where the loaded history ends.\n ...Option.match(currentPage, {\n onNone: () => ({}),\n onSome: (value) =>\n ({\n page: {\n beforeCursor: value.beforeCursor,\n hasMore: value.hasMore,\n snapshotSequence,\n },\n }) as const,\n }),\n });\n }\n });\n\n const setDeleted = Effect.fn(\"EnvironmentThreadState.setDeleted\")(function* () {\n yield* Ref.set(awaitingCompletion, false);\n yield* Ref.update(historyEpoch, (epoch) => epoch + 1);\n yield* SubscriptionRef.set(state, {\n data: Option.none(),\n status: \"deleted\",\n error: Option.none(),\n page: Option.none(),\n });\n yield* cache.removeThread(environmentId, threadId).pipe(\n Effect.catch((error) =>\n Effect.logWarning(\"Could not remove the cached thread.\").pipe(\n Effect.annotateLogs({\n environmentId,\n threadId,\n error: error.message,\n }),\n ),\n ),\n );\n });\n\n // Body of applyItem, running under applyLock.\n const applyItemLocked = Effect.fn(\"EnvironmentThreadState.applyItemLocked\")(function* (\n item: OrchestrationThreadStreamItem,\n ) {\n if (item.kind === \"synchronized\") {\n yield* Ref.set(awaitingCompletion, false);\n yield* SubscriptionRef.update(state, (current) =>\n Option.isSome(current.data) && current.status !== \"deleted\"\n ? { ...current, status: \"live\" as const, error: Option.none() }\n : current,\n );\n return;\n }\n\n if (item.kind === \"snapshot\") {\n // A fresh snapshot replaces all loaded history, including older\n // pages: a turn reverted while disconnected would otherwise survive\n // in the preserved history with no event left to remove it. The\n // epoch bump discards any older-page fetch racing this snapshot.\n yield* Ref.update(historyEpoch, (epoch) => epoch + 1);\n yield* SubscriptionRef.set(lastSequence, item.snapshot.snapshotSequence);\n yield* setThread(item.snapshot.thread, pageStateFromSnapshot(item.snapshot.page));\n return;", - "checksum": "a0068277d9fe46a387efaa9dbd9414d8eb43bb6f0d99cc1a8e353269d62d4838" + "code": " // A cached windowed snapshot restores its page cursor so \"load earlier\"\n // works while rendering from cache; a cached full snapshot has no page.\n page: Option.flatMap(cached, (snapshot) => pageStateFromSnapshot(snapshot.page)),\n };\n const state = yield* SubscriptionRef.make(initialState);\n // Seed the resume cursor from the cached snapshot so a warm cache can catch up\n // via `afterSequence` instead of re-downloading the full thread body.\n const initialSequence =\n retained?.sequence ??\n Option.match(cached, { onNone: () => 0, onSome: (snapshot) => snapshot.snapshotSequence });\n const lastSequence = yield* SubscriptionRef.make(initialSequence);\n let committed: ThreadResumeSnapshot = {\n state: initialState,\n sequence: initialSequence,\n persisted: retained?.persisted ?? Option.isSome(cached),\n };\n if (resumeCache?.owner === owner) resumeCache.snapshot = committed;\n const awaitingCompletion = yield* Ref.make(false);\n // Bumped whenever loaded history may have been rewritten out from under an\n // in-flight older-page fetch (snapshot replacement, revert, deletion). A\n // page response captured under an older epoch is discarded, not merged.\n const historyEpoch = yield* Ref.make(0);\n // Serializes stream-item application against older-page staleness checks +\n // merges. Without it, a revert or snapshot processed between loadOlderTurns'\n // epoch check and its merge could still slip resurrected history in.\n const applyLock = yield* Semaphore.make(1);\n // Save only completed data/cursor updates. A canceled scope must not cache\n // a cursor whose event has not reached the data yet.\n const remember = Effect.gen(function* () {\n const current = yield* SubscriptionRef.get(state);\n const sequence = yield* SubscriptionRef.get(lastSequence);\n committed = {\n state: current,\n sequence,\n persisted:\n committed.persisted &&\n matchesThreadSnapshot(\n committed,\n Option.getOrNull(current.data),\n sequence,\n Option.getOrUndefined(current.page),\n ),\n };\n if (resumeCache?.owner === owner) resumeCache.snapshot = committed;\n });\n // Whether the connected server accepts windowed reads; set per subscription\n // from the session config. Gates loadOlderTurns so a reconnect to a\n // pre-pagination server never sends unsupported window parameters.\n const paginationSupported = yield* Ref.make(false);\n // An older page whose thread watermark is ahead of the live state, parked\n // until the subscription catches up (see mergeOlderPage's caller). At most\n // one can exist because loadOlderTurns no-ops while loadingOlder is true.\n const pendingOlderPage = yield* Ref.make<{\n readonly snapshot: OrchestrationThreadDetailSnapshot;\n readonly epoch: number;\n } | null>(null);\n const persistence = yield* Queue.sliding(1);\n\n const persist = Effect.fn(\"EnvironmentThreadState.persist\")(function* (\n snapshot: OrchestrationThreadDetailSnapshot,\n ) {\n if (resumeCache !== undefined && resumeCache.owner !== owner) return;\n if (\n committed.persisted &&\n matchesThreadSnapshot(committed, snapshot.thread, snapshot.snapshotSequence, snapshot.page)\n )\n return;\n yield* cache.saveThread(environmentId, snapshot).pipe(\n Effect.tap(() =>\n Effect.sync(() => {\n if (\n !matchesThreadSnapshot(\n committed,\n snapshot.thread,\n snapshot.snapshotSequence,\n snapshot.page,\n )\n )\n return;\n committed = { ...committed, persisted: true };\n if (resumeCache?.owner === owner) resumeCache.snapshot = committed;\n }),\n ),\n Effect.catch((error) =>\n Effect.logWarning(\"Could not persist the thread cache.\").pipe(\n Effect.annotateLogs({\n environmentId,\n threadId,\n error: error.message,\n }),\n ),\n ),\n );\n });\n\n yield* Stream.fromQueue(persistence).pipe(\n Stream.debounce(\"500 millis\"),\n Stream.runForEach(persist),\n Effect.forkScoped,\n );\n\n const setConnecting = SubscriptionRef.update(state, (current) =>\n current.status === \"deleted\" || Option.isSome(current.error)\n ? current\n : {\n ...current,\n status: \"synchronizing\" as const,\n error: Option.none(),\n },\n );\n const setReady = SubscriptionRef.update(state, (current) =>\n current.status === \"live\" || current.status === \"deleted\" || Option.isSome(current.error)\n ? current\n : {\n ...current,\n status: \"synchronizing\" as const,\n error: Option.none(),\n },\n );\n const setDisconnected = Effect.gen(function* () {\n yield* Ref.set(awaitingCompletion, false);\n // The capability belongs to the session that advertised it. During a\n // reconnect, a new prepared connection can exist before the new session's\n // config arrives; leaving the old value would let loadOlderTurns send\n // window parameters to a server that may not accept them (review\n // finding). makeSubscribeInput re-sets it from the next session's config.\n yield* Ref.set(paginationSupported, false);", + "checksum": "079071f811c25328d8eebce87b8ff25a678c00322ce3f7881bcd2461c4d7f77a" }, "reconnect-mobile-background-demand": { "id": "reconnect-mobile-background-demand", @@ -1766,8 +1766,8 @@ "end": 133, "language": "typescript", "label": "Awareness event filtering and projected-state sanitization", - "code": "export function shouldPublishAgentAwarenessEvent(event: OrchestrationEvent): boolean {\n switch (event.type) {\n case \"thread.message-sent\":\n case \"thread.turn-start-requested\":\n // These events express intent to start work, but the shell still contains\n // the previous turn's terminal state until the provider acknowledges the\n // new turn. Publishing that snapshot can queue a fresh \"Done\" alert just\n // before the real running state arrives. Provider lifecycle events publish\n // the authoritative starting/running state instead.\n return false;\n case \"thread.proposed-plan-upserted\":\n case \"thread.runtime-mode-set\":\n case \"thread.interaction-mode-set\":\n return false;\n case \"thread.activity-appended\":\n return (\n event.payload.activity.kind === \"approval.requested\" ||\n event.payload.activity.kind === \"approval.resolved\" ||\n event.payload.activity.kind === \"provider.approval.respond.failed\" ||\n event.payload.activity.kind === \"user-input.requested\" ||\n event.payload.activity.kind === \"user-input.resolved\" ||\n event.payload.activity.kind === \"runtime.error\"\n );\n default:\n return true;\n }\n}\n\nexport function agentAwarenessPublishIdentity(state: RelayAgentActivityState | null): string {\n if (state === null) {\n return \"null\";\n }\n const { updatedAt: _updatedAt, ...meaningfulState } = state;\n return JSON.stringify(meaningfulState);\n}\n\nexport function isAgentActivityPublishingEnabled(value: string | null): boolean {\n return isAgentActivityPublishingEnabledValue(value);\n}\n\nexport function resolveAgentActivityPublishingStartupState(input: {\n readonly relayConfigured: boolean;\n readonly publishEnabled: boolean;\n}): \"waiting-for-link\" | \"disabled\" | \"enabled\" {\n if (!input.relayConfigured) {\n return \"waiting-for-link\";\n }\n return input.publishEnabled ? \"enabled\" : \"disabled\";\n}\n\nconst RELAY_AGENT_ACTIVITY_DETAIL_MAX_LENGTH = 160;\nconst REDACTED_RELAY_AGENT_FAILURE_DETAIL = \"The agent run failed.\";\n\nexport function sanitizeRelayAgentActivityState(\n state: RelayAgentActivityState | null,\n): RelayAgentActivityState | null {\n if (state === null) {\n return null;\n }\n const { detail: _detail, ...rest } = state;\n const detail = (state.phase === \"failed\" ? REDACTED_RELAY_AGENT_FAILURE_DETAIL : state.detail)\n ?.trim()\n .slice(0, RELAY_AGENT_ACTIVITY_DETAIL_MAX_LENGTH)\n .trim();\n return detail ? { ...rest, detail } : rest;", - "checksum": "cf128f94bb354ee125fea15b45caa64e3fd9014530925e9164bc6530d11d9818" + "code": "export function shouldPublishAgentAwarenessEvent(event: OrchestrationEvent): boolean {\n if (event.metadata.historyImport === true) {\n return false;\n }\n switch (event.type) {\n case \"thread.message-sent\":\n case \"thread.turn-start-requested\":\n // These events express intent to start work, but the shell still contains\n // the previous turn's terminal state until the provider acknowledges the\n // new turn. Publishing that snapshot can queue a fresh \"Done\" alert just\n // before the real running state arrives. Provider lifecycle events publish\n // the authoritative starting/running state instead.\n return false;\n case \"thread.proposed-plan-upserted\":\n case \"thread.runtime-mode-set\":\n case \"thread.interaction-mode-set\":\n return false;\n case \"thread.activity-appended\":\n return (\n event.payload.activity.kind === \"approval.requested\" ||\n event.payload.activity.kind === \"approval.resolved\" ||\n event.payload.activity.kind === \"provider.approval.respond.failed\" ||\n event.payload.activity.kind === \"user-input.requested\" ||\n event.payload.activity.kind === \"user-input.resolved\" ||\n event.payload.activity.kind === \"runtime.error\"\n );\n default:\n return true;\n }\n}\n\nexport function agentAwarenessPublishIdentity(state: RelayAgentActivityState | null): string {\n if (state === null) {\n return \"null\";\n }\n const { updatedAt: _updatedAt, ...meaningfulState } = state;\n return JSON.stringify(meaningfulState);\n}\n\nexport function resolveAgentActivityPublishingStartupState(input: {\n readonly relayConfigured: boolean;\n readonly publishEnabled: boolean;\n}): \"waiting-for-link\" | \"disabled\" | \"enabled\" {\n if (!input.relayConfigured) {\n return \"waiting-for-link\";\n }\n return input.publishEnabled ? \"enabled\" : \"disabled\";\n}\n\nconst RELAY_AGENT_ACTIVITY_DETAIL_MAX_LENGTH = 160;\nconst REDACTED_RELAY_AGENT_FAILURE_DETAIL = \"The agent run failed.\";\n\nexport function sanitizeRelayAgentActivityState(\n state: RelayAgentActivityState | null,\n): RelayAgentActivityState | null {\n if (state === null) {\n return null;\n }\n const { detail: _detail, ...rest } = state;\n const detail = (state.phase === \"failed\" ? REDACTED_RELAY_AGENT_FAILURE_DETAIL : state.detail)\n ?.trim()\n .slice(0, RELAY_AGENT_ACTIVITY_DETAIL_MAX_LENGTH)\n .trim();\n return detail ? { ...rest, detail } : rest;\n}", + "checksum": "ebb8c05e3d4e336509139d793663b2c14faf780c794780a29055d903c99debae" }, "awareness-relay-projected-publish": { "id": "awareness-relay-projected-publish", @@ -1786,8 +1786,8 @@ "end": 326, "language": "typescript", "label": "APNs alert gating and Live Activity delivery cadence", - "code": "function aggregateNeedsAttention(aggregate: RelayAgentActivityAggregateState): boolean {\n return aggregate.activities.some(\n (row) => row.phase === \"waiting_for_approval\" || row.phase === \"waiting_for_input\",\n );\n}\n\nfunction isAttentionPhase(phase: string): boolean {\n return phase === \"waiting_for_approval\" || phase === \"waiting_for_input\";\n}\n\n// Honors the same per-event notification switches the push channel uses; a\n// missing/corrupt preferences blob only disables nothing (matching how the\n// liveActivitiesEnabled check treats it), since every registration writes one.\nfunction alertAllowedForPhase(\n preferences: RelayAgentAwarenessPreferences | null,\n phase: string,\n): boolean {\n if (preferences === null) {\n return true;\n }\n switch (phase) {\n case \"waiting_for_approval\":\n return preferences.notifyOnApproval;\n case \"waiting_for_input\":\n return preferences.notifyOnInput;\n case \"completed\":\n return preferences.notifyOnCompletion;\n case \"failed\":\n return preferences.notifyOnFailure;\n default:\n return false;\n }\n}\n\n// Alert copy for an update whose aggregate contains threads that were NOT in an\n// attention phase in the previously delivered aggregate. A null previous\n// aggregate means there is no known baseline (fresh registration, replay after\n// data loss) — alerting there would buzz on reconnect, not on a transition.\nexport function alertForAttentionTransition(input: {\n readonly previousAggregate: RelayAgentActivityAggregateState | null;\n readonly nextAggregate: RelayAgentActivityAggregateState;\n readonly preferences: RelayAgentAwarenessPreferences | null;\n}): ApnsLiveActivityAlert | null {\n if (input.previousAggregate === null) {\n return null;\n }\n const previouslyAttention = new Set(\n input.previousAggregate.activities\n .filter((row) => isAttentionPhase(row.phase))\n .map((row) => row.threadId),\n );\n const newlyAttention = input.nextAggregate.activities.filter(\n (row) =>\n isAttentionPhase(row.phase) &&\n !previouslyAttention.has(row.threadId) &&\n alertAllowedForPhase(input.preferences, row.phase),\n );\n const first = newlyAttention[0];\n if (!first) {\n return null;\n }\n if (newlyAttention.length === 1) {\n return { title: first.threadTitle, body: `${first.status}: ${first.projectTitle}` };\n }\n return {\n title: `${newlyAttention.length} agents need attention`,\n body: newlyAttention.map((row) => row.threadTitle).join(\", \"),\n };\n}\n\n// Alert copy for an update whose aggregate contains threads that finished\n// (Done/Failed) since the previously delivered aggregate — the mid-flight\n// completion buzz while other agents keep the activity alive. Requires the\n// thread to have been present and non-terminal before, so a baseline-less\n// replay or a row that merely fell off the display cap never rings.\nfunction newlyTerminalRows(\n previousAggregate: RelayAgentActivityAggregateState | null,\n nextAggregate: RelayAgentActivityAggregateState,\n): ReadonlyArray {\n if (previousAggregate === null) {\n return [];\n }\n const previousPhases = new Map(\n previousAggregate.activities.map((row) => [row.threadId, row.phase]),\n );\n return nextAggregate.activities.filter((row) => {\n if (row.phase !== \"completed\" && row.phase !== \"failed\") {\n return false;\n }\n const previousPhase = previousPhases.get(row.threadId);\n return (\n previousPhase !== undefined && previousPhase !== \"completed\" && previousPhase !== \"failed\"\n );\n });\n}\n\nfunction isFreshTerminalRow(\n row: RelayAgentActivityAggregateState[\"activities\"][number],\n nowMs: number,\n): boolean {\n const updatedAtMs = Option.match(DateTime.make(row.updatedAt), {\n onNone: () => null,\n onSome: (dt) => dt.epochMilliseconds,\n });\n return updatedAtMs !== null && nowMs - updatedAtMs <= TERMINAL_NOTIFICATION_FRESHNESS_MS;\n}\n\nexport function alertForNewlyTerminal(input: {\n readonly previousAggregate: RelayAgentActivityAggregateState | null;\n readonly nextAggregate: RelayAgentActivityAggregateState;\n readonly preferences: RelayAgentAwarenessPreferences | null;\n readonly nowMs: number;\n}): ApnsLiveActivityAlert | null {\n const newlyTerminal = newlyTerminalRows(input.previousAggregate, input.nextAggregate).filter(\n (row) =>\n alertAllowedForPhase(input.preferences, row.phase) &&\n // Replays of old aggregates (server restarts, redeliveries) repaint\n // state without ringing; only fresh completions buzz.\n isFreshTerminalRow(row, input.nowMs),\n );\n const first = newlyTerminal[0];\n if (!first) {\n return null;\n }\n if (newlyTerminal.length === 1) {\n return { title: first.threadTitle, body: `${first.status}: ${first.projectTitle}` };\n }\n return {\n title: `${newlyTerminal.length} agents finished`,\n body: newlyTerminal.map((row) => row.threadTitle).join(\", \"),\n };\n}\n\n// Alert copy for an end event carrying a terminal (Done/Failed) aggregate.\nexport function alertForTerminalAggregate(input: {\n readonly aggregate: RelayAgentActivityAggregateState | null;\n readonly preferences: RelayAgentAwarenessPreferences | null;\n}): ApnsLiveActivityAlert | null {\n const row = input.aggregate?.activities[0];\n if (!row || (row.phase !== \"completed\" && row.phase !== \"failed\")) {\n return null;\n }\n if (!alertAllowedForPhase(input.preferences, row.phase)) {\n return null;\n }\n return { title: row.threadTitle, body: `${row.status}: ${row.projectTitle}` };\n}\n\nfunction shouldUpdateLiveActivity(input: {\n readonly previousAggregate: RelayAgentActivityAggregateState | null;\n readonly nextAggregate: RelayAgentActivityAggregateState;\n readonly lastDeliveryAt: string | null;\n readonly nowMs: number;\n}): boolean {\n if (!input.previousAggregate) {\n return true;\n }\n if (JSON.stringify(input.previousAggregate) === JSON.stringify(input.nextAggregate)) {\n return false;\n }\n if (input.previousAggregate.activeCount !== input.nextAggregate.activeCount) {\n return true;\n }\n if (aggregateNeedsAttention(input.nextAggregate)) {\n return true;\n }\n // A thread finishing must never be throttled away: when a completion and a\n // new start land in the same window, activeCount is unchanged and the Done\n // transition (and its alert) would otherwise be suppressed.\n if (newlyTerminalRows(input.previousAggregate, input.nextAggregate).length > 0) {\n return true;\n }\n const lastDeliveryAtMs =\n input.lastDeliveryAt === null\n ? null\n : Option.match(DateTime.make(input.lastDeliveryAt), {\n onNone: () => Number.NaN,\n onSome: (dt) => dt.epochMilliseconds,\n });\n return (\n lastDeliveryAtMs === null ||\n Number.isNaN(lastDeliveryAtMs) ||\n input.nowMs - lastDeliveryAtMs >= MIN_LIVE_ACTIVITY_UPDATE_INTERVAL_MS\n );", - "checksum": "50723205408ac50c442297f150778cbc2e74d222107af1646291a50932203a8e" + "code": ");\nconst decodeSignedApnsDeliveryJob = Schema.decodeUnknownEffect(SignedApnsDeliveryJob);\n\nfunction parseAggregate(value: string | null): RelayAgentActivityAggregateState | null {\n if (!value) {\n return null;\n }\n return Option.getOrNull(decodeRelayAgentActivityAggregateStateJson(value));\n}\n\nfunction parsePreferences(value: string): RelayAgentAwarenessPreferences | null {\n return Option.getOrNull(decodeRelayAgentAwarenessPreferencesJson(value));\n}\n\nfunction aggregateNeedsAttention(aggregate: RelayAgentActivityAggregateState): boolean {\n return aggregate.activities.some(\n (row) => row.phase === \"waiting_for_approval\" || row.phase === \"waiting_for_input\",\n );\n}\n\nfunction shouldUpdateLiveActivity(input: {\n readonly previousAggregate: RelayAgentActivityAggregateState | null;\n readonly nextAggregate: RelayAgentActivityAggregateState;\n readonly lastDeliveryAt: string | null;\n readonly nowMs: number;\n}): boolean {\n if (!input.previousAggregate) {\n return true;\n }\n if (JSON.stringify(input.previousAggregate) === JSON.stringify(input.nextAggregate)) {\n return false;\n }\n if (input.previousAggregate.activeCount !== input.nextAggregate.activeCount) {\n return true;\n }\n if (aggregateNeedsAttention(input.nextAggregate)) {\n return true;\n }\n // A thread finishing must never be throttled away: when a completion and a\n // new start land in the same window, activeCount is unchanged and the Done\n // transition (and its alert) would otherwise be suppressed.\n if (newlyTerminalRows(input.previousAggregate, input.nextAggregate, true).length > 0) {\n return true;\n }\n const lastDeliveryAtMs =\n input.lastDeliveryAt === null\n ? null\n : Option.match(DateTime.make(input.lastDeliveryAt), {\n onNone: () => Number.NaN,\n onSome: (dt) => dt.epochMilliseconds,\n });\n return (\n lastDeliveryAtMs === null ||\n Number.isNaN(lastDeliveryAtMs) ||\n input.nowMs - lastDeliveryAtMs >= MIN_LIVE_ACTIVITY_UPDATE_INTERVAL_MS\n );\n}\n\n// Completions replayed long after the fact (server restarts republish every\n// recently-finished thread) must not ring the device again.\n\nfunction notificationForAggregate(input: {\n readonly target: LiveActivities.TargetRow;\n readonly aggregate: RelayAgentActivityAggregateState | null;\n readonly nowMs: number;\n}): ApnsNotificationPayload | null {\n if (!input.target.push_token || input.aggregate === null) {\n return null;\n }\n const preferences = parsePreferences(input.target.preferences_json);\n if (!preferences?.notificationsEnabled) {\n return null;\n }\n const activity = input.aggregate.activities[0];\n if (!activity) {\n return null;\n }\n if (!shouldAlertForActivity({ ...activity, preferences, nowMs: input.nowMs })) return null;\n return notificationForActivity(activity);\n}\n\n// \"suppressed\" means a Live Activity owns this state but no update is due\n// (unchanged or throttled); callers must not fall back to an alert push, or\n// every republish of a waiting aggregate would ring the device.\nfunction chooseLiveActivityDelivery(input: {\n readonly target: LiveActivities.TargetRow;\n readonly aggregate: RelayAgentActivityAggregateState | null;\n readonly nowMs: number;\n readonly replay?: boolean;\n}): ChosenLiveActivityDelivery | \"suppressed\" | null {\n const preferences = parsePreferences(input.target.preferences_json);\n if (preferences?.liveActivitiesEnabled === false) {\n return input.target.activity_push_token\n ? {\n kind: \"live_activity_end\",\n token: input.target.activity_push_token,\n aggregate: null,\n alert: null,\n }\n : null;\n }\n // Activities are started by the app in the foreground, never remotely.\n // Without a registered token there is nothing addressable; attention\n // transitions fall back to the push notification channel until the user\n // next arms the card from the app.\n if (!input.target.activity_push_token) {\n return null;\n }\n // An armed card always shows content: live agents, or recently finished\n // ones (the publisher keeps Done/Failed rows in the aggregate for a\n // while). A null aggregate means there is truly nothing left to show, so\n // the card ends — arming is cheap now that the app re-arms on any open\n // with content.\n if (input.aggregate === null) {\n // Except right after arming: the app arms the card the moment the user\n // starts work, and the token registration's replay can land before the\n // environment's first publish for the brand-new thread. Ending here\n // would retire the token and orphan the card at its seed content, so a\n // freshly armed card keeps its seed until real state arrives.\n const armedAtMs = Option.match(\n input.target.remote_started_at === null\n ? Option.none()\n : DateTime.make(input.target.remote_started_at),\n { onNone: () => null, onSome: (dt) => dt.epochMilliseconds },\n );\n if (armedAtMs !== null && input.nowMs - armedAtMs < FRESHLY_ARMED_GRACE_MS) {\n return null;\n }\n return {\n kind: \"live_activity_end\",\n token: input.target.activity_push_token,\n aggregate: null,\n alert: null,\n };\n }\n const nextAggregate = input.aggregate;\n const previousAggregate = parseAggregate(input.target.last_aggregate_json);\n return shouldUpdateLiveActivity({\n previousAggregate,\n nextAggregate,\n lastDeliveryAt: input.target.last_live_activity_delivery_at,\n nowMs: input.nowMs,\n })\n ? {\n kind: \"live_activity_update\",\n token: input.target.activity_push_token,\n aggregate: nextAggregate,\n alert: input.replay\n ? null\n : (alertForAttentionTransition({\n previousAggregate,\n nextAggregate,\n preferences,\n }) ??\n alertForNewlyTerminal({\n previousAggregate,\n nextAggregate,\n preferences,\n nowMs: input.nowMs,\n includeUnobserved: true,\n })),\n }\n : \"suppressed\";\n}\n\nfunction chooseDelivery(input: {\n readonly target: LiveActivities.TargetRow;\n readonly aggregate: RelayAgentActivityAggregateState | null;\n readonly nowMs: number;\n readonly replay?: boolean;\n}): ChosenDelivery | null {\n const liveActivityDelivery = chooseLiveActivityDelivery(input);\n if (liveActivityDelivery === \"suppressed\") {\n return null;\n }\n if (liveActivityDelivery) {\n return liveActivityDelivery;\n }\n const notification = input.replay ? null : notificationForAggregate(input);\n return notification && input.target.push_token\n ? {\n kind: \"push_notification\",\n token: input.target.push_token,\n notification,", + "checksum": "83bba199b74af010e4a576253c8ba1828ae5cedd7b9ac83ec7921e86a9d6843c" }, "awareness-mobile-registration-gate": { "id": "awareness-mobile-registration-gate", @@ -1796,8 +1796,8 @@ "end": 90, "language": "typescript", "label": "Relay-accepted mobile registration and replay repair boundary", - "code": "const environmentConnections = new Map();\nconst activityPushTokenListeners = new WeakSet>();\n// Activity tokens the relay recently accepted, by acceptance time. The refresh\n// runs on sign-in, every app foreground, and every environment-connection\n// update, which arrive in bursts and spammed identical registrations. But the\n// registration is not a pure no-op: the relay replays the current aggregate to\n// this device on every accepted registration, and that replay is the\n// foreground reconciliation that repairs drifted or orphaned activities. So\n// dedupe only within a short window — bursts collapse to one request, while a\n// foreground after real time away still triggers a replay. Cleared on\n// sign-out/identity change alongside the device registration state.\nconst ACTIVITY_TOKEN_REREGISTER_INTERVAL_MS = 60_000;\nconst registeredActivityPushTokens = new Map();\nlet pushTokenSubscription: { remove: () => void } | null = null;\nlet appStateSubscription: { remove: () => void } | null = null;\n\n// Whether the relay has actually accepted this device's registration. The\n// notification/Live Activity settings toggles must reflect this rather than\n// only local iOS permission or saved preferences: if the registration request\n// never succeeded, the device cannot receive anything, so the switches must\n// not read as enabled.\nexport type AgentAwarenessRegistrationStatus = \"unknown\" | \"pending\" | \"registered\" | \"failed\";", - "checksum": "20d6bf8e2a049ae450c3958d6c8e90cdbcaff94bb0cc90602bed5aeee8b0b3a1" + "code": " return `Agent awareness operation ${this.operation} failed.`;\n }\n}\n\nconst environmentConnections = new Map();\nconst activityPushTokenListeners = new WeakSet>();\n// Activity tokens the relay recently accepted, by acceptance time. The refresh\n// runs on sign-in, every app foreground, and every environment-connection\n// update, which arrive in bursts and spammed identical registrations. But the\n// registration is not a pure no-op: the relay replays the current aggregate to\n// this device on every accepted registration, and that replay is the\n// foreground reconciliation that repairs drifted or orphaned activities. So\n// dedupe only within a short window — bursts collapse to one request, while a\n// foreground after real time away still triggers a replay. Cleared on\n// sign-out/identity change alongside the device registration state.\nconst ACTIVITY_TOKEN_REREGISTER_INTERVAL_MS = 60_000;\nconst registeredActivityPushTokens = new Map();\nlet androidDeviceReplayedAt: number | null = null;\nlet pushTokenSubscription: { remove: () => void } | null = null;\nlet appStateSubscription: { remove: () => void } | null = null;\n\n// Whether the relay has actually accepted this device's registration. The", + "checksum": "2e138ffac309a3785b406a2549b8425dbafb0c5f817a7f51e7d387a3a806802c" }, "awareness-notification-navigation": { "id": "awareness-notification-navigation", @@ -1816,8 +1816,8 @@ "end": 92, "language": "typescript", "label": "Environment version and optional capability descriptor", - "code": "/** How a server can replace itself with another version when asked over RPC.\n New servers only advertise the stable launcher-backed \"boot-service\" path;\n \"respawn\" remains decodable for compatibility with older servers. */\nexport const ServerSelfUpdateMethod = Schema.Literals([\"boot-service\", \"respawn\"]);\nexport type ServerSelfUpdateMethod = typeof ServerSelfUpdateMethod.Type;\n\n/** What update path a client should offer for a server: one of the RPC\n self-update methods above, or \"desktop-managed\" when the backend's\n version belongs to the T3 Code desktop app supervising it — updating the\n app on that machine is the only way to update the server. */\nexport const ServerSelfUpdateCapability = Schema.Literals([\n \"boot-service\",\n \"respawn\",\n \"desktop-managed\",\n]);\nexport type ServerSelfUpdateCapability = typeof ServerSelfUpdateCapability.Type;\n\nexport const ExecutionEnvironmentCapabilities = Schema.Struct({\n repositoryIdentity: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),\n connectionProbe: Schema.optionalKey(Schema.Boolean),\n /** Server exposes the pull-request list, detail, activity, diff, and mutation APIs. Absent on\n servers from before the pull-request workspace shipped, so clients must not probe them. */\n pullRequests: Schema.optionalKey(Schema.Boolean),\n /** Server understands thread.settle / thread.unsettle commands. Absent on\n pre-settlement servers, so clients treat missing as unsupported and\n never send the commands under version skew. */\n threadSettlement: Schema.optionalKey(Schema.Boolean),\n /** Server understands thread.snooze / thread.unsnooze commands. Same\n version-skew contract as threadSettlement. */\n threadSnooze: Schema.optionalKey(Schema.Boolean),\n /** Server understands thread.pin / thread.unpin commands. Same\n version-skew contract as threadSettlement. */\n threadPinning: Schema.optionalKey(Schema.Boolean),\n /** Server understands thread.pin.reorder (and orderKey on thread.pin).\n Same version-skew contract as threadSettlement. */\n threadPinReorder: Schema.optionalKey(Schema.Boolean),\n /** Server understands regenerateTitle on thread.meta.update. Absent on\n older servers, so clients hide the action instead of sending it. */\n threadTitleRegeneration: Schema.optionalKey(Schema.Boolean),\n /** The update path clients should offer for this server. Absent on\n servers that must be relaunched manually (dev checkouts, Windows\n foreground runs, pre-update servers). */\n serverSelfUpdate: Schema.optionalKey(ServerSelfUpdateCapability),\n /** Server can stream self-update progress before acknowledging the\n restart. Clients fall back to server.updateServer when absent. */\n serverSelfUpdateProgress: Schema.optionalKey(Schema.Boolean),\n /** Agent-activity publishes (push notifications and Live Activities)\n currently leave this environment: the publish opt-in is enabled and the\n relay link credentials exist. Clients skip seeding a Live Activity when\n this is false — no update would ever repaint it. Absent on older\n servers, which may still publish, so only an explicit false skips. */\n agentActivityPublishing: Schema.optionalKey(Schema.Boolean),\n});\nexport type ExecutionEnvironmentCapabilities = typeof ExecutionEnvironmentCapabilities.Type;\n\nexport const ExecutionEnvironmentDescriptor = Schema.Struct({\n environmentId: EnvironmentId,\n label: TrimmedNonEmptyString,\n platform: ExecutionEnvironmentPlatform,\n serverVersion: TrimmedNonEmptyString,\n capabilities: ExecutionEnvironmentCapabilities,\n});", - "checksum": "5dbb79216344a256d038ab70c0befe0773721b63fa7d60caef52901ed5fa7cca" + "code": " \"linux\",\n \"desktop\",\n \"laptop\",\n \"mac-mini\",\n \"mac-studio\",\n] as const;\nexport const EnvironmentMachineKind = Schema.Literals(ENVIRONMENT_MACHINE_KINDS);\nexport type EnvironmentMachineKind = typeof EnvironmentMachineKind.Type;\nexport const isEnvironmentMachineKind = Schema.is(EnvironmentMachineKind);\n\nexport const ExecutionEnvironmentPlatform = Schema.Struct({\n os: ExecutionEnvironmentPlatformOs,\n arch: ExecutionEnvironmentPlatformArch,\n /** Hardware shape detected at startup. Absent when the host gives no usable\n signal (containers, Windows, unknown DMI), on servers that predate it, or\n when a newer server names a kind this build cannot draw. */\n machine: ForwardCompatibleOptional(EnvironmentMachineKind),\n});\n\n/**\n * Where a new thread runs: the project's current checkout (\"local\") or a\n * fresh git worktree (\"worktree\"). Lives here (not settings.ts) so\n * orchestration contracts can reference it without an import cycle.\n */\nexport const ThreadEnvMode = Schema.Literals([\"local\", \"worktree\"]);\nexport type ThreadEnvMode = typeof ThreadEnvMode.Type;\nexport type ExecutionEnvironmentPlatform = typeof ExecutionEnvironmentPlatform.Type;\n\n/** How a server can replace itself with another version when asked over RPC.\n New servers only advertise the stable launcher-backed \"boot-service\" path;\n \"respawn\" remains decodable for compatibility with older servers.\n \"desktop-app\" means the supervising desktop app updated and relaunched\n itself, bringing the server back with it. */\nexport const ServerSelfUpdateMethod = Schema.Literals([\"boot-service\", \"respawn\", \"desktop-app\"]);\nexport type ServerSelfUpdateMethod = typeof ServerSelfUpdateMethod.Type;\n\n/** What update path a client should offer for a server: one of the RPC\n self-update methods above, or \"desktop-managed\" when the backend's\n version belongs to the T3 Code desktop app supervising it — updating the\n app on that machine is the only way to update the server. */\nexport const ServerSelfUpdateCapability = Schema.Literals([\n \"boot-service\",\n \"respawn\",\n \"desktop-managed\",\n]);\nexport type ServerSelfUpdateCapability = typeof ServerSelfUpdateCapability.Type;\n\nexport const ExecutionEnvironmentCapabilities = Schema.Struct({\n repositoryIdentity: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),\n connectionProbe: Schema.optionalKey(Schema.Boolean),\n /** Missing on older servers, which still accept inline image attachments. */\n attachmentUploads: Schema.optionalKey(Schema.Boolean),\n /** Uploaded files may accompany question answers. */\n questionAttachments: Schema.optionalKey(Schema.Boolean),\n /** Missing on servers that only accept image attachments. */\n fileAttachments: Schema.optionalKey(\n Schema.Struct({\n maxUploadBytes: Schema.Int.check(Schema.isGreaterThanOrEqualTo(1)),\n }),\n ),\n /** Server exposes the pull-request list, detail, activity, diff, and mutation APIs. Absent on\n servers from before the pull-request workspace shipped, so clients must not probe them. */", + "checksum": "5b0edb4b090b66cd680619cf5b71e4c88eef7577bf2771ad795f580fb85cdcbe" }, "reconnect-version-skew-guidance": { "id": "reconnect-version-skew-guidance", @@ -1826,8 +1826,8 @@ "end": 130, "language": "typescript", "label": "Exact client-server version comparison and update guidance", - "code": "function normalizeVersion(version: string | null | undefined): string | null {\n const trimmed = version?.trim();\n return trimmed && trimmed.length > 0 ? trimmed : null;\n}\n\nexport function resolveVersionMismatch(\n serverVersion: string | null | undefined,\n): VersionMismatch | null {\n const normalizedClientVersion = normalizeVersion(APP_VERSION);\n const normalizedServerVersion = normalizeVersion(serverVersion);\n if (\n !normalizedClientVersion ||\n !normalizedServerVersion ||\n normalizedClientVersion === normalizedServerVersion\n ) {\n return null;\n }\n\n return {\n clientVersion: normalizedClientVersion,\n serverVersion: normalizedServerVersion,\n hint: \"Version mismatch. Try syncing the client and server to the same T3 Code version.\",\n };\n}\n\nexport function resolveServerConfigVersionMismatch(\n serverConfig: Pick | null | undefined,\n): VersionMismatch | null {\n return resolveVersionMismatch(serverConfig?.environment.serverVersion);\n}\n\n/** The update path the connected server offers, or null when it only\n supports a manual relaunch (older servers, dev checkouts, Windows). */\nexport function resolveServerSelfUpdateCapability(\n serverConfig: Pick | null | undefined,\n): ServerSelfUpdateCapability | null {\n return serverConfig?.environment.capabilities.serverSelfUpdate ?? null;\n}\n\n/** The command to hand users whose server cannot update itself. */\nexport function manualServerUpdateCommand(targetVersion: string): string {\n return `npx t3@${targetVersion}`;\n}\n\n/** One sentence telling the user how to resolve version skew for a server,\n matched to the update path it offers. */\nexport function serverUpdateGuidance(\n capability: ServerSelfUpdateCapability | null,\n serverLabel: string,\n): string {\n switch (capability) {\n case \"boot-service\":\n case \"respawn\":\n return `Update the ${serverLabel} so they stay in sync.`;\n case \"desktop-managed\":\n return `Update the desktop app that runs the ${serverLabel}.`;\n default:\n return `Relaunch the ${serverLabel} with the copied command to sync them.`;\n }\n}\n\nexport function buildVersionMismatchDismissalKey(\n environmentId: EnvironmentId,\n mismatch: Pick,\n): string {\n return `${environmentId}:${mismatch.clientVersion}:${mismatch.serverVersion}`;\n}\n\nfunction readVersionMismatchDismissals(): VersionMismatchDismissals {\n try {\n return (\n getLocalStorageItem(\n VERSION_MISMATCH_DISMISSALS_STORAGE_KEY,\n VersionMismatchDismissalsSchema,\n ) ?? { keys: [] }\n );\n } catch (error) {\n console.error(\"Could not read version-mismatch dismissals.\", error);\n return { keys: [] };\n }\n}\n\nfunction writeVersionMismatchDismissals(document: VersionMismatchDismissals): void {\n try {\n setLocalStorageItem(\n VERSION_MISMATCH_DISMISSALS_STORAGE_KEY,\n document,\n VersionMismatchDismissalsSchema,\n );\n } catch (error) {\n console.error(\"Could not persist version-mismatch dismissals.\", error);\n }\n}\n\nexport function isVersionMismatchDismissed(dismissalKey: string | null | undefined): boolean {\n if (!dismissalKey) {\n return false;\n }\n return readVersionMismatchDismissals().keys.includes(dismissalKey);\n}\n\nexport function dismissVersionMismatch(dismissalKey: string | null | undefined): void {\n if (!dismissalKey) {\n return;\n }\n const document = readVersionMismatchDismissals();\n if (document.keys.includes(dismissalKey)) {\n return;\n }\n writeVersionMismatchDismissals({", - "checksum": "493e23986ed26727ea268f4e5ebc7a939ca984654515b3fccfad44cc899386f0" + "code": "export function isServerUpdateFailureDismissed(state: ServerUpdateState): boolean {\n return state.status === \"failed\" && dismissedServerUpdateFailures.has(state);\n}\n\nexport function dismissServerUpdateFailure(state: ServerUpdateState): void {\n if (state.status === \"failed\") dismissedServerUpdateFailures.add(state);\n}\n\nconst VersionMismatchDismissalsSchema = Schema.Struct({\n keys: Schema.Array(Schema.String),\n});\n\ntype VersionMismatchDismissals = typeof VersionMismatchDismissalsSchema.Type;\n\nfunction normalizeVersion(version: string | null | undefined): string | null {\n const trimmed = version?.trim();\n return trimmed && trimmed.length > 0 ? trimmed : null;\n}\n\n/** Core `major.minor.patch`, dropping any prerelease or build suffix. */\nfunction versionCore(version: string): string {\n return version.replace(/[-+].*$/, \"\");\n}\n\n/**\n * The skew a user can act on: the connected server runs an older T3 Code than\n * this client, so the server is the side that needs updating.\n *\n * Two nightly builds compare their full versions, including the date and run.\n * Other combinations compare their core `major.minor.patch` only, so a stable\n * build and a nightly build with the same core do not cause an update warning.\n * A server ahead of the client does not need an update. Versions that do not\n * parse as semver fall back to plain string inequality.\n */\nexport function resolveVersionMismatch(\n serverVersion: string | null | undefined,\n): VersionMismatch | null {\n const normalizedClientVersion = normalizeVersion(APP_VERSION);\n const normalizedServerVersion = normalizeVersion(serverVersion);\n if (!normalizedClientVersion || !normalizedServerVersion) {\n return null;\n }\n\n const clientCore = versionCore(normalizedClientVersion);\n const serverCore = versionCore(normalizedServerVersion);\n const compareNightlyBuilds =\n parseSemver(normalizedClientVersion)?.prerelease[0] === \"nightly\" &&\n parseSemver(normalizedServerVersion)?.prerelease[0] === \"nightly\";\n const serverIsBehind =\n parseSemver(clientCore) && parseSemver(serverCore)\n ? compareSemverVersions(\n compareNightlyBuilds ? normalizedServerVersion : serverCore,\n compareNightlyBuilds ? normalizedClientVersion : clientCore,\n ) < 0\n : normalizedServerVersion !== normalizedClientVersion;\n if (!serverIsBehind) {\n return null;\n }\n\n return {\n clientVersion: normalizedClientVersion,\n serverVersion: normalizedServerVersion,\n hint: \"Version mismatch. Try syncing the client and server to the same T3 Code version.\",\n };\n}\n\nexport function resolveServerConfigVersionMismatch(\n serverConfig: Pick | null | undefined,\n): VersionMismatch | null {\n return resolveVersionMismatch(serverConfig?.environment.serverVersion);\n}\n\n/** The update path the connected server offers, or null when it only\n supports a manual relaunch (older servers, dev checkouts, Windows). */\nexport function resolveServerSelfUpdateCapability(\n serverConfig: Pick | null | undefined,\n): ServerSelfUpdateCapability | null {\n return serverConfig?.environment.capabilities.serverSelfUpdate ?? null;\n}\n\n/** True when the desktop app supervising this server can be told to update\n itself over RPC. Older desktop servers only get the manual instruction. */\nexport function supportsDesktopAppUpdate(\n serverConfig: Pick | null | undefined,\n): boolean {\n return serverConfig?.environment.capabilities.desktopAppUpdate === true;\n}\n\n/** True when the connected server can recover opted-in running turns after\n its self-update restart. */\nexport function supportsServerUpdateThreadContinuation(\n serverConfig: Pick | null | undefined,\n): boolean {\n return serverConfig?.environment.capabilities.serverUpdateThreadContinuation === true;\n}\n\n/** The command to hand users whose server cannot update itself. */\nexport function manualServerUpdateCommand(targetVersion: string): string {\n return `npx t3@${targetVersion}`;\n}\n\nexport function serverUpdateGuidance(capability: ServerSelfUpdateCapability): string {\n return capability === \"desktop-managed\" ? \"Update the desktop app\" : \"Update to stay in sync\";\n}\n\nexport function buildVersionMismatchDismissalKey(\n environmentId: EnvironmentId,\n mismatch: Pick,\n): string {\n return `${environmentId}:${mismatch.clientVersion}:${mismatch.serverVersion}`;", + "checksum": "e5bab2c0bdc42d57270799bb648e43ab4beb06317186925b1f47938c0cc64b85" }, "reconnect-exact-version-update": { "id": "reconnect-exact-version-update", @@ -1836,7 +1836,7 @@ "end": 190, "language": "typescript", "label": "Exact-version staging, preflight, and launcher activation handoff", - "code": "export function resolveServerSelfUpdateCapability(input: {\n readonly desktopManaged: boolean;\n readonly launcherManaged: boolean;\n}): ServerSelfUpdateCapability | null {\n if (input.desktopManaged) return \"desktop-managed\" as const;\n return input.launcherManaged ? (\"boot-service\" as const) : null;\n}\n\nexport class ServerSelfUpdate extends Context.Service<\n ServerSelfUpdate,\n {\n readonly update: (\n input: ServerSelfUpdateInput,\n reportProgress?: (stage: ServerSelfUpdateProgressStage) => Effect.Effect,\n ) => Effect.Effect;\n }\n>()(\"t3/cloud/selfUpdate/ServerSelfUpdate\") {}\n\nexport const make = Effect.fn(\"cloud.server_self_update.make\")(function* () {\n const serverConfig = yield* ServerConfig.ServerConfig;\n const launcher = yield* ServiceLauncherClient.ServiceLauncherClient;\n const runner = yield* ProcessRunner.ProcessRunner;\n const fs = yield* FileSystem.FileSystem;\n const path = yield* Path.Path;\n const execPath = yield* HostProcessExecutablePath;\n const inFlight = yield* Ref.make(false);\n\n const capability: ServerSelfUpdateCapability | null =\n serverConfig.mode === \"desktop\" ? \"desktop-managed\" : launcher.managed ? \"boot-service\" : null;\n const failWith = (reason: string, cause?: unknown) =>\n cause === undefined\n ? new ServerSelfUpdateError({ reason })\n : new ServerSelfUpdateError({ reason, cause });\n\n const update: ServerSelfUpdate[\"Service\"][\"update\"] = Effect.fn(\n \"cloud.server_self_update.update\",\n )(function* (input, reportProgress = () => Effect.void) {\n if (capability === \"desktop-managed\") {\n return yield* failWith(\n \"This server is managed by the T3 Code desktop app on its machine; update the desktop app to update it.\",\n );\n }\n if (capability === null) {\n return yield* failWith(\n \"Remote updates require the T3 Code background service. Run `t3 service install` on the server machine.\",\n );\n }\n\n const targetVersion = input.targetVersion.trim();\n if (!isExactServiceVersion(targetVersion)) {\n return yield* failWith(`'${targetVersion}' is not an exact t3 version.`);\n }\n if (yield* Ref.getAndSet(inFlight, true)) {\n return yield* failWith(\"A server update is already in progress.\");\n }\n\n return yield* Effect.gen(function* () {\n yield* reportProgress(\"downloading\");\n const paths = yield* ensurePinnedRuntimeInstalled({\n baseDir: serverConfig.baseDir,\n version: targetVersion,\n fs,\n path,\n runner,\n validate: (runtime) =>\n runner\n .run({\n command: execPath,\n args: [\n runtime.entryPath,\n \"__service-preflight\",\n \"--database-path\",\n serverConfig.dbPath,\n \"--launcher-protocol\",\n String(SERVICE_LAUNCHER_PROTOCOL),\n ],\n timeout: PREFLIGHT_TIMEOUT,\n })\n .pipe(\n Effect.mapError(\n (cause) =>\n new PinnedRuntimeInstallError({\n step: \"running the staged service preflight\",\n cause,\n }),\n ),\n Effect.flatMap(\n (\n result,\n ): Effect.Effect<\n void,\n PinnedRuntimeInstallError | PinnedRuntimePreflightBlockedError\n > => {\n if (result.code !== 0) {\n return Effect.fail(\n new PinnedRuntimeInstallError({\n step: \"running the staged service preflight\",\n exitCode: Number(result.code),\n stdoutLength: result.stdout.length,\n stderrLength: result.stderr.length,\n }),\n );\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(result.stdout.trim());\n } catch (cause) {\n return Effect.fail(\n new PinnedRuntimeInstallError({\n step: \"decoding the staged service preflight\",\n cause,\n }),\n );\n }\n const preflight = decodeServicePreflightResult(parsed);\n if (preflight === undefined || preflight.version !== targetVersion) {\n return Effect.fail(\n new PinnedRuntimeInstallError({\n step: \"verifying the staged service preflight\",\n }),\n );\n }\n return preflight.status === \"ready\"\n ? Effect.void\n : Effect.fail(\n new PinnedRuntimePreflightBlockedError({\n version: targetVersion,\n reason: preflight.reason,\n }),\n );\n },\n ),\n ),\n }).pipe(\n Effect.mapError((error) =>\n error._tag === \"PinnedRuntimePreflightBlockedError\"\n ? failWith(error.reason, error)\n : failWith(`Could not prepare t3@${targetVersion}.`, error),\n ),\n );\n\n yield* reportProgress(\"installing\");\n const updateId = yield* launcher\n .requestUpdate({ targetVersion, dbPath: serverConfig.dbPath })\n .pipe(\n Effect.mapError((error) =>\n failWith(\n error._tag === \"ServiceLauncherRejectedError\"\n ? error.reason\n : \"Could not ask the service launcher to activate the prepared update.\",\n error,\n ),\n ),\n );\n\n yield* Effect.logInfo(\"Server update prepared; handing off to the service launcher.\", {\n updateId,\n targetVersion,\n runtimePath: paths.entryPath,\n });\n return { targetVersion, method: \"boot-service\" as const, updateId };", - "checksum": "1cde992413bb70e3aa3870a47b638f195f71bea15ad23b31c52f713e142bcc06" + "code": "import { isExactServiceVersion, SERVICE_LAUNCHER_PROTOCOL } from \"./serviceProtocol.ts\";\n\nconst PREFLIGHT_TIMEOUT = Duration.seconds(30);\n\nexport function resolveServerSelfUpdateCapability(input: {\n readonly desktopManaged: boolean;\n readonly launcherManaged: boolean;\n}): ServerSelfUpdateCapability | null {\n if (input.desktopManaged) return \"desktop-managed\" as const;\n return input.launcherManaged ? (\"boot-service\" as const) : null;\n}\n\nexport class ServerSelfUpdate extends Context.Service<\n ServerSelfUpdate,\n {\n readonly update: (\n input: ServerSelfUpdateInput,\n reportProgress?: (\n stage: ServerSelfUpdateProgressStage,\n ) => Effect.Effect,\n onHandoffAccepted?: () => Effect.Effect,\n ) => Effect.Effect;\n readonly commitDesktopUpdate: (\n requestId: string,\n onHandoffAccepted?: () => Effect.Effect,\n ) => Effect.Effect;\n }\n>()(\"t3/cloud/selfUpdate/ServerSelfUpdate\") {}\n\nexport const withRunningThreadContinuation = Effect.fn(\n \"cloud.server_self_update.withRunningThreadContinuation\",\n)(function* (input: {\n readonly mode: ServerConfig.RuntimeMode;\n readonly selfUpdate: ServerSelfUpdate[\"Service\"];\n readonly prepare: Effect.Effect, ServerSelfUpdateError>;\n readonly clear: (\n threadIds: ReadonlyArray,\n ) => Effect.Effect;\n}) {\n const desktopContinuationTokens = yield* Ref.make(HashSet.empty());\n const clearOnError = (\n effect: Effect.Effect,\n threadIds: () => ReadonlyArray,\n handoffAccepted: () => boolean,\n ): Effect.Effect =>\n effect.pipe(\n Effect.catchCause((cause) =>\n (handoffAccepted() && Cause.hasInterruptsOnly(cause)\n ? Effect.void\n : input.clear(threadIds())\n ).pipe(Effect.andThen(Effect.failCause(cause))),\n ),\n );\n\n const update: ServerSelfUpdate[\"Service\"][\"update\"] = (\n request,\n reportProgress = () => Effect.void,\n ) => {\n let prepared = false;\n let handoffAccepted = false;\n let continuationThreadIds: ReadonlyArray = [];\n return clearOnError(\n input.selfUpdate\n .update(\n request,\n (stage) =>\n (request.continueRunningThreads === true &&\n input.mode !== \"desktop\" &&\n stage === \"installing\" &&\n !prepared\n ? input.prepare.pipe(\n Effect.tap((threadIds) =>\n Effect.sync(() => {\n prepared = true;\n continuationThreadIds = threadIds;\n }),\n ),\n Effect.asVoid,\n )\n : Effect.void\n ).pipe(Effect.andThen(reportProgress(stage))),\n () =>\n Effect.sync(() => {\n handoffAccepted = true;\n }),\n )\n .pipe(\n Effect.tap((result) => {\n if (\n result.method === \"desktop-app\" &&\n result.desktopUpdateToken !== undefined &&\n request.continueRunningThreads === true\n ) {\n return Ref.update(desktopContinuationTokens, HashSet.add(result.desktopUpdateToken));\n }\n return Effect.void;\n }),\n ),\n () => continuationThreadIds,\n () => handoffAccepted,\n );\n };\n\n return ServerSelfUpdate.of({\n update,\n commitDesktopUpdate: (requestId) =>\n Effect.gen(function* () {\n const shouldContinue = yield* Ref.modify(desktopContinuationTokens, (tokens) => [\n HashSet.has(tokens, requestId),\n HashSet.remove(tokens, requestId),\n ]);\n let handoffAccepted = false;\n let continuationThreadIds: ReadonlyArray = [];\n return yield* clearOnError(\n Effect.gen(function* () {\n continuationThreadIds = shouldContinue ? yield* input.prepare : [];\n return yield* input.selfUpdate.commitDesktopUpdate(requestId, () =>\n Effect.sync(() => {\n handoffAccepted = true;\n }),\n );\n }),\n () => continuationThreadIds,\n () => handoffAccepted,\n ).pipe(\n Effect.catchCause((cause) =>\n (shouldContinue && !handoffAccepted\n ? Ref.update(desktopContinuationTokens, HashSet.add(requestId))\n : Effect.void\n ).pipe(Effect.andThen(Effect.failCause(cause))),\n ),\n );\n }),\n });\n});\n\nexport const make = Effect.fn(\"cloud.server_self_update.make\")(function* () {\n const serverConfig = yield* ServerConfig.ServerConfig;\n const desktopAppUpdate = yield* DesktopAppUpdate.DesktopAppUpdate;\n const launcher = yield* ServiceLauncherClient.ServiceLauncherClient;\n const runner = yield* ProcessRunner.ProcessRunner;\n const fs = yield* FileSystem.FileSystem;\n const path = yield* Path.Path;\n const execPath = yield* HostProcessExecutablePath;\n const inFlight = yield* Ref.make(false);\n\n const capability: ServerSelfUpdateCapability | null =\n serverConfig.mode === \"desktop\" ? \"desktop-managed\" : launcher.managed ? \"boot-service\" : null;\n const failWith = (reason: string, cause?: unknown) =>\n cause === undefined\n ? new ServerSelfUpdateError({ reason })\n : new ServerSelfUpdateError({ reason, cause });\n\n const update: ServerSelfUpdate[\"Service\"][\"update\"] = Effect.fn(\n \"cloud.server_self_update.update\",\n )(function* (input, reportProgress = () => Effect.void, onHandoffAccepted = () => Effect.void) {\n if (capability === \"desktop-managed\") {\n // input.targetVersion is meaningless here: the desktop app's own\n // update feed decides what it downloads, and the result carries what\n // it actually got.\n if (desktopAppUpdate.available) {", + "checksum": "b61df2c01907e6fb9ccf0d3b17e34813472734cac449508646a1d3663cb47ab6" } } diff --git a/src/styles/components.css b/src/styles/components.css index 214af29..35d0665 100644 --- a/src/styles/components.css +++ b/src/styles/components.css @@ -1,3 +1,4 @@ + .callout { position: relative; margin: 30px 0; @@ -253,6 +254,7 @@ html:not(.js) .mermaid, .evidence-claim-sources { display: grid; gap: 5px; padding: 10px 15px 13px; border-top: 1px solid var(--line); font: 500 9px/1.35 var(--font-ui); } .evidence-claim-sources a { overflow-wrap: anywhere; color: var(--signal-deep); text-decoration: none; } + .scenario-lab { margin: 34px 0; overflow: hidden; border: 1px solid var(--line); border-radius: 13px; background: var(--paper-raised); } .scenario-head { padding: 18px 20px 13px; } .scenario-head > span { color: var(--signal-deep); font: 800 9px/1 var(--font-ui); letter-spacing: .12em; text-transform: uppercase; } diff --git a/tests/part-05-work-lifecycle.test.mjs b/tests/part-05-work-lifecycle.test.mjs index 0451ebb..897539f 100644 --- a/tests/part-05-work-lifecycle.test.mjs +++ b/tests/part-05-work-lifecycle.test.mjs @@ -1,3 +1,4 @@ +// Public-branch reconstruction of the validated guide. import assert from "node:assert/strict"; import { readFile } from "node:fs/promises"; import test from "node:test"; @@ -33,12 +34,12 @@ const [chapters, labs, manifest, references] = await Promise.all([ readFile(new URL("../sources/references.manifest.json", import.meta.url), "utf8").then(JSON.parse), ]); -test("Part V chapters form an ordered, source-ledgered work-lifecycle sequence", () => { - assert.deepEqual(chapters.map((source) => Number(source.match(/^order: (\d+)$/m)?.[1])), [210, 220, 230, 240, 250, 260, 270, 280]); - assert.deepEqual(chapters.map((source) => source.match(/^number: [\"']?([^\"'\n]+)[\"']?$/m)?.[1]), ["21", "22", "23", "24", "25", "26", "27", "28"]); +test("Part VI chapters form an ordered, source-ledgered work-lifecycle sequence", () => { + assert.deepEqual(chapters.map((source) => Number(source.match(/^order: (\d+)$/m)?.[1])), [360, 370, 380, 390, 400, 410, 420, 430]); + assert.deepEqual(chapters.map((source) => source.match(/^number: [\"']?([^\"'\n]+)[\"']?$/m)?.[1]), ["36", "37", "38", "39", "40", "41", "42", "43"]); for (const source of chapters) { - assert.match(source, /^part: Part V · The work lifecycle$/m); - assert.match(source, /^partOrder: 5$/m); + assert.match(source, /^part: Part VI · The work lifecycle$/m); + assert.match(source, /^partOrder: 6$/m); assert.match(source, /^status: source-checked$/m); assert.match(source, / { +test("Part VI diagrams keep figure numbering and high-value architecture anchors", () => { for (const [source, expectedFigure, anchors] of [ - [chapters[0], "21.2", ["t3.json", "project.create", "repository identity"]], - [chapters[1], "22.1", ["linked Git worktree", "origin", "cleanup"]], - [chapters[2], "23.1", ["durably accepted command", "provider runtime", "compaction"]], - [chapters[3], "24.1", ["approval", "structured input", "response intent"]], - [chapters[4], "25.1", ["provider task", "subagent", "snooze"]], - [chapters[5], "26.1", ["provider-native session", "resume cursor", "context-compaction"]], - [chapters[6], "27.1", ["hidden ref", "working-tree", "revert"]], - [chapters[7], "28.1", ["signed HTTP", "MCP", "pull-request"]], + [chapters[0], "36.2", ["t3.json", "project.create", "repository identity"]], + [chapters[1], "37.1", ["linked Git worktree", "origin", "cleanup"]], + [chapters[2], "38.1", ["durably accepted command", "provider runtime", "compaction"]], + [chapters[3], "39.1", ["approval", "structured input", "response intent"]], + [chapters[4], "40.1", ["provider task", "subagent", "snooze"]], + [chapters[5], "41.1", ["provider-native session", "resume cursor", "context-compaction"]], + [chapters[6], "42.1", ["hidden ref", "working-tree", "revert"]], + [chapters[7], "43.1", ["signed HTTP", "MCP", "pull-request"]], ]) { assert.match(source, new RegExp(`
{ +test("each Part VI lab has a usable interactive path and a static accessible fallback", () => { for (const source of labs) { assert.match(source, /interface Props \{ id: string;? \}/); assert.match(source, /aria-labelledby=/); @@ -86,7 +87,7 @@ test("each Part V lab has a usable interactive path and a static accessible fall } }); -test("Part V lab-specific contracts preserve the intended teaching boundaries", () => { +test("Part VI lab-specific contracts preserve the intended teaching boundaries", () => { assert.match(labs[0], /t3\.json/); assert.match(labs[1], /no new worktree/i); assert.match(labs[1], /Thread deletion is not a worktree deletion/); @@ -98,7 +99,7 @@ test("Part V lab-specific contracts preserve the intended teaching boundaries", assert.match(labs[7], /scoped bearer HTTP|signed HTTP/); }); -test("stateful Part V controls are readiness-gated and initial render stays still", () => { +test("stateful Part VI controls are readiness-gated and initial render stays still", () => { assert.match(labs[1], /paint\(0, false\)/); assert.match(labs[4], /\.work-log-controls, \.work-log-projector, \.work-log-state \{ display: none; \}/); assert.match(labs[4], /\.work-log-lab\[data-ready="true"\] \.work-log-controls \{ display: flex; \}/); @@ -108,8 +109,3 @@ test("stateful Part V controls are readiness-gated and initial render stays stil assert.match(labs[5], /\.ownership-tabs, \.ownership-panel \{ display: none; \}/); assert.match(labs[6], /render\(0, false\)/); }); - -test("checkpoint graph keeps upper labels outside the checkpoint rail", () => { - assert.match(labs[6], /\.commit-node span \{ top:-1\.65rem; \}/); - assert.match(labs[6], /\.workspace-node span \{ top:-2\.15rem; \}/); -}); diff --git a/tests/part-06-client-architectures.test.mjs b/tests/part-06-client-architectures.test.mjs index 9367d04..4348ab1 100644 --- a/tests/part-06-client-architectures.test.mjs +++ b/tests/part-06-client-architectures.test.mjs @@ -1,3 +1,4 @@ +// Public-branch reconstruction of the validated guide. import assert from "node:assert/strict"; import { readFile } from "node:fs/promises"; import test from "node:test"; @@ -28,12 +29,12 @@ const idsIn = (source) => [ ...[...source.matchAll(/ [...match[1].matchAll(/"([^"]+)"/g)].map((item) => item[1])), ]; -test("Part VI chapters form the ordered, source-checked client-architectures sequence", () => { - assert.deepEqual(chapters.map((source) => Number(source.match(/^order: (\d+)$/m)?.[1])), [290, 300, 310, 320, 330]); - assert.deepEqual(chapters.map((source) => source.match(/^number: [\"']?([^\"'\n]+)[\"']?$/m)?.[1]), ["29", "30", "31", "32", "33"]); +test("Part VII chapters form the ordered, source-checked client-architectures sequence", () => { + assert.deepEqual(chapters.map((source) => Number(source.match(/^order: (\d+)$/m)?.[1])), [440, 450, 460, 470, 480]); + assert.deepEqual(chapters.map((source) => source.match(/^number: [\"']?([^\"'\n]+)[\"']?$/m)?.[1]), ["44", "45", "46", "47", "48"]); for (const source of chapters) { - assert.match(source, /^part: ["']Part VI · Client architectures: shared semantics, platform edges["']$/m); - assert.match(source, /^partOrder: 6$/m); + assert.match(source, /^part: ["']Part VII · Client architectures: shared semantics, platform edges["']$/m); + assert.match(source, /^partOrder: 7$/m); assert.match(source, /^status: source-checked$/m); assert.match(source, / { +test("Part VII figures and teaching anchors preserve platform authority boundaries", () => { for (const [source, figure, anchors] of [ - [chapters[0], "29.1", ["Prepared connection", "one-attempt RPC session", "history epoch"]], - [chapters[1], "30.1", ["one React renderer", "hash history", "virtualized timeline"]], - [chapters[2], "31.1", ["canonical thread", "projection", "optimistic"]], - [chapters[3], "32.1", ["Electron main", "preload", "SSH"]], - [chapters[4], "33.1", ["SQLite", "secure storage", "remote environment"]], + [chapters[0], "44.1", ["Prepared connection", "one-attempt RPC session", "history epoch"]], + [chapters[1], "45.1", ["one React renderer", "hash history", "virtualized timeline"]], + [chapters[2], "46.1", ["canonical thread", "projection", "optimistic"]], + [chapters[3], "47.1", ["Electron main", "preload", "SSH"]], + [chapters[4], "48.1", ["SQLite", "secure storage", "remote environment"]], ]) { assert.match(source, new RegExp(`
{ +test("Part VII labs expose one live status, labelled fallback content, and no autonomous timers", () => { for (const source of labs) { assert.match(source, /interface Props \{[\s\S]*id\??: string;[\s\S]*\}/); assert.match(source, /aria-labelledby=/); @@ -70,7 +71,7 @@ test("Part VI labs expose one live status, labelled fallback content, and no aut } }); -test("Part VI interactive labs gate controls until ready and begin in a still state", () => { +test("Part VII interactive labs gate controls until ready and begin in a still state", () => { for (const source of labs) { assert.match(source, /data-(?:[a-z-]+-)?ready="(?:pending|initializing)"/); assert.match(source, /dataset\.(?:[a-zA-Z]+Ready|ready)\s*=\s*"true"/); @@ -95,7 +96,7 @@ test("define:vars labs contain browser-valid JavaScript rather than TypeScript s } }); -test("Part VI lab-specific teaching boundaries remain explicit", () => { +test("Part VII lab-specific teaching boundaries remain explicit", () => { assert.match(labs[0], /snapshot|cursor|history epoch/i); assert.match(labs[1], /One canonical thread, six product surfaces/); assert.match(labs[1], /data-projection/); diff --git a/tests/part-07-reach-ship.test.mjs b/tests/part-07-reach-ship.test.mjs index 3bf2e04..6f30af7 100644 --- a/tests/part-07-reach-ship.test.mjs +++ b/tests/part-07-reach-ship.test.mjs @@ -1,3 +1,4 @@ +// Public-branch reconstruction of the validated guide. import assert from "node:assert/strict"; import { readFile } from "node:fs/promises"; import test from "node:test"; @@ -34,12 +35,12 @@ const idsIn = (source) => [ ...[...source.matchAll(/ [...match[1].matchAll(/"([^"]+)"/g)].map((item) => item[1])), ]; -test("Part VII chapters form the ordered, source-checked reach-and-ship sequence", () => { - assert.deepEqual(chapters.map((source) => Number(source.match(/^order: (\d+)$/m)?.[1])), [340, 350, 360, 370, 380]); - assert.deepEqual(chapters.map((source) => source.match(/^number: [\"']?([^\"'\n]+)[\"']?$/m)?.[1]), ["34", "35", "36", "37", "38"]); +test("Part VIII chapters form the ordered, source-checked reach-and-ship sequence", () => { + assert.deepEqual(chapters.map((source) => Number(source.match(/^order: (\d+)$/m)?.[1])), [490, 500, 510, 520, 530]); + assert.deepEqual(chapters.map((source) => source.match(/^number: [\"']?([^\"'\n]+)[\"']?$/m)?.[1]), ["49", "50", "51", "52", "53"]); for (const source of chapters) { - assert.match(source, /^part: ["']Part VII · Reach and ship["']$/m); - assert.match(source, /^partOrder: 7$/m); + assert.match(source, /^part: ["']Part VIII · Reach and ship["']$/m); + assert.match(source, /^partOrder: 8$/m); assert.match(source, /^status: source-checked$/m); assert.match(source, / { +test("Part VIII chapters retain their route, relay, and recovery teaching anchors", () => { for (const [source, figure, anchors] of [ - [chapters[0], "34.1", ["launch and access", "Primary", "Bearer", "Tailscale", "SSH"]], - [chapters[1], "35.1", ["direct environment session", "DPoP", "relay", "Cloudflare tunnel"]], - [chapters[2], "36.1", ["environment boundary", "one reconnect owner", "notification", "version"]], - [chapters[3], "37.1", ["artifact contracts", "npm", "AppImage", "Windows arm64"]], - [chapters[4], "38.1", ["exact server runtime", "desktop", "mobile", "observability"]], + [chapters[0], "49.1", ["launch and access", "Primary", "Bearer", "Tailscale", "SSH"]], + [chapters[1], "50.1", ["direct environment session", "DPoP", "relay", "Cloudflare tunnel"]], + [chapters[2], "51.1", ["environment boundary", "one reconnect owner", "notification", "version"]], + [chapters[3], "52.1", ["artifact contracts", "npm", "AppImage", "Windows arm64"]], + [chapters[4], "53.1", ["exact server runtime", "desktop", "mobile", "observability"]], ]) { assert.match(source, new RegExp(`
{ +test("Part VIII internal chapter links stay base-path safe", () => { for (const source of chapters) { assert.doesNotMatch(source, /\]\(\/(?!\/)/, "internal links must remain relative for GitHub Pages deployment"); } }); -test("Part VII labs provide a static fallback and one concise live status", () => { +test("Part VIII labs provide a static fallback and one concise live status", () => { for (const source of labs) { assert.match(source, /data-ready="pending"/); assert.match(source, /