Skip to content

Load thread snapshots over HTTP before live sync#3719

Merged
juliusmarminge merged 6 commits into
mainfrom
t3code/http-snapshot-loading
Jul 6, 2026
Merged

Load thread snapshots over HTTP before live sync#3719
juliusmarminge merged 6 commits into
mainfrom
t3code/http-snapshot-loading

Conversation

@juliusmarminge

@juliusmarminge juliusmarminge commented Jul 5, 2026

Copy link
Copy Markdown
Member

Summary

  • Load thread detail snapshots over HTTP first, then resume the WebSocket subscription from the returned snapshot sequence.
  • Add a dedicated orchestration HTTP endpoint for thread snapshots, including 404 handling when the thread is missing.
  • Update client runtime state to fall back to the socket-embedded snapshot when HTTP loading fails, while deduplicating overlapping replayed events.
  • Extend shared contracts and error mapping to support the new snapshot fetch path across web, mobile, and server.
  • Add tests covering HTTP snapshot seeding and live event resume behavior.

Testing

  • vp check
  • vp run typecheck
  • vp run lint
  • vp test / vp run test for the updated client-runtime state tests
  • Not run: full mobile-specific lint (vp run lint:mobile)

Note

Medium Risk
Changes the orchestration sync path end-to-end (HTTP + WS replay ordering) and bumps on-disk thread cache schema, so upgrades force a one-time cold cache until snapshots are refetched.

Overview
Shell and thread sync now prefer loading snapshots from local cache or new HTTP endpoints, then opening WebSocket subscriptions with afterSequence so large snapshot frames are not re-sent on the socket. HTTP failures (and thread 404) fall back to the socket-embedded snapshot; overlapping replay is deduped by sequence on the client.

Server: Adds GET /api/orchestration/shell and GET /api/orchestration/threads/:threadId, getThreadDetailSnapshot (thread detail + projection sequence in one transaction), EnvironmentResourceNotFoundError, and optional limit on readEvents. subscribeShell / subscribeThread buffer live events before catch-up replay when afterSequence is set.

Clients (web/mobile): Thread cache schema v2 stores OrchestrationThreadDetailSnapshot (sequence + thread); v1 entries no longer decode. ShellSnapshotLoader / ThreadSnapshotLoader layers and shared environmentHttpAuth (cookies, Bearer, DPoP) wire into connection runtimes.

Reviewed by Cursor Bugbot for commit 9254fe5. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Load thread and shell snapshots over HTTP before live WebSocket sync

  • Adds HTTP endpoints GET /api/orchestration/shell and GET /api/orchestration/threads/:threadId on the server to serve snapshots before a WebSocket connection is established.
  • Extends subscribeShell and subscribeThread RPCs to accept an optional afterSequence parameter; when provided, the server replays persisted events after that sequence instead of sending a full initial snapshot.
  • On the client, shell and thread state initialization attempts to load a snapshot from cache or HTTP (via new ShellSnapshotLoader and ThreadSnapshotLoader services), then passes the snapshot sequence as afterSequence to the WebSocket subscription, reducing initial socket payload.
  • Bumps the thread snapshot cache schema from v1 to v2, replacing the stored thread field with an OrchestrationThreadDetailSnapshot that includes snapshotSequence.
  • Risk: existing v1 cache entries will not match the new schema and will be ignored, causing a one-time full sync on first load after upgrade.

Macroscope summarized 9254fe5.

- Fetch initial thread snapshots via HTTP and resume subscriptions after the snapshot sequence
- Add not-found handling for thread snapshot requests
@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 500fc1b7-e1b1-4e76-8444-eeacea5bd39f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch t3code/http-snapshot-loading

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Jul 5, 2026
Comment thread packages/client-runtime/src/state/threadSnapshotHttp.ts
retryExpectedFailureAfter: "250 millis",
},
).pipe(Stream.runForEach(applyItem), Effect.forkScoped);
yield* Effect.forkScoped(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium state/threads.ts:193

makeEnvironmentThreadState now awaits snapshotLoader.load(...) before calling subscribe(...), so when the HTTP snapshot endpoint is slow or times out, live WebSocket updates are blocked for the entire duration even though the socket session is already healthy. The UI stays in stale cached/synchronizing state and misses live updates until the HTTP fallback completes. Consider subscribing to live events immediately and applying the HTTP snapshot in parallel when it arrives — applyItem already deduplicates by sequence, so overlapping events are safe.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/client-runtime/src/state/threads.ts around line 193:

`makeEnvironmentThreadState` now awaits `snapshotLoader.load(...)` before calling `subscribe(...)`, so when the HTTP snapshot endpoint is slow or times out, live WebSocket updates are blocked for the entire duration even though the socket session is already healthy. The UI stays in stale `cached`/`synchronizing` state and misses live updates until the HTTP fallback completes. Consider subscribing to live events immediately and applying the HTTP snapshot in parallel when it arrives — `applyItem` already deduplicates by sequence, so overlapping events are safe.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mitigated in c420e9e. The snapshot fetch requires the sequence before we can resume live events over the socket, so some coupling is inherent, but: the cached thread renders while the fetch runs (so first paint is not blocked), the wait is bounded (timeout lowered to 6s with fallback to the socket snapshot), and this only affects the initial mount — reconnects resume via the socket without re-fetching.

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 3 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for all 3 issues found in the latest run.

  • ✅ Fixed: PubSub attached after replay
    • Added OrchestrationEngine.subscribeDomainEvents, which attaches the PubSub subscription (scoped to the RPC stream's lifetime) before subscribeThread reads its catch-up or snapshot baseline, so events published during the baseline read are buffered instead of dropped.
  • ✅ Fixed: Thread catch-up hits replay cap
    • readEvents now accepts an optional limit forwarded to the event store, and the afterSequence catch-up passes Number.MAX_SAFE_INTEGER so the replay is exhaustive (internally paged) instead of truncating at the global 1,000-event default.
  • ✅ Fixed: HTTP snapshot reads race
    • Added a transactional ProjectionSnapshotQuery.getThreadDetailSnapshot that reads the thread detail and snapshot sequence in one sql.withTransaction, now used by both the HTTP threadSnapshot endpoint and the WS snapshot path so the sequence never runs ahead of the embedded thread.

Create PR

Or push these changes by commenting:

@cursor push 29677d1179
Preview (29677d1179)
diff --git a/apps/server/src/checkpointing/CheckpointDiffQuery.test.ts b/apps/server/src/checkpointing/CheckpointDiffQuery.test.ts
--- a/apps/server/src/checkpointing/CheckpointDiffQuery.test.ts
+++ b/apps/server/src/checkpointing/CheckpointDiffQuery.test.ts
@@ -107,6 +107,7 @@
               }),
             getThreadShellById: () => Effect.succeed(Option.none()),
             getThreadDetailById: () => Effect.succeed(Option.none()),
