@runtypelabs/persona 4.5.0 → 4.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/animations/glyph-cycle.d.cts +1 -1
  2. package/dist/animations/glyph-cycle.d.ts +1 -1
  3. package/dist/animations/{types-C6tFDxKy.d.cts → types-CSmiKRVa.d.cts} +11 -0
  4. package/dist/animations/{types-C6tFDxKy.d.ts → types-CSmiKRVa.d.ts} +11 -0
  5. package/dist/animations/wipe.d.cts +1 -1
  6. package/dist/animations/wipe.d.ts +1 -1
  7. package/dist/chunk-DFBSCFYN.js +1 -0
  8. package/dist/codegen.cjs +4 -4
  9. package/dist/codegen.js +4 -4
  10. package/dist/index.cjs +51 -51
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.cts +238 -3
  13. package/dist/index.d.ts +238 -3
  14. package/dist/index.global.js +37 -37
  15. package/dist/index.global.js.map +1 -1
  16. package/dist/index.js +51 -51
  17. package/dist/index.js.map +1 -1
  18. package/dist/launcher.global.js.map +1 -1
  19. package/dist/markdown-parsers-entry-NVFT3TE6.js +1 -0
  20. package/dist/runtype-tts-entry-HFUV2UF7.js +1 -0
  21. package/dist/session-reconnect-U77QFUR7.js +1 -0
  22. package/dist/smart-dom-reader.d.cts +95 -0
  23. package/dist/smart-dom-reader.d.ts +95 -0
  24. package/dist/theme-editor-preview.cjs +48 -48
  25. package/dist/theme-editor-preview.d.cts +135 -1
  26. package/dist/theme-editor-preview.d.ts +135 -1
  27. package/dist/theme-editor-preview.js +48 -48
  28. package/dist/theme-editor.d.cts +95 -0
  29. package/dist/theme-editor.d.ts +95 -0
  30. package/package.json +5 -3
  31. package/src/client.ts +55 -9
  32. package/src/generated/runtype-openapi-contract.ts +16 -1
  33. package/src/reconnect-wake.test.ts +162 -0
  34. package/src/reconnect.test.ts +430 -0
  35. package/src/session-reconnect.ts +282 -0
  36. package/src/session.ts +408 -5
  37. package/src/types.ts +107 -1
  38. package/src/ui.stream-animation-update.test.ts +99 -0
  39. package/src/ui.ts +71 -1
  40. package/src/utils/constants.ts +3 -1
  41. package/src/utils/morph.test.ts +47 -0
  42. package/src/utils/morph.ts +18 -0
@@ -0,0 +1,282 @@
1
+ // Durable-session reconnect orchestration (the cold path).
2
+ //
3
+ // This module holds the heavy reconnect machinery — the bounded backoff loop,
4
+ // the focus/online wake listeners, and the give-up finalizer. It is only ever
5
+ // reached through a dynamic `import("./session-reconnect")` in `session.ts`,
6
+ // gated on `config.reconnectStream`, so a widget that never opts into durable
7
+ // reconnect (the overwhelming majority, including the theme-editor preview)
8
+ // never loads it. With code-splitting enabled on the ESM subpath builds, that
9
+ // keeps this code out of those bundles entirely.
10
+ //
11
+ // IMPORTANT: this module must NOT statically import from `session.ts` (that
12
+ // would form a runtime cycle and pull the whole session back into the split
13
+ // chunk, defeating the point). It reaches everything it needs through the
14
+ // `ReconnectHost` interface the session hands it. Type-only imports are erased
15
+ // at build time, so importing `AgentWidgetSessionStatus` as a type is fine.
16
+
17
+ import type {
18
+ AgentWidgetConfig,
19
+ AgentWidgetMessage,
20
+ ResumableHandle,
21
+ } from "./types";
22
+ import type { AgentWidgetSessionStatus } from "./session";
23
+
24
+ /**
25
+ * The narrow surface the reconnect loop needs from the owning session. The
26
+ * session builds this once (lazily, alongside the controller) so the loop can
27
+ * read/advance the resume handle, drive status/streaming, and pipe the resumed
28
+ * stream back through the session's own `connectStream`.
29
+ */
30
+ export interface ReconnectHost {
31
+ readonly config: AgentWidgetConfig;
32
+ getResumable(): ResumableHandle | null;
33
+ clearResumable(): void;
34
+ getStatus(): AgentWidgetSessionStatus;
35
+ setStatus(status: AgentWidgetSessionStatus): void;
36
+ setStreaming(streaming: boolean): void;
37
+ setReconnecting(value: boolean): void;
38
+ setAbortController(controller: AbortController | null): void;
39
+ getMessages(): AgentWidgetMessage[];
40
+ notifyMessagesChanged(): void;
41
+ /** Pipe a resumed SSE body through the session, filling the same bubble. */
42
+ resumeConnect(
43
+ body: ReadableStream<Uint8Array>,
44
+ assistantMessageId: string,
45
+ seedContent: string
46
+ ): Promise<void>;
47
+ appendMessage(message: AgentWidgetMessage): void;
48
+ nextSequence(): number;
49
+ emitReconnect(event: {
50
+ phase: "paused" | "resuming" | "resumed";
51
+ handle: ResumableHandle;
52
+ attempt?: number;
53
+ }): void;
54
+ /** Build the give-up bubble content (honors `config.errorMessage`). */
55
+ buildErrorContent(message: string): string;
56
+ onError(error: Error): void;
57
+ }
58
+
59
+ export interface ReconnectController {
60
+ /** Start the backoff loop. The session has already flipped the visible state
61
+ * (streaming + `resuming` status + `paused` event) synchronously. */
62
+ begin(): void;
63
+ /** Cancel any pending backoff and drop the wake listeners. */
64
+ teardown(): void;
65
+ /** Short-circuit the current backoff sleep (focus/online or manual retry). */
66
+ wake(): void;
67
+ }
68
+
69
+ const DEFAULT_BACKOFF_MS = [1000, 2000, 4000, 8000, 8000];
70
+
71
+ export function createReconnectController(
72
+ host: ReconnectHost
73
+ ): ReconnectController {
74
+ // 1-based attempt counter for the active run (0 = not running).
75
+ let attempt = 0;
76
+ let waitResolve: (() => void) | null = null;
77
+ let waitTimer: ReturnType<typeof setTimeout> | null = null;
78
+ let listenersAttached = false;
79
+
80
+ const wake = (): void => {
81
+ if (waitTimer) {
82
+ clearTimeout(waitTimer);
83
+ waitTimer = null;
84
+ }
85
+ if (waitResolve) {
86
+ const resolve = waitResolve;
87
+ waitResolve = null;
88
+ resolve();
89
+ }
90
+ };
91
+
92
+ // Short-circuit the active backoff sleep, but only while a reconnect is
93
+ // genuinely in flight (`resuming`/`paused`).
94
+ const wakeIfActive = (): void => {
95
+ const status = host.getStatus();
96
+ if (status !== "resuming" && status !== "paused") return;
97
+ wake();
98
+ };
99
+
100
+ // `visibilitychange` fires on both show and hide; only wake on the show
101
+ // transition (ignore the tab being backgrounded).
102
+ const handleVisibility = (): void => {
103
+ if (
104
+ typeof document !== "undefined" &&
105
+ document.visibilityState === "hidden"
106
+ ) {
107
+ return;
108
+ }
109
+ wakeIfActive();
110
+ };
111
+
112
+ // Connectivity restored: retry immediately, even in a background tab — the
113
+ // whole point of the `online` listener is to react before the backoff timer,
114
+ // so it must NOT inherit the visibility guard above.
115
+ const handleOnline = (): void => {
116
+ wakeIfActive();
117
+ };
118
+
119
+ const attachListeners = (): void => {
120
+ if (listenersAttached) return;
121
+ if (typeof document !== "undefined") {
122
+ document.addEventListener("visibilitychange", handleVisibility);
123
+ }
124
+ if (typeof window !== "undefined") {
125
+ window.addEventListener("online", handleOnline);
126
+ }
127
+ listenersAttached = true;
128
+ };
129
+
130
+ const detachListeners = (): void => {
131
+ if (!listenersAttached) return;
132
+ if (typeof document !== "undefined") {
133
+ document.removeEventListener("visibilitychange", handleVisibility);
134
+ }
135
+ if (typeof window !== "undefined") {
136
+ window.removeEventListener("online", handleOnline);
137
+ }
138
+ listenersAttached = false;
139
+ };
140
+
141
+ const waitBackoff = (ms: number): Promise<void> =>
142
+ new Promise<void>((resolve) => {
143
+ waitResolve = resolve;
144
+ waitTimer = setTimeout(() => {
145
+ waitTimer = null;
146
+ waitResolve = null;
147
+ resolve();
148
+ }, ms);
149
+ });
150
+
151
+ /** Give up after exhausting attempts: finalize the bubble and surface error. */
152
+ const finalizeFailure = (): void => {
153
+ const handle = host.getResumable();
154
+ host.clearResumable();
155
+ host.setReconnecting(false);
156
+ attempt = 0;
157
+ detachListeners();
158
+ host.setAbortController(null);
159
+
160
+ let changed = false;
161
+ for (const msg of host.getMessages()) {
162
+ if (msg.streaming) {
163
+ msg.streaming = false;
164
+ changed = true;
165
+ }
166
+ }
167
+
168
+ const content = host.buildErrorContent(
169
+ "Connection lost and the response could not be resumed."
170
+ );
171
+ if (content) {
172
+ host.appendMessage({
173
+ id: `reconnect-failed-${handle?.executionId ?? host.nextSequence()}`,
174
+ role: "assistant",
175
+ content,
176
+ createdAt: new Date().toISOString(),
177
+ streaming: false,
178
+ sequence: host.nextSequence(),
179
+ });
180
+ } else if (changed) {
181
+ host.notifyMessagesChanged();
182
+ }
183
+
184
+ host.setStreaming(false);
185
+ host.setStatus("idle");
186
+ host.onError(new Error("Durable session reconnect failed."));
187
+ };
188
+
189
+ const runLoop = async (): Promise<void> => {
190
+ const backoff = host.config.reconnect?.backoffMs ?? DEFAULT_BACKOFF_MS;
191
+ const maxAttempts = host.config.reconnect?.maxAttempts ?? backoff.length;
192
+ const reconnectStream = host.config.reconnectStream;
193
+ if (!reconnectStream) {
194
+ host.setReconnecting(false);
195
+ return;
196
+ }
197
+
198
+ while (host.getResumable() && attempt < maxAttempts) {
199
+ attempt += 1;
200
+ // An attempt is in flight. No-op for attempt 1 (the session's synchronous
201
+ // `beginReconnect` already set `resuming`); flips back from `paused` after
202
+ // each backoff wait.
203
+ host.setStatus("resuming");
204
+ const handle = host.getResumable()!;
205
+ const before = handle.lastEventId;
206
+ const controller = new AbortController();
207
+ host.setAbortController(controller);
208
+ host.emitReconnect({ phase: "resuming", handle, attempt });
209
+
210
+ let body: ReadableStream<Uint8Array> | null = null;
211
+ try {
212
+ const res = await reconnectStream({
213
+ executionId: handle.executionId,
214
+ after: handle.lastEventId,
215
+ signal: controller.signal,
216
+ });
217
+ if (res && res.ok && res.body) body = res.body;
218
+ } catch {
219
+ body = null;
220
+ }
221
+ if (controller.signal.aborted) return; // torn down (cancel/new turn)
222
+
223
+ if (body) {
224
+ // Seed the resume with the text already shown so post-cursor deltas
225
+ // append rather than clobber the bubble.
226
+ const found = host
227
+ .getMessages()
228
+ .find((m) => m.id === handle.assistantMessageId)?.content;
229
+ const seedContent = typeof found === "string" ? found : "";
230
+ try {
231
+ await host.resumeConnect(body, handle.assistantMessageId, seedContent);
232
+ } catch {
233
+ // resumeConnect swallows resume-stream errors while `resuming`;
234
+ // anything escaping here just falls through to backoff/retry.
235
+ }
236
+ if (controller.signal.aborted) return;
237
+ // Reconnect reached a terminal: the idle(terminal)/error handler cleared
238
+ // the handle. We're done.
239
+ if (!host.getResumable()) {
240
+ host.setReconnecting(false);
241
+ attempt = 0;
242
+ detachListeners();
243
+ host.emitReconnect({ phase: "resumed", handle });
244
+ return;
245
+ }
246
+ // Made forward progress before dropping again → reset the attempt
247
+ // budget so a long, flaky run isn't capped by total drops.
248
+ if (host.getResumable()!.lastEventId !== before) attempt = 0;
249
+ }
250
+
251
+ if (host.getResumable() && attempt < maxAttempts) {
252
+ // Dropped and waiting to retry: surface `paused` so
253
+ // `statusIndicator.pausedText` renders during the backoff sleep.
254
+ host.setStatus("paused");
255
+ await waitBackoff(
256
+ backoff[Math.min(attempt - 1, backoff.length - 1)] ?? 1000
257
+ );
258
+ if (controller.signal.aborted) return;
259
+ }
260
+ }
261
+
262
+ if (host.getResumable()) finalizeFailure();
263
+ };
264
+
265
+ return {
266
+ begin(): void {
267
+ attempt = 0;
268
+ attachListeners();
269
+ void runLoop();
270
+ },
271
+ teardown(): void {
272
+ if (waitTimer) {
273
+ clearTimeout(waitTimer);
274
+ waitTimer = null;
275
+ }
276
+ waitResolve = null;
277
+ attempt = 0;
278
+ detachListeners();
279
+ },
280
+ wake,
281
+ };
282
+ }