@agent-native/core 0.98.6 → 0.98.7

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 (80) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +6 -0
  3. package/corpus/core/docs/content/external-agents.mdx +15 -11
  4. package/corpus/core/docs/content/locales/ar-SA/external-agents.mdx +3 -1
  5. package/corpus/core/docs/content/locales/de-DE/external-agents.mdx +3 -1
  6. package/corpus/core/docs/content/locales/es-ES/external-agents.mdx +3 -1
  7. package/corpus/core/docs/content/locales/fr-FR/external-agents.mdx +3 -1
  8. package/corpus/core/docs/content/locales/hi-IN/external-agents.mdx +3 -1
  9. package/corpus/core/docs/content/locales/ja-JP/external-agents.mdx +3 -1
  10. package/corpus/core/docs/content/locales/ko-KR/external-agents.mdx +3 -1
  11. package/corpus/core/docs/content/locales/pt-BR/external-agents.mdx +3 -1
  12. package/corpus/core/docs/content/locales/zh-CN/external-agents.mdx +3 -1
  13. package/corpus/core/docs/content/locales/zh-TW/external-agents.mdx +3 -1
  14. package/corpus/core/package.json +1 -1
  15. package/corpus/core/src/a2a/handlers.ts +6 -5
  16. package/corpus/core/src/a2a/task-store.ts +63 -0
  17. package/corpus/core/src/client/session-replay.ts +67 -5
  18. package/corpus/core/src/integrations/adapters/slack.ts +99 -19
  19. package/corpus/core/src/integrations/identity.ts +103 -39
  20. package/corpus/core/src/integrations/plugin.ts +81 -9
  21. package/corpus/core/src/integrations/types.ts +30 -0
  22. package/corpus/core/src/integrations/webhook-handler.ts +1 -0
  23. package/corpus/core/src/mcp/builtin-tools.ts +5 -2
  24. package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +4 -1
  25. package/corpus/templates/analytics/AGENTS.md +9 -9
  26. package/corpus/templates/analytics/changelog/2026-07-12-analytics-uses-the-full-in-app-agent-for-multi-step-incident.md +6 -0
  27. package/corpus/templates/analytics/server/handlers/session-replay.ts +15 -9
  28. package/corpus/templates/analytics/server/lib/session-replay-agent-context.ts +23 -5
  29. package/corpus/templates/analytics/server/lib/session-replay.ts +26 -5
  30. package/corpus/templates/analytics/server/plugins/agent-chat.ts +4 -1
  31. package/corpus/templates/analytics/server/routes/api/session-replay/agent-diagnostics.json.get.ts +8 -5
  32. package/corpus/templates/analytics/server/routes/api/session-replay/agent-events.json.get.ts +8 -3
  33. package/corpus/templates/calendar/app/components/calendar/DeleteEventDialog.tsx +12 -2
  34. package/dist/a2a/handlers.d.ts.map +1 -1
  35. package/dist/a2a/handlers.js +6 -6
  36. package/dist/a2a/handlers.js.map +1 -1
  37. package/dist/a2a/task-store.d.ts +12 -0
  38. package/dist/a2a/task-store.d.ts.map +1 -1
  39. package/dist/a2a/task-store.js +54 -0
  40. package/dist/a2a/task-store.js.map +1 -1
  41. package/dist/client/session-replay.d.ts.map +1 -1
  42. package/dist/client/session-replay.js +53 -8
  43. package/dist/client/session-replay.js.map +1 -1
  44. package/dist/collab/routes.d.ts +1 -1
  45. package/dist/collab/struct-routes.d.ts +1 -1
  46. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  47. package/dist/integrations/adapters/slack.d.ts.map +1 -1
  48. package/dist/integrations/adapters/slack.js +85 -11
  49. package/dist/integrations/adapters/slack.js.map +1 -1
  50. package/dist/integrations/identity.d.ts +19 -2
  51. package/dist/integrations/identity.d.ts.map +1 -1
  52. package/dist/integrations/identity.js +76 -34
  53. package/dist/integrations/identity.js.map +1 -1
  54. package/dist/integrations/plugin.d.ts.map +1 -1
  55. package/dist/integrations/plugin.js +56 -6
  56. package/dist/integrations/plugin.js.map +1 -1
  57. package/dist/integrations/types.d.ts +25 -0
  58. package/dist/integrations/types.d.ts.map +1 -1
  59. package/dist/integrations/types.js.map +1 -1
  60. package/dist/integrations/webhook-handler.js +1 -0
  61. package/dist/integrations/webhook-handler.js.map +1 -1
  62. package/dist/mcp/builtin-tools.d.ts.map +1 -1
  63. package/dist/mcp/builtin-tools.js +5 -2
  64. package/dist/mcp/builtin-tools.js.map +1 -1
  65. package/dist/progress/routes.d.ts +1 -1
  66. package/dist/resources/handlers.d.ts +3 -3
  67. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  68. package/dist/server/transcribe-voice.d.ts +1 -1
  69. package/docs/content/external-agents.mdx +15 -11
  70. package/docs/content/locales/ar-SA/external-agents.mdx +3 -1
  71. package/docs/content/locales/de-DE/external-agents.mdx +3 -1
  72. package/docs/content/locales/es-ES/external-agents.mdx +3 -1
  73. package/docs/content/locales/fr-FR/external-agents.mdx +3 -1
  74. package/docs/content/locales/hi-IN/external-agents.mdx +3 -1
  75. package/docs/content/locales/ja-JP/external-agents.mdx +3 -1
  76. package/docs/content/locales/ko-KR/external-agents.mdx +3 -1
  77. package/docs/content/locales/pt-BR/external-agents.mdx +3 -1
  78. package/docs/content/locales/zh-CN/external-agents.mdx +3 -1
  79. package/docs/content/locales/zh-TW/external-agents.mdx +3 -1
  80. 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: 2239
31
- - template files: 5507
31
+ - template files: 5508
@@ -1,5 +1,11 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.98.7
4
+
5
+ ### Patch Changes
6
+
7
+ - b94940e: Keep replay recording recoverable across transient upload failures and tab races, preserve object resources, and prevent late A2A workers from overwriting terminal task states.
8
+
3
9
  ## 0.98.6
4
10
 
5
11
  ### Patch Changes
@@ -417,17 +417,21 @@ rejected.
417
417
 
418
418
  The default Slack integration follows the same rule. A verified Slack DM is
419
419
  matched to an existing Agent Native organization member and persisted as a