+            getThreadDetailSnapshot: () => Effect.succeed(Option.none()),
           }),
         ),
       );
@@ -199,6 +200,7 @@
             getFullThreadDiffContext: () => Effect.die("unused"),
             getThreadShellById: () => Effect.succeed(Option.none()),
             getThreadDetailById: () => Effect.succeed(Option.none()),
+            getThreadDetailSnapshot: () => Effect.succeed(Option.none()),
           }),
         ),
       );
@@ -281,6 +283,7 @@
             getFullThreadDiffContext: () => Effect.die("unused"),
             getThreadShellById: () => Effect.succeed(Option.none()),
             getThreadDetailById: () => Effect.succeed(Option.none()),
+            getThreadDetailSnapshot: () => Effect.succeed(Option.none()),
           }),
         ),
       );
@@ -348,6 +351,7 @@
             getFullThreadDiffContext: () => Effect.die("unused"),
             getThreadShellById: () => Effect.succeed(Option.none()),
             getThreadDetailById: () => Effect.succeed(Option.none()),
+            getThreadDetailSnapshot: () => Effect.succeed(Option.none()),
           }),
         ),
       );
@@ -400,6 +404,7 @@
             getFullThreadDiffContext: () => Effect.succeed(Option.none()),
             getThreadShellById: () => Effect.succeed(Option.none()),
             getThreadDetailById: () => Effect.succeed(Option.none()),
+            getThreadDetailSnapshot: () => Effect.succeed(Option.none()),
           }),
         ),
       );

diff --git a/apps/server/src/observability/RpcInstrumentation.ts b/apps/server/src/observability/RpcInstrumentation.ts
--- a/apps/server/src/observability/RpcInstrumentation.ts
+++ b/apps/server/src/observability/RpcInstrumentation.ts
@@ -5,6 +5,7 @@
 import * as Exit from "effect/Exit";
 import * as Metric from "effect/Metric";
 import * as References from "effect/References";
+import type * as Scope from "effect/Scope";
 import * as Stream from "effect/Stream";
 
 import { outcomeFromExit } from "./Attributes.ts";
@@ -123,7 +124,14 @@
   method: string,
   effect: Effect.Effect<Stream.Stream<A, StreamError, StreamContext>, EffectError, EffectContext>,
   traceAttributes?: Readonly<Record<string, unknown>>,
-): Stream.Stream<A, StreamError | EffectError, StreamContext | EffectContext> => {
+  // `Stream.unwrap` scopes the setup effect to the stream's lifetime, so a
+  // `Scope` requirement (e.g. PubSub subscriptions attached before a snapshot
+  // is loaded) is satisfied by the stream itself rather than the caller.
+): Stream.Stream<
+  A,
+  StreamError | EffectError,
+  StreamContext | Exclude<EffectContext, Scope.Scope>
+> => {
   const instrumented = Stream.unwrap(
     Effect.gen(function* () {
       const startedAt = yield* Clock.currentTimeNanos;

diff --git a/apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts b/apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts
--- a/apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts
+++ b/apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts
@@ -200,6 +200,7 @@
           getFullThreadDiffContext: () => Effect.succeed(Option.none()),
           getThreadShellById: () => Effect.succeed(Option.none()),
           getThreadDetailById: () => Effect.succeed(Option.none()),
+          getThreadDetailSnapshot: () => Effect.succeed(Option.none()),
         }),
       ),
       Layer.provide(

diff --git a/apps/server/src/orchestration/Layers/OrchestrationEngine.ts b/apps/server/src/orchestration/Layers/OrchestrationEngine.ts
--- a/apps/server/src/orchestration/Layers/OrchestrationEngine.ts
+++ b/apps/server/src/orchestration/Layers/OrchestrationEngine.ts
@@ -306,8 +306,8 @@
     Effect.annotateLogs({ sequence: commandReadModel.snapshotSequence }),
   );
 
-  const readEvents: OrchestrationEngineShape["readEvents"] = (fromSequenceExclusive) =>
-    eventStore.readFromSequence(fromSequenceExclusive);
+  const readEvents: OrchestrationEngineShape["readEvents"] = (fromSequenceExclusive, limit) =>
+    eventStore.readFromSequence(fromSequenceExclusive, limit);
 
   const dispatch: OrchestrationEngineShape["dispatch"] = (command) =>
     Effect.gen(function* () {
@@ -329,6 +329,7 @@
     get streamDomainEvents(): OrchestrationEngineShape["streamDomainEvents"] {
       return Stream.fromPubSub(eventPubSub);
     },
+    subscribeDomainEvents: Effect.map(PubSub.subscribe(eventPubSub), Stream.fromSubscription),
   } satisfies OrchestrationEngineShape;
 });
 

diff --git a/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts b/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts
--- a/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts
+++ b/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts
@@ -2033,6 +2033,23 @@
       );
     });
 
