History

feat(orchestration): bound thread history and resume payloads

- Add paginated thread history with bounded snapshots and replay limits - Trim oversized wire payloads and support progressive mobile history loading

Julius Marminge committed 3 months ago

Commit 7d42d311 parent e3883a943 files · +4239 −211Browse files

Showing the changes to packages/client-runtime/src/state/threadSnapshotHttp.ts (1 of 43 files). Show all

Unified · Split

packages/client-runtime/src/state/threadSnapshotHttp.ts+42−13
@@ -18,6 +18,30 @@ import { executeAuthenticatedEnvironmentHttpRequest } from "./environmentHttpAut · 17 unmodified lines
1818 // delays the transition to live data on the first open, not the initial paint.
1919 const DEFAULT_THREAD_SNAPSHOT_TIMEOUT_MS = 6_000;
2020
21+/** Progressive history metadata returned by a bounded snapshot loader. */
22+export type ThreadSnapshotHistoryMeta = {
23+ readonly historyCursor: string | null;
24+ readonly hasMoreHistory: boolean;
25+ /** Max local turn ordinal from the full projection; optional on older servers. */
26+ readonly latestLocalTurnOrdinal?: number | null;
27+};
28+
29+/**
30+ * Outcome of an HTTP thread-detail snapshot load.
31+ *
32+ * - `present`: snapshot body is available (seed projection, resume via socket).
33+ * - `missing`: server definitively reported the thread does not exist (404).
34+ * - `unavailable`: transport/timeout/5xx/etc.; fall back to the socket path.
35+ */
36+export type ThreadSnapshotLoadResult =
37+ | {
38+ readonly _tag: "present";
39+ readonly snapshot: OrchestrationV2ThreadDetailSnapshot;
40+ readonly history?: ThreadSnapshotHistoryMeta;
41+ }
42+ | { readonly _tag: "missing" }
43+ | { readonly _tag: "unavailable" };
44+
2145 /**
2246 * Load a thread's detail snapshot over HTTP instead of embedding it in the
2347 * WebSocket subscription's first frame. The response is gzip-compressible by
@@ -49,10 +73,12 @@ export const fetchEnvironmentThreadSnapshot = Effect.fn( · 25 unmodified lines
4973 export type FetchEnvironmentThreadSnapshotError = RemoteEnvironmentRequestError;
5074
5175 /**
52− * Loads a thread's detail snapshot over HTTP, returning `Option.none()` when it
53− * cannot be loaded (so the caller falls back to the socket-embedded snapshot).
54− * Decouples the thread state machine from the underlying HTTP + DPoP details and
55− * keeps them out of test contexts.
76+ * Loads a thread's detail snapshot over HTTP.
77+ *
78+ * Distinguishes a definitive missing thread (HTTP 404) from transient snapshot
79+ * unavailability so the thread state machine can mark the thread deleted without
80+ * opening a socket subscription, while still falling back to the socket when the
81+ * HTTP path is merely unavailable.
5682 */
5783 export class ThreadSnapshotLoader extends Context.Service<
5884 ThreadSnapshotLoader,
@@ -60,7 +86,7 @@ export class ThreadSnapshotLoader extends Context.Service< · 1 unmodified line
6086 readonly load: (
6187 prepared: PreparedConnection,
6288 threadId: ThreadId,
63− ) => Effect.Effect<Option.Option<OrchestrationV2ThreadDetailSnapshot>>;
89+ ) => Effect.Effect<ThreadSnapshotLoadResult>;
6490 }
6591 >()("@t3tools/client-runtime/state/threadSnapshotHttp/ThreadSnapshotLoader") {}
6692
@@ -80,19 +106,22 @@ export const threadSnapshotLoaderLayer: Layer.Layer< · 13 unmodified lines
80106 return ThreadSnapshotLoader.of({
81107 load: (prepared: PreparedConnection, threadId: ThreadId) =>
82108 fetchEnvironmentThreadSnapshot({ prepared, threadId, signer }).pipe(
83− Effect.map(Option.some<OrchestrationV2ThreadDetailSnapshot>),
109+ Effect.map(
110+ (snapshot): ThreadSnapshotLoadResult => ({
111+ _tag: "present",
112+ snapshot,
113+ }),
114+ ),
84115 Effect.provideService(HttpClient.HttpClient, httpClient),
85− // A genuinely missing thread (404) is expected — the socket
86− // subscription is the source of truth for thread existence and will
87− // surface the deletion — so don't treat it as an error worth warning
88− // about; just defer to the socket path.
116+ // A genuinely missing thread (404) is definitive: do not fall back to
117+ // the socket or retry. Callers mark the thread deleted and clear cache.
89118 Effect.catchTags({
90119 EnvironmentResourceNotFoundError: () =>
91120 Effect.logDebug(
92− "Thread snapshot not found over HTTP; deferring to the socket subscription.",
121+ "Thread snapshot not found over HTTP; treating the thread as deleted.",
93122 ).pipe(
94123 Effect.annotateLogs({ threadId }),
95− Effect.as(Option.none<OrchestrationV2ThreadDetailSnapshot>()),
124+ Effect.as({ _tag: "missing" } satisfies ThreadSnapshotLoadResult),
96125 ),
97126 }),
98127 Effect.catchCause((cause) =>
@@ -100,7 +129,7 @@ export const threadSnapshotLoaderLayer: Layer.Layer< · 1 unmodified line
100129 "Could not load the thread snapshot over HTTP; using the socket snapshot instead.",
101130 ).pipe(
102131 Effect.annotateLogs({ threadId, cause: Cause.pretty(cause) }),
103− Effect.as(Option.none<OrchestrationV2ThreadDetailSnapshot>()),
132+ Effect.as({ _tag: "unavailable" } satisfies ThreadSnapshotLoadResult),
104133 ),
105134 ),
106135 ),

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t