420
- workspace/user identity link before the agent runs. The resulting user/org
421
- context loads that user's resources, instructions, and skills. Shared Slack
422
- channels use a service principal instead of borrowing one participant's
423
- private permissions; guests and external Slack members cannot use personal
424
- Agent Native access by default. If a Slack identity cannot be verified or its
425
- link changes, the message is rejected rather than downgraded to a broad
426
- service identity. Managed Slack OAuth requests the `users:read.email` bot
427
- scope, and the generated Slack app manifest requests it too. Existing Slack
428
- installs must be reconnected/reinstalled to grant a newly added scope; legacy
429
- bot-token installs must add the scope in Slack manually. Without it, personal
430
- DM execution fails closed.
420
+ workspace/user identity link before the agent runs, and the resulting user/org
421
+ context loads that user's resources, instructions, and skills. Hydrated
422
+ workspace members whose email is missing or not yet an organization member run
423
+ as an anonymous org-scoped service principal instead the same
424
+ org-wide-visibility tier shared channels get with an agent-visible note and
425
+ a one-time Slack heads-up explaining how to gain personal access. Hydration
426
+ failures, guests and external (Slack Connect) members, and workspaces not
427
+ connected to an organization receive a polite decline reply instead of a
428
+ silent drop. Shared Slack channels use a service principal instead of
429
+ borrowing one participant's private permissions. Managed Slack OAuth requests
430
+ the `users:read.email` bot scope, and the generated Slack app manifest
431
+ requests it too. Existing Slack installs must be reconnected/reinstalled to
432
+ grant a newly added scope; legacy bot-token installs must add the scope in
433
+ Slack manually. Without it, DMs run in the anonymous org-scoped tier rather
434
+ than with personal access.
431
435
 
432
436
  ### Full tier (explicit opt-in only) {#full-tier}
433
437
 
@@ -402,7 +402,9 @@ export default createAgentChatPlugin({
402
402
 
403
403
  لا يتم تضمين `db-schema` و`db-query` الأساسيين تلقائيًا في القراءات الخارجية. يظلان متاحين لوكيل التطبيق داخل التطبيق عبر مسار SQL المقيّد المعتاد، لكن الوصول العام إلى المخطط وSQL واسع جدًا لاستنتاجه من بيانات القراءة فقط. إذا احتاج التطبيق إلى استعلام خارجي مباشر، فعليه إضافة action مملوك للتطبيق بحدود وصول واضحة أو قائمة سماح صريحة للجداول والأعمدة مع حدود للصفوف والبايت والمهلة والتدقيق. يبقى `db-exec` و`db-patch` خارج السطح التلقائي.
404
404
 
405
- تستخدم رسائل Slack الخاصة هوية المستخدم فقط بعد التحقق من البريد الإلكتروني وربطه بعضوية موجودة في مؤسسة Agent Native. أما القنوات المشتركة فتستخدم هوية خدمة ولا تستعير أذونات أحد المشاركين الخاصة. يطلب OAuth المُدار والنموذج المُنشأ لتطبيق Slack النطاق `users:read.email`؛ يجب إعادة ربط التثبيتات الحالية بعد إضافة النطاق، بينما يجب تحديث الرموز القديمة يدويًا في Slack.
405
+ هذا استبعاد صارم يعتمد على الاسم، وليس مجرد إغفال في البيانات الوصفية: أدوات قاعدة البيانات وseed وجلسة المتصفح والامتدادات وسياق الأشعة السينية العامة لا يُكشف عنها تلقائيًا أبدًا، حتى لو وسم تغيير مستقبلي إحداها عن طريق الخطأ بمجموعة أعلام القراءة المصادق عليها الكاملة فهي تتطلب دائمًا إدخالًا صريحًا في `connectorCatalog`.
406
+
407
+ تطابق رسائل Slack الخاصة التي تم التحقق منها عضوًا موجودًا في مؤسسة Agent Native، وتحفظ رابط هوية مساحة العمل/المستخدم قبل تشغيل الوكيل؛ ثم يحمّل سياق المستخدم/المؤسسة الناتج موارد ذلك المستخدم وتعليماته ومهاراته. أما أعضاء مساحة العمل الذين نجحت عملية جلب بياناتهم لكن بريدهم الإلكتروني مفقود أو لم ينضموا بعد إلى المؤسسة، فيعملون بهوية خدمة مجهولة ضمن نطاق المؤسسة — وهي طبقة الرؤية نفسها على مستوى المؤسسة التي تستخدمها القنوات المشتركة — مع ملاحظة يراها الوكيل وتنبيه لمرة واحدة في Slack يشرح كيفية الحصول على وصول شخصي. تتلقى حالات فشل جلب البيانات والضيوف والأعضاء الخارجيون (Slack Connect) ومساحات العمل غير المرتبطة بمؤسسة ردًا مهذبًا بالرفض بدل إسقاط الرسالة بصمت. تستخدم قنوات Slack المشتركة هوية خدمة ولا تستعير الأذونات الخاصة لأي مشارك. يطلب OAuth المُدار والنموذج المُنشأ لتطبيق Slack نطاق البوت `users:read.email`. يجب إعادة ربط/تثبيت عمليات التثبيت الحالية لمنح نطاق أُضيف حديثًا، بينما يجب أن تضيف عمليات التثبيت القديمة التي تستخدم رمز بوت النطاق يدويًا في Slack. من دونه، تعمل الرسائل الخاصة ضمن الطبقة المجهولة ذات نطاق المؤسسة بدل الوصول الشخصي.
406
408
 
407
409
  ### الطبقة الكاملة (الاشتراك الصريح فقط) {#full-tier}
408
410
 
@@ -404,7 +404,9 @@ Die Richtlinie ist authentifiziert, nicht anonym. Die MCP-OAuth-/Connect-Identit
404
404
 
405
405
  Core-`db-schema` und `db-query` werden nicht automatisch als externe Lesezugriffe veröffentlicht. Sie bleiben für den internen App-Agenten über den normalen, abgegrenzten SQL-Pfad verfügbar, aber ein breiter Schema-/SQL-Zugriff ist zu mächtig, um ihn allein aus Read-only-Metadaten abzuleiten. Benötigt eine App direkte externe Abfragen, muss sie eine eigene GET-Action mit Zugriffsprüfungen und festen Grenzen oder eine explizite Tabellen-/Spalten-Allowlist mit Zeilen-, Byte-, Zeit- und Audit-Limits bereitstellen. `db-exec` und `db-patch` bleiben außerhalb der automatischen Oberfläche.
406
406
 
407
- Slack-DMs verwenden die Benutzeridentität erst nach verifizierter E-Mail und einer bestehenden Mitgliedschaft in der Agent-Native-Organisation. Gemeinsame Kanäle verwenden dagegen einen Service-Principal und übernehmen keine privaten Berechtigungen eines Teilnehmers. Managed OAuth und das generierte Slack-Manifest fordern `users:read.email`; bestehende Installationen müssen nach einer Scope-Änderung neu verbunden werden, alte Bot-Tokens müssen in Slack manuell aktualisiert werden.
407
+ Dies ist ein harter, namensbasierter Ausschluss und keine bloße Auslassung in den Metadaten: generische Datenbank-, Seed-, Browser-Sitzungs-, Erweiterungs- und Kontext-Röntgen-Tools werden niemals automatisch freigegeben, selbst wenn eine zukünftige Änderung eines davon versehentlich mit dem vollständigen Satz an Flags für authentifizierte Lesezugriffe versieht sie benötigen immer einen expliziten `connectorCatalog`-Eintrag.
408
+
409
+ Eine verifizierte Slack-DM wird vor dem Start des Agenten einem bestehenden Mitglied der Agent-Native-Organisation zugeordnet und als Identitätsverknüpfung zwischen Workspace und Benutzer gespeichert. Der daraus entstehende Benutzer-/Organisationskontext lädt die Ressourcen, Anweisungen und Skills dieses Benutzers. Erfolgreich geladene Workspace-Mitglieder, deren E-Mail-Adresse fehlt oder die noch kein Organisationsmitglied sind, werden stattdessen als anonymer, auf die Organisation begrenzter Service-Principal ausgeführt — mit derselben organisationsweiten Sichtbarkeit wie gemeinsame Kanäle. Der Agent erhält dazu einen sichtbaren Hinweis, und Slack erklärt einmalig, wie persönlicher Zugriff erlangt werden kann. Bei fehlgeschlagener Profilladung, Gästen und externen Mitgliedern (Slack Connect) sowie Workspaces ohne Organisationsverknüpfung wird höflich abgelehnt, statt die Nachricht stillschweigend zu verwerfen. Gemeinsame Slack-Kanäle verwenden einen Service-Principal und übernehmen keine privaten Berechtigungen eines Teilnehmers. Managed OAuth und das generierte Slack-App-Manifest fordern den Bot-Scope `users:read.email`. Bestehende Installationen müssen erneut verbunden/installiert werden, um einen neu hinzugefügten Scope zu gewähren; ältere Bot-Token-Installationen müssen ihn in Slack manuell hinzufügen. Ohne diesen Scope werden DMs in der anonymen, organisationsbegrenzten Stufe statt mit persönlichem Zugriff ausgeführt.
408
410
 
409
411
  ### Vollständige Stufe (nur explizites Opt-in) {#full-tier}
410
412
 
@@ -404,7 +404,9 @@ La política es autenticada, no anónima. La identidad de MCP OAuth/connect se c
404
404
 
405
405
  `db-schema` y `db-query` de Core no se exponen automáticamente como lecturas externas. Siguen disponibles para el agente interno de la aplicación mediante el flujo SQL con ámbito habitual, pero el acceso amplio al esquema/SQL es demasiado potente para inferirlo solo de metadatos de lectura. Si una aplicación necesita consultas externas directas, debe exponer una action GET propia con controles y límites de acceso, o una lista explícita de tablas/columnas con límites de filas, bytes, tiempo y auditoría. `db-exec` y `db-patch` quedan fuera de la superficie automática.
406
406
 
407
- Los mensajes directos de Slack usan la identidad del usuario solo después de verificar su correo y confirmar que pertenece a la organización de Agent Native. Los canales compartidos usan un principal de servicio y no heredan los permisos privados de un participante. OAuth administrado y el manifiesto generado de Slack solicitan `users:read.email`; las instalaciones existentes deben reconectarse tras añadir el ámbito y los tokens de bot antiguos deben actualizarse manualmente en Slack.
407
+ Se trata de una exclusión estricta basada en el nombre, no solo de una omisión de metadatos: las herramientas genéricas de base de datos, seed, sesión del navegador, extensiones y rayos X de contexto nunca se exponen automáticamente, aunque un cambio futuro anote alguna de ellas por error con el conjunto completo de indicadores de lectura autenticada siempre requieren una entrada explícita en `connectorCatalog`.
408
+
409
+ Antes de ejecutar el agente, un mensaje directo verificado de Slack se asocia a un miembro existente de la organización de Agent Native y se guarda el vínculo de identidad entre el espacio de trabajo y el usuario. El contexto resultante del usuario y la organización carga los recursos, las instrucciones y las habilidades de ese usuario. Los miembros del espacio de trabajo cuyo perfil se ha obtenido correctamente, pero que no tienen correo electrónico o aún no pertenecen a la organización, se ejecutan como un principal de servicio anónimo limitado a la organización: el mismo nivel de visibilidad para toda la organización que usan los canales compartidos. El agente recibe una nota visible y Slack muestra una sola vez cómo obtener acceso personal. Si falla la obtención del perfil, si se trata de invitados o miembros externos (Slack Connect), o si el espacio de trabajo no está conectado a una organización, se envía una respuesta cortés de rechazo en vez de descartar el mensaje en silencio. Los canales compartidos de Slack usan un principal de servicio y no heredan los permisos privados de ningún participante. OAuth administrado y el manifiesto generado de la aplicación de Slack solicitan el ámbito de bot `users:read.email`. Las instalaciones existentes deben volver a conectarse o instalarse para conceder un ámbito recién añadido; las instalaciones antiguas con token de bot deben añadirlo manualmente en Slack. Sin él, los mensajes directos se ejecutan en el nivel anónimo limitado a la organización, no con acceso personal.
408
410
 
409
411
  ### Nivel completo (solo suscripción explícita) {#full-tier}
410
412
 
@@ -404,7 +404,9 @@ La politique est authentifiée, et non anonyme. L'identité MCP OAuth/connect es
404
404
 
405
405
  Les outils Core `db-schema` et `db-query` ne sont pas automatiquement exposés comme lectures externes. Ils restent disponibles pour l'agent interne de l'application via le chemin SQL habituellement délimité, mais un accès large au schéma/SQL est trop puissant pour être déduit de simples métadonnées de lecture. Si une application a besoin de requêtes externes directes, elle doit exposer une action GET propre avec des contrôles d'accès et des limites, ou une liste explicite de tables/colonnes avec des limites de lignes, d'octets, de durée et d'audit. `db-exec` et `db-patch` restent hors de la surface automatique.
406
406
 
407
- Les messages directs Slack utilisent l'identité de l'utilisateur uniquement après vérification de son adresse e-mail et confirmation de son appartenance à l'organisation Agent Native. Les canaux partagés utilisent un principal de service et n'empruntent pas les autorisations privées d'un participant. OAuth géré et le manifeste Slack généré demandent `users:read.email` ; les installations existantes doivent être reconnectées après l'ajout de cette portée et les anciens jetons bot doivent être mis à jour manuellement dans Slack.
407
+ Il s'agit d'une exclusion stricte basée sur le nom, et non d'une simple omission de métadonnées : les outils génériques de base de données, de seed, de session de navigateur, d'extensions et de radiographie contextuelle ne sont jamais exposés automatiquement, même si une modification future annote accidentellement l'un d'eux avec l'ensemble complet des indicateurs de lecture authentifiée ils nécessitent toujours une entrée explicite dans `connectorCatalog`.
408
+
409
+ Avant l'exécution de l'agent, un message direct Slack vérifié est associé à un membre existant de l'organisation Agent Native et le lien d'identité entre l'espace de travail et l'utilisateur est enregistré. Le contexte utilisateur/organisation qui en résulte charge les ressources, les instructions et les compétences de cet utilisateur. Les membres de l'espace de travail dont le profil a bien été chargé, mais dont l'adresse e-mail est absente ou qui ne sont pas encore membres de l'organisation, sont exécutés comme principal de service anonyme limité à l'organisation : le même niveau de visibilité à l'échelle de l'organisation que celui des canaux partagés. L'agent reçoit une note visible et Slack explique une fois comment obtenir un accès personnel. En cas d'échec du chargement du profil, pour les invités et membres externes (Slack Connect), ou pour les espaces de travail non connectés à une organisation, une réponse de refus polie est envoyée au lieu d'ignorer silencieusement le message. Les canaux Slack partagés utilisent un principal de service et n'empruntent pas les autorisations privées d'un participant. OAuth géré et le manifeste d'application Slack généré demandent la portée de bot `users:read.email`. Les installations existantes doivent être reconnectées ou réinstallées pour accorder une portée nouvellement ajoutée ; les anciennes installations par jeton de bot doivent l'ajouter manuellement dans Slack. Sans cette portée, les messages directs s'exécutent dans le niveau anonyme limité à l'organisation plutôt qu'avec un accès personnel.
408
410
 
409
411
  ### Niveau complet (adhésion explicite uniquement) {#full-tier}
410
412
 
@@ -402,7 +402,9 @@ export default createAgentChatPlugin({
402
402
 
403
403
  Core के `db-schema` और `db-query` को स्वचालित बाहरी रीड के रूप में प्रकाशित नहीं किया जाता। वे सामान्य स्कोप किए गए SQL पथ के माध्यम से ऐप एजेंट के लिए उपलब्ध रहते हैं, लेकिन व्यापक स्कीमा/SQL पहुँच केवल read-only मेटाडेटा से अनुमान लगाने के लिए बहुत शक्तिशाली है। सीधे बाहरी क्वेरी की आवश्यकता होने पर ऐप को अपने access checks और सीमाओं वाला GET action, या rows, bytes, timeout और audit limits वाली स्पष्ट table/column allow-list देनी चाहिए। `db-exec` और `db-patch` स्वचालित सतह से बाहर रहते हैं।
404
404
 
405
- Slack DM में उपयोगकर्ता की पहचान तभी इस्तेमाल होती है जब ईमेल सत्यापित हो और वह Agent Native संगठन का सदस्य हो। साझा चैनल service principal का उपयोग करते हैं और किसी प्रतिभागी की निजी permissions नहीं लेते। Managed OAuth और generated Slack manifest दोनों `users:read.email` मांगते हैं; मौजूदा installations को scope जोड़ने के बाद reconnect करना होगा और पुराने bot tokens को Slack में manually update करना होगा।
405
+ यह नाम-आधारित सख्त बहिष्करण है, केवल मेटाडेटा की चूक नहीं: सामान्य डेटाबेस/seed/ब्राउज़र-सत्र/एक्सटेंशन/संदर्भ-एक्सरे टूल कभी भी स्वचालित रूप से उजागर नहीं होते, भले ही भविष्य का कोई बदलाव गलती से इनमें से किसी एक को पूर्ण authenticated-read फ़्लैग सेट के साथ चिह्नित कर दे उन्हें हमेशा एक स्पष्ट `connectorCatalog` प्रविष्टि की आवश्यकता होती है।
406
+
407
+ एजेंट चलने से पहले सत्यापित Slack DM को Agent Native संगठन के किसी मौजूदा सदस्य से मिलाया जाता है और workspace/user पहचान लिंक सहेजा जाता है। इससे बने user/org संदर्भ में उस उपयोगकर्ता के संसाधन, निर्देश और skills लोड होते हैं। जिन workspace सदस्यों का प्रोफ़ाइल सफलतापूर्वक मिला है लेकिन ईमेल उपलब्ध नहीं है या वे अभी संगठन के सदस्य नहीं हैं, वे इसके बजाय संगठन-स्कोप वाले अनाम service principal के रूप में चलते हैं — साझा चैनलों वाली संगठन-व्यापी दृश्यता के उसी स्तर पर — साथ में एजेंट को दिखाई देने वाला नोट और व्यक्तिगत पहुँच पाने का तरीका बताने वाली एक बार की Slack सूचना। प्रोफ़ाइल लोड न होने, मेहमान या बाहरी (Slack Connect) सदस्य होने, या workspace के किसी संगठन से न जुड़े होने पर संदेश को चुपचाप छोड़ने के बजाय विनम्र अस्वीकृति भेजी जाती है। साझा Slack चैनल service principal का उपयोग करते हैं और किसी प्रतिभागी की निजी permissions नहीं लेते। Managed OAuth और generated Slack app manifest दोनों bot scope `users:read.email` मांगते हैं। नया scope देने के लिए मौजूदा installations को reconnect/reinstall करना होगा; पुराने bot-token installations को इसे Slack में manually जोड़ना होगा। इसके बिना DM व्यक्तिगत पहुँच के बजाय संगठन-स्कोप वाले अनाम स्तर में चलते हैं।
406
408
 
407
409
  ### पूर्ण स्तरीय (केवल स्पष्ट ऑप्ट-इन) {#full-tier}
408
410
 
@@ -402,7 +402,9 @@ export default createAgentChatPlugin({
402
402
 
403
403
  Core の `db-schema` と `db-query` は外部の自動読み取りとして公開されません。通常のスコープ付き SQL パスを通じてアプリ内エージェントからは利用できますが、広範なスキーマ/SQL アクセスは read-only メタデータだけから推測するには強力すぎます。外部クエリを直接許可する場合は、アクセスチェックと上限を持つアプリ所有の GET action、または行数・バイト数・タイムアウト・監査の制限を備えた明示的なテーブル/カラム許可リストを追加してください。`db-exec` と `db-patch` は自動公開の対象外です。
404
404
 
405
- Slack DM は、メールアドレスを検証し Agent Native 組織のメンバーであることを確認した後にだけユーザー ID を使用します。共有チャンネルはサービスプリンシパルを使い、参加者の個人的な権限を借用しません。管理対象 OAuth と生成される Slack マニフェストは `users:read.email` を要求します。既存のインストールはスコープ追加後に再接続し、古い bot トークンは Slack で手動更新してください。
405
+ これは名前ベースの厳格な除外であり、単なるメタデータの記載漏れではありません。汎用のデータベース、seed、ブラウザ セッション、拡張、コンテキスト Xray のツールは、将来の変更で誤って完全な認証済み読み取りフラグセットが付与された場合でも、自動的に公開されることは決してありません これらには常に明示的な `connectorCatalog` エントリが必要です。
406
+
407
+ 検証済みの Slack DM は、エージェントを実行する前に既存の Agent Native 組織メンバーと照合され、ワークスペース/ユーザーの ID リンクとして保存されます。生成されたユーザー/組織コンテキストには、そのユーザーのリソース、指示、スキルが読み込まれます。プロフィールの取得には成功したものの、メールアドレスがない、またはまだ組織メンバーではないワークスペース メンバーは、代わりに組織スコープの匿名サービスプリンシパルとして実行されます。これは共有チャンネルと同じ組織全体の可視性レベルであり、エージェントに表示される注記と、個人アクセスの取得方法を説明する 1 回限りの Slack 通知が付きます。プロフィール取得の失敗、ゲストや外部 (Slack Connect) メンバー、組織に接続されていないワークスペースには、メッセージを黙って破棄せず、丁寧な拒否応答を返します。Slack の共有チャンネルはサービスプリンシパルを使い、参加者の個人的な権限を借用しません。管理対象 OAuth と生成される Slack アプリ マニフェストは、bot スコープ `users:read.email` を要求します。新たに追加されたスコープを付与するには、既存のインストールを再接続/再インストールする必要があります。従来の bot トークンによるインストールでは、Slack でスコープを手動追加してください。このスコープがない場合、DM は個人アクセスではなく組織スコープの匿名レベルで実行されます。
406
408
 
407
409
  ### フルティア (明示的なオプトインのみ) {#full-tier}
408
410
 
@@ -402,7 +402,9 @@ export default createAgentChatPlugin({
402
402
 
403
403
  Core `db-schema`와 `db-query`는 자동 외부 읽기로 공개되지 않습니다. 일반적인 범위 지정 SQL 경로를 통해 앱 내부 에이전트에서는 사용할 수 있지만, 광범위한 스키마/SQL 접근은 읽기 전용 메타데이터만으로 추론하기에는 너무 강력합니다. 외부 쿼리가 필요하면 접근 검사와 제한을 갖춘 앱 소유 GET action 또는 행·바이트·시간 제한과 감사를 포함한 명시적 테이블/열 허용 목록을 제공하세요. `db-exec`와 `db-patch`는 자동 표면에 포함되지 않습니다.
404
404
 
405
- Slack DM은 이메일을 검증하고 Agent Native 조직의 구성원임을 확인한 뒤에만 사용자 ID로 실행됩니다. 공유 채널은 서비스 주체를 사용하며 참여자의 개인 권한을 빌리지 않습니다. 관리형 OAuth와 생성된 Slack 매니페스트는 모두 `users:read.email`을 요청합니다. 기존 설치는 범위를 추가한 다시 연결해야 하며, 이전 봇 토큰은 Slack에서 수동으로 업데이트해야 합니다.
405
+ 이는 이름 기반의 강력한 제외이며 단순한 메타데이터 누락이 아닙니다. 일반 데이터베이스, seed, 브라우저 세션, 확장, context-xray 도구는 향후 변경으로 인해 실수로 전체 인증된 읽기 플래그 집합이 지정되더라도 절대 자동으로 노출되지 않습니다 이러한 도구는 항상 명시적인 `connectorCatalog` 항목이 필요합니다.
406
+
407
+ 검증된 Slack DM은 에이전트가 실행되기 전에 기존 Agent Native 조직 구성원과 연결되고 workspace/user ID 링크로 저장됩니다. 그 결과 생성된 사용자/조직 컨텍스트에는 해당 사용자의 리소스, 지침 및 skills가 로드됩니다. 프로필을 정상적으로 가져왔지만 이메일이 없거나 아직 조직 구성원이 아닌 workspace 구성원은 조직 범위의 익명 service principal로 실행됩니다. 이는 공유 채널과 동일한 조직 전체 공개 수준이며, 에이전트에 표시되는 메모와 개인 액세스를 얻는 방법을 설명하는 일회성 Slack 안내가 함께 제공됩니다. 프로필 가져오기 실패, 게스트 및 외부(Slack Connect) 구성원, 조직에 연결되지 않은 workspace에는 메시지를 조용히 버리는 대신 정중한 거절 응답을 보냅니다. Slack 공유 채널은 service principal을 사용하며 참여자의 개인 권한을 빌리지 않습니다. 관리형 OAuth와 생성된 Slack 앱 매니페스트는 모두 bot scope `users:read.email`을 요청합니다. 새로 추가된 scope를 부여하려면 기존 설치를 다시 연결/설치해야 하며, 이전 bot-token 설치는 Slack에서 scope를 수동으로 추가해야 합니다. 이 scope가 없으면 DM은 개인 액세스 대신 조직 범위의 익명 수준에서 실행됩니다.
406
408
 
407
409
  ### 전체 등급(명시적 선택만 해당) {#full-tier}
408
410
 
@@ -404,7 +404,9 @@ A política é autenticada, não anônima. A identidade MCP OAuth/connect é tra
404
404
 
405
405
  Os `db-schema` e `db-query` do Core não são expostos automaticamente como leituras externas. Eles continuam disponíveis ao agente interno do aplicativo pelo caminho SQL com escopo normal, mas o acesso amplo ao esquema/SQL é poderoso demais para ser inferido apenas de metadados de leitura. Se o aplicativo precisar de consultas externas diretas, exponha uma action GET própria com controles e limites de acesso, ou uma lista explícita de tabelas/colunas com limites de linhas, bytes, tempo e auditoria. `db-exec` e `db-patch` permanecem fora da superfície automática.
406
406
 
407
- DMs do Slack usam a identidade do usuário somente após verificar o e-mail e confirmar a associação à organização Agent Native. Canais compartilhados usam um principal de serviço e não herdam as permissões privadas de um participante. O OAuth gerenciado e o manifesto gerado do Slack solicitam `users:read.email`; instalações existentes precisam ser reconectadas após adicionar o escopo e tokens antigos de bot precisam ser atualizados manualmente no Slack.
407
+ Trata-se de uma exclusão rígida baseada em nome, não apenas uma omissão de metadados: ferramentas genéricas de banco de dados, seed, sessão de navegador, extensões e raio X de contexto nunca são expostas automaticamente, mesmo que uma mudança futura marque acidentalmente uma delas com o conjunto completo de flags de leitura autenticada elas sempre exigem uma entrada explícita em `connectorCatalog`.
408
+
409
+ Antes de o agente ser executado, uma DM verificada do Slack é associada a um membro existente da organização Agent Native e o vínculo de identidade entre workspace e usuário é salvo. O contexto resultante de usuário/organização carrega os recursos, as instruções e as skills desse usuário. Membros do workspace cujo perfil foi carregado, mas cujo e-mail está ausente ou que ainda não pertencem à organização, são executados como um principal de serviço anônimo com escopo da organização — o mesmo nível de visibilidade para toda a organização usado pelos canais compartilhados — com uma observação visível ao agente e um aviso único no Slack explicando como obter acesso pessoal. Falhas no carregamento do perfil, convidados e membros externos (Slack Connect), e workspaces não conectados a uma organização recebem uma resposta educada de recusa em vez de a mensagem ser descartada silenciosamente. Canais compartilhados do Slack usam um principal de serviço e não herdam as permissões privadas de um participante. O OAuth gerenciado e o manifesto gerado do aplicativo Slack solicitam o escopo de bot `users:read.email`. Instalações existentes precisam ser reconectadas/reinstaladas para conceder um escopo recém-adicionado; instalações antigas com token de bot precisam adicioná-lo manualmente no Slack. Sem ele, as DMs são executadas no nível anônimo com escopo da organização, e não com acesso pessoal.
408
410
 
409
411
  ### Nível completo (somente aceitação explícita) {#full-tier}
410
412
 
@@ -401,7 +401,9 @@ export default createAgentChatPlugin({
401
401
 
402
402
  Core `db-schema` 和 `db-query` 不会自动作为外部读取公开。它们仍可通过正常的作用域 SQL 路径供应用内代理使用,但广泛的架构/SQL 访问过于强大,不能仅凭只读元数据推断。如果应用需要直接外部查询,应提供带访问检查和边界的应用自有 GET action,或明确的表/列允许列表,并限制行数、字节数、超时和审计。`db-exec` 和 `db-patch` 仍不属于自动公开范围。
403
403
 
404
- Slack 私信只有在验证电子邮件并确认用户属于 Agent Native 组织后,才会使用用户身份运行。共享频道使用服务主体,不会借用参与者的私人权限。托管 OAuth 和生成的 Slack manifest 都会请求 `users:read.email`;已有安装在添加该 scope 后必须重新连接,旧 bot token 则必须在 Slack 中手动更新。
404
+ 这是一种基于名称的硬性排除,而不仅仅是元数据遗漏:通用的数据库、seed、浏览器会话、扩展和上下文 X 射线工具永远不会被自动公开,即使未来的更改意外地为其中某个工具标注了完整的已认证读取标志组合也是如此 它们始终需要在 `connectorCatalog` 中显式声明。
405
+
406
+ 经过验证的 Slack 私信会在代理运行前匹配现有的 Agent Native 组织成员,并保存工作区/用户身份关联;由此生成的用户/组织上下文会加载该用户的资源、指令和技能。对于已成功获取资料但缺少电子邮件或尚未成为组织成员的工作区成员,系统会改用组织范围的匿名服务主体运行——其组织级可见性与共享频道相同——同时向代理显示说明,并通过 Slack 一次性提示如何获得个人访问权限。如果资料获取失败、用户是访客或外部(Slack Connect)成员,或者工作区尚未连接组织,系统会礼貌回复拒绝,而不是静默丢弃消息。Slack 共享频道使用服务主体,不会借用任何参与者的私人权限。托管 OAuth 和生成的 Slack 应用 manifest 都会请求 bot scope `users:read.email`。已有安装必须重新连接/安装才能授予新增的 scope;旧版 bot token 安装则必须在 Slack 中手动添加该 scope。没有此 scope 时,私信会在组织范围的匿名层级运行,而不会获得个人访问权限。
405
407
 
406
408
  ### 完整层(仅限明确选择加入) {#full-tier}
407
409
 
@@ -401,7 +401,9 @@ export default createAgentChatPlugin({
401
401
 
402
402
  Core `db-schema` 與 `db-query` 不會自動公開為外部讀取。它們仍可透過一般的範圍 SQL 路徑供應用程式內代理使用,但廣泛的結構描述/SQL 存取過於強大,不能只從唯讀中繼資料推斷。若應用程式需要直接外部查詢,請提供具有存取檢查與限制的應用程式自有 GET action,或明確的資料表/欄位允許清單,並限制資料列、位元組、逾時與稽核。`db-exec` 與 `db-patch` 仍不屬於自動公開範圍。
403
403
 
404
- Slack 私訊只有在驗證電子郵件並確認使用者屬於 Agent Native 組織後,才會使用使用者身分執行。共用頻道使用服務主體,不會借用參與者的私人權限。託管 OAuth 與產生的 Slack manifest 都會要求 `users:read.email`;現有安裝在新增 scope 後必須重新連線,舊 bot token 則必須在 Slack 中手動更新。
404
+ 這是一種以名稱為依據的硬性排除,而不只是中繼資料的遺漏:通用的資料庫、seed、瀏覽器工作階段、擴充功能與 context-xray 工具永遠不會自動公開,即使未來的變更意外為其中某個工具標註了完整的已驗證讀取旗標組合也一樣 它們一律需要在 `connectorCatalog` 中明確宣告。
405
+
406
+ 經過驗證的 Slack 私訊會在代理執行前配對現有的 Agent Native 組織成員,並儲存工作區/使用者身分連結;由此產生的使用者/組織內容會載入該使用者的資源、指示和技能。對於已成功取得資料但缺少電子郵件或尚未成為組織成員的工作區成員,系統會改以組織範圍的匿名服務主體執行——其組織級可見性與共用頻道相同——同時向代理顯示說明,並透過 Slack 一次性提示如何取得個人存取權。如果資料取得失敗、使用者是訪客或外部(Slack Connect)成員,或工作區尚未連接組織,系統會禮貌回覆拒絕,而不是靜默捨棄訊息。Slack 共用頻道使用服務主體,不會借用任何參與者的私人權限。託管 OAuth 與產生的 Slack 應用程式 manifest 都會要求 bot scope `users:read.email`。現有安裝必須重新連接/安裝,才能授予新增的 scope;舊版 bot token 安裝則必須在 Slack 中手動加入該 scope。若沒有此 scope,私訊會在組織範圍的匿名層級執行,而不會取得個人存取權。
405
407
 
406
408
  ### 完整層(僅限明確選取加入) {#full-tier}
407
409
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.98.6",
3
+ "version": "0.98.7",
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,7 @@ import {
23
23
  getA2ATaskDispatchState,
24
24
  failStuckA2ATask,
25
25
  failStuckQueuedA2ATask,
26
+ settleProcessingA2ATask,
26
27
  touchQueuedA2ATaskDispatch,
27
28
  touchProcessingA2ATask,
28
29
  } from "./task-store.js";
@@ -132,7 +133,7 @@ export async function processA2ATaskFromQueue(
132
133
 
133
134
  const message = claimed.history?.[0];
134
135
  if (!message) {
135
- await updateTask(taskId, {
136
+ await settleProcessingA2ATask(taskId, {
136
137
  state: "failed",
137
138
  message: {
138
139
  role: "agent",
@@ -184,7 +185,7 @@ export async function processA2ATaskFromQueue(
184
185
  );
185
186
  } catch (err: any) {
186
187
  try {
187
- await updateTask(taskId, {
188
+ await settleProcessingA2ATask(taskId, {
188
189
  state: "failed",
189
190
  message: {
190
191
  role: "agent",
@@ -404,7 +405,7 @@ async function runHandlerAndPersist(
404
405
  for await (const msg of result as AsyncGenerator<Message>) {
405
406
  lastMessage = msg;
406
407
  }
407
- await updateTask(taskId, {
408
+ await settleProcessingA2ATask(taskId, {
408
409
  state: "completed",
409
410
  message: lastMessage,
410
411
  artifacts: artifacts.length > 0 ? artifacts : undefined,
@@ -414,13 +415,13 @@ async function runHandlerAndPersist(
414
415
 
415
416
  const handlerResult = await (result as Promise<A2AHandlerResult>);
416
417
  const allArtifacts = [...artifacts, ...(handlerResult.artifacts ?? [])];
417
- await updateTask(taskId, {
418
+ await settleProcessingA2ATask(taskId, {
418
419
  state: "completed",
419
420
  message: handlerResult.message,
420
421
  artifacts: allArtifacts.length > 0 ? allArtifacts : undefined,
421
422
  });
422
423
  } catch (err: any) {
423
- await updateTask(taskId, {
424
+ await settleProcessingA2ATask(taskId, {
424
425
  state: "failed",
425
426
  message: {
426
427
  role: "agent",
@@ -423,6 +423,69 @@ export async function updateTask(
423
423
  return task;
424
424
  }
425
425
 
426
+ /**
427
+ * Persist the terminal result produced by the async processor, but only while
428
+ * that processor still owns a task in `processing`. A tasks/get request may
429
+ * fail an over-lifetime processor while its handler is still running; the
430
+ * handler cannot be canceled reliably, so this compare-and-set is what keeps
431
+ * its eventual completion (or error) from overwriting the timeout result.
432
+ */
433
+ export async function settleProcessingA2ATask(
434
+ id: string,
435
+ update: {
436
+ state: "completed" | "failed";
437
+ message?: Message;
438
+ artifacts?: Artifact[];
439
+ },
440
+ ): Promise<Task | null> {
441
+ await ensureTable();
442
+ const client = getDbExec();
443
+
444
+ const { rows } = await client.execute({
445
+ sql: `SELECT * FROM a2a_tasks WHERE id = ?`,
446
+ args: [id],
447
+ });
448
+ if (rows.length === 0) return null;
449
+
450
+ const task = taskFromRow(rows[0]);
451
+ const now = Date.now();
452
+ task.status = {
453
+ state: update.state,
454
+ message: update.message ?? task.status.message,
455
+ timestamp: new Date().toISOString(),
456
+ };
457
+ if (update.message && task.history) {
458
+ task.history.push(update.message);
459
+ }
460
+ if (update.artifacts) {
461
+ task.artifacts = [...(task.artifacts ?? []), ...update.artifacts];
462
+ }
463
+
464
+ const result = await client.execute({
465
+ sql: `UPDATE a2a_tasks
466
+ SET status_state = ?,
467
+ status_message = ?,
468
+ status_timestamp = ?,
469
+ history = ?,
470
+ artifacts = ?,
471
+ updated_at = ?
472
+ WHERE id = ?
473
+ AND status_state = 'processing'`,
474
+ args: [
475
+ task.status.state,
476
+ task.status.message ? JSON.stringify(task.status.message) : null,
477
+ task.status.timestamp,
478
+ JSON.stringify(task.history),
479
+ JSON.stringify(task.artifacts),
480
+ now,
481
+ id,
482
+ ],
483
+ });
484
+ const affected = getAffectedRowCount(result);
485
+ if (affected === 0) return null;
486
+ return task;
487
+ }
488
+
426
489
  export async function updateTaskStatusMessage(
427
490
  id: string,
428
491
  message: Message,
@@ -65,6 +65,8 @@ interface RrwebRecordModule {
65
65
  interface SessionReplayState {
66
66
  active: boolean;
67
67
  startPromise: Promise<SessionReplayStartResult> | null;
68
+ /** Invalidates deferred recorder startup when stop is requested mid-start. */
69
+ startGeneration: number;
68
70
  replayId: string | null;
69
71
  startedAtMs: number | null;
70
72
  sequence: number;
@@ -432,6 +434,7 @@ function getState(): SessionReplayState {
432
434
  g[SESSION_REPLAY_STATE_KEY] = {
433
435
  active: false,
434
436
  startPromise: null,
437
+ startGeneration: 0,
435
438
  replayId: null,
436
439
  startedAtMs: null,
437
440
  sequence: 0,
@@ -464,6 +467,7 @@ function getState(): SessionReplayState {
464
467
  state.pendingFlushReason ??= null;
465
468
  state.pendingFlushWaiters ??= [];
466
469
  state.awaitingFullSnapshot ??= false;
470
+ state.startGeneration ??= 0;
467
471
  return state;
468
472
  }
469
473
 
@@ -1054,12 +1058,14 @@ const REPLAY_RESOURCE_TAGS = new Set([
1054
1058
  "audio",
1055
1059
  "track",
1056
1060
  "input",
1061
+ "object",
1057
1062
  "link",
1058
1063
  ]);
1059
1064
  const NO_REPLAY_RESOURCE_ATTRIBUTES = new Set<string>();
1060
1065
  const REPLAY_SRC_ATTRIBUTES = new Set(["src"]);
1061
1066
  const REPLAY_SRCSET_ATTRIBUTES = new Set(["src", "srcset"]);
1062
1067
  const REPLAY_VIDEO_ATTRIBUTES = new Set(["src", "poster"]);
1068
+ const REPLAY_OBJECT_ATTRIBUTES = new Set(["data"]);
1063
1069
  const REPLAY_HREF_ATTRIBUTES = new Set(["href"]);
1064
1070
 
1065
1071
  function replayAttributeString(
@@ -1101,6 +1107,8 @@ function replayPreservedResourceAttributes(
1101
1107
  case "audio":
1102
1108
  case "track":
1103
1109
  return REPLAY_SRC_ATTRIBUTES;
1110
+ case "object":
1111
+ return REPLAY_OBJECT_ATTRIBUTES;
1104
1112
  case "input":
1105
1113
  return node.type === "image"
1106
1114
  ? REPLAY_SRC_ATTRIBUTES
@@ -1452,12 +1460,12 @@ class ReplayUploadHttpError extends Error {
1452
1460
  }
1453
1461
  }
1454
1462
 
1455
- /** 4xx statuses where retrying the exact same batch can never succeed --
1456
- * e.g. a 409 chunk-sequence/checksum conflict, or a 400 the server will
1457
- * reject again. 408 (timeout) and 429 (rate limit) are excluded because a
1458
- * later retry can plausibly succeed. */
1463
+ /** 4xx statuses where retrying the exact same batch can never succeed.
1464
+ * Keep this deliberately narrow: 401/403/404 can be temporary during auth or
1465
+ * deploy transitions, and stopping forever on one of those would silently
1466
+ * black out the rest of a long-lived SPA session. */
1459
1467
  function isDefinitiveReplayUploadClientError(status: number): boolean {
1460
- return status >= 400 && status < 500 && status !== 408 && status !== 429;
1468
+ return status === 400 || status === 409 || status === 413 || status === 422;
1461
1469
  }
1462
1470
 
1463
1471
  async function sendReplayUpload(
@@ -1903,6 +1911,31 @@ async function restartSessionReplayAfterConflict(
1903
1911
  state: SessionReplayState,
1904
1912
  options: NormalizedSessionReplayOptions,
1905
1913
  sessionId: string,
1914
+ ): Promise<SessionReplayStartResult> {
1915
+ // Share the public-start mutex. A consumer may call start in the await gap
1916
+ // after the rejected recorder stops; both paths must converge on one rrweb
1917
+ // instance rather than racing two fresh identities.
1918
+ if (state.startPromise) return state.startPromise;
1919
+
1920
+ const startGeneration = ++state.startGeneration;
1921
+ let startPromise: Promise<SessionReplayStartResult>;
1922
+ startPromise = restartSessionReplayAfterConflictInternal(
1923
+ state,
1924
+ options,
1925
+ sessionId,
1926
+ startGeneration,
1927
+ ).finally(() => {
1928
+ if (state.startPromise === startPromise) state.startPromise = null;
1929
+ });
1930
+ state.startPromise = startPromise;
1931
+ return startPromise;
1932
+ }
1933
+
1934
+ async function restartSessionReplayAfterConflictInternal(
1935
+ state: SessionReplayState,
1936
+ options: NormalizedSessionReplayOptions,
1937
+ sessionId: string,
1938
+ startGeneration: number,
1906
1939
  ): Promise<SessionReplayStartResult> {
1907
1940
  if (options.shouldStart && !options.shouldStart()) {
1908
1941
  return { started: false, reason: "disabled", sessionId, sampled: true };
@@ -1932,6 +1965,7 @@ async function restartSessionReplayAfterConflict(
1932
1965
  sessionId,
1933
1966
  true,
1934
1967
  initialProperties,
1968
+ startGeneration,
1935
1969
  );
1936
1970
  }
1937
1971
 
@@ -2879,6 +2913,11 @@ export async function startSessionReplay(
2879
2913
  }
2880
2914
  if (state.startPromise) return state.startPromise;
2881
2915
 
2916
+ // This is a new caller-initiated recording episode. A prior episode's
2917
+ // conflict-loop guard must not prevent this one from recovering once.
2918
+ state.automaticConflictRestartAttempted = false;
2919
+ const startGeneration = ++state.startGeneration;
2920
+
2882
2921
  let startPromise: Promise<SessionReplayStartResult>;
2883
2922
  startPromise = startSessionReplayRecorder(
2884
2923
  state,
@@ -2886,6 +2925,7 @@ export async function startSessionReplay(
2886
2925
  sessionId,
2887
2926
  sampled,
2888
2927
  initialProperties,
2928
+ startGeneration,
2889
2929
  ).finally(() => {
2890
2930
  if (state.startPromise === startPromise) {
2891
2931
  state.startPromise = null;
@@ -2901,6 +2941,7 @@ async function startSessionReplayRecorder(
2901
2941
  sessionId: string,
2902
2942
  sampled: boolean,
2903
2943
  initialProperties: Record<string, unknown> | undefined,
2944
+ startGeneration: number,
2904
2945
  ): Promise<SessionReplayStartResult> {
2905
2946
  if (state.active && state.replayId) {
2906
2947
  return {
@@ -2918,6 +2959,9 @@ async function startSessionReplayRecorder(
2918
2959
  } catch {
2919
2960
  return { started: false, reason: "import-failed", sessionId, sampled };
2920
2961
  }
2962
+ if (state.startGeneration !== startGeneration) {
2963
+ return { started: false, reason: "disabled", sessionId, sampled };
2964
+ }
2921
2965
  if (normalized.shouldStart && !normalized.shouldStart()) {
2922
2966
  return { started: false, reason: "disabled", sessionId, sampled };
2923
2967
  }
@@ -2950,6 +2994,20 @@ async function startSessionReplayRecorder(
2950
2994
  };
2951
2995
  }
2952
2996
  }
2997
+ // stopSessionReplay may be called while the duplicate-tab probe is waiting.
2998
+ // Recheck both cancellation and the caller's live eligibility before rrweb
2999
+ // is activated so a deferred start cannot escape a route/auth teardown.
3000
+ if (
3001
+ state.startGeneration !== startGeneration ||
3002
+ (normalized.shouldStart && !normalized.shouldStart())
3003
+ ) {
3004
+ try {
3005
+ replayChannel?.close();
3006
+ } catch {
3007
+ // best-effort cleanup
3008
+ }
3009
+ return { started: false, reason: "disabled", sessionId, sampled };
3010
+ }
2953
3011
  state.options = normalized;
2954
3012
  state.replayId = replaySession.replayId;
2955
3013
  state.startedAtMs = replaySession.startedAtMs;
@@ -3049,6 +3107,10 @@ async function startSessionReplayRecorder(
3049
3107
 
3050
3108
  export async function stopSessionReplay(reason = "manual"): Promise<void> {
3051
3109
  const state = getState();
3110
+ // Invalidate an import/probe that has not set `active` yet. Without this,
3111
+ // stop during the duplicated-tab claim window was a no-op and rrweb started
3112
+ // after the caller believed recording had been disabled.
3113
+ state.startGeneration += 1;
3052
3114
  if (!state.active) return;
3053
3115
  // Restore console/fetch/XHR before tearing down the recorder: the restore
3054
3116
  // flushes any pending collapsed console duplicate, which must still be able