@agent-native/core 0.114.8 → 0.114.10

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 (128) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +21 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/engine/ai-sdk-engine.ts +31 -3
  5. package/corpus/core/src/agent/engine/anthropic-engine.ts +20 -3
  6. package/corpus/core/src/agent/engine/builder-engine.ts +56 -9
  7. package/corpus/core/src/agent/engine/first-event-timeout.ts +64 -0
  8. package/corpus/core/src/client/AssistantChat.tsx +7 -3
  9. package/corpus/core/src/client/resources/ResourcesPanel.tsx +1 -1
  10. package/corpus/core/src/integrations/computer-supervision-store.ts +4 -4
  11. package/corpus/core/src/integrations/pending-tasks-store.ts +1 -1
  12. package/corpus/core/src/integrations/remote-commands-store.ts +5 -5
  13. package/corpus/core/src/integrations/remote-devices-store.ts +3 -3
  14. package/corpus/core/src/integrations/remote-push-store.ts +3 -3
  15. package/corpus/core/src/observability/traces.ts +38 -1
  16. package/corpus/core/src/server/action-routes.ts +23 -1
  17. package/corpus/core/src/server/http-response-telemetry.ts +6 -1
  18. package/corpus/core/src/templates/workspace-core/.agents/skills/observability/SKILL.md +1 -1
  19. package/corpus/core/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +1 -1
  20. package/corpus/templates/analytics/actions/account-deep-dive.ts +41 -6
  21. package/corpus/templates/analytics/changelog/2026-07-20-account-deep-dives-finish-with-partial-data-when-hubspot-lo.md +6 -0
  22. package/corpus/templates/analytics/changelog/2026-07-20-dashboard-email-reports-now-include-an-image-when-a-single-p.md +6 -0
  23. package/corpus/templates/analytics/server/lib/dashboard-report.ts +11 -3
  24. package/corpus/templates/calendar/actions/create-event.ts +5 -0
  25. package/corpus/templates/calendar/app/components/ThemeToggle.tsx +4 -1
  26. package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +0 -1
  27. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +309 -421
  28. package/corpus/templates/calendar/app/components/calendar/InlineEventPickers.tsx +349 -0
  29. package/corpus/templates/calendar/app/i18n-data.ts +110 -0
  30. package/corpus/templates/calendar/app/lib/event-form-utils.ts +6 -0
  31. package/corpus/templates/calendar/app/pages/CalendarView.tsx +3 -0
  32. package/corpus/templates/calendar/app/root.tsx +4 -2
  33. package/corpus/templates/calendar/changelog/2026-07-20-events-can-be-updated-inline-with-date-time-timezone-and-rep.md +6 -0
  34. package/corpus/templates/calendar/changelog/2026-07-20-public-booking-pages-no-longer-hydrate-with-a-theme-mismatch.md +6 -0
  35. package/corpus/templates/calendar/shared/api.ts +1 -0
  36. package/corpus/templates/clips/AGENTS.md +7 -0
  37. package/corpus/templates/clips/actions/create-recording.ts +51 -10
  38. package/corpus/templates/clips/actions/get-recording-player-data.ts +34 -5
  39. package/corpus/templates/clips/actions/lib/transcript-preview.ts +48 -0
  40. package/corpus/templates/clips/actions/list-recordings.ts +15 -4
  41. package/corpus/templates/clips/actions/regenerate-summary.ts +32 -0
  42. package/corpus/templates/clips/actions/search-recordings.ts +28 -6
  43. package/corpus/templates/clips/actions/view-screen.ts +54 -47
  44. package/corpus/templates/clips/app/components/library/library-grid.tsx +1 -5
  45. package/corpus/templates/clips/app/components/library/recording-card.tsx +29 -32
  46. package/corpus/templates/clips/app/components/meetings/quick-ask-sidebar.tsx +13 -0
  47. package/corpus/templates/clips/changelog/2026-07-20-agent-summaries-use-bounded-transcript-context.md +6 -0
  48. package/corpus/templates/clips/changelog/2026-07-20-clip-summaries-now-finish-without-sending-a-long-transcript-.md +6 -0
  49. package/corpus/templates/clips/changelog/2026-07-20-clips-agents-only-discover-recordings-you-own-or-have-alread.md +6 -0
  50. package/corpus/templates/clips/changelog/2026-07-20-hosted-recordings-no-longer-start-with-an-impossible-buffered-upload.md +6 -0
  51. package/corpus/templates/clips/changelog/2026-07-20-meeting-recordings-now-offer-a-cleaner-transcript-view-with-.md +6 -0
  52. package/corpus/templates/clips/changelog/2026-07-20-shared-clips-can-be-shared-with-limited-access-and-cards-sho.md +6 -0
  53. package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +12 -8
  54. package/corpus/templates/clips/desktop/src/overlays/pill-logo.tsx +2 -15
  55. package/corpus/templates/clips/desktop/src/overlays/recording-pill.tsx +107 -340
  56. package/corpus/templates/clips/desktop/src/styles.css +27 -145
  57. package/corpus/templates/clips/desktop/src-tauri/src/recording_indicator.rs +6 -7
  58. package/corpus/templates/clips/server/lib/agent-recording-access.ts +73 -0
  59. package/corpus/templates/clips/server/plugins/agent-chat.ts +5 -2
  60. package/corpus/templates/plan/app/lib/plan-local-bridge.ts +18 -0
  61. package/corpus/templates/plan/app/pages/PlansPage.tsx +10 -11
  62. package/corpus/templates/plan/changelog/2026-07-20-bridged-local-plans-now-load-comments-from-the-local-workspace.md +6 -0
  63. package/dist/agent/engine/ai-sdk-engine.d.ts.map +1 -1
  64. package/dist/agent/engine/ai-sdk-engine.js +27 -4
  65. package/dist/agent/engine/ai-sdk-engine.js.map +1 -1
  66. package/dist/agent/engine/anthropic-engine.d.ts.map +1 -1
  67. package/dist/agent/engine/anthropic-engine.js +19 -4
  68. package/dist/agent/engine/anthropic-engine.js.map +1 -1
  69. package/dist/agent/engine/builder-engine.d.ts.map +1 -1
  70. package/dist/agent/engine/builder-engine.js +40 -7
  71. package/dist/agent/engine/builder-engine.js.map +1 -1
  72. package/dist/agent/engine/first-event-timeout.d.ts +25 -0
  73. package/dist/agent/engine/first-event-timeout.d.ts.map +1 -0
  74. package/dist/agent/engine/first-event-timeout.js +49 -0
  75. package/dist/agent/engine/first-event-timeout.js.map +1 -0
  76. package/dist/client/AssistantChat.d.ts.map +1 -1
  77. package/dist/client/AssistantChat.js +7 -3
  78. package/dist/client/AssistantChat.js.map +1 -1
  79. package/dist/client/resources/ResourcesPanel.js +1 -1
  80. package/dist/client/resources/ResourcesPanel.js.map +1 -1
  81. package/dist/collab/routes.d.ts +1 -1
  82. package/dist/collab/struct-routes.d.ts +1 -1
  83. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  84. package/dist/integrations/computer-supervision-store.js +4 -4
  85. package/dist/integrations/computer-supervision-store.js.map +1 -1
  86. package/dist/integrations/pending-tasks-store.js +1 -1
  87. package/dist/integrations/pending-tasks-store.js.map +1 -1
  88. package/dist/integrations/remote-commands-store.js +5 -5
  89. package/dist/integrations/remote-commands-store.js.map +1 -1
  90. package/dist/integrations/remote-devices-store.js +3 -3
  91. package/dist/integrations/remote-devices-store.js.map +1 -1
  92. package/dist/integrations/remote-push-store.js +3 -3
  93. package/dist/integrations/remote-push-store.js.map +1 -1
  94. package/dist/notifications/routes.d.ts +1 -1
  95. package/dist/observability/routes.d.ts +3 -3
  96. package/dist/observability/traces.d.ts.map +1 -1
  97. package/dist/observability/traces.js +23 -1
  98. package/dist/observability/traces.js.map +1 -1
  99. package/dist/progress/routes.d.ts +1 -1
  100. package/dist/resources/handlers.d.ts +1 -1
  101. package/dist/server/action-routes.d.ts.map +1 -1
  102. package/dist/server/action-routes.js +23 -1
  103. package/dist/server/action-routes.js.map +1 -1
  104. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  105. package/dist/server/http-response-telemetry.d.ts +2 -0
  106. package/dist/server/http-response-telemetry.d.ts.map +1 -1
  107. package/dist/server/http-response-telemetry.js +5 -1
  108. package/dist/server/http-response-telemetry.js.map +1 -1
  109. package/dist/server/transcribe-voice.d.ts +1 -1
  110. package/dist/templates/workspace-core/.agents/skills/observability/SKILL.md +1 -1
  111. package/dist/templates/workspace-core/.agents/skills/tracking/SKILL.md +1 -1
  112. package/package.json +2 -2
  113. package/src/agent/engine/ai-sdk-engine.ts +31 -3
  114. package/src/agent/engine/anthropic-engine.ts +20 -3
  115. package/src/agent/engine/builder-engine.ts +56 -9
  116. package/src/agent/engine/first-event-timeout.ts +64 -0
  117. package/src/client/AssistantChat.tsx +7 -3
  118. package/src/client/resources/ResourcesPanel.tsx +1 -1
  119. package/src/integrations/computer-supervision-store.ts +4 -4
  120. package/src/integrations/pending-tasks-store.ts +1 -1
  121. package/src/integrations/remote-commands-store.ts +5 -5
  122. package/src/integrations/remote-devices-store.ts +3 -3
  123. package/src/integrations/remote-push-store.ts +3 -3
  124. package/src/observability/traces.ts +38 -1
  125. package/src/server/action-routes.ts +23 -1
  126. package/src/server/http-response-telemetry.ts +6 -1
  127. package/src/templates/workspace-core/.agents/skills/observability/SKILL.md +1 -1
  128. package/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +1 -1
package/corpus/README.md CHANGED
@@ -28,6 +28,6 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
28
28
 
29
29
  ## Generated Counts
30
30
 
31
- - core files: 1518
31
+ - core files: 1519
32
32
  - toolkit files: 137
33
- - template files: 6163
33
+ - template files: 6176
@@ -1,5 +1,26 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.114.10
4
+
5
+ ### Patch Changes
6
+
7
+ - 687fefc: Capture unexpected HTTP action failures with request-correlated server diagnostics.
8
+ - 687fefc: Capture uncategorized HTTP action failures through the configured server error monitor and correlate them with request telemetry.
9
+ - 687fefc: Abort and retry model requests that produce no stream events within two minutes instead of hanging until run watchdogs fire.
10
+ - 687fefc: Fix nullable organization scope parameters for remote integration stores.
11
+ - 687fefc: Capture truncated, secret-scrubbed tool error text on generation telemetry when `captureToolResults` is enabled.
12
+ - 687fefc: Distinguish ended agent-chat streams from true reconnect stalls in error monitoring and recovery messaging.
13
+ - Updated dependencies [687fefc]
14
+ - @agent-native/recap-cli@0.4.7
15
+
16
+ ## 0.114.9
17
+
18
+ ### Patch Changes
19
+
20
+ - fb32d85: Run configurable PR Visual Recap jobs with Bash on every runner platform.
21
+ - Updated dependencies [fb32d85]
22
+ - @agent-native/recap-cli@0.4.6
23
+
3
24
  ## 0.114.8
4
25
 
5
26
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.114.8",
3
+ "version": "0.114.10",
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": {
@@ -23,6 +23,10 @@ import {
23
23
  supportsClaudeAdaptiveThinking,
24
24
  } from "../../shared/reasoning-effort.js";
25
25
  import { AI_SDK_MODEL_CONFIG, type AISDKProvider } from "../model-config.js";
26
+ import {
27
+ createFirstEventAbortController,
28
+ FIRST_STREAM_EVENT_TIMEOUT_MS,
29
+ } from "./first-event-timeout.js";
26
30
  import {
27
31
  clampThinkingBudgetTokens,
28
32
  resolveMaxOutputTokensForEngine,
@@ -379,6 +383,7 @@ class AISDKEngine implements AgentEngine {
379
383
  }
380
384
 
381
385
  let assistantContent: EngineContentPart[] = [];
386
+ const firstEventAbort = createFirstEventAbortController(opts.abortSignal);
382
387
 
383
388
  try {
384
389
  const result = streamText({
@@ -390,7 +395,7 @@ class AISDKEngine implements AgentEngine {
390
395
  ...(opts.temperature !== undefined
391
396
  ? { temperature: opts.temperature }
392
397
  : {}),
393
- abortSignal: opts.abortSignal,
398
+ abortSignal: firstEventAbort.signal,
394
399
  onStepFinish: (step: any) => {
395
400
  assistantContent = aiSdkStepToAssistantContent(step);
396
401
  },
@@ -402,8 +407,18 @@ class AISDKEngine implements AgentEngine {
402
407
  // Buffer the terminal stop so assistant-content can be emitted just
403
408
  // before it, regardless of where `finish` arrives in the stream.
404
409
  let bufferedStop: EngineEvent | undefined;
410
+ let sawFirstEvent = false;
405
411
 
406
412
  for await (const part of result.fullStream) {
413
+ // "start" is a synthetic lifecycle marker the AI SDK enqueues
414
+ // synchronously when the stream begins — before any provider bytes
415
+ // arrive — so it does not count as real progress. Every other part
416
+ // (including "start-step", only enqueued on the step's first real
417
+ // chunk) proves the provider is actually responding.
418
+ if (!sawFirstEvent && part?.type !== "start") {
419
+ sawFirstEvent = true;
420
+ firstEventAbort.markFirstEvent();
421
+ }
407
422
  for (const event of aiSdkPartToEngineEvents(part)) {
408
423
  if (event.type === "stop") {
409
424
  bufferedStop = event;
@@ -413,6 +428,15 @@ class AISDKEngine implements AgentEngine {
413
428
  }
414
429
  }
415
430
 
431
+ // AI SDK surfaces an aborted stream as a graceful `{type: "abort"}`
432
+ // part rather than a thrown error, so a first-event timeout would
433
+ // otherwise fall through to the normal end_turn completion below.
434
+ if (!sawFirstEvent && firstEventAbort.didTimeout()) {
435
+ throw new Error(
436
+ `Model request produced no stream events within ${FIRST_STREAM_EVENT_TIMEOUT_MS / 1000}s; the connection appears wedged.`,
437
+ );
438
+ }
439
+
416
440
  yield { type: "assistant-content", parts: assistantContent };
417
441
  await clearProviderCredentialAuthFailure({
418
442
  key: PROVIDER_ENV_VARS[this.provider][0],
@@ -420,6 +444,7 @@ class AISDKEngine implements AgentEngine {
420
444
  });
421
445
  yield bufferedStop ?? { type: "stop", reason: "end_turn" };
422
446
  } catch (err: any) {
447
+ const timedOut = firstEventAbort.didTimeout();
423
448
  // Surface structured fields from AI SDK's APICallError so
424
449
  // isRetryableError can check statusCode/providerRetryable directly
425
450
  // rather than keyword-matching the message string.
@@ -427,12 +452,13 @@ class AISDKEngine implements AgentEngine {
427
452
  typeof err?.statusCode === "number" ? err.statusCode : undefined;
428
453
  const errorMessage = err?.message ?? String(err);
429
454
  const isConnectionError =
455
+ !timedOut &&
430
456
  statusCode === undefined &&
431
457
  String(errorMessage).trim().toLowerCase() === "connection error.";
432
458
  const providerRetryable: boolean | undefined =
433
459
  typeof err?.isRetryable === "boolean"
434
460
  ? err.isRetryable
435
- : isConnectionError
461
+ : isConnectionError || timedOut
436
462
  ? true
437
463
  : undefined;
438
464
  if (statusCode === 401) {
@@ -455,12 +481,14 @@ class AISDKEngine implements AgentEngine {
455
481
  // auto-resume too — matching the Builder gateway path.
456
482
  ...(statusCode !== undefined
457
483
  ? { errorCode: `http_${statusCode}`, statusCode }
458
- : isConnectionError
484
+ : isConnectionError || timedOut
459
485
  ? { errorCode: "provider_network_error" }
460
486
  : {}),
461
487
  ...(providerRetryable !== undefined ? { providerRetryable } : {}),
462
488
  };
463
489
  throw err;
490
+ } finally {
491
+ firstEventAbort.cleanup();
464
492
  }
465
493
  }
466
494
 
@@ -23,6 +23,10 @@ import {
23
23
  LLM_MISSING_CREDENTIALS_ERROR_CODE,
24
24
  LLM_MISSING_CREDENTIALS_MESSAGE,
25
25
  } from "./credential-errors.js";
26
+ import {
27
+ createFirstEventAbortController,
28
+ FIRST_STREAM_EVENT_TIMEOUT_MS,
29
+ } from "./first-event-timeout.js";
26
30
  import {
27
31
  clampThinkingBudgetTokens,
28
32
  resolveMaxOutputTokensForEngine,
@@ -186,8 +190,9 @@ class AnthropicEngine implements AgentEngine {
186
190
  // Remove undefined tools to avoid Anthropic API validation errors
187
191
  if (!requestParams.tools) delete requestParams.tools;
188
192
 
193
+ const firstEventAbort = createFirstEventAbortController(opts.abortSignal);
189
194
  const apiStream = client.messages.stream(requestParams, {
190
- signal: opts.abortSignal,
195
+ signal: firstEventAbort.signal,
191
196
  });
192
197
 
193
198
  // Per-stream state lets the translator carry each tool-call's id/name from
@@ -198,6 +203,9 @@ class AnthropicEngine implements AgentEngine {
198
203
 
199
204
  try {
200
205
  for await (const chunk of apiStream) {
206
+ // The SDK's SSE parsing already drops `ping` keepalives before they
207
+ // reach this loop, so any chunk here is real provider progress.
208
+ firstEventAbort.markFirstEvent();
201
209
  const events = anthropicChunkToEngineEvents(chunk, chunkState);
202
210
  for (const event of events) {
203
211
  yield event;
@@ -238,17 +246,24 @@ class AnthropicEngine implements AgentEngine {
238
246
  : "end_turn",
239
247
  };
240
248
  } catch (err: any) {
249
+ const timedOut = firstEventAbort.didTimeout();
241
250
  const statusCode: number | undefined =
242
251
  typeof err?.status === "number"
243
252
  ? err.status
244
253
  : typeof err?.statusCode === "number"
245
254
  ? err.statusCode
246
255
  : undefined;
247
- const errorMessage = err?.message ?? String(err);
256
+ // A first-event abort surfaces from the SDK as a generic
257
+ // APIUserAbortError ("Request was aborted.") — replace it with a
258
+ // message that actually explains what happened.
259
+ const errorMessage = timedOut
260
+ ? `Model request produced no stream events within ${FIRST_STREAM_EVENT_TIMEOUT_MS / 1000}s; the connection appears wedged.`
261
+ : (err?.message ?? String(err));
248
262
  // Anthropic SDK APIConnectionError defaults to "Connection error." with
249
263
  // no HTTP status. Tag it so in-run retries and run-level resume treat
250
264
  // the failure as a transient network interruption.
251
265
  const isConnectionError =
266
+ !timedOut &&
252
267
  statusCode === undefined &&
253
268
  String(errorMessage).trim().toLowerCase() === "connection error.";
254
269
  if (statusCode === 401) {
@@ -274,7 +289,7 @@ class AnthropicEngine implements AgentEngine {
274
289
  // continuation logic auto-resume a rate-limited turn.
275
290
  ...(statusCode !== undefined
276
291
  ? { errorCode: `http_${statusCode}`, statusCode }
277
- : isConnectionError
292
+ : isConnectionError || timedOut
278
293
  ? {
279
294
  errorCode: "provider_network_error",
280
295
  providerRetryable: true,
@@ -282,6 +297,8 @@ class AnthropicEngine implements AgentEngine {
282
297
  : {}),
283
298
  };
284
299
  throw err;
300
+ } finally {
301
+ firstEventAbort.cleanup();
285
302
  }
286
303
  }
287
304
  }
@@ -31,6 +31,7 @@ import {
31
31
  LLM_MISSING_CREDENTIALS_ERROR_CODE,
32
32
  LLM_MISSING_CREDENTIALS_MESSAGE,
33
33
  } from "./credential-errors.js";
34
+ import { FIRST_STREAM_EVENT_TIMEOUT_MS } from "./first-event-timeout.js";
34
35
  import { resolveMaxOutputTokensForEngine } from "./output-tokens.js";
35
36
  import {
36
37
  engineMessagesToBuilderGatewayAnthropic,
@@ -272,12 +273,16 @@ class BuilderEngine implements AgentEngine {
272
273
  phase: "request",
273
274
  model: opts.model,
274
275
  gatewayUrl,
275
- timeoutMs: gatewayTimeoutMs,
276
+ timeoutMs: gatewayAbort.effectiveTimeoutMs(),
276
277
  timedOut,
277
278
  elapsedMs: Date.now() - tStart,
278
279
  });
279
280
  }
280
- yield createBuilderGatewayTimeoutStop(err, timedOut, gatewayTimeoutMs);
281
+ yield createBuilderGatewayTimeoutStop(
282
+ err,
283
+ timedOut,
284
+ gatewayAbort.effectiveTimeoutMs(),
285
+ );
281
286
  return;
282
287
  }
283
288
 
@@ -331,7 +336,8 @@ class BuilderEngine implements AgentEngine {
331
336
  yield* parseJsonlStream(reader, opts.model, {
332
337
  abortSignal: gatewayAbort.signal,
333
338
  didGatewayTimeout: gatewayAbort.didTimeout,
334
- gatewayTimeoutMs,
339
+ getGatewayTimeoutMs: gatewayAbort.effectiveTimeoutMs,
340
+ onFirstEvent: gatewayAbort.markFirstEvent,
335
341
  gatewayUrl,
336
342
  requestStartedAt: tStart,
337
343
  });
@@ -475,13 +481,12 @@ async function* parseJsonlStream(
475
481
  captureContext: {
476
482
  abortSignal?: AbortSignal;
477
483
  didGatewayTimeout?: () => boolean;
478
- gatewayTimeoutMs?: number;
484
+ getGatewayTimeoutMs?: () => number;
485
+ onFirstEvent?: () => void;
479
486
  gatewayUrl?: URL;
480
487
  requestStartedAt?: number;
481
488
  } = {},
482
489
  ): AsyncIterable<EngineEvent> {
483
- const gatewayTimeoutMs =
484
- captureContext.gatewayTimeoutMs ?? DEFAULT_BUILDER_GATEWAY_TIMEOUT_MS;
485
490
  const parts: EngineContentPart[] = [];
486
491
  let pendingText = "";
487
492
  let pendingThinking: { text: string; signature?: string } | null = null;
@@ -533,6 +538,12 @@ async function* parseJsonlStream(
533
538
  return;
534
539
  }
535
540
 
541
+ // Heartbeats are transport-level keepalives, not proof the model is
542
+ // producing output — every other parsed event counts as first progress.
543
+ if (event?.type !== "heartbeat") {
544
+ captureContext.onFirstEvent?.();
545
+ }
546
+
536
547
  switch (event.type) {
537
548
  case "text-delta": {
538
549
  const text = event.text ?? "";
@@ -735,6 +746,9 @@ async function* parseJsonlStream(
735
746
  };
736
747
  } catch (err) {
737
748
  const timedOut = captureContext.didGatewayTimeout?.() ?? false;
749
+ const gatewayTimeoutMs =
750
+ captureContext.getGatewayTimeoutMs?.() ??
751
+ DEFAULT_BUILDER_GATEWAY_TIMEOUT_MS;
738
752
  if (timedOut || isBuilderGatewayNetworkError(err)) {
739
753
  captureBuilderGatewayTransportError(err, {
740
754
  phase: "stream",
@@ -880,16 +894,28 @@ function getBuilderGatewayTimeoutMs(): number {
880
894
  return Math.min(parsed, maxMs);
881
895
  }
882
896
 
897
+ /**
898
+ * Two-stage abort deadline: until the first real stream event arrives, the
899
+ * effective deadline is min(totalTimeoutMs, FIRST_STREAM_EVENT_TIMEOUT_MS) —
900
+ * a wedged gateway that never streams anything gets cut off in ~2 minutes
901
+ * instead of riding the full flat timeout. Once `markFirstEvent()` fires, the
902
+ * timer reschedules for whatever remains of the original total deadline, so
903
+ * a request that starts streaming still gets the full budget it always did.
904
+ */
883
905
  function createGatewayAbortSignal(
884
906
  parentSignal: AbortSignal,
885
- timeoutMs: number,
907
+ totalTimeoutMs: number,
886
908
  ): {
887
909
  signal: AbortSignal;
888
910
  didTimeout: () => boolean;
911
+ effectiveTimeoutMs: () => number;
912
+ markFirstEvent: () => void;
889
913
  cleanup: () => void;
890
914
  } {
891
915
  const controller = new AbortController();
892
916
  let timedOut = false;
917
+ let firstEventSeen = false;
918
+ const startedAt = Date.now();
893
919
 
894
920
  const abortFromParent = () => {
895
921
  if (!controller.signal.aborted) {
@@ -897,12 +923,18 @@ function createGatewayAbortSignal(
897
923
  }
898
924
  };
899
925
 
900
- const timeout = setTimeout(() => {
926
+ const fireTimeout = () => {
901
927
  timedOut = true;
902
928
  if (!controller.signal.aborted) {
903
929
  controller.abort(new Error("Builder gateway request timed out"));
904
930
  }
905
- }, timeoutMs);
931
+ };
932
+
933
+ const firstEventDeadlineMs = Math.min(
934
+ totalTimeoutMs,
935
+ FIRST_STREAM_EVENT_TIMEOUT_MS,
936
+ );
937
+ let timeout = setTimeout(fireTimeout, firstEventDeadlineMs);
906
938
 
907
939
  if (parentSignal.aborted) abortFromParent();
908
940
  parentSignal.addEventListener("abort", abortFromParent, { once: true });
@@ -910,6 +942,21 @@ function createGatewayAbortSignal(
910
942
  return {
911
943
  signal: controller.signal,
912
944
  didTimeout: () => timedOut,
945
+ effectiveTimeoutMs: () =>
946
+ firstEventSeen ? totalTimeoutMs : firstEventDeadlineMs,
947
+ markFirstEvent: () => {
948
+ if (firstEventSeen || timedOut) return;
949
+ firstEventSeen = true;
950
+ // The first-event window was already the binding constraint (total
951
+ // timeout <= it) — nothing to reschedule.
952
+ if (firstEventDeadlineMs >= totalTimeoutMs) return;
953
+ clearTimeout(timeout);
954
+ const remainingMs = Math.max(
955
+ 0,
956
+ totalTimeoutMs - (Date.now() - startedAt),
957
+ );
958
+ timeout = setTimeout(fireTimeout, remainingMs);
959
+ },
913
960
  cleanup: () => {
914
961
  clearTimeout(timeout);
915
962
  parentSignal.removeEventListener("abort", abortFromParent);
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Shared "first stream event" deadline for model-request engines.
3
+ *
4
+ * A request that connects successfully but then streams zero events means the
5
+ * transport or gateway is wedged, not slow — real models (including deep
6
+ * thinking ones) emit their first event within seconds. Bounding this window
7
+ * separately from any total-request deadline turns a silent multi-minute hang
8
+ * into a fast abort-and-retry.
9
+ */
10
+ export const FIRST_STREAM_EVENT_TIMEOUT_MS = 120_000;
11
+
12
+ export interface FirstEventAbortController {
13
+ readonly signal: AbortSignal;
14
+ /** Idempotent. Call once the first real (non-keepalive) stream event arrives. */
15
+ markFirstEvent: () => void;
16
+ didTimeout: () => boolean;
17
+ cleanup: () => void;
18
+ }
19
+
20
+ /**
21
+ * Layer a first-event deadline on top of a caller's AbortSignal. Aborts if
22
+ * `markFirstEvent()` is not called within `FIRST_STREAM_EVENT_TIMEOUT_MS`.
23
+ * Has no opinion on a total-request deadline — callers that need one (e.g.
24
+ * builder-engine's flat gateway timeout) compose their own on top.
25
+ */
26
+ export function createFirstEventAbortController(
27
+ parentSignal: AbortSignal,
28
+ ): FirstEventAbortController {
29
+ const controller = new AbortController();
30
+ let timedOut = false;
31
+ let firstEventSeen = false;
32
+
33
+ const abortFromParent = () => {
34
+ if (!controller.signal.aborted) controller.abort(parentSignal.reason);
35
+ };
36
+
37
+ const timeout = setTimeout(() => {
38
+ timedOut = true;
39
+ if (!controller.signal.aborted) {
40
+ controller.abort(
41
+ new Error(
42
+ `Model request produced no stream events within ${FIRST_STREAM_EVENT_TIMEOUT_MS / 1000}s`,
43
+ ),
44
+ );
45
+ }
46
+ }, FIRST_STREAM_EVENT_TIMEOUT_MS);
47
+
48
+ if (parentSignal.aborted) abortFromParent();
49
+ parentSignal.addEventListener("abort", abortFromParent, { once: true });
50
+
51
+ return {
52
+ signal: controller.signal,
53
+ markFirstEvent: () => {
54
+ if (firstEventSeen) return;
55
+ firstEventSeen = true;
56
+ clearTimeout(timeout);
57
+ },
58
+ didTimeout: () => timedOut,
59
+ cleanup: () => {
60
+ clearTimeout(timeout);
61
+ parentSignal.removeEventListener("abort", abortFromParent);
62
+ },
63
+ };
64
+ }
@@ -3237,7 +3237,9 @@ const AssistantChatInner = forwardRef<
3237
3237
  const reconnectErrorCode =
3238
3238
  reconnectTerminalReason === "run_timeout"
3239
3239
  ? "run_timeout"
3240
- : "reconnect_no_progress";
3240
+ : reconnectTerminalReason === "stream_ended"
3241
+ ? "reconnect_stream_ended"
3242
+ : "reconnect_no_progress";
3241
3243
  captureError(new Error(`agent-chat:${reconnectErrorCode}`), {
3242
3244
  tags: {
3243
3245
  context: "agent-native-chat",
@@ -3309,9 +3311,11 @@ const AssistantChatInner = forwardRef<
3309
3311
  }
3310
3312
  setRunErrorInfo({
3311
3313
  message:
3312
- reconnectTerminalReason === "run_timeout"
3314
+ reconnectErrorCode === "run_timeout"
3313
3315
  ? "The previous background agent run reached its time limit before finishing. The partial work was preserved; continue or retry to pick up from here."
3314
- : "The previous agent run stopped producing visible progress during recovery, so it was stopped before it could keep looping.",
3316
+ : reconnectErrorCode === "reconnect_stream_ended"
3317
+ ? "The previous agent stream ended while the run was recovering. Continue or retry to reconnect to the run."
3318
+ : "The previous agent run stopped producing visible progress during recovery, so it was stopped before it could keep looping.",
3315
3319
  errorCode: reconnectErrorCode,
3316
3320
  recoverable: true,
3317
3321
  runId,
@@ -613,7 +613,7 @@ The result should be a reusable agent profile, not a one-off task response.`,
613
613
  icon: <IconBulb className="h-3.5 w-3.5" />,
614
614
  label: "Create Skill",
615
615
  desc: "Teach the agent a new ability",
616
- action: openSkillFlyout,
616
+ action: () => openSkillFlyout(),
617
617
  hoverAction: openSkillFlyout,
618
618
  },
619
619
  {
@@ -175,7 +175,7 @@ export async function decideComputerApproval(input: {
175
175
  SET status = ?, decision_result_json = ?, decided_by = ?,
176
176
  decided_at = ?, updated_at = ?
177
177
  WHERE id = ? AND owner_email = ?
178
- AND ((org_id IS NULL AND ? IS NULL) OR org_id = ?)
178
+ AND ((org_id IS NULL AND CAST(? AS TEXT) IS NULL) OR org_id = ?)
179
179
  AND action_hash = ? AND status = 'pending' AND expires_at > ?`,
180
180
  args: [
181
181
  input.decision,
@@ -204,7 +204,7 @@ export async function getComputerApprovalForOwner(input: {
204
204
  const { rows } = await getDbExec().execute({
205
205
  sql: `SELECT * FROM integration_computer_approvals
206
206
  WHERE id = ? AND owner_email = ?
207
- AND ((org_id IS NULL AND ? IS NULL) OR org_id = ?)
207
+ AND ((org_id IS NULL AND CAST(? AS TEXT) IS NULL) OR org_id = ?)
208
208
  LIMIT 1`,
209
209
  args: [
210
210
  input.id,
@@ -228,7 +228,7 @@ export async function listComputerApprovalsForOwner(input: {
228
228
  await ensureComputerApprovalStore();
229
229
  const clauses = [
230
230
  "owner_email = ?",
231
- "((org_id IS NULL AND ? IS NULL) OR org_id = ?)",
231
+ "((org_id IS NULL AND CAST(? AS TEXT) IS NULL) OR org_id = ?)",
232
232
  ];
233
233
  const args: Array<string | number | null> = [
234
234
  input.ownerEmail,
@@ -295,7 +295,7 @@ export async function authorizeComputerOperation(
295
295
  const { rows } = await client.execute({
296
296
  sql: `SELECT * FROM integration_computer_approvals
297
297
  WHERE id = ? AND owner_email = ?
298
- AND ((org_id IS NULL AND ? IS NULL) OR org_id = ?)
298
+ AND ((org_id IS NULL AND CAST(? AS TEXT) IS NULL) OR org_id = ?)
299
299
  AND device_id = ? AND task_id = ? AND run_id = ?
300
300
  LIMIT 1`,
301
301
  args: [
@@ -316,7 +316,7 @@ export async function resolveIntegrationSourceContext(
316
316
  FROM integration_pending_tasks
317
317
  WHERE id = ?
318
318
  AND owner_email = ?
319
- AND (org_id = ? OR (org_id IS NULL AND ? IS NULL))
319
+ AND (org_id = ? OR (org_id IS NULL AND CAST(? AS TEXT) IS NULL))
320
320
  AND platform = 'slack'
321
321
  LIMIT 1`,
322
322
  args: [id, ownerEmail, orgId, orgId],
@@ -368,7 +368,7 @@ export async function listRemoteCommandsForOwner(input: {
368
368
  const { rows } = await getDbExec().execute({
369
369
  sql: `SELECT * FROM integration_remote_commands
370
370
  WHERE owner_email = ?
371
- AND ((org_id IS NULL AND ? IS NULL) OR org_id = ?)
371
+ AND ((org_id IS NULL AND CAST(? AS TEXT) IS NULL) OR org_id = ?)
372
372
  ORDER BY updated_at DESC
373
373
  LIMIT ?`,
374
374
  args: [input.ownerEmail, input.orgId ?? null, input.orgId ?? null, limit],
@@ -435,7 +435,7 @@ export async function claimNextComputerCommand(input: {
435
435
  const { rows } = await client.execute({
436
436
  sql: `SELECT * FROM integration_remote_commands
437
437
  WHERE device_id = ? AND owner_email = ?
438
- AND ((org_id IS NULL AND ? IS NULL) OR org_id = ?)
438
+ AND ((org_id IS NULL AND CAST(? AS TEXT) IS NULL) OR org_id = ?)
439
439
  AND kind = 'computer-operation' AND status = 'pending'
440
440
  AND next_check_at <= ?${operationClassClause}
441
441
  ORDER BY computer_sequence ASC, created_at ASC
@@ -477,7 +477,7 @@ export async function claimNextComputerCommand(input: {
477
477
  sql: `UPDATE integration_remote_commands
478
478
  SET status = 'failed', error_message = ?, completed_at = ?, updated_at = ?
479
479
  WHERE id = ? AND device_id = ? AND owner_email = ?
480
- AND ((org_id IS NULL AND ? IS NULL) OR org_id = ?)
480
+ AND ((org_id IS NULL AND CAST(? AS TEXT) IS NULL) OR org_id = ?)
481
481
  AND status = 'pending'`,
482
482
  args: [
483
483
  error instanceof Error
@@ -500,13 +500,13 @@ export async function claimNextComputerCommand(input: {
500
500
  ? `UPDATE integration_remote_commands
501
501
  SET status = 'claimed', attempts = attempts + 1, claimed_at = ?, updated_at = ?
502
502
  WHERE id = ? AND device_id = ? AND owner_email = ?
503
- AND ((org_id IS NULL AND ? IS NULL) OR org_id = ?)
503
+ AND ((org_id IS NULL AND CAST(? AS TEXT) IS NULL) OR org_id = ?)
504
504
  AND status = 'pending' AND lease_expires_at > ?
505
505
  RETURNING *`
506
506
  : `UPDATE integration_remote_commands
507
507
  SET status = 'claimed', attempts = attempts + 1, claimed_at = ?, updated_at = ?
508
508
  WHERE id = ? AND device_id = ? AND owner_email = ?
509
- AND ((org_id IS NULL AND ? IS NULL) OR org_id = ?)
509
+ AND ((org_id IS NULL AND CAST(? AS TEXT) IS NULL) OR org_id = ?)
510
510
  AND status = 'pending' AND lease_expires_at > ?`,
511
511
  args: [
512
512
  now,
@@ -243,7 +243,7 @@ export async function getRemoteDeviceForOwner(input: {
243
243
  sql: `SELECT * FROM integration_remote_devices
244
244
  WHERE id = ?
245
245
  AND owner_email = ?
246
- AND ((org_id IS NULL AND ? IS NULL) OR org_id = ?)
246
+ AND ((org_id IS NULL AND CAST(? AS TEXT) IS NULL) OR org_id = ?)
247
247
  LIMIT 1`,
248
248
  args: [
249
249
  input.id,
@@ -287,7 +287,7 @@ export async function listRemoteDevicesForOwner(input: {
287
287
  const { rows } = await getDbExec().execute({
288
288
  sql: `SELECT * FROM integration_remote_devices
289
289
  WHERE owner_email = ?
290
- AND ((org_id IS NULL AND ? IS NULL) OR org_id = ?)${statusClause}
290
+ AND ((org_id IS NULL AND CAST(? AS TEXT) IS NULL) OR org_id = ?)${statusClause}
291
291
  ORDER BY COALESCE(last_seen_at, updated_at) DESC
292
292
  LIMIT ?`,
293
293
  args,
@@ -379,7 +379,7 @@ export async function revokeRemoteDeviceForOwner(input: {
379
379
  updated_at = ?
380
380
  WHERE id = ?
381
381
  AND owner_email = ?
382
- AND ((org_id IS NULL AND ? IS NULL) OR org_id = ?)`,
382
+ AND ((org_id IS NULL AND CAST(? AS TEXT) IS NULL) OR org_id = ?)`,
383
383
  args: [
384
384
  now,
385
385
  now,
@@ -330,7 +330,7 @@ export async function listRemotePushRegistrationsForOwner(input: {
330
330
  const { rows } = await getDbExec().execute({
331
331
  sql: `SELECT * FROM integration_remote_push_registrations
332
332
  WHERE owner_email = ?
333
- AND ((org_id IS NULL AND ? IS NULL) OR org_id = ?)${statusClause}
333
+ AND ((org_id IS NULL AND CAST(? AS TEXT) IS NULL) OR org_id = ?)${statusClause}
334
334
  ORDER BY COALESCE(last_seen_at, updated_at) DESC
335
335
  LIMIT ?`,
336
336
  args: [input.ownerEmail, input.orgId ?? null, input.orgId ?? null, limit],
@@ -352,7 +352,7 @@ export async function unregisterRemotePushRegistrationForOwner(input: {
352
352
  sql: `UPDATE integration_remote_push_registrations
353
353
  SET status = 'inactive', updated_at = ?
354
354
  WHERE owner_email = ?
355
- AND ((org_id IS NULL AND ? IS NULL) OR org_id = ?)
355
+ AND ((org_id IS NULL AND CAST(? AS TEXT) IS NULL) OR org_id = ?)
356
356
  AND (${input.id ? "id = ?" : "0 = 1"} OR ${
357
357
  tokenHash ? "token_hash = ?" : "0 = 1"
358
358
  })`,
@@ -432,7 +432,7 @@ export async function listRemotePushNotificationsForOwner(input: {
432
432
  const { rows } = await getDbExec().execute({
433
433
  sql: `SELECT * FROM integration_remote_push_notifications
434
434
  WHERE owner_email = ?
435
- AND ((org_id IS NULL AND ? IS NULL) OR org_id = ?)${statusClause}
435
+ AND ((org_id IS NULL AND CAST(? AS TEXT) IS NULL) OR org_id = ?)${statusClause}
436
436
  ORDER BY created_at DESC
437
437
  LIMIT ?`,
438
438
  args,