+  const getThreadDetailSnapshot: ProjectionSnapshotQueryShape["getThreadDetailSnapshot"] = (
+    threadId,
+  ) =>
+    sql.withTransaction(Effect.all([getThreadDetailById(threadId), getSnapshotSequence()])).pipe(
+      Effect.map(([threadDetail, { snapshotSequence }]) =>
+        Option.map(threadDetail, (thread) => ({ snapshotSequence, thread })),
+      ),
+      Effect.mapError((error) => {
+        if (isPersistenceError(error)) {
+          return error;
+        }
+        return toPersistenceSqlError("ProjectionSnapshotQuery.getThreadDetailSnapshot:query")(
+          error,
+        );
+      }),
+    );
+
   return {
     getCommandReadModel,
     getSnapshot,
@@ -2047,6 +2064,7 @@
     getFullThreadDiffContext,
     getThreadShellById,
     getThreadDetailById,
+    getThreadDetailSnapshot,
   } satisfies ProjectionSnapshotQueryShape;
 });
 

diff --git a/apps/server/src/orchestration/Services/OrchestrationEngine.ts b/apps/server/src/orchestration/Services/OrchestrationEngine.ts
--- a/apps/server/src/orchestration/Services/OrchestrationEngine.ts
+++ b/apps/server/src/orchestration/Services/OrchestrationEngine.ts
@@ -13,6 +13,7 @@
 import type { OrchestrationCommand, OrchestrationEvent } from "@t3tools/contracts";
 import * as Context from "effect/Context";
 import type * as Effect from "effect/Effect";
+import type * as Scope from "effect/Scope";
 import type * as Stream from "effect/Stream";
 
 import type { OrchestrationDispatchError } from "../Errors.ts";
@@ -26,10 +27,14 @@
    * Replay persisted orchestration events from an exclusive sequence cursor.
    *
    * @param fromSequenceExclusive - Sequence cursor (exclusive).
+   * @param limit - Optional maximum number of events to replay. Defaults to
+   *   the event store's replay cap; pass `Number.MAX_SAFE_INTEGER` for an
+   *   exhaustive catch-up replay.
    * @returns Stream containing ordered events.
    */
   readonly readEvents: (
     fromSequenceExclusive: number,
+    limit?: number,
   ) => Stream.Stream<OrchestrationEvent, OrchestrationEventStoreError, never>;
 
   /**
@@ -49,8 +54,26 @@
    * Stream persisted domain events in dispatch order.
    *
    * This is a hot runtime stream (new events only), not a historical replay.
+   * The underlying PubSub subscription is only attached once the stream is
+   * pulled; use `subscribeDomainEvents` when the subscription must be
+   * attached before other work (e.g. loading a snapshot baseline).
    */
   readonly streamDomainEvents: Stream.Stream<OrchestrationEvent>;
+
+  /**
+   * Attach a domain event subscription immediately and return the stream of
+   * events it receives.
+   *
+   * Unlike `streamDomainEvents`, events published between running this effect
+   * and pulling the returned stream are buffered by the subscription instead
+   * of dropped, which is required for snapshot/catch-up + live combinations.
+   * The subscription is released when the surrounding scope closes.
+   */
+  readonly subscribeDomainEvents: Effect.Effect<
+    Stream.Stream<OrchestrationEvent>,
+    never,
+    Scope.Scope
+  >;
 }
 
 /**

diff --git a/apps/server/src/orchestration/Services/ProjectionSnapshotQuery.ts b/apps/server/src/orchestration/Services/ProjectionSnapshotQuery.ts
--- a/apps/server/src/orchestration/Services/ProjectionSnapshotQuery.ts
+++ b/apps/server/src/orchestration/Services/ProjectionSnapshotQuery.ts
@@ -14,6 +14,7 @@
   OrchestrationReadModel,
   OrchestrationShellSnapshot,
   OrchestrationThread,
+  OrchestrationThreadDetailSnapshot,
   OrchestrationThreadShell,
   ProjectId,
   ThreadId,
@@ -157,6 +158,15 @@
   readonly getThreadDetailById: (
     threadId: ThreadId,
   ) => Effect.Effect<Option.Option<OrchestrationThread>, ProjectionRepositoryError>;
+
+  /**
+   * Read a single active thread detail together with the projection snapshot
+   * sequence, both observed inside one transaction so the sequence never runs
+   * ahead of the thread rows it is paired with.
+   */
+  readonly getThreadDetailSnapshot: (
+    threadId: ThreadId,
+  ) => Effect.Effect<Option.Option<OrchestrationThreadDetailSnapshot>, ProjectionRepositoryError>;
 }
 
 /**

diff --git a/apps/server/src/orchestration/http.ts b/apps/server/src/orchestration/http.ts
--- a/apps/server/src/orchestration/http.ts
+++ b/apps/server/src/orchestration/http.ts
@@ -45,18 +45,17 @@
         Effect.fn("environment.orchestration.threadSnapshot")(function* (args) {
           yield* annotateEnvironmentRequest(args.endpoint.name);
           yield* requireEnvironmentScope(AuthOrchestrationReadScope);
-          const [threadDetail, { snapshotSequence }] = yield* Effect.all([
-            projectionSnapshotQuery.getThreadDetailById(args.params.threadId),
-            projectionSnapshotQuery.getSnapshotSequence(),
-          ]).pipe(
-            Effect.catch((cause) =>
-              failEnvironmentInternal("orchestration_thread_snapshot_failed", cause),
-            ),
-          );
-          if (Option.isNone(threadDetail)) {
+          const snapshot = yield* projectionSnapshotQuery
+            .getThreadDetailSnapshot(args.params.threadId)
+            .pipe(
+              Effect.catch((cause) =>
+                failEnvironmentInternal("orchestration_thread_snapshot_failed", cause),
+              ),
+            );
+          if (Option.isNone(snapshot)) {
             return yield* failEnvironmentNotFound("thread_not_found");
           }
-          return { snapshotSequence, thread: threadDetail.value };
+          return snapshot.value;
         }),
       )
       .handle(

diff --git a/apps/server/src/project/ProjectSetupScriptRunner.test.ts b/apps/server/src/project/ProjectSetupScriptRunner.test.ts
--- a/apps/server/src/project/ProjectSetupScriptRunner.test.ts
+++ b/apps/server/src/project/ProjectSetupScriptRunner.test.ts
@@ -43,6 +43,7 @@
     getFullThreadDiffContext: () => Effect.die("unused"),
     getThreadShellById: () => Effect.die("unused"),
     getThreadDetailById: () => Effect.die("unused"),
+    getThreadDetailSnapshot: () => Effect.die("unused"),
   });
 
 const makeTerminalManagerLayer = (

diff --git a/apps/server/src/provider/Layers/ProviderSessionReaper.test.ts b/apps/server/src/provider/Layers/ProviderSessionReaper.test.ts
--- a/apps/server/src/provider/Layers/ProviderSessionReaper.test.ts
+++ b/apps/server/src/provider/Layers/ProviderSessionReaper.test.ts
@@ -209,6 +209,7 @@
                 : Option.none(),
             ),
           getThreadDetailById: () => Effect.die("unused"),
+          getThreadDetailSnapshot: () => Effect.die("unused"),
         }),
       ),
       Layer.provideMerge(NodeServices.layer),

diff --git a/apps/server/src/relay/AgentAwarenessRelay.test.ts b/apps/server/src/relay/AgentAwarenessRelay.test.ts
--- a/apps/server/src/relay/AgentAwarenessRelay.test.ts
+++ b/apps/server/src/relay/AgentAwarenessRelay.test.ts
@@ -461,6 +461,7 @@
           readEvents: () => Stream.empty,
           dispatch: () => Effect.succeed({ sequence: 1 }),
           streamDomainEvents: Stream.fromQueue(events),
+          subscribeDomainEvents: Effect.sync(() => Stream.fromQueue(events)),
         } satisfies OrchestrationEngineShape;
 
         const snapshotQuery = {
@@ -650,6 +651,7 @@
             readEvents: () => Stream.empty,
             dispatch: () => Effect.succeed({ sequence: 1 }),
             streamDomainEvents: Stream.fromQueue(events),
+            subscribeDomainEvents: Effect.sync(() => Stream.fromQueue(events)),
           } satisfies OrchestrationEngineShape),
           Layer.succeed(ProjectionSnapshotQuery, {
             getShellSnapshot: () =>

diff --git a/apps/server/src/serverRuntimeStartup.test.ts b/apps/server/src/serverRuntimeStartup.test.ts
--- a/apps/server/src/serverRuntimeStartup.test.ts
+++ b/apps/server/src/serverRuntimeStartup.test.ts
@@ -96,6 +96,7 @@
           getFullThreadDiffContext: () => Effect.succeed(Option.none()),
           getThreadShellById: () => Effect.succeed(Option.none()),
           getThreadDetailById: () => Effect.succeed(Option.none()),
+          getThreadDetailSnapshot: () => Effect.succeed(Option.none()),
         }),
         Effect.provideService(AnalyticsService.AnalyticsService, {
           record: () => Effect.void,
@@ -158,6 +159,7 @@
         getFullThreadDiffContext: () => Effect.succeed(Option.none()),
         getThreadShellById: () => Effect.die("unused"),
         getThreadDetailById: () => Effect.die("unused"),
+        getThreadDetailSnapshot: () => Effect.die("unused"),
       }),
       Effect.provideService(OrchestrationEngine.OrchestrationEngineService, {
         readEvents: () => Stream.empty,
@@ -166,6 +168,7 @@
             Effect.as({ sequence: 1 }),
           ),
         streamDomainEvents: Stream.empty,
+        subscribeDomainEvents: Effect.succeed(Stream.empty),
       } satisfies OrchestrationEngine.OrchestrationEngineService["Service"]),
       Effect.provide(NodeServices.layer),
     );
@@ -200,6 +203,7 @@
         getFullThreadDiffContext: () => Effect.succeed(Option.none()),
         getThreadShellById: () => Effect.die("unused"),
         getThreadDetailById: () => Effect.die("unused"),
+        getThreadDetailSnapshot: () => Effect.die("unused"),
       }),
       Effect.provideService(OrchestrationEngine.OrchestrationEngineService, {
         readEvents: () => Stream.empty,
@@ -208,6 +212,7 @@
             Effect.as({ sequence: 1 }),
           ),
         streamDomainEvents: Stream.empty,
+        subscribeDomainEvents: Effect.succeed(Stream.empty),
       } satisfies OrchestrationEngine.OrchestrationEngineService["Service"]),
       Effect.provide(NodeServices.layer),
     );
@@ -248,6 +253,7 @@
         getFullThreadDiffContext: () => Effect.succeed(Option.none()),
         getThreadShellById: () => Effect.die("unused"),
         getThreadDetailById: () => Effect.die("unused"),
+        getThreadDetailSnapshot: () => Effect.die("unused"),
       }),
       Effect.provideService(OrchestrationEngine.OrchestrationEngineService, {
         readEvents: () => Stream.empty,
@@ -256,6 +262,7 @@
             Effect.as({ sequence: 1 }),
           ),
         streamDomainEvents: Stream.empty,
+        subscribeDomainEvents: Effect.succeed(Stream.empty),
       } satisfies OrchestrationEngine.OrchestrationEngineService["Service"]),
       Effect.provideService(Crypto.Crypto, {
         ...crypto,

diff --git a/apps/server/src/ws.ts b/apps/server/src/ws.ts
--- a/apps/server/src/ws.ts
+++ b/apps/server/src/ws.ts
@@ -1119,7 +1119,12 @@
                 event.aggregateId === input.threadId &&
                 isThreadDetailEvent(event);
 
-              const liveStream = orchestrationEngine.streamDomainEvents.pipe(
+              // Attach the domain event subscription before loading the
+              // snapshot/catch-up baseline: the PubSub does not buffer for
+              // late subscribers, so events published while the baseline is
+              // read would otherwise be dropped. Overlapping events are
+              // deduped by sequence on the client.
+              const liveStream = (yield* orchestrationEngine.subscribeDomainEvents).pipe(
                 Stream.filter(isThisThreadDetailEvent),
                 Stream.map((event) => ({
                   kind: "event" as const,
@@ -1131,26 +1136,29 @@
               // that snapshot's sequence, and we resume the live subscription by
               // replaying persisted events after it instead of re-sending the
               // (potentially multi-KB) snapshot frame over the socket. The
-              // catch-up replay + live stream carry the same ordering guarantees
-              // as the snapshot-then-live path below; overlapping events are
-              // deduped by sequence on the client.
+              // catch-up replay must be exhaustive — a truncated replay would
+              // silently drop thread updates — so it bypasses the default
+              // replay cap.
               if (input.afterSequence !== undefined) {
-                const catchUpStream = orchestrationEngine.readEvents(input.afterSequence).pipe(
-                  Stream.filter(isThisThreadDetailEvent),
-                  Stream.map((event) => ({ kind: "event" as const, event })),
-                  Stream.mapError(
-                    (cause) =>
-                      new OrchestrationGetSnapshotError({
-                        message: `Failed to replay thread ${input.threadId} events`,
-                        cause,
-                      }),
-                  ),
-                );
+                const catchUpStream = orchestrationEngine
+                  .readEvents(input.afterSequence, Number.MAX_SAFE_INTEGER)
+                  .pipe(
+                    Stream.filter(isThisThreadDetailEvent),
+                    Stream.map((event) => ({ kind: "event" as const, event })),
+                    Stream.mapError(
+                      (cause) =>
+                        new OrchestrationGetSnapshotError({
+                          message: `Failed to replay thread ${input.threadId} events`,
+                          cause,
+                        }),
+                    ),
+                  );
                 return Stream.concat(catchUpStream, liveStream);
               }
 
-              const [threadDetail, snapshotSequence] = yield* Effect.all([
-                projectionSnapshotQuery.getThreadDetailById(input.threadId).pipe(
+              const threadSnapshot = yield* projectionSnapshotQuery
+                .getThreadDetailSnapshot(input.threadId)
+                .pipe(
                   Effect.mapError(
                     (cause) =>
                       new OrchestrationGetSnapshotError({
@@ -1158,20 +1166,9 @@
                         cause,
                       }),
                   ),
-                ),
-                projectionSnapshotQuery.getSnapshotSequence().pipe(
-                  Effect.map(({ snapshotSequence }) => snapshotSequence),
-                  Effect.mapError(
-                    (cause) =>
-                      new OrchestrationGetSnapshotError({
-                        message: "Failed to load orchestration snapshot sequence",
-                        cause,
-                      }),
-                  ),
-                ),
-              ]);
+                );
 
-              if (Option.isNone(threadDetail)) {
+              if (Option.isNone(threadSnapshot)) {
                 return yield* new OrchestrationGetSnapshotError({
                   message: `Thread ${input.threadId} was not found`,
                   cause: input.threadId,
@@ -1181,10 +1178,7 @@
               return Stream.concat(
                 Stream.make({
                   kind: "snapshot" as const,
-                  snapshot: {
-                    snapshotSequence,
-                    thread: threadDetail.value,
-                  },
+                  snapshot: threadSnapshot.value,
                 }),
                 liveStream,
               );

You can send follow-ups to the cloud agent here.

Comment thread apps/server/src/ws.ts Outdated
Comment thread apps/server/src/ws.ts Outdated
Comment thread apps/server/src/orchestration/http.ts Outdated
@macroscopeapp

macroscopeapp Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

1 blocking correctness issue found. This PR introduces new HTTP snapshot loading that changes the client-server sync flow. Two unresolved review comments identify potential correctness issues: live updates may be blocked during HTTP fetch, and shell catch-up may use incorrect state. These substantive concerns combined with the new feature scope warrant human review.

You can customize Macroscope's approvability policy. Learn more.

- ws.ts (afterSequence resume): attach the live PubSub subscription into a
  scope-bound buffer BEFORE draining the catch-up replay, so events published
  during the replay window are not lost; read the full range after the cursor
  (readEvents limit) so the per-thread filter can't be starved by a global cap.
- ProjectionSnapshotQuery: add getThreadDetailSnapshot that reads thread detail
  + snapshot sequence in one transaction, so the sequence is consistent with the
  returned state; use it in the HTTP handler and the WS snapshot path.
- OrchestrationEngine.readEvents: accept an optional limit.
- client loader: bound the HTTP snapshot timeout (cached data renders meanwhile)
  and treat a 404 as an expected defer-to-socket case rather than a warning.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread packages/client-runtime/src/state/threadSnapshotHttp.ts
juliusmarminge and others added 3 commits July 5, 2026 14:01
The loader layer resolved ManagedRelayDpopSigner eagerly at build, hard-requiring
it even though only relay/DPoP connections need it. Resolve it via
Effect.serviceOption and pass it through to buildAuthHeaders, so the layer only
requires HttpClient; bearer/primary connections no longer depend on a signer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Effect service conventions require catchTags (object form) over catchTag even
for a single tag.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…napshot

Part 1 (thread warm-cache): persist the snapshot sequence alongside the cached
thread (StoredThreadSnapshot v2) so a re-opened thread resumes from the cached
sequence via afterSequence — receiving only events since then — instead of
re-downloading its full body. Cold cache still loads the full snapshot over HTTP.

Part 2 (shell): the shell subscription now resumes the same way. Added an
afterSequence input to subscribeShell (server replays shell events after the
cursor, buffering live events before the catch-up replay, as in the thread path)
and a GET /api/orchestration/shell endpoint so a cold-cache full shell rides HTTP
instead of the socket. The client passes the cached/loaded shell sequence.

Shared the environment auth-header builder across the thread and shell HTTP
loaders.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions github-actions Bot added size:XL 500-999 changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels Jul 6, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix is ON. A cloud agent has been kicked off to fix the reported issue. You can view the agent here.

Reviewed by Cursor Bugbot for commit 9bc191b. Configure here.

Comment thread apps/server/src/ws.ts
cause,
}),
),
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shell catch-up uses current DB

Medium Severity

When subscribeShell resumes with afterSequence, catch-up replays persisted events through toShellStreamEvent, which loads project/thread shells from the current projection (getProjectShellById / getThreadShellById). Those reads reflect today’s state, not the state at each replayed sequence, so the client can apply the wrong shell data (or drop upserts for deleted rows) while still advancing snapshotSequence.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 9bc191b. Configure here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bugbot Autofix determined this is a false positive.

The shell stream replicates current state (whole-row upserts deduped by monotonic sequence), and because event append and projection update commit in one transaction, replayed events always carry data at a version >= their sequence while every deletion/archive emits its own payload-derived removal event later in the same replay, so the client provably converges to the server's current shell with no dropped changes — the same read-current-projection semantics the pre-existing live path already uses.

You can send follow-ups to the cloud agent here.

Primary/local environments with no bearer or DPoP credential authenticate the
browser via a session cookie, which a cross-origin fetch does not send by
default — so the thread/shell snapshot requests 401'd against a primary env on a
different origin. Add withEnvironmentCredentials (shared in environmentHttpAuth)
which opts those requests into credentials: "include"; bearer/DPoP connections
carry their credential in a header and are unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@juliusmarminge
juliusmarminge merged commit 482d562 into main Jul 6, 2026
16 checks passed
@juliusmarminge
juliusmarminge deleted the t3code/http-snapshot-loading branch July 6, 2026 05:00
mwolson added a commit to mwolson/t3code that referenced this pull request Jul 6, 2026
…pping

Remove dead upstream pingdotgg#3719 snapshot-loader modules and restore the
stack's versions of client-runtime rpc/connection files and the web
connection runtime/auth, which referenced contracts the stack replaced.
Integration tree only.
mwolson added a commit to mwolson/t3code that referenced this pull request Jul 7, 2026
…pping

Remove dead upstream pingdotgg#3719 snapshot-loader modules and restore the
stack's versions of client-runtime rpc/connection files and the web
connection runtime/auth, which referenced contracts the stack replaced.
Integration tree only.
juliusmarminge pushed a commit that referenced this pull request Jul 7, 2026
Ports main's features onto the V2 orchestrator and wire protocol:

- HTTP snapshot loading (#3719): new OrchestrationV2ThreadDetailSnapshot
  contract, /api/orchestration/{shell,threads/:threadId} endpoints served
  from the V2 engine (orchestration-v2/http.ts), afterSequence resume on
  the V2 subscribeShell/subscribeThread WS methods, and V2-typed client
  snapshot loaders wired into shell/thread sync with warm-cache resume.
- Thread cache now persists the snapshot sequence (cache schema v3) so
  warm caches resume via afterSequence instead of refetching.
- Mobile: main's react-navigation architecture retained; branch's V2
  surfaces (relationships banner, queue control, activity inspector,
  work-log thread links, fork-from-run) ported off expo-router; thread
  status presentation and awareness diagnostics mapped to V2 runtime
  fields; pending-task start-turn helper gains creationSource "mobile".
- Retired v1-only additions from main (v1 RPC schemas, session/latestTurn
  awareness heuristics, v1 engine test updates) where the branch already
  replaced those systems with V2 equivalents.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
olafura added a commit to olafura/t3code that referenced this pull request Jul 8, 2026
…nostics + snapshot loaders)

- Regenerate pnpm-lock (client-runtime's react devDeps for the shared
  lazy-load hook were missing from the pnpm-11 lockfile).
- Repair a doc comment the rebase union broke in Services/ProjectionSnapshotQuery.
- Satisfy main's new @effect/language-service diagnostics: TUI tsconfig
  disables nodeBuiltinImport/globalDate/globalTimers (a Node/Bun-native
  terminal app); file directives for cli/tui.ts, connection.ts, and the
  terminal Manager's CPR-gate wall-clock reads (Clock threading is a
  follow-up); return yield* at generator exits.
- Adapt the TUI to main's HTTP snapshot preloading (pingdotgg#3719): the in-memory
  cache stores {snapshotSequence, thread} detail snapshots, and no-op
  Thread/ShellSnapshotLoader layers fall back to the socket-embedded
  snapshots (the TUI's existing path).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
snipemanmike added a commit to snipemanmike/t3code that referenced this pull request Jul 10, 2026
…ation and transport negotiation

Opening a large thread still transfers the entire thread body — every
message, activity, and inline base64 blob since the thread began. On
cellular this never completes for media-heavy threads, and parsing the
resulting payload can OOM-kill the mobile app outright (observed: a
268MB thread snapshot). pingdotgg#3719 moved the snapshot off the socket; this
change bounds what is transferred at all.

Measured on a physical Android device over Tailscale:
- One media-heavy thread's snapshot was ~25MB on the wire; gzip only
  buys 1.6x on inline base64. With the bounded tail it renders in
  seconds over 5G.
- Sequence-cursor reconnects: 21,528KB re-sent per reconnect before,
  36-54KB after (~400x).

What this adds, all capability-gated so old peers are unaffected:

- subscribeThreadV2: a small thread head (metadata, latest turn,
  session, active plan, pending approval/input requests, counts) plus
  the last 32 messages / 128 compact activities under a 512KiB inline
  budget, emitted as <=256KiB chunks with keepalives between them, then
  live events after the snapshot watermark. Catch-up replays are
  epoch-checked and bounded to the watermark; a bounded (2048) live
  buffer signals an explicit resync instead of queueing unboundedly.
- getThreadHistoryPage: keyset pagination for older messages and
  activities (scroll-up on mobile), invalidated across thread.reverted
  via a history epoch so pages never mix pre/post-revert history.
- Activity payload externalization: base64 data-URLs and oversized
  payloads are stored as content-addressed attachments served by the
  existing /api/assets route; the wire carries references and mobile
  renders tap-to-load placeholders. (Message attachments already work
  this way — activity payloads were the unbounded route.)
- Transport negotiation: the environment descriptor advertises
  rpcTransports and threadSyncVersions (with decode defaults, so new
  clients read old descriptors); /ws stays byte-for-byte plain JSON and
  an optional gzip transport lives at /ws-compressed behind an optional
  RpcCompressionCodec (defaults to null; only opted-in platforms
  provide one). Cached-token reuse treats the negotiation probe as
  best-effort so offline reconnects keep working.
- Client: a distinct windowed thread state (never passed through APIs
  typed as a complete OrchestrationThread), atomic snapshot staging
  committed together with its cursor, resyncs that restart the
  subscription with a logged reason, and thread caches that store
  either a legacy detail snapshot or a v2 window (older records evict
  as cache misses). Array-by-copy methods are avoided in mobile-bound
  code: Hermes on current retail devices lacks toSorted and fails as a
  silent fiber defect.

Tests: v2 contract decode/round-trip, transactional tail and keyset
page queries with revert invalidation, wire round-trip -> staging
integration, windowed merge/revert reducers, mobile feed windowing, and
an opt-in real-data replay suite (T3_THREAD_SYNC_REALDATA_DB) that
replays production-scale threads through the pipeline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
tarik02 added a commit to tarik02/t3code that referenced this pull request Jul 13, 2026
* Add middle-click close for right panel tabs (pingdotgg#3161)

Co-authored-by: Julius Marminge <jmarminge@gmail.com>

* fix: warm WSL before preflight in WSL-only backend mode (pingdotgg#3588)

* Add Claude Sonnet 5 as the default Claude model (pingdotgg#3620)

* Restore the ultrathink frame border effect (pingdotgg#3625)

* fix(dev): Fix electron dev launch and add test (pingdotgg#3662)

* Add adaptive split-view layout for iPad/mobile workspace (pingdotgg#3514)

Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: Julius Marminge <julius@mac.lan>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* fix(mobile): compile patched native pods from source on EAS (pingdotgg#3667)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* Make the thread composer read as elevated liquid glass (pingdotgg#3668)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* Upgrade Vite Plus and enable bundled dev opt-in (pingdotgg#3679)

* Surface pending tasks in mobile home and draft flow (pingdotgg#3670)

* fix(mobile): combined test branch — scroll, back-swipe, thread lists, computer switching (pingdotgg#3687)

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* Add repo-root favicon.svg so t3 code shows its own icon (pingdotgg#3683)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* Load thread snapshots over HTTP before live sync (pingdotgg#3719)

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* Fix mobile legend anchor under automatic iOS insets (pingdotgg#3684)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* Improve live activity routing and diagnostics (pingdotgg#3685)

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* Prevent Add Project sheet from collapsing on relayout (pingdotgg#3759)

* Use variant-specific splash icons in mobile app (pingdotgg#3762)

* Fix Expo widget asset wiring order (pingdotgg#3763)

* Extend Done display to 15 minutes and show up to 5 Live Activity banner rows (pingdotgg#3761)

* Clear VCS presentation state on finish (pingdotgg#3764)

* Lead with the outcome when no agents are active in the Live Activity (pingdotgg#3768)

* Add T3 Connect onboarding for mobile and web (pingdotgg#3765)

* Revert "Add T3 Connect onboarding for mobile and web" (pingdotgg#3776)

* Expose Clerk Google sign-in env vars to Expo (pingdotgg#3772)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* Set up Cursor Cloud dev environment (web + Android toolchain) (pingdotgg#3755)

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Theo Browne <t3dotgg@users.noreply.github.com>

* Revert "Revert "Add T3 Connect onboarding for mobile and web"" (pingdotgg#3777)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* Use rounded depth logo for production splash screen (pingdotgg#3780)

Co-authored-by: Cursor Agent <cursoragent@cursor.com>

* fix(release): stage pnpm 11 allowBuilds for desktop installs (pingdotgg#3781)

Co-authored-by: Cursor Agent <cursoragent@cursor.com>

* Upgrade Clerk toolchain to latest versions (pingdotgg#3785)

* fix(release): bump electron-builder so pnpm 11 deduped deps land in the asar (pingdotgg#3790)

* Fix desktop native optional dependency packaging (pingdotgg#3816)

* [codex] Upgrade Clerk stack (pingdotgg#3821)

Co-authored-by: codex <codex@users.noreply.github.com>

* [codex] Preserve worktree metadata during branch sync (pingdotgg#3822)

Co-authored-by: codex <codex@users.noreply.github.com>

* feat(client): persist offline environment data and mobile preferences (pingdotgg#3795)

Co-authored-by: Julius Marminge <julius@mac.lan>
Co-authored-by: codex <codex@users.noreply.github.com>

* [codex] Label max and ultra reasoning (pingdotgg#3824)

Co-authored-by: codex <codex@users.noreply.github.com>

* fix(mobile): embed fonts and render project favicons reliably (pingdotgg#3823)

Co-authored-by: codex <codex@users.noreply.github.com>

* Show compact PR number badges in mobile thread rows (pingdotgg#3827)

Co-authored-by: codex <codex@users.noreply.github.com>

* Expose mobile PR indicator labels to accessibility (pingdotgg#3828)

Co-authored-by: codex <codex@users.noreply.github.com>

* Fix truncated chat error alert layout (pingdotgg#3899)

* fix(marketing): show platform-appropriate commit shortcut on the website (pingdotgg#3644)

* [codex] Add Android mobile support (pingdotgg#3579)

Co-authored-by: Horus Lugo <horusgoul@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: maria-rcks <maria@kuuro.net>
Co-authored-by: Shivam Sharma <91240327+shivamhwp@users.noreply.github.com>
Co-authored-by: Ben Davis <45952064+bmdavis419@users.noreply.github.com>
Co-authored-by: Alex <me@pixp.cc>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: Julius Marminge <julius@mac.lan>

---------

Co-authored-by: Hugo Blom <6117705+huxcrux@users.noreply.github.com>
Co-authored-by: Julius Marminge <jmarminge@gmail.com>
Co-authored-by: Utkarsh Patil <73941998+UtkarshUsername@users.noreply.github.com>
Co-authored-by: Julius Marminge <julius0216@outlook.com>
Co-authored-by: Theo Browne <me@t3.gg>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: Julius Marminge <julius@mac.lan>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Theo Browne <t3dotgg@users.noreply.github.com>
Co-authored-by: Rowan <rowan@cardow.co>
Co-authored-by: Patricio Gómez Meneses <107218376+Prgm-code@users.noreply.github.com>
Co-authored-by: Jake Leventhal <jakeleventhal@me.com>
Co-authored-by: Vedank Purohit <VedankPurohit2@gmail.com>
Co-authored-by: Horus Lugo <horusgoul@gmail.com>
Co-authored-by: maria-rcks <maria@kuuro.net>
Co-authored-by: Shivam Sharma <91240327+shivamhwp@users.noreply.github.com>
Co-authored-by: Ben Davis <45952064+bmdavis419@users.noreply.github.com>
Co-authored-by: Alex <me@pixp.cc>
olafura added a commit to olafura/t3code that referenced this pull request Jul 13, 2026
…nostics + snapshot loaders)

- Regenerate pnpm-lock (client-runtime's react devDeps for the shared
  lazy-load hook were missing from the pnpm-11 lockfile).
- Repair a doc comment the rebase union broke in Services/ProjectionSnapshotQuery.
- Satisfy main's new @effect/language-service diagnostics: TUI tsconfig
  disables nodeBuiltinImport/globalDate/globalTimers (a Node/Bun-native
  terminal app); file directives for cli/tui.ts, connection.ts, and the
  terminal Manager's CPR-gate wall-clock reads (Clock threading is a
  follow-up); return yield* at generator exits.
- Adapt the TUI to main's HTTP snapshot preloading (pingdotgg#3719): the in-memory
  cache stores {snapshotSequence, thread} detail snapshots, and no-op
  Thread/ShellSnapshotLoader layers fall back to the socket-embedded
  snapshots (the TUI's existing path).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
olafura added a commit to olafura/t3code that referenced this pull request Jul 15, 2026
…nostics + snapshot loaders)

- Regenerate pnpm-lock (client-runtime's react devDeps for the shared
  lazy-load hook were missing from the pnpm-11 lockfile).
- Repair a doc comment the rebase union broke in Services/ProjectionSnapshotQuery.
- Satisfy main's new @effect/language-service diagnostics: TUI tsconfig
  disables nodeBuiltinImport/globalDate/globalTimers (a Node/Bun-native
  terminal app); file directives for cli/tui.ts, connection.ts, and the
  terminal Manager's CPR-gate wall-clock reads (Clock threading is a
  follow-up); return yield* at generator exits.
- Adapt the TUI to main's HTTP snapshot preloading (pingdotgg#3719): the in-memory
  cache stores {snapshotSequence, thread} detail snapshots, and no-op
  Thread/ShellSnapshotLoader layers fall back to the socket-embedded
  snapshots (the TUI's existing path).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
olafura added a commit to olafura/t3code that referenced this pull request Jul 16, 2026
…nostics + snapshot loaders)

- Regenerate pnpm-lock (client-runtime's react devDeps for the shared
  lazy-load hook were missing from the pnpm-11 lockfile).
- Repair a doc comment the rebase union broke in Services/ProjectionSnapshotQuery.
- Satisfy main's new @effect/language-service diagnostics: TUI tsconfig
  disables nodeBuiltinImport/globalDate/globalTimers (a Node/Bun-native
  terminal app); file directives for cli/tui.ts, connection.ts, and the
  terminal Manager's CPR-gate wall-clock reads (Clock threading is a
  follow-up); return yield* at generator exits.
- Adapt the TUI to main's HTTP snapshot preloading (pingdotgg#3719): the in-memory
  cache stores {snapshotSequence, thread} detail snapshots, and no-op
  Thread/ShellSnapshotLoader layers fall back to the socket-embedded
  snapshots (the TUI's existing path).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
olafura added a commit to olafura/t3code that referenced this pull request Jul 21, 2026
…nostics + snapshot loaders)

- Regenerate pnpm-lock (client-runtime's react devDeps for the shared
  lazy-load hook were missing from the pnpm-11 lockfile).
- Repair a doc comment the rebase union broke in Services/ProjectionSnapshotQuery.
- Satisfy main's new @effect/language-service diagnostics: TUI tsconfig
  disables nodeBuiltinImport/globalDate/globalTimers (a Node/Bun-native
  terminal app); file directives for cli/tui.ts, connection.ts, and the
  terminal Manager's CPR-gate wall-clock reads (Clock threading is a
  follow-up); return yield* at generator exits.
- Adapt the TUI to main's HTTP snapshot preloading (pingdotgg#3719): the in-memory
  cache stores {snapshotSequence, thread} detail snapshots, and no-op
  Thread/ShellSnapshotLoader layers fall back to the socket-embedded
  snapshots (the TUI's existing path).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL 500-999 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant