[codex] Rewrite client connection architecture#2978
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
|
🚀 Expo continuous deployment is ready!
|
16c9aba to
2a29e34
Compare
2a29e34 to
b976d5f
Compare
| const openDatabase = Effect.fn("web.connectionStorage.openDatabase")(function* () { | ||
| return yield* Effect.callback<IDBDatabase, ConnectionTransientError>((resume) => { | ||
| if (typeof indexedDB === "undefined") { | ||
| resume( | ||
| Effect.fail(catalogError("open", "IndexedDB is unavailable in this browser context.")), | ||
| ); | ||
| return; | ||
| } | ||
| const request = indexedDB.open(DATABASE_NAME, DATABASE_VERSION); | ||
| request.addEventListener("upgradeneeded", () => { | ||
| if (!request.result.objectStoreNames.contains(CATALOG_STORE_NAME)) { | ||
| request.result.createObjectStore(CATALOG_STORE_NAME); | ||
| } | ||
| if (!request.result.objectStoreNames.contains(SHELL_STORE_NAME)) { | ||
| request.result.createObjectStore(SHELL_STORE_NAME); | ||
| } | ||
| if (!request.result.objectStoreNames.contains(THREAD_STORE_NAME)) { | ||
| request.result.createObjectStore(THREAD_STORE_NAME); | ||
| } | ||
| }); | ||
| request.addEventListener("error", () => { | ||
| resume(Effect.fail(catalogError("open", request.error ?? "Unknown IndexedDB error"))); | ||
| }); | ||
| request.addEventListener("success", () => { | ||
| resume(Effect.succeed(request.result)); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
🟡 Medium connection/webConnectionStorage.ts:87
When indexedDB.open() needs to upgrade the schema but another tab holds an older version, the blocked event fires and the Effect never completes — neither success nor error handlers execute until the blocking tab closes. If that tab never closes, openDatabase hangs indefinitely. Consider handling the blocked event by resuming with a descriptive error or triggering a retry with timeout.
+ request.addEventListener("blocked", () => {
+ resume(Effect.fail(catalogError("open", "Database upgrade blocked by another tab")));
+ });🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/connection/webConnectionStorage.ts around lines 87-113:
When `indexedDB.open()` needs to upgrade the schema but another tab holds an older version, the `blocked` event fires and the Effect never completes — neither `success` nor `error` handlers execute until the blocking tab closes. If that tab never closes, `openDatabase` hangs indefinitely. Consider handling the `blocked` event by resuming with a descriptive error or triggering a retry with timeout.
Evidence trail:
apps/web/src/connection/webConnectionStorage.ts lines 87-114 at REVIEWED_COMMIT: `openDatabase` registers handlers for `upgradeneeded` (line 96), `error` (line 107), and `success` (line 110), but not for `blocked`. IndexedDB spec: https://w3c.github.io/IndexedDB/#request-api — the `blocked` event fires on IDBOpenDBRequest when the open operation is blocked by existing connections, and `success`/`error` don't fire until the block is resolved.
fb36d5e to
8de82fb
Compare
9ba3552 to
a01b7f9
Compare
ced8bdf to
22b4b24
Compare
409fd6c to
ecd84a7
Compare
ecd84a7 to
f5c0afe
Compare
| ); | ||
| if (!isSignedIn || !userId) { | ||
| setManagedRelaySession(appAtomRegistry, null); | ||
| if (previousAccount !== null) { |
There was a problem hiding this comment.
🟡 Medium cloud/managedAuth.tsx:59
On first render when signed out, previousAccount is undefined, so undefined !== null evaluates to true and queueAccountCleanup() runs unnecessarily. This triggers removeRelayEnvironments() and relay client token cache reset even though no account was ever established. Change the condition to if (previousAccount) so cleanup only happens when transitioning from an actual signed-in state.
- if (previousAccount !== null) {
+ if (previousAccount) {🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/cloud/managedAuth.tsx around line 59:
On first render when signed out, `previousAccount` is `undefined`, so `undefined !== null` evaluates to `true` and `queueAccountCleanup()` runs unnecessarily. This triggers `removeRelayEnvironments()` and relay client token cache reset even though no account was ever established. Change the condition to `if (previousAccount)` so cleanup only happens when transitioning from an actual signed-in state.
Evidence trail:
apps/web/src/cloud/managedAuth.tsx lines 26, 35, 37, 57-61, 63 at REVIEWED_COMMIT. Line 26: ref initialized to `undefined`. Line 35: `previousAccount = observedAccountRef.current` (undefined on first render). Line 59: condition `previousAccount !== null` doesn't account for `undefined`. Line 63: correctly checks all three states (`!== undefined && !== null && !== userId`).
| ? (activeSavedEnvironmentRuntime?.connectionState ?? "disconnected") | ||
| : "connected"; | ||
| const primaryEnvironmentId = primaryEnvironment?.environmentId ?? null; | ||
| const activeEnvironment = |
There was a problem hiding this comment.
🟡 Medium components/ChatView.tsx:1119
The refactored code removed the guard that excluded the primary environment from unavailability checks. Now activeEnvironmentUnavailable is computed for all environments including the primary, so if the primary's connection.phase is temporarily "connecting" or "available" during startup, the UI shows an unavailable banner and blocks message dispatch — behavior that never occurred before.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/components/ChatView.tsx around line 1119:
The refactored code removed the guard that excluded the primary environment from unavailability checks. Now `activeEnvironmentUnavailable` is computed for all environments including the primary, so if the primary's `connection.phase` is temporarily `"connecting"` or `"available"` during startup, the UI shows an unavailable banner and blocks message dispatch — behavior that never occurred before.
Evidence trail:
Old guard at MERGE_BASE in ChatView.tsx: `activeThread.environmentId !== primaryEnvironmentId` check in `activeSavedEnvironmentRecord` assignment; new code at REVIEWED_COMMIT ChatView.tsx lines 1119-1123 with no primary guard; `AVAILABLE_CONNECTION_STATE` defined at packages/client-runtime/src/connection/model.ts:148-154 with `phase: "available"`; `activeEnvironmentUnavailable` used to block sends at ChatView.tsx:2806, ChatView.tsx:3427; used to block revert at ChatView.tsx:2746; used for banner at ChatView.tsx:1353; `isConnecting` always false at ChatView.tsx:904 (`_setIsConnecting` unused).
| (activeThread.session !== null && activeThread.session.status !== "stopped")), |
There was a problem hiding this comment.
🟡 Medium components/ChatView.tsx:1808
The envLocked check at line 1808 only excludes status !== "stopped", but the new session model has two additional terminal states "interrupted" and "error". Threads with these statuses will incorrectly have envLocked = true, blocking users from changing branch/env-mode settings on effectively terminated threads. Consider updating the condition to also exclude "interrupted" and "error".
- (activeThread.session !== null && activeThread.session.status !== "stopped")),
+ (activeThread.session !== null &&
+ activeThread.session.status !== "stopped" &&
+ activeThread.session.status !== "interrupted" &&
+ activeThread.session.status !== "error")),🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/components/ChatView.tsx around line 1808:
The `envLocked` check at line 1808 only excludes `status !== "stopped"`, but the new session model has two additional terminal states `"interrupted"` and `"error"`. Threads with these statuses will incorrectly have `envLocked = true`, blocking users from changing branch/env-mode settings on effectively terminated threads. Consider updating the condition to also exclude `"interrupted"` and `"error"`.
Evidence trail:
packages/contracts/src/orchestration.ts:249-257 (OrchestrationSessionStatus defines: idle, starting, running, ready, interrupted, stopped, error); apps/web/src/session-logic.ts:1252-1260 (derivePhase treats stopped/interrupted/error all as 'disconnected' terminal states); apps/web/src/components/ChatView.tsx:1805-1809 (envLocked check only excludes 'stopped'); apps/web/src/components/ChatView.tsx:1816 (envLocked blocks onEnvironmentChange); apps/web/src/components/ChatView.tsx:2497 (envLocked blocks canOverrideServerThreadEnvMode); apps/web/src/components/ChatView.tsx:3820 (envLocked passed to child component)
03c78cc to
4c38462
Compare
4c38462 to
f37abe6
Compare
04d6d46 to
582b006
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Spurious relay cleanup on load
- Added
previousObservedAccount !== undefinedguard to the signed-out branch to match the account-switch branch, preventing cleanup when no user was ever observed as signed in.
- Added
Or push these changes by commenting:
@cursor push 0d6918a8b3
Preview (0d6918a8b3)
diff --git a/apps/mobile/src/features/cloud/CloudAuthProvider.tsx b/apps/mobile/src/features/cloud/CloudAuthProvider.tsx
--- a/apps/mobile/src/features/cloud/CloudAuthProvider.tsx
+++ b/apps/mobile/src/features/cloud/CloudAuthProvider.tsx
@@ -72,7 +72,7 @@
previousTokenProviderRef.current = null;
setAgentAwarenessRelayTokenProvider(null);
setManagedRelaySession(appAtomRegistry, null);
- if (previousObservedAccount !== null) {
+ if (previousObservedAccount !== undefined && previousObservedAccount !== null) {
void queueAccountCleanup(previous);
}
return;You can send follow-ups to the cloud agent here.
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
…t and tracing - Updated the EnvironmentSupervisor to handle connection states more effectively, including new states for connecting and available. - Introduced tracing for connection attempts to capture detailed error information and improve debugging. - Removed the environment runtime state management code as it was deemed unnecessary. - Adjusted tests to reflect changes in connection state handling and ensure proper functionality. - Enhanced error handling in relay tracing to ensure safe error propagation without affecting application behavior.
- Refactored EnvironmentRegistry to utilize new EnvironmentServices and EnvironmentServicesFactory. - Replaced runtime-related methods with service-based methods for better abstraction. - Introduced new layers for environment services, including commands and threads. - Removed deprecated rpcGenerationChanges and other unused methods from EnvironmentRegistryService. - Updated tests to reflect changes in the registry and runtime structure. - Cleaned up unused imports and adjusted types accordingly.
…ment - Deleted filesystemBrowseState and sourceControlDiscoveryState modules along with their associated tests. - Removed related imports from index.ts and knownEnvironment.ts. - Updated knownEnvironment tests to remove unused HTTP base URL checks. - Refactored shellSnapshotState and threadDetailState to simplify interfaces. - Cleaned up vcsRefState and vcsStatusState by removing unused types and functions.
f37abe6 to
fe78bce
Compare
Split connection, authorization, RPC, state, and platform concerns into composable modules. Migrate web and mobile consumers to narrow subpath exports and remove legacy stores, barrels, and compatibility directories. Co-authored-by: codex <codex@users.noreply.github.com>
- Implemented `useEnvironmentQuery` for handling environment data fetching and error management. - Created state management for relay, review, server, session, shell, source control, terminal, and threads. - Introduced hooks for managing terminal sessions and actions, including attaching, writing, resizing, and clearing terminals. - Added support for source control actions such as pulling, publishing repositories, and managing threads. - Enhanced VCS integration with actions for listing refs and managing worktrees.
- keep subscriptions alive across transport and supervisor replacement - bound probes and improve retry, authorization, and state projections - add deterministic regression coverage for connection lifecycle failures Co-authored-by: codex <codex@users.noreply.github.com>
- self-heal persisted connection catalogs and clean stale temporary data - preserve bearer authorization across edits and attachment requests - centralize mobile runtime reuse and background outbox draining Co-authored-by: codex <codex@users.noreply.github.com>
- project server snapshots and request latency into focused atoms - surface slow RPC requests without coupling shared runtime to React - reuse the web runtime and make hosted connection discovery resilient Co-authored-by: codex <codex@users.noreply.github.com>
- recover desktop connection catalogs from partial writes - remove stale temporary catalog files after recovery - trust incoming relay trace context only after authentication Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
a71ceef to
25717b6
Compare
- Add UIKit-backed iOS composer module for atomic skill and file tokens - Unify mobile chrome colors and shared composer token parsing - Update web mention handling and add token layout tests
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Stale tokens break selection mapping
- Extracted a currentValidTokens() helper that filters tokens against the current value string, and used it in displayOffset(forSourceOffset:) so stale tokens no longer participate in offset calculations.
Or push these changes by commenting:
@cursor push b5abc57d3c
Preview (b5abc57d3c)
diff --git a/apps/mobile/modules/t3-composer-editor/ios/T3ComposerEditorView.swift b/apps/mobile/modules/t3-composer-editor/ios/T3ComposerEditorView.swift
--- a/apps/mobile/modules/t3-composer-editor/ios/T3ComposerEditorView.swift
+++ b/apps/mobile/modules/t3-composer-editor/ios/T3ComposerEditorView.swift
@@ -464,12 +464,7 @@
let result = NSMutableAttributedString()
let source = value as NSString
var cursor = 0
- let validTokens = tokens.filter {
- $0.start >= cursor &&
- $0.end > $0.start &&
- $0.end <= source.length &&
- source.substring(with: NSRange(location: $0.start, length: $0.end - $0.start)) == $0.source
- }
+ let validTokens = currentValidTokens()
for token in validTokens {
if token.start < cursor {
@@ -678,11 +673,12 @@
private func displayOffset(forSourceOffset sourceOffset: Int) -> Int {
let boundedOffset = max(0, min((value as NSString).length, sourceOffset))
+ let active = currentValidTokens()
var collapsedLength = 0
- for token in tokens where token.end <= boundedOffset {
+ for token in active where token.end <= boundedOffset {
collapsedLength += max(0, token.end - token.start - 1)
}
- if let token = tokens.first(where: { $0.start < boundedOffset && boundedOffset < $0.end }) {
+ if let token = active.first(where: { $0.start < boundedOffset && boundedOffset < $0.end }) {
return token.start - collapsedLength + 1
}
return boundedOffset - collapsedLength
@@ -723,9 +719,9 @@
return try? JSONDecoder().decode(type, from: data)
}
- private func tokensMatchCurrentValue() -> Bool {
+ private func currentValidTokens() -> [ComposerTokenPayload] {
let source = value as NSString
- return tokens.allSatisfy {
+ return tokens.filter {
$0.start >= 0 &&
$0.end > $0.start &&
$0.end <= source.length &&
@@ -733,19 +729,12 @@
}
}
+ private func tokensMatchCurrentValue() -> Bool {
+ currentValidTokens().count == tokens.count
+ }
+
private func documentMatchesExpectedTokens() -> Bool {
- let source = value as NSString
- let expectedSources = tokens.compactMap { token -> String? in
- guard token.start >= 0,
- token.end > token.start,
- token.end <= source.length,
- source.substring(
- with: NSRange(location: token.start, length: token.end - token.start)
- ) == token.source else {
- return nil
- }
- return token.source
- }
+ let expectedSources = currentValidTokens().map(\.source)
var renderedSources: [String] = []
textView.attributedText.enumerateAttribute(
.attachment,You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit c99480f. Configure here.
| return token.start - collapsedLength + 1 | ||
| } | ||
| return boundedOffset - collapsedLength | ||
| } |
There was a problem hiding this comment.
Stale tokens break selection mapping
Medium Severity
displayOffset(forSourceOffset:) adjusts the caret using every entry in tokens, but makeAttributedDocument() only renders tokens whose start/end/source still match value. After edits or while React props lag the native text, stale token ranges can remain in tokens and still participate in offset math (including the “cursor inside token” branch), so controlled selection and rebuilds can place the caret at the wrong display index.
Reviewed by Cursor Bugbot for commit c99480f. Configure here.
| ...(context.headingLevel ? { headingLevel: context.headingLevel } : {}), | ||
| ...(context.depth ? { depth: context.depth } : {}), | ||
| ...(context.spacing ? { spacing: context.spacing } : {}), |
There was a problem hiding this comment.
🟢 Low lib/nativeMarkdownText.ts:166
Lines 166-168 use truthy checks for headingLevel, depth, and spacing, which silently drops these properties when their value is 0. Since appendDocumentBlock passes depth: 0 at the document level (line 592), a depth of 0 becomes indistinguishable from undefined, breaking downstream logic that relies on explicit zero values. Lines 169-175 already use !== undefined checks for other numeric properties; consider applying the same pattern to lines 166-168.
- ...(context.headingLevel ? { headingLevel: context.headingLevel } : {}),
- ...(context.depth ? { depth: context.depth } : {}),
- ...(context.spacing ? { spacing: context.spacing } : {}),
+ ...(context.headingLevel !== undefined
+ ? { headingLevel: context.headingLevel }
+ : {}),
+ ...(context.depth !== undefined ? { depth: context.depth } : {}),
+ ...(context.spacing !== undefined ? { spacing: context.spacing } : {}),🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/mobile/src/lib/nativeMarkdownText.ts around lines 166-168:
Lines 166-168 use truthy checks for `headingLevel`, `depth`, and `spacing`, which silently drops these properties when their value is `0`. Since `appendDocumentBlock` passes `depth: 0` at the document level (line 592), a `depth` of `0` becomes indistinguishable from `undefined`, breaking downstream logic that relies on explicit zero values. Lines 169-175 already use `!== undefined` checks for other numeric properties; consider applying the same pattern to lines 166-168.
Evidence trail:
apps/mobile/src/lib/nativeMarkdownText.ts lines 166-175 (truthy check vs !== undefined check pattern), line 564 (depth = 0 default), line 592 (depth passed into RunContext), line 128 (sameRunStyle compares .depth), apps/mobile/src/native/NativeMarkdownSelectableText.ios.tsx line 22 (run.depth used in key join)
- Adjust composer spacing and chrome measurements - Tighten the patched UITextView chip rendering metrics



Summary
Replace the legacy web and mobile connection implementations with a shared Effect-based client runtime, then move the mobile thread experience onto the same current contracts and turn lifecycle behavior as web.
@t3tools/client-runtime@t3tools/client-runtimeinto explicit subpath exports and migrate web/mobile away from their independent stores and RPC clientsserviceTierinstead of the legacy hard-coded Fast Mode toggleStack
mainReview this PR against #2995. The tracing infrastructure and release configuration remain isolated in the base PR.
Validation
vp checkvp run typecheckvp run lint:mobileNote
High Risk
Touches encrypted connection catalogs, credential migration, and broad mobile connection/state rewiring—errors could drop saved environments or break cloud pairing until users reconnect.
Overview
Adds desktop persistence for the shared connection catalog: an encrypted on-disk store (Electron safe storage), atomic writes, corrupt/undecryptable catalog discard, and get/set/clear IPC exposed through preload—wired into the main app layer. Safe-storage types move to
ElectronSafeStorageServiceso tests and stores can depend on the interface without the Electron implementation.On mobile, introduces a dedicated
connection/stack on@t3tools/client-runtime: secure-store catalog with legacyt3code.connectionsmigration, file-backed shell/thread snapshot caches, connectivity and app-lifecycle wakeups, and platform hooks (Clerk relay token, DPoP token store, SSH blocked). Screens shift from the old remote-catalog/registry hooks to workspace/entity atoms,useConnectionControllerfor T3 Cloud rows (switches, trace IDs, relay refresh), and a/settings/authsheet route with expandable Clerk detent instead of native auth modals. Theme tokens replace hard-coded colors in chrome; favicons use DPoP-aware HTTP headers.Ships a new iOS
T3ComposerEditorExpo module (UIKit rich text, atomic skill/file chips, image paste) plus CSS tokens for inline skills/glass. Smaller fixes: cloud authsetHeadersshape, TailscaleorElseSucceed, SSH IPC imports from client-runtime subpaths,mobileRuntime→runtimein settings/agent code.Reviewed by Cursor Bugbot for commit f32e94b. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Rewrite client connection architecture to use atom-based environment state
connectionLayer,EnvironmentRegistry, andEnvironmentSupervisormanage environment lifecycle, persistence, and reconnection with exponential backoff.ConnectionTargetvariants (Primary, Bearer, Relay, SSH), encrypted catalog persistence, and cache-backed shell/thread snapshots.useEnvironmentQueryanduseAtomSethooks.@t3tools/client-runtimeroot imports with explicit subpath exports (e.g./relay,/rpc,/state/vcs); a new ESLint rule enforces this.DesktopConnectionCatalogStorefor encrypted catalog persistence on desktop andmanagedRelayAccessTokenStoreusing SecureStore on mobile; session tokens are now cached with JWT-expiry awareness.ComposerEditorreplacing plainTextInput.LocalApibrowser implementation no longer proxies to a live backend;server.*andshell.*methods always reject until pairing.cwd→workspaceRoot,name→title,turnDiffSummaries→checkpoints,orchestrationStatus→status,closed→stopped); any consumers outside the monorepo relying on@t3tools/client-runtimeroot imports or these field names will break.Macroscope summarized f32e94b.