@agent-native/core 0.98.7 → 0.98.8

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 (49) hide show
  1. package/corpus/core/CHANGELOG.md +6 -0
  2. package/corpus/core/docs/content/external-agents.mdx +17 -11
  3. package/corpus/core/docs/content/locales/ar-SA/external-agents.mdx +6 -2
  4. package/corpus/core/docs/content/locales/de-DE/external-agents.mdx +6 -2
  5. package/corpus/core/docs/content/locales/es-ES/external-agents.mdx +6 -2
  6. package/corpus/core/docs/content/locales/fr-FR/external-agents.mdx +6 -2
  7. package/corpus/core/docs/content/locales/hi-IN/external-agents.mdx +6 -2
  8. package/corpus/core/docs/content/locales/ja-JP/external-agents.mdx +6 -2
  9. package/corpus/core/docs/content/locales/ko-KR/external-agents.mdx +6 -2
  10. package/corpus/core/docs/content/locales/pt-BR/external-agents.mdx +6 -2
  11. package/corpus/core/docs/content/locales/zh-CN/external-agents.mdx +6 -2
  12. package/corpus/core/docs/content/locales/zh-TW/external-agents.mdx +6 -2
  13. package/corpus/core/package.json +1 -1
  14. package/corpus/core/src/client/session-replay.ts +29 -7
  15. package/corpus/core/src/integrations/adapters/slack.ts +13 -2
  16. package/corpus/core/src/integrations/pending-tasks-retry-job.ts +2 -1
  17. package/corpus/core/src/integrations/pending-tasks-store.ts +1 -1
  18. package/corpus/core/src/integrations/plugin.ts +183 -48
  19. package/corpus/core/src/integrations/types.ts +7 -0
  20. package/dist/client/session-replay.d.ts.map +1 -1
  21. package/dist/client/session-replay.js +24 -7
  22. package/dist/client/session-replay.js.map +1 -1
  23. package/dist/collab/routes.d.ts +2 -2
  24. package/dist/integrations/adapters/slack.js +11 -2
  25. package/dist/integrations/adapters/slack.js.map +1 -1
  26. package/dist/integrations/pending-tasks-retry-job.d.ts.map +1 -1
  27. package/dist/integrations/pending-tasks-retry-job.js +2 -1
  28. package/dist/integrations/pending-tasks-retry-job.js.map +1 -1
  29. package/dist/integrations/pending-tasks-store.js +1 -1
  30. package/dist/integrations/pending-tasks-store.js.map +1 -1
  31. package/dist/integrations/plugin.d.ts.map +1 -1
  32. package/dist/integrations/plugin.js +140 -34
  33. package/dist/integrations/plugin.js.map +1 -1
  34. package/dist/integrations/types.d.ts +7 -0
  35. package/dist/integrations/types.d.ts.map +1 -1
  36. package/dist/integrations/types.js.map +1 -1
  37. package/dist/notifications/routes.d.ts +1 -1
  38. package/docs/content/external-agents.mdx +17 -11
  39. package/docs/content/locales/ar-SA/external-agents.mdx +6 -2
  40. package/docs/content/locales/de-DE/external-agents.mdx +6 -2
  41. package/docs/content/locales/es-ES/external-agents.mdx +6 -2
  42. package/docs/content/locales/fr-FR/external-agents.mdx +6 -2
  43. package/docs/content/locales/hi-IN/external-agents.mdx +6 -2
  44. package/docs/content/locales/ja-JP/external-agents.mdx +6 -2
  45. package/docs/content/locales/ko-KR/external-agents.mdx +6 -2
  46. package/docs/content/locales/pt-BR/external-agents.mdx +6 -2
  47. package/docs/content/locales/zh-CN/external-agents.mdx +6 -2
  48. package/docs/content/locales/zh-TW/external-agents.mdx +6 -2
  49. package/package.json +1 -1
@@ -80,6 +80,7 @@ import {
80
80
  claimPendingTask,
81
81
  getNextPendingTaskIdForThread,
82
82
  insertPendingTask,
83
+ isDuplicateEventError,
83
84
  MAX_PENDING_TASK_ATTEMPTS,
84
85
  markTaskCompleted,
85
86
  markTaskFailed,
@@ -140,6 +141,7 @@ import type {
140
141
  IntegrationsPluginOptions,
141
142
  IntegrationStatus,
142
143
  IntegrationExecutionContext,
144
+ IncomingMessage,
143
145
  } from "./types.js";
144
146
  import {
145
147
  listIntegrationUsageBudgets,
@@ -259,6 +261,23 @@ const REMOTE_DEVICE_ONLINE_MS = 90_000;
259
261
  // API outage every message would otherwise get another identical "try again"
260
262
  // reply. Short enough that a persistent condition still reminds the sender.
261
263
  const DECLINE_NOTICE_DEDUPE_TTL_MS = 5 * 60 * 1_000;
264
+ const SYSTEM_NOTICE_DEDUPE_TTL_MS = 24 * 60 * 60 * 1_000;
265
+
266
+ type IntegrationSystemNoticeTaskPayload = {
267
+ kind: "system-notice";
268
+ incoming: IncomingMessage;
269
+ text: string;
270
+ dedupeKey?: string;
271
+ dedupeTtlMs?: number;
272
+ };
273
+
274
+ function systemNoticeEventKey(
275
+ dedupeKey: string,
276
+ ttlMs: number,
277
+ now = Date.now(),
278
+ ): string {
279
+ return `system-notice:${dedupeKey}:${Math.floor(now / ttlMs)}`;
280
+ }
262
281
 
263
282
  export async function enqueueRemoteCommand(
264
283
  envelope: RemoteCodeCommandEnvelope,
@@ -637,6 +656,66 @@ export function createIntegrationsPlugin(
637
656
  const h3 = getH3App(nitroApp);
638
657
  const P = `${FRAMEWORK_ROUTE_PREFIX}/integrations`;
639
658
 
659
+ async function enqueueSystemNotice(
660
+ event: any,
661
+ incoming: IncomingMessage,
662
+ text: string,
663
+ opts?: { dedupeKey?: string; dedupeTtlMs?: number },
664
+ ): Promise<void> {
665
+ if (!text.trim()) return;
666
+ const taskId = `notice-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
667
+ const dedupeTtlMs = Math.max(
668
+ 1,
669
+ opts?.dedupeTtlMs ?? SYSTEM_NOTICE_DEDUPE_TTL_MS,
670
+ );
671
+ const noticeThreadId = `system-notice:${taskId}`;
672
+ const payload: IntegrationSystemNoticeTaskPayload = {
673
+ kind: "system-notice",
674
+ incoming,
675
+ text,
676
+ ...(opts?.dedupeKey ? { dedupeKey: opts.dedupeKey } : {}),
677
+ ...(opts?.dedupeTtlMs ? { dedupeTtlMs: opts.dedupeTtlMs } : {}),
678
+ };
679
+ try {
680
+ await insertPendingTask({
681
+ id: taskId,
682
+ platform: incoming.platform,
683
+ // System notices are auxiliary delivery work, not the user's agent
684
+ // run. Give each notice its own queue lane so a retrying notice cannot
685
+ // block the real message task for this Slack/Telegram thread.
686
+ externalThreadId: noticeThreadId,
687
+ payload: JSON.stringify(payload),
688
+ ownerEmail: `integration@${incoming.platform}`,
689
+ externalEventKey: opts?.dedupeKey
690
+ ? systemNoticeEventKey(opts.dedupeKey, dedupeTtlMs)
691
+ : undefined,
692
+ });
693
+ } catch (err) {
694
+ if (isDuplicateEventError(err)) return;
695
+ throw err;
696
+ }
697
+
698
+ // The SQL row is the durable source of truth. This best-effort self-call
699
+ // only reduces latency; the recurring pending-task sweep retries a row
700
+ // if the serverless host freezes this webhook execution immediately.
701
+ let token: string | undefined;
702
+ try {
703
+ token = signInternalToken(taskId);
704
+ } catch (err) {
705
+ if (process.env.NODE_ENV === "production") throw err;
706
+ }
707
+ void fetch(`${getBaseUrl(event)}${P}/process-task`, {
708
+ method: "POST",
709
+ headers: {
710
+ "Content-Type": "application/json",
711
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
712
+ },
713
+ body: JSON.stringify({ taskId }),
714
+ }).catch((err) => {
715
+ console.warn("[integrations] System notice dispatch failed:", err);
716
+ });
717
+ }
718
+
640
719
  async function requireSession(event: any): Promise<boolean> {
641
720
  const session = await getSession(event).catch(() => null);
642
721
  if (session?.email) return true;
@@ -1550,6 +1629,34 @@ export function createIntegrationsPlugin(
1550
1629
  isIntegrationCaller: true,
1551
1630
  },
1552
1631
  async () => {
1632
+ const taskPayload = JSON.parse(task.payload) as
1633
+ | IntegrationSystemNoticeTaskPayload
1634
+ | { kind?: undefined };
1635
+ if (taskPayload.kind === "system-notice") {
1636
+ if (!adapter.sendSystemNotice) {
1637
+ throw new Error(
1638
+ `Platform ${task.platform} cannot deliver system notices`,
1639
+ );
1640
+ }
1641
+ const config = await getIntegrationConfig(task.platform);
1642
+ const credentialContext =
1643
+ await credentialContextForIntegrationConfig(config);
1644
+ await withCredentialContext(credentialContext, () =>
1645
+ adapter.sendSystemNotice!(
1646
+ taskPayload.incoming,
1647
+ taskPayload.text,
1648
+ {
1649
+ ...(taskPayload.dedupeKey
1650
+ ? { dedupeKey: taskPayload.dedupeKey }
1651
+ : {}),
1652
+ ...(taskPayload.dedupeTtlMs
1653
+ ? { dedupeTtlMs: taskPayload.dedupeTtlMs }
1654
+ : {}),
1655
+ },
1656
+ ),
1657
+ );
1658
+ return;
1659
+ }
1553
1660
  const resources = await loadResourcesForPrompt(
1554
1661
  task.ownerEmail,
1555
1662
  true,
@@ -2378,7 +2485,8 @@ export function createIntegrationsPlugin(
2378
2485
  null;
2379
2486
  if (
2380
2487
  incoming.platform === "slack" &&
2381
- incoming.conversationType === "dm"
2488
+ incoming.conversationType === "dm" &&
2489
+ !options?.resolveExecutionContext
2382
2490
  ) {
2383
2491
  try {
2384
2492
  defaultExecutionContext = await withCredentialContext(
@@ -2386,51 +2494,48 @@ export function createIntegrationsPlugin(
2386
2494
  () => resolveDefaultIntegrationExecutionContext(incoming!),
2387
2495
  );
2388
2496
  } catch (err) {
2389
- // Only an explicit execution-context resolver may override the
2390
- // default Slack identity ladder. The legacy owner-only resolver
2391
- // predates org-bound identities and must not turn a rejected DM
2392
- // into an authenticated owner run.
2393
- if (!options?.resolveExecutionContext) {
2394
- const declined =
2395
- err instanceof IntegrationIdentityDeclinedError ? err : null;
2396
- if (declined) {
2397
- console.warn(
2398
- `[integrations] default Slack DM identity declined message:`,
2399
- declined.message,
2400
- );
2401
- // Best-effort polite reply back to the DM, deduped per
2402
- // sender + reason so an outage doesn't produce an identical
2403
- // reply for every message. Do not await Slack's API: webhook
2404
- // acknowledgement has a stricter deadline than notice
2405
- // delivery and must not be delayed by a slow postMessage.
2406
- void withCredentialContext(credentialContext, () =>
2407
- adapter.sendSystemNotice
2408
- ? adapter.sendSystemNotice(
2409
- incoming!,
2410
- declined.userFacingMessage,
2411
- {
2412
- dedupeKey: `decline:${incoming!.tenantId ?? "unknown"}:${incoming!.senderId ?? "unknown"}:${declined.reason}`,
2413
- dedupeTtlMs: DECLINE_NOTICE_DEDUPE_TTL_MS,
2414
- },
2415
- )
2416
- : Promise.resolve(),
2417
- ).catch((noticeErr) => {
2497
+ // The legacy owner-only resolver predates org-bound identities
2498
+ // and must not turn a rejected Slack DM into an authenticated
2499
+ // owner run. Custom resolveExecutionContext is checked above and
2500
+ // skips this default ladder entirely so apps can fully own auth
2501
+ // without framework membership checks or identity side effects.
2502
+ const declined =
2503
+ err instanceof IntegrationIdentityDeclinedError ? err : null;
2504
+ if (declined) {
2505
+ console.warn(
2506
+ `[integrations] default Slack DM identity declined message:`,
2507
+ declined.message,
2508
+ );
2509
+ if (adapter.sendSystemNotice) {
2510
+ try {
2511
+ await enqueueSystemNotice(
2512
+ event,
2513
+ incoming!,
2514
+ declined.userFacingMessage,
2515
+ {
2516
+ dedupeKey: `decline:${incoming!.tenantId ?? "unknown"}:${incoming!.senderId ?? "unknown"}:${declined.reason}`,
2517
+ dedupeTtlMs: DECLINE_NOTICE_DEDUPE_TTL_MS,
2518
+ },
2519
+ );
2520
+ } catch (noticeErr) {
2418
2521
  console.warn(
2419
- `[integrations] decline notice failed:`,
2522
+ `[integrations] could not persist decline notice:`,
2420
2523
  noticeErr instanceof Error
2421
2524
  ? noticeErr.message
2422
2525
  : noticeErr,
2423
2526
  );
2424
- });
2425
- } else {
2426
- console.error(
2427
- `[integrations] default Slack DM identity denied message:`,
2428
- err,
2429
- );
2527
+ setResponseStatus(event, 500);
2528
+ return { error: "notice enqueue failed" };
2529
+ }
2430
2530
  }
2431
- setResponseStatus(event, 200);
2432
- return "ok";
2531
+ } else {
2532
+ console.error(
2533
+ `[integrations] default Slack DM identity denied message:`,
2534
+ err,
2535
+ );
2433
2536
  }
2537
+ setResponseStatus(event, 200);
2538
+ return "ok";
2434
2539
  }
2435
2540
  }
2436
2541
  let executionContext: IntegrationExecutionContext = {
@@ -2456,14 +2561,42 @@ export function createIntegrationsPlugin(
2456
2561
  } else if (defaultExecutionContext) {
2457
2562
  executionContext = defaultExecutionContext;
2458
2563
  if (defaultExecutionContext.anonymousMember) {
2564
+ if (!options?.allowAnonymousOrgScopedSlackDm) {
2565
+ const senderEmail =
2566
+ typeof incoming.senderEmail === "string" &&
2567
+ incoming.senderEmail.trim()
2568
+ ? incoming.senderEmail.trim()
2569
+ : null;
2570
+ const noticeText = senderEmail
2571
+ ? `I couldn't match your Slack account to an organization member, so I can't run this request. Ask an organization admin to add ${senderEmail}, then try again.`
2572
+ : "I couldn't verify your Slack account email, so I can't run this request. Ask an organization admin to reconnect Slack with the users:read.email scope, then try again.";
2573
+ if (adapter.sendSystemNotice) {
2574
+ try {
2575
+ await enqueueSystemNotice(event, incoming, noticeText, {
2576
+ dedupeKey: `anonymous-tier-disabled:${incoming.tenantId ?? "unknown"}:${incoming.senderId ?? "unknown"}`,
2577
+ });
2578
+ } catch (noticeErr) {
2579
+ console.warn(
2580
+ `[integrations] could not persist unlinked-member notice:`,
2581
+ noticeErr instanceof Error
2582
+ ? noticeErr.message
2583
+ : noticeErr,
2584
+ );
2585
+ setResponseStatus(event, 500);
2586
+ return { error: "notice enqueue failed" };
2587
+ }
2588
+ }
2589
+ setResponseStatus(event, 200);
2590
+ return "ok";
2591
+ }
2459
2592
  // The anonymous tier must never be silent. (1) The agent run
2460
2593
  // can tell: the note rides the serialized `incoming` into the
2461
2594
  // queued task and surfaces via <integration-context>.
2462
2595
  incoming.identityNote =
2463
2596
  "Caller is an unlinked Slack workspace member running with organization-wide visibility only; personal or privately-shared data is not accessible. They can get personal access by having an admin add their Slack email to the organization (or by reconnecting Slack with the users:read.email scope).";
2464
- // (2) The sender gets a one-time heads-up (adapter-throttled per
2465
- // sender). Fire-and-forget so it never delays or replaces the
2466
- // normal agent run.
2597
+ // (2) The sender gets a one-time heads-up through the same
2598
+ // durable SQL queue as agent work. The self-dispatch is only a
2599
+ // latency optimization; the retry sweep guarantees delivery.
2467
2600
  if (adapter.sendSystemNotice) {
2468
2601
  const senderEmail =
2469
2602
  typeof incoming.senderEmail === "string" &&
@@ -2473,16 +2606,18 @@ export function createIntegrationsPlugin(
2473
2606
  const noticeText = senderEmail
2474
2607
  ? `Heads up: I couldn't match your Slack account to an organization member, so I can only use org-wide data. Ask an admin to add ${senderEmail} to the organization for personal access.`
2475
2608
  : "Heads up: I couldn't verify your Slack account's email, so I can only use org-wide data. Ask an admin to update the Slack connection with the users:read.email scope for personal access.";
2476
- void withCredentialContext(credentialContext, () =>
2477
- adapter.sendSystemNotice!(incoming!, noticeText, {
2609
+ try {
2610
+ await enqueueSystemNotice(event, incoming, noticeText, {
2478
2611
  dedupeKey: `anonymous-tier:${incoming.tenantId ?? "unknown"}:${incoming.senderId ?? "unknown"}`,
2479
- }),
2480
- ).catch((noticeErr) => {
2612
+ });
2613
+ } catch (noticeErr) {
2481
2614
  console.warn(
2482
- `[integrations] anonymous-tier notice failed:`,
2615
+ `[integrations] could not persist anonymous-tier notice:`,
2483
2616
  noticeErr instanceof Error ? noticeErr.message : noticeErr,
2484
2617
  );
2485
- });
2618
+ setResponseStatus(event, 500);
2619
+ return { error: "notice enqueue failed" };
2620
+ }
2486
2621
  }
2487
2622
  }
2488
2623
  } else if (options?.resolveOwner) {
@@ -438,6 +438,13 @@ export interface IntegrationsPluginOptions {
438
438
  resolveExecutionContext?: (
439
439
  incoming: IncomingMessage,
440
440
  ) => IntegrationExecutionContext | Promise<IntegrationExecutionContext>;
441
+ /**
442
+ * Explicitly allow an unlinked, verified Slack workspace member to run a DM
443
+ * with the installation organization's shared/service visibility. Disabled
444
+ * by default: DM identity resolution fails closed unless an app deliberately
445
+ * accepts this wider access tier.
446
+ */
447
+ allowAnonymousOrgScopedSlackDm?: boolean;
441
448
  /**
442
449
  * Optional preprocessor for inbound platform messages. Can intercept special
443
450
  * commands (such as `/link`) before the agent loop runs.
@@ -1 +1 @@
1
- {"version":3,"file":"session-replay.d.ts","sourceRoot":"","sources":["../../src/client/session-replay.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"session-replay.d.ts","sourceRoot":"","sources":["../../src/client/session-replay.ts"],"names":[],"mappings":"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"}
@@ -127,6 +127,7 @@ function getState() {
127
127
  queue: [],
128
128
  queuedBytes: 0,
129
129
  retryBatches: [],
130
+ transientClientErrorFailures: 0,
130
131
  flushTimer: null,
131
132
  maxDurationTimer: null,
132
133
  flushing: false,
@@ -149,6 +150,7 @@ function getState() {
149
150
  const state = g[SESSION_REPLAY_STATE_KEY];
150
151
  // Keep Vite HMR safe when an older recorder state survives a module reload.
151
152
  state.resourceNodes ??= new Map();
153
+ state.transientClientErrorFailures ??= 0;
152
154
  state.restoreIframeBridge ??= null;
153
155
  state.pendingFlushReason ??= null;
154
156
  state.pendingFlushWaiters ??= [];
@@ -658,14 +660,12 @@ const REPLAY_RESOURCE_TAGS = new Set([
658
660
  "audio",
659
661
  "track",
660
662
  "input",
661
- "object",
662
663
  "link",
663
664
  ]);
664
665
  const NO_REPLAY_RESOURCE_ATTRIBUTES = new Set();
665
666
  const REPLAY_SRC_ATTRIBUTES = new Set(["src"]);
666
667
  const REPLAY_SRCSET_ATTRIBUTES = new Set(["src", "srcset"]);
667
668
  const REPLAY_VIDEO_ATTRIBUTES = new Set(["src", "poster"]);
668
- const REPLAY_OBJECT_ATTRIBUTES = new Set(["data"]);
669
669
  const REPLAY_HREF_ATTRIBUTES = new Set(["href"]);
670
670
  function replayAttributeString(attributes, key) {
671
671
  return typeof attributes[key] === "string"
@@ -696,8 +696,6 @@ function replayPreservedResourceAttributes(node) {
696
696
  case "audio":
697
697
  case "track":
698
698
  return REPLAY_SRC_ATTRIBUTES;
699
- case "object":
700
- return REPLAY_OBJECT_ATTRIBUTES;
701
699
  case "input":
702
700
  return node.type === "image"
703
701
  ? REPLAY_SRC_ATTRIBUTES
@@ -991,11 +989,16 @@ class ReplayUploadHttpError extends Error {
991
989
  }
992
990
  /** 4xx statuses where retrying the exact same batch can never succeed.
993
991
  * Keep this deliberately narrow: 401/403/404 can be temporary during auth or
994
- * deploy transitions, and stopping forever on one of those would silently
995
- * black out the rest of a long-lived SPA session. */
992
+ * deploy transitions, so they get a small retry budget before the rejected
993
+ * episode is stopped. The budget prevents a persistent configuration failure
994
+ * from pinning retryBatches while rrweb events grow without bound. */
996
995
  function isDefinitiveReplayUploadClientError(status) {
997
996
  return status === 400 || status === 409 || status === 413 || status === 422;
998
997
  }
998
+ const MAX_TRANSIENT_REPLAY_CLIENT_FAILURES = 3;
999
+ function isTransientReplayUploadClientError(status) {
1000
+ return status === 401 || status === 403 || status === 404;
1001
+ }
999
1002
  async function sendReplayUpload(options, body, callbacks = {}) {
1000
1003
  if (isCrossOriginReplayEndpoint(options.endpoint)) {
1001
1004
  const canUseKeepalive = canUseReplayKeepalive(body);
@@ -1229,6 +1232,7 @@ export async function flushSessionReplay(reason = "manual") {
1229
1232
  if (!reservedSequence)
1230
1233
  advanceReplaySequence(state, payload);
1231
1234
  state.automaticConflictRestartAttempted = false;
1235
+ state.transientClientErrorFailures = 0;
1232
1236
  uploaded = true;
1233
1237
  }
1234
1238
  catch (error) {
@@ -1241,8 +1245,20 @@ export async function flushSessionReplay(reason = "manual") {
1241
1245
  const rejectedStatus = error instanceof ReplayUploadHttpError ? error.status : null;
1242
1246
  const splitBatch = rejectedStatus === 413 ? splitReplayBatch(events) : null;
1243
1247
  const isUnsplittableOversizedBatch = rejectedStatus === 413 && splitBatch === null;
1248
+ const isTransientClientError = rejectedStatus !== null &&
1249
+ isTransientReplayUploadClientError(rejectedStatus);
1250
+ if (isTransientClientError) {
1251
+ state.transientClientErrorFailures += 1;
1252
+ }
1253
+ else {
1254
+ state.transientClientErrorFailures = 0;
1255
+ }
1256
+ const exhaustedTransientClientRetries = isTransientClientError &&
1257
+ state.transientClientErrorFailures >=
1258
+ MAX_TRANSIENT_REPLAY_CLIENT_FAILURES;
1244
1259
  const isDefinitiveClientError = error instanceof ReplayUploadHttpError &&
1245
- isDefinitiveReplayUploadClientError(error.status) &&
1260
+ (isDefinitiveReplayUploadClientError(error.status) ||
1261
+ exhaustedTransientClientRetries) &&
1246
1262
  !splitBatch &&
1247
1263
  !isUnsplittableOversizedBatch;
1248
1264
  if (splitBatch) {
@@ -2202,6 +2218,7 @@ export async function startSessionReplay(options = {}) {
2202
2218
  // This is a new caller-initiated recording episode. A prior episode's
2203
2219
  // conflict-loop guard must not prevent this one from recovering once.
2204
2220
  state.automaticConflictRestartAttempted = false;
2221
+ state.transientClientErrorFailures = 0;
2205
2222
  const startGeneration = ++state.startGeneration;
2206
2223
  let startPromise;
2207
2224
  startPromise = startSessionReplayRecorder(state, normalized, sessionId, sampled, initialProperties, startGeneration).finally(() => {