@agent-native/core 0.101.3 → 0.101.4

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 (41) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +7 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/client/RunStuckBanner.tsx +12 -5
  5. package/corpus/core/src/client/sse-event-processor.ts +36 -1
  6. package/corpus/core/src/client/use-run-stuck-detection.ts +162 -25
  7. package/corpus/core/src/deploy/build.ts +78 -25
  8. package/corpus/templates/chat/changelog/2026-07-14-chat-opens-reliably-on-hosted-deployments-instead-of-failing.md +6 -0
  9. package/corpus/templates/clips/app/components/player/media-duration.ts +24 -0
  10. package/corpus/templates/clips/app/components/player/video-player.tsx +9 -18
  11. package/corpus/templates/clips/changelog/2026-07-14-paused-time-no-longer-counts-toward-chrome-extension-recordi.md +6 -0
  12. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +30 -4
  13. package/corpus/templates/clips/chrome-extension/src/recording-duration.ts +32 -0
  14. package/corpus/templates/slides/actions/_uploaded-files.ts +13 -2
  15. package/corpus/templates/slides/actions/import-docx.ts +3 -9
  16. package/corpus/templates/slides/actions/import-file.ts +11 -14
  17. package/corpus/templates/slides/actions/import-pptx.ts +3 -9
  18. package/corpus/templates/slides/changelog/2026-07-14-powerpoint-template-uploads-now-work-in-hosted-slides-deploy.md +6 -0
  19. package/corpus/templates/slides/server/handlers/uploads.ts +100 -50
  20. package/corpus/templates/slides/server/lib/tenant-files.ts +38 -11
  21. package/corpus/templates/slides/server/lib/uploaded-reference-storage.ts +108 -0
  22. package/dist/client/RunStuckBanner.d.ts.map +1 -1
  23. package/dist/client/RunStuckBanner.js +9 -5
  24. package/dist/client/RunStuckBanner.js.map +1 -1
  25. package/dist/client/sse-event-processor.d.ts.map +1 -1
  26. package/dist/client/sse-event-processor.js +33 -1
  27. package/dist/client/sse-event-processor.js.map +1 -1
  28. package/dist/client/use-run-stuck-detection.d.ts +4 -4
  29. package/dist/client/use-run-stuck-detection.d.ts.map +1 -1
  30. package/dist/client/use-run-stuck-detection.js +104 -19
  31. package/dist/client/use-run-stuck-detection.js.map +1 -1
  32. package/dist/collab/awareness.d.ts +2 -2
  33. package/dist/collab/awareness.d.ts.map +1 -1
  34. package/dist/deploy/build.d.ts +15 -14
  35. package/dist/deploy/build.d.ts.map +1 -1
  36. package/dist/deploy/build.js +64 -23
  37. package/dist/deploy/build.js.map +1 -1
  38. package/dist/notifications/routes.d.ts +3 -3
  39. package/dist/progress/routes.d.ts +1 -1
  40. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  41. package/package.json +1 -1
package/corpus/README.md CHANGED
@@ -28,4 +28,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
28
28
  ## Generated Counts
29
29
 
30
30
  - core files: 2258
31
- - template files: 5622
31
+ - template files: 5628
@@ -1,5 +1,12 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.101.4
4
+
5
+ ### Patch Changes
6
+
7
+ - 7d72df1: Keep chat responsive while replaying dense agent event bursts, and surface safe cancellation when a live background run has gone quiet.
8
+ - 7d72df1: Bundle one complete Yjs runtime into serverless deploy output so SSR starts without missing named exports.
9
+
3
10
  ## 0.101.3
4
11
 
5
12
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.101.3",
3
+ "version": "0.101.4",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -323,10 +323,17 @@ export function RunStuckBanner({
323
323
  };
324
324
 
325
325
  const handleRetry = async () => {
326
- // Defense in depth: the button is hidden while `inFlightWork` is true
327
- // (see render below), but guard the handler itself too so an abort can
328
- // never be triggered against live work through this path.
329
- if (!state.runId || busy.type !== "none" || inFlightWork) return;
326
+ // Defense in depth: Retry is hidden while a background worker is still
327
+ // heartbeating or work is explicitly in flight (see render below). Guard
328
+ // the handler too so this path cannot abort a run the server says is alive.
329
+ if (
330
+ !state.runId ||
331
+ busy.type !== "none" ||
332
+ backgroundWorkerStillAlive ||
333
+ inFlightWork
334
+ ) {
335
+ return;
336
+ }
330
337
  const runId = state.runId;
331
338
  setBusy({ type: "retry", runId });
332
339
  trackEvent("agent_chat_stuck_retry", {
@@ -380,7 +387,7 @@ export function RunStuckBanner({
380
387
  </span>
381
388
  </div>
382
389
  <div className="flex flex-wrap items-center gap-2">
383
- {inFlightWork ? null : (
390
+ {stillWorking ? null : (
384
391
  <button
385
392
  type="button"
386
393
  onClick={handleRetry}
@@ -112,6 +112,20 @@ const INTERRUPTED_TOOL_RESULT =
112
112
  "Interrupted before this tool returned a result.";
113
113
  const INTERRUPTED_ACTIVITY_RESULT = "Stopped before this action started.";
114
114
 
115
+ /**
116
+ * Maximum number of assistant-ui repository updates we deliver in one browser
117
+ * event-loop turn. Durable-run replay can put hundreds of SSE frames into the
118
+ * stream queue before the client attaches; draining all of them through
119
+ * assistant-ui without a macrotask boundary synchronously nests React external
120
+ * store notifications until React throws "Maximum update depth exceeded."
121
+ *
122
+ * A timer scheduled on the first result resets the count when the stream is
123
+ * naturally idle between network chunks. We only await it when results are
124
+ * arriving densely enough to hit this bound, so normal live token streaming
125
+ * keeps its existing latency while replay bursts yield cooperatively.
126
+ */
127
+ const SSE_RENDER_UPDATES_PER_EVENT_LOOP_TURN = 20;
128
+
115
129
  export function settleInterruptedToolCalls(
116
130
  content: ContentPart[],
117
131
  result = INTERRUPTED_TOOL_RESULT,
@@ -1609,6 +1623,24 @@ export async function* readSSEStream(
1609
1623
  completedToolsAfterLastAssistantText: new Set(),
1610
1624
  streamProgressDispatched: false,
1611
1625
  };
1626
+ let renderUpdatesThisTurn = 0;
1627
+ let nextEventLoopTurn: Promise<void> | null = null;
1628
+
1629
+ const paceRenderUpdate = async (): Promise<void> => {
1630
+ renderUpdatesThisTurn += 1;
1631
+ if (!nextEventLoopTurn) {
1632
+ nextEventLoopTurn = new Promise<void>((resolve) => {
1633
+ setTimeout(() => {
1634
+ renderUpdatesThisTurn = 0;
1635
+ nextEventLoopTurn = null;
1636
+ resolve();
1637
+ }, 0);
1638
+ });
1639
+ }
1640
+ if (renderUpdatesThisTurn >= SSE_RENDER_UPDATES_PER_EVENT_LOOP_TURN) {
1641
+ await nextEventLoopTurn;
1642
+ }
1643
+ };
1612
1644
 
1613
1645
  const withStreamMetadata = (r: ChatModelRunResult): ChatModelRunResult => {
1614
1646
  if (!runId && activityTrail.length === 0) return r;
@@ -1726,7 +1758,10 @@ export async function* readSSEStream(
1726
1758
  processEventState,
1727
1759
  );
1728
1760
 
1729
- if (result) yield withStreamMetadata(result);
1761
+ if (result) {
1762
+ await paceRenderUpdate();
1763
+ yield withStreamMetadata(result);
1764
+ }
1730
1765
  if (
1731
1766
  hasStalledPreparingAction(
1732
1767
  preparingActionState,
@@ -63,10 +63,10 @@ export interface UseRunStuckDetectionOptions {
63
63
  */
64
64
  backgroundStuckThresholdMs?: number;
65
65
  /**
66
- * Threshold for a claimed durable background worker that is still sending
67
- * fresh process heartbeats. These workers can legitimately spend up to the
68
- * 12-minute tool/no-progress window on large Design, Plan, or Assets work.
69
- * Default 13 minutes, matching the durable chunk handoff boundary.
66
+ * Legacy upper bound for a claimed durable background worker that is still
67
+ * sending fresh process heartbeats. Informational quiet-run UI is never
68
+ * delayed past `backgroundStuckThresholdMs`; this option can only request an
69
+ * earlier notice for live workers. Default 13 minutes.
70
70
  */
71
71
  liveBackgroundStuckThresholdMs?: number;
72
72
  /** Poll interval. Default 5_000ms. */
@@ -127,6 +127,137 @@ export function useRunStuckDetection({
127
127
  const base = apiUrl ?? agentNativePath("/_agent-native/agent-chat");
128
128
  let cancelled = false;
129
129
  let timer: ReturnType<typeof setTimeout> | null = null;
130
+ let snapshotTransitionTimer: ReturnType<typeof setTimeout> | null = null;
131
+ let snapshotVersion = 0;
132
+
133
+ type RunHealthSnapshot = {
134
+ active: boolean;
135
+ runId: string | null;
136
+ status: string | null;
137
+ lastProgressAt: number | null;
138
+ stuckSinceMs: number | null;
139
+ heartbeatAt: number | null;
140
+ heartbeatSinceMs: number | null;
141
+ dispatchMode: string | null;
142
+ };
143
+
144
+ const effectiveThresholdFor = (
145
+ dispatchMode: string | null,
146
+ heartbeatSinceMs: number | null,
147
+ ) => {
148
+ const liveBackgroundWorker =
149
+ dispatchMode === "background-processing" &&
150
+ heartbeatSinceMs != null &&
151
+ heartbeatSinceMs >= 0 &&
152
+ heartbeatSinceMs < FRESH_BACKGROUND_HEARTBEAT_MS;
153
+ const serverContinued =
154
+ dispatchMode === "foreground-self-chain" ||
155
+ dispatchMode?.startsWith("background") === true;
156
+ return liveBackgroundWorker
157
+ ? Math.min(backgroundStuckThresholdMs, liveBackgroundStuckThresholdMs)
158
+ : serverContinued
159
+ ? backgroundStuckThresholdMs
160
+ : stuckThresholdMs;
161
+ };
162
+
163
+ const scheduleSnapshotTransition = (
164
+ snapshot: RunHealthSnapshot,
165
+ observedAtMs: number,
166
+ version: number,
167
+ ) => {
168
+ if (snapshotTransitionTimer) clearTimeout(snapshotTransitionTimer);
169
+ snapshotTransitionTimer = null;
170
+ if (
171
+ cancelled ||
172
+ version !== snapshotVersion ||
173
+ !snapshot.active ||
174
+ snapshot.status !== "running" ||
175
+ snapshot.runId == null
176
+ ) {
177
+ return;
178
+ }
179
+
180
+ const currentElapsedMs = Math.max(0, Date.now() - observedAtMs);
181
+ const currentHeartbeatSinceMs =
182
+ snapshot.heartbeatSinceMs == null
183
+ ? null
184
+ : snapshot.heartbeatSinceMs + currentElapsedMs;
185
+ const currentStuckSinceMs =
186
+ snapshot.stuckSinceMs == null
187
+ ? null
188
+ : snapshot.stuckSinceMs + currentElapsedMs;
189
+ const effectiveThresholdMs = effectiveThresholdFor(
190
+ snapshot.dispatchMode,
191
+ currentHeartbeatSinceMs,
192
+ );
193
+ const currentlyStuck = Boolean(
194
+ currentStuckSinceMs != null &&
195
+ currentStuckSinceMs > effectiveThresholdMs,
196
+ );
197
+ const transitionDelaysMs: number[] = [];
198
+ if (
199
+ currentHeartbeatSinceMs != null &&
200
+ currentHeartbeatSinceMs >= 0 &&
201
+ currentHeartbeatSinceMs < FRESH_BACKGROUND_HEARTBEAT_MS
202
+ ) {
203
+ transitionDelaysMs.push(
204
+ FRESH_BACKGROUND_HEARTBEAT_MS - currentHeartbeatSinceMs + 1,
205
+ );
206
+ }
207
+ if (currentStuckSinceMs != null && !currentlyStuck) {
208
+ transitionDelaysMs.push(
209
+ Math.max(1, effectiveThresholdMs - currentStuckSinceMs + 1),
210
+ );
211
+ }
212
+ if (transitionDelaysMs.length === 0) return;
213
+
214
+ // Both ages originate from the server clock. Advance those snapshots by
215
+ // only a local elapsed duration, which remains safe when client/server
216
+ // wall clocks differ. Wake at the earlier semantic boundary, then
217
+ // reschedule if the other boundary is still ahead.
218
+ const delayMs = Math.max(1, Math.min(...transitionDelaysMs));
219
+ snapshotTransitionTimer = setTimeout(() => {
220
+ snapshotTransitionTimer = null;
221
+ if (cancelled || version !== snapshotVersion) return;
222
+ const elapsedSinceObservationMs = Math.max(
223
+ 0,
224
+ Date.now() - observedAtMs,
225
+ );
226
+ const nextHeartbeatSinceMs =
227
+ snapshot.heartbeatSinceMs == null
228
+ ? null
229
+ : snapshot.heartbeatSinceMs + elapsedSinceObservationMs;
230
+ const nextStuckSinceMs =
231
+ snapshot.stuckSinceMs == null
232
+ ? null
233
+ : snapshot.stuckSinceMs + elapsedSinceObservationMs;
234
+ const nextEffectiveThresholdMs = effectiveThresholdFor(
235
+ snapshot.dispatchMode,
236
+ nextHeartbeatSinceMs,
237
+ );
238
+ const nextIsStuck = Boolean(
239
+ nextStuckSinceMs != null &&
240
+ nextStuckSinceMs > nextEffectiveThresholdMs,
241
+ );
242
+ setState((current) => {
243
+ if (
244
+ version !== snapshotVersion ||
245
+ current.runId !== snapshot.runId ||
246
+ current.lastProgressAt !== snapshot.lastProgressAt ||
247
+ current.heartbeatAt !== snapshot.heartbeatAt
248
+ ) {
249
+ return current;
250
+ }
251
+ return {
252
+ ...current,
253
+ isStuck: nextIsStuck,
254
+ stuckSinceMs: nextStuckSinceMs,
255
+ heartbeatSinceMs: nextHeartbeatSinceMs,
256
+ };
257
+ });
258
+ scheduleSnapshotTransition(snapshot, observedAtMs, version);
259
+ }, delayMs);
260
+ };
130
261
 
131
262
  const poll = async () => {
132
263
  if (cancelled) return;
@@ -153,34 +284,38 @@ export function useRunStuckDetection({
153
284
  heartbeatAt != null ? nowMs - heartbeatAt : null;
154
285
  const dispatchMode =
155
286
  typeof data.dispatchMode === "string" ? data.dispatchMode : null;
156
- // Server-continued runs get the wider threshold: the server's own
157
- // recovery (150s no-progress backstop + chained continuations) must
158
- // get its chance before the user sees a "stuck" affordance.
159
- const serverContinued =
160
- dispatchMode === "foreground-self-chain" ||
161
- dispatchMode?.startsWith("background") === true;
162
287
  // A claimed durable worker with a fresh heartbeat can legitimately
163
- // be waiting on a bounded long-running tool/sub-agent call. Showing
164
- // Retry at the generic 3-minute continuation threshold aborts healthy
165
- // work and starts the same call again. Let the worker's own 12-minute
166
- // watchdog act first; a dead/stale heartbeat still gets the earlier
167
- // background fallback below.
168
- const liveBackgroundWorker =
169
- dispatchMode === "background-processing" &&
170
- heartbeatSinceMs != null &&
171
- heartbeatSinceMs >= 0 &&
172
- heartbeatSinceMs < FRESH_BACKGROUND_HEARTBEAT_MS;
173
- const effectiveThresholdMs = liveBackgroundWorker
174
- ? liveBackgroundStuckThresholdMs
175
- : serverContinued
176
- ? backgroundStuckThresholdMs
177
- : stuckThresholdMs;
288
+ // be waiting on a bounded long-running tool/sub-agent call. Still
289
+ // surface informational status at the normal background threshold;
290
+ // RunStuckBanner uses the fresh heartbeat to withhold Retry while
291
+ // keeping an explicit Cancel available. The legacy live-worker
292
+ // threshold may make that notice earlier, but never later.
293
+ const effectiveThresholdMs = effectiveThresholdFor(
294
+ dispatchMode,
295
+ heartbeatSinceMs,
296
+ );
178
297
  const isStuck = Boolean(
179
298
  data.active &&
180
299
  data.status === "running" &&
181
300
  stuckSinceMs != null &&
182
301
  stuckSinceMs > effectiveThresholdMs,
183
302
  );
303
+ const observedAtMs = Date.now();
304
+ const version = ++snapshotVersion;
305
+ scheduleSnapshotTransition(
306
+ {
307
+ active: data.active,
308
+ runId: data.runId ?? null,
309
+ status: data.status ?? null,
310
+ lastProgressAt,
311
+ stuckSinceMs,
312
+ heartbeatAt,
313
+ heartbeatSinceMs,
314
+ dispatchMode,
315
+ },
316
+ observedAtMs,
317
+ version,
318
+ );
184
319
  setState({
185
320
  isStuck,
186
321
  runId: data.runId ?? null,
@@ -217,7 +352,9 @@ export function useRunStuckDetection({
217
352
 
218
353
  return () => {
219
354
  cancelled = true;
355
+ snapshotVersion += 1;
220
356
  if (timer) clearTimeout(timer);
357
+ if (snapshotTransitionTimer) clearTimeout(snapshotTransitionTimer);
221
358
  };
222
359
  }, [
223
360
  threadId,
@@ -2535,17 +2535,18 @@ function walkServerJavaScriptFiles(
2535
2535
  }
2536
2536
 
2537
2537
  /**
2538
- * Nitro can preserve Vite's `yjs` external in a split server chunk even when
2539
- * its own server build has emitted `_libs/yjs.mjs`. Netlify does not install
2540
- * that bare package at runtime, so make every emitted server chunk use the
2541
- * bundled copy before its function is packaged.
2538
+ * Nitro receives the React Router SSR build as prebuilt chunks, so its normal
2539
+ * dependency resolver cannot reliably fold the preserved bare `yjs` imports
2540
+ * into the same module instance used by core's server collaboration code.
2541
+ * Keep Yjs external through Nitro, bundle its complete public ESM surface once,
2542
+ * then point every emitted server chunk at that one portable runtime module.
2542
2543
  */
2543
- export function rewriteBareYjsImportsForServerlessOutput(
2544
+ export function bundleYjsRuntimeForServerlessOutput(
2544
2545
  serverDir: string,
2546
+ projectCwd: string,
2545
2547
  ): string[] {
2546
2548
  const bareImports: string[] = [];
2547
2549
  const unsupportedSubpathImports: string[] = [];
2548
- const bundledYjsPath = path.join(serverDir, "_libs", "yjs.mjs");
2549
2550
 
2550
2551
  walkServerJavaScriptFiles(serverDir, (filePath) => {
2551
2552
  const source = fs.readFileSync(filePath, "utf-8");
@@ -2563,11 +2564,22 @@ export function rewriteBareYjsImportsForServerlessOutput(
2563
2564
  );
2564
2565
  }
2565
2566
  if (bareImports.length === 0) return [];
2566
- if (!fs.existsSync(bundledYjsPath)) {
2567
- throw new Error(
2568
- `[deploy] Serverless output left yjs as a runtime import but did not emit ${bundledYjsPath}`,
2569
- );
2570
- }
2567
+
2568
+ const bundledYjsPath = path.join(serverDir, "_libs", "yjs-runtime.mjs");
2569
+ fs.mkdirSync(path.dirname(bundledYjsPath), { recursive: true });
2570
+ execFileSync(
2571
+ findEsbuild(),
2572
+ [
2573
+ resolveNitroBundledYjsEntry(),
2574
+ "--bundle",
2575
+ "--format=esm",
2576
+ "--platform=node",
2577
+ "--target=node22",
2578
+ "--minify",
2579
+ `--outfile=${bundledYjsPath}`,
2580
+ ],
2581
+ { cwd: projectCwd, stdio: "pipe" },
2582
+ );
2571
2583
 
2572
2584
  for (const filePath of bareImports) {
2573
2585
  const bundledImport = path
@@ -2702,6 +2714,26 @@ export function assertSingleTemplateNetlifyBuildOutput(
2702
2714
  );
2703
2715
  }
2704
2716
 
2717
+ // Nitro's `_libs/yjs.mjs` is a private tree-shaken chunk, not a package
2718
+ // facade. Repointing a prebuilt SSR chunk at it can request public exports
2719
+ // (notably `Text`) that the private chunk did not retain. The controlled
2720
+ // serverless pass must instead target the complete `yjs-runtime.mjs` bundle.
2721
+ const privateYjsImports: string[] = [];
2722
+ walkServerJavaScriptFiles(serverDir, (filePath) => {
2723
+ if (
2724
+ /\b(?:from\s*|import\s*\(\s*|import\s*)(["'])[^"']*_libs\/yjs\.mjs\1/.test(
2725
+ fs.readFileSync(filePath, "utf-8"),
2726
+ )
2727
+ ) {
2728
+ privateYjsImports.push(path.relative(projectCwd, filePath));
2729
+ }
2730
+ });
2731
+ if (privateYjsImports.length > 0) {
2732
+ failures.push(
2733
+ `Netlify server bundle imports Nitro's internal tree-shaken _libs/yjs.mjs: ${privateYjsImports.join(", ")}`,
2734
+ );
2735
+ }
2736
+
2705
2737
  if (isDurableBackgroundDeployEnabled()) {
2706
2738
  const backgroundDir = path.join(
2707
2739
  internalDir,
@@ -3153,18 +3185,27 @@ const BROWSER_ONLY_SERVER_LIBS = [
3153
3185
  ];
3154
3186
 
3155
3187
  /**
3156
- * Dependencies that must be bundled into every Nitro server output instead of
3157
- * being left as runtime package imports.
3158
- *
3159
- * `yjs` is a direct core dependency, but it is deliberately externalized from
3160
- * the intermediate Vite SSR graph so that Vite and Nitro do not create two
3161
- * incompatible Yjs constructors. On file-traced serverless presets, leaving it
3162
- * external at Nitro's final build can emit `import "yjs"` into a function
3163
- * chunk without placing the package in that function's `node_modules`. Bundle
3164
- * it in Nitro's final output so every template receives the one portable copy.
3188
+ * Dependencies Nitro itself must bundle outside the controlled serverless
3189
+ * output pass. Netlify, Vercel, and Lambda keep Yjs external through Nitro;
3190
+ * `bundleYjsRuntimeForServerlessOutput` then creates their one portable copy.
3165
3191
  */
3166
3192
  export const NITRO_SERVER_RUNTIME_BUNDLED_DEPS = ["yjs"] as const;
3167
3193
 
3194
+ /**
3195
+ * Locate the core-owned ESM entry used by the controlled serverless bundling
3196
+ * pass. Resolving from this module keeps the build independent of whether a
3197
+ * template exposes core's transitive Yjs dependency at its own package root.
3198
+ */
3199
+ export function resolveNitroBundledYjsEntry(): string {
3200
+ const requireFromCore = createRequire(import.meta.url);
3201
+ const packageDir = path.dirname(requireFromCore.resolve("yjs/package.json"));
3202
+ const entry = path.join(packageDir, "dist", "yjs.mjs");
3203
+ if (!fs.existsSync(entry)) {
3204
+ throw new Error(`[build] Could not resolve the Yjs ESM entry at ${entry}`);
3205
+ }
3206
+ return entry;
3207
+ }
3208
+
3168
3209
  /**
3169
3210
  * Edge runtimes have no node_modules, while Node/serverless outputs only need
3170
3211
  * the small set above bundled to keep their package manifests traceable.
@@ -3175,7 +3216,11 @@ export function nitroNoExternalsForPreset(
3175
3216
  return targetPreset.startsWith("cloudflare") ||
3176
3217
  targetPreset.startsWith("deno")
3177
3218
  ? true
3178
- : NITRO_SERVER_RUNTIME_BUNDLED_DEPS;
3219
+ : targetPreset === "netlify" ||
3220
+ targetPreset === "vercel" ||
3221
+ targetPreset === "aws-lambda"
3222
+ ? []
3223
+ : NITRO_SERVER_RUNTIME_BUNDLED_DEPS;
3179
3224
  }
3180
3225
 
3181
3226
  /**
@@ -3327,6 +3372,14 @@ export default bundle;
3327
3372
  // (ReferenceError: window is not defined → every request 502s). Mirrors the
3328
3373
  // Vite `ssrStubPlugin`, which only covers the `build/server` step.
3329
3374
  rollupConfig: {
3375
+ // Nitro treats the intermediate React Router SSR files as prebuilt
3376
+ // chunks, while core's server collaboration files participate in the
3377
+ // final Rolldown graph. Externalize Yjs consistently on serverless so
3378
+ // both graphs retain their public import shapes; the controlled
3379
+ // post-build pass below bundles and rewrites them to one module.
3380
+ ...(preset === "netlify" || preset === "vercel" || preset === "aws-lambda"
3381
+ ? { external: ["yjs"] }
3382
+ : {}),
3330
3383
  plugins: [createBrowserOnlyServerStubPlugin()],
3331
3384
  },
3332
3385
  ...(providedPluginsNitroPlugin
@@ -3334,9 +3387,9 @@ export default bundle;
3334
3387
  : {}),
3335
3388
  routeRules: mcpEmbedStaticAssetRouteRules(appBasePath),
3336
3389
  // Edge presets (cloudflare, deno) bundle all deps because node_modules are
3337
- // unavailable at runtime. Node/serverless presets also bundle Yjs: Nitro's
3338
- // file tracer otherwise leaves a bare import that Netlify function bundles
3339
- // cannot resolve under pnpm.
3390
+ // unavailable at runtime. Ordinary Node presets bundle Yjs through Nitro.
3391
+ // Controlled serverless presets externalize it above, then emit one full
3392
+ // runtime module after Nitro has preserved every consumer's public imports.
3340
3393
  noExternals: nitroNoExternalsForPreset(preset),
3341
3394
  } as any);
3342
3395
 
@@ -3354,7 +3407,7 @@ export default bundle;
3354
3407
  copyInstalledResvgPackages(nitro.options.output.serverDir);
3355
3408
  copyInstalledFfmpegStaticPackage(nitro.options.output.serverDir);
3356
3409
  sanitizeServerlessFunctionPackageManifest(nitro.options.output.serverDir);
3357
- rewriteBareYjsImportsForServerlessOutput(nitro.options.output.serverDir);
3410
+ bundleYjsRuntimeForServerlessOutput(nitro.options.output.serverDir, cwd);
3358
3411
  }
3359
3412
 
3360
3413
  // Durable background agent runs (default-OFF / opt-in; enable with a truthy
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-14
4
+ ---
5
+
6
+ Chat opens reliably on hosted deployments instead of failing during startup
@@ -0,0 +1,24 @@
1
+ const MEDIARECORDER_DURATION_DRIFT_MS = 3_000;
2
+
3
+ function finitePositiveMs(value: number): number {
4
+ return Number.isFinite(value) && value > 0 ? Math.round(value) : 0;
5
+ }
6
+
7
+ /**
8
+ * Prefer recorder metadata for normal MediaRecorder timeslice drift, but use
9
+ * the playable media's duration when the two values clearly describe
10
+ * different timelines (for example, metadata that accidentally counted a
11
+ * long pause).
12
+ */
13
+ export function resolveMediaDurationMs(
14
+ recordedDurationMs: number,
15
+ mediaDurationSeconds: number,
16
+ ): number {
17
+ const recordedMs = finitePositiveMs(recordedDurationMs);
18
+ const mediaMs = finitePositiveMs(mediaDurationSeconds * 1000);
19
+ if (mediaMs === 0) return recordedMs;
20
+ if (recordedMs === 0) return mediaMs;
21
+ return Math.abs(recordedMs - mediaMs) > MEDIARECORDER_DURATION_DRIFT_MS
22
+ ? mediaMs
23
+ : recordedMs;
24
+ }
@@ -19,6 +19,7 @@ import {
19
19
  useState,
20
20
  } from "react";
21
21
 
22
+ import { resolveMediaDurationMs } from "@/components/player/media-duration";
22
23
  import {
23
24
  DropdownMenu,
24
25
  DropdownMenuContent,
@@ -824,22 +825,16 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
824
825
  durationProbedRef.current = false;
825
826
  }, [activeVideoSrc, durationMs]);
826
827
 
827
- // The recorder's elapsed-time counter (durationMs prop) is the most
828
- // trustworthy length we have. A MediaRecorder WebM's own duration is
829
- // cluster-estimated and lands short by up to one timeslice, so we never
830
- // let it overwrite a real prop — doing so makes the scrubber jump to a
831
- // shorter length than the actual recording on first watch.
832
- const hasReliableDurationProp =
833
- Number.isFinite(durationMs) && durationMs > 0;
834
-
828
+ // Prefer the recorder's elapsed-time counter for ordinary WebM timeslice
829
+ // drift, but let clearly different playable-media metadata win. This also
830
+ // repairs playback controls for older clips whose stored duration counted
831
+ // time spent paused.
835
832
  const probeDurationIfNeeded = useCallback(
836
833
  (v: HTMLVideoElement) => {
837
834
  if (durationProbedRef.current) return;
838
835
  if (Number.isFinite(v.duration) && v.duration > 0) {
839
836
  durationProbedRef.current = true;
840
- if (!hasReliableDurationProp) {
841
- setResolvedDurationMs(Math.round(v.duration * 1000));
842
- }
837
+ setResolvedDurationMs(resolveMediaDurationMs(durationMs, v.duration));
843
838
  return;
844
839
  }
845
840
  if (playAttemptPendingRef.current || !v.paused) return;
@@ -855,7 +850,7 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
855
850
  // picks up the real duration.
856
851
  }
857
852
  },
858
- [hasReliableDurationProp],
853
+ [durationMs],
859
854
  );
860
855
 
861
856
  // Resolve the WebM-duration-is-Infinity Chrome quirk: when a video created
@@ -873,11 +868,7 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
873
868
 
874
869
  const onDurationChange = () => {
875
870
  if (Number.isFinite(v.duration) && v.duration > 0) {
876
- // Don't downgrade a trustworthy recorder duration to the
877
- // cluster-estimated WebM duration; only adopt it as a fallback.
878
- if (!hasReliableDurationProp) {
879
- setResolvedDurationMs(Math.round(v.duration * 1000));
880
- }
871
+ setResolvedDurationMs(resolveMediaDurationMs(durationMs, v.duration));
881
872
  // After we've resolved the real duration, rewind back to 0 so the
882
873
  // user isn't sitting at the end of the clip.
883
874
  if (durationProbedRef.current && v.currentTime > v.duration) {
@@ -900,7 +891,7 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
900
891
  v.removeEventListener("loadedmetadata", onLoadedMetadata);
901
892
  v.removeEventListener("durationchange", onDurationChange);
902
893
  };
903
- }, [activeVideoSrc, hasReliableDurationProp, probeDurationIfNeeded]);
894
+ }, [activeVideoSrc, durationMs, probeDurationIfNeeded]);
904
895
 
905
896
  // Reset the thumbnail-capture flag when the source changes (e.g. the
906
897
  // player is reused for a different recording via React Router).
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-14
4
+ ---
5
+
6
+ Paused time no longer counts toward Chrome extension recording durations.