@agent-native/core 0.141.3 → 0.141.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/analysis-workspace/SKILL.md +1 -1
  3. package/corpus/templates/analytics/actions/gong-calls.ts +56 -18
  4. package/corpus/templates/analytics/changelog/2026-08-05-analytics-queries-keep-every-table-tenant-scoped.md +6 -0
  5. package/corpus/templates/analytics/changelog/2026-08-05-fixed-sign-in-and-dashboard-requests-failing-when-background.md +6 -0
  6. package/corpus/templates/analytics/changelog/2026-08-05-historical-rollups-stay-complete-while-live-events-arrive.md +6 -0
  7. package/corpus/templates/analytics/server/db/schema.ts +1 -0
  8. package/corpus/templates/analytics/server/lib/dashboard-report.ts +2 -0
  9. package/corpus/templates/analytics/server/lib/emails.ts +51 -0
  10. package/corpus/templates/analytics/server/lib/first-party-analytics-health.ts +8 -12
  11. package/corpus/templates/analytics/server/lib/first-party-analytics-rollups.ts +9 -0
  12. package/corpus/templates/analytics/server/lib/first-party-analytics.ts +262 -13
  13. package/corpus/templates/analytics/server/lib/gong.ts +132 -33
  14. package/corpus/templates/analytics/server/plugins/agent-chat.ts +1 -1
  15. package/corpus/templates/analytics/server/plugins/auth.ts +21 -2
  16. package/corpus/templates/analytics/server/plugins/db.ts +11 -3
  17. package/corpus/templates/analytics/server/plugins/transactional-emails.ts +5 -0
  18. package/corpus/templates/calendar/app/components/calendar/InlineEventPickers.tsx +17 -7
  19. package/corpus/templates/calendar/server/lib/booking-emails.ts +154 -70
  20. package/corpus/templates/calendar/server/lib/emails.ts +167 -0
  21. package/corpus/templates/calendar/server/lib/event-guest-notifications.ts +64 -28
  22. package/corpus/templates/calendar/server/plugins/transactional-emails.ts +5 -0
  23. package/corpus/templates/clips/actions/invite-member.ts +39 -16
  24. package/corpus/templates/clips/app/components/player/share-dialog.tsx +73 -1
  25. package/corpus/templates/clips/app/i18n/en-US.ts +3 -0
  26. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +10 -25
  27. package/corpus/templates/clips/app/routes/share.$shareId.tsx +2 -0
  28. package/corpus/templates/clips/changelog/2026-08-05-activity-is-now-the-default-clips-recording-tab-and-ai-setup.md +6 -0
  29. package/corpus/templates/clips/changelog/2026-08-05-clips-can-copy-a-linked-email-preview-for-public-recordings.md +6 -0
  30. package/corpus/templates/clips/changelog/2026-08-05-preparing-recording-now-uses-a-clean-shadow-free-status-card.md +6 -0
  31. package/corpus/templates/clips/desktop/src/styles.css +0 -3
  32. package/corpus/templates/clips/server/lib/emails.ts +256 -0
  33. package/corpus/templates/clips/server/lib/transactional-email-templates.ts +24 -0
  34. package/corpus/templates/clips/server/plugins/transactional-email-catalog.ts +5 -0
  35. package/corpus/templates/clips/server/routes/api/thumbnail/[recordingId].get.ts +4 -1
  36. package/corpus/templates/clips/shared/email-preview.ts +61 -0
  37. package/corpus/templates/content/server/lib/comment-notifications.ts +57 -26
  38. package/corpus/templates/content/server/lib/emails.ts +70 -0
  39. package/corpus/templates/content/server/plugins/transactional-emails.ts +5 -0
  40. package/corpus/templates/design/actions/import-design-tokens.ts +11 -8
  41. package/corpus/templates/design/actions/index-design-tokens.ts +13 -6
  42. package/corpus/templates/design/changelog/2026-08-05-design-token-imports-classify-typography-dimensions-correctly.md +6 -0
  43. package/corpus/templates/dispatch/app/routes/transactional-email.$appId.$id.tsx +4 -0
  44. package/corpus/templates/dispatch/app/routes/transactional-email.tsx +4 -0
  45. package/corpus/templates/dispatch/changelog/2026-08-05-transactional-email-catalogs-and-delivery-activity-are-available-again.md +6 -0
  46. package/corpus/templates/factory/AGENTS.md +18 -17
  47. package/corpus/templates/factory/actions/get-factory-automation-health.ts +50 -0
  48. package/corpus/templates/factory/actions/get-triage-config.ts +8 -0
  49. package/corpus/templates/factory/actions/save-triage-config.ts +26 -0
  50. package/corpus/templates/factory/app/i18n/en-US.ts +27 -0
  51. package/corpus/templates/factory/app/routes/factory.tsx +376 -206
  52. package/corpus/templates/factory/changelog/2026-08-05-factory-now-keeps-production-automations-running-shows-sched.md +6 -0
  53. package/corpus/templates/factory/server/db/schema.ts +6 -0
  54. package/corpus/templates/factory/server/plugins/factory-migrations.ts +10 -0
  55. package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +141 -2
  56. package/corpus/templates/forms/app/pages/FormsListPage.tsx +41 -21
  57. package/corpus/templates/forms/changelog/2026-08-05-form-links-can-now-be-cmd-clicked-to-open-in-a-new-tab.md +6 -0
  58. package/corpus/templates/forms/server/lib/emails.ts +52 -0
  59. package/corpus/templates/forms/server/lib/response-email.ts +2 -0
  60. package/corpus/templates/forms/server/plugins/transactional-emails.ts +5 -0
  61. package/corpus/templates/plan/actions/request-plan-access.ts +33 -12
  62. package/corpus/templates/plan/server/lib/comment-notifications.ts +53 -27
  63. package/corpus/templates/plan/server/lib/emails.ts +67 -0
  64. package/corpus/templates/plan/server/plugins/transactional-emails.ts +5 -0
  65. package/corpus/templates/slides/actions/import-from-url.ts +1 -0
  66. package/corpus/templates/slides/app/components/editor/NewDeckReferenceStep.tsx +492 -381
  67. package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +8 -7
  68. package/corpus/templates/slides/app/components/layout/Layout.tsx +27 -23
  69. package/corpus/templates/slides/app/components/onboarding/FirstDeckOnboardingFlow.tsx +313 -0
  70. package/corpus/templates/slides/app/components/ui/command.tsx +1 -0
  71. package/corpus/templates/slides/app/i18n/en-US.ts +8 -2
  72. package/corpus/templates/slides/app/lib/create-deck-generation.ts +305 -0
  73. package/corpus/templates/slides/app/pages/DeckEditor.tsx +12 -12
  74. package/corpus/templates/slides/app/pages/Index.tsx +127 -137
  75. package/corpus/templates/slides/app/root.tsx +8 -0
  76. package/corpus/templates/slides/changelog/2026-08-05-deck-browsing-keeps-all-and-mine-in-the-header-with-a-tighte.md +6 -0
  77. package/corpus/templates/slides/changelog/2026-08-05-new-deck-creation-now-flows-through-a-clean-full-screen-refe.md +6 -0
  78. package/corpus/templates/slides/changelog/2026-08-05-website-imports-now-include-linked-stylesheets-for-more-accu.md +6 -0
  79. package/corpus/templates/slides/server/lib/comment-notifications.ts +44 -21
  80. package/corpus/templates/slides/server/lib/emails.ts +40 -0
  81. package/corpus/templates/slides/server/plugins/transactional-emails.ts +5 -0
  82. package/dist/brand-kit/index.d.ts +1 -0
  83. package/dist/brand-kit/index.js +1 -0
  84. package/dist/brand-kit/tokens.d.ts +91 -0
  85. package/dist/brand-kit/tokens.js +279 -0
  86. package/dist/brand-kit/types.d.ts +25 -0
  87. package/dist/client/AssistantChat.js +7 -7
  88. package/dist/client/onboarding/FirstRunOnboarding.js +26 -2
  89. package/dist/client/onboarding/first-run-registry.d.ts +25 -0
  90. package/dist/client/onboarding/first-run-registry.js +18 -0
  91. package/dist/client/onboarding/index.d.ts +2 -0
  92. package/dist/client/onboarding/index.js +1 -0
  93. package/dist/collab/routes.d.ts +1 -1
  94. package/dist/deploy/build.d.ts +8 -0
  95. package/dist/deploy/build.js +92 -3
  96. package/dist/deploy/workspace-deploy.js +86 -3
  97. package/dist/email-catalog/actions/list-email-activity.d.ts +5 -0
  98. package/dist/email-catalog/actions/list-email-activity.js +19 -0
  99. package/dist/email-catalog/actions/list-email-engagement.d.ts +5 -0
  100. package/dist/email-catalog/actions/list-email-engagement.js +15 -0
  101. package/dist/email-catalog/actions/list-email-log.d.ts +7 -0
  102. package/dist/email-catalog/actions/list-email-log.js +23 -0
  103. package/dist/email-catalog/actions/list-transactional-emails.d.ts +22 -0
  104. package/dist/email-catalog/actions/list-transactional-emails.js +67 -0
  105. package/dist/email-catalog/actions/render-transactional-email-preview.d.ts +9 -0
  106. package/dist/email-catalog/actions/render-transactional-email-preview.js +25 -0
  107. package/dist/email-catalog/authorize.d.ts +1 -0
  108. package/dist/email-catalog/authorize.js +9 -0
  109. package/dist/email-catalog/log.d.ts +60 -0
  110. package/dist/email-catalog/log.js +142 -0
  111. package/dist/email-catalog/provider-metrics.d.ts +34 -0
  112. package/dist/email-catalog/provider-metrics.js +182 -0
  113. package/dist/email-catalog/registry.d.ts +78 -0
  114. package/dist/email-catalog/registry.js +70 -0
  115. package/dist/email-catalog/schema.d.ts +231 -0
  116. package/dist/email-catalog/schema.js +53 -0
  117. package/dist/email-catalog/system-emails.d.ts +13 -0
  118. package/dist/email-catalog/system-emails.js +67 -0
  119. package/dist/jobs/index.d.ts +2 -0
  120. package/dist/jobs/index.js +2 -0
  121. package/dist/jobs/run-history.js +48 -1
  122. package/dist/jobs/scheduler-dispatch.d.ts +7 -0
  123. package/dist/jobs/scheduler-dispatch.js +7 -0
  124. package/dist/jobs/scheduler-health.d.ts +22 -0
  125. package/dist/jobs/scheduler-health.js +172 -0
  126. package/dist/jobs/scheduler.js +67 -2
  127. package/dist/localization/default-messages.d.ts +45 -0
  128. package/dist/localization/default-messages.js +45 -0
  129. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  130. package/dist/notifications/routes.d.ts +3 -3
  131. package/dist/observability/routes.d.ts +5 -5
  132. package/dist/org/handlers.js +9 -1
  133. package/dist/progress/routes.d.ts +1 -1
  134. package/dist/provider-api/actions/custom-provider-registration.d.ts +10 -10
  135. package/dist/provider-api/actions/provider-api.d.ts +8 -8
  136. package/dist/secrets/routes.d.ts +9 -9
  137. package/dist/server/action-discovery.js +22 -0
  138. package/dist/server/agent-chat/recurring-jobs-runtime.d.ts +7 -1
  139. package/dist/server/agent-chat/recurring-jobs-runtime.js +12 -0
  140. package/dist/server/agent-chat-plugin.d.ts +2 -1
  141. package/dist/server/agent-chat-plugin.js +39 -1
  142. package/dist/server/better-auth-instance.js +17 -2
  143. package/dist/server/design-token-utils.d.ts +6 -1
  144. package/dist/server/design-token-utils.js +252 -59
  145. package/dist/server/email.d.ts +11 -0
  146. package/dist/server/email.js +54 -3
  147. package/dist/server/index.d.ts +1 -0
  148. package/dist/server/index.js +1 -0
  149. package/dist/server/realtime-token.d.ts +1 -1
  150. package/dist/server/transcribe-voice.d.ts +1 -1
  151. package/dist/vite/client.js +20 -14
  152. package/package.json +2 -1
@@ -1,4 +1,4 @@
1
- import { getDbExec } from "@agent-native/core/db";
1
+ import { getDbExec, isPostgres } from "@agent-native/core/db";
2
2
  import { and, eq, isNull, or } from "drizzle-orm";
3
3
 
4
4
  import { FIRST_PARTY_ANALYTICS_QUERY_TIMEOUT_MS } from "../../shared/dashboard-report-timeouts.js";
@@ -17,7 +17,11 @@ import {
17
17
  queryOutcomeFromError,
18
18
  recordFirstPartyAnalyticsQueryPressure,
19
19
  } from "./first-party-analytics-health.js";
20
- import { upsertFirstPartyAnalyticsRollups } from "./first-party-analytics-rollups.js";
20
+ import {
21
+ FIRST_PARTY_ANALYTICS_ROLLUP_LOCK_KEY,
22
+ FIRST_PARTY_ANALYTICS_ROLLUP_LOCK_SQL,
23
+ upsertFirstPartyAnalyticsRollups,
24
+ } from "./first-party-analytics-rollups.js";
21
25
 
22
26
  export interface AnalyticsScope {
23
27
  userEmail: string;
@@ -506,6 +510,16 @@ export async function recordAnalyticsEvents(
506
510
 
507
511
  if (rows.length) {
508
512
  await db.transaction(async (tx: any) => {
513
+ if (isPostgres()) {
514
+ // The historical backfill takes this lock before its raw-event
515
+ // snapshot. Holding it through ingest's rollup increment prevents the
516
+ // backfill's monotonic upsert from losing a concurrently committed
517
+ // event.
518
+ await tx.execute({
519
+ sql: FIRST_PARTY_ANALYTICS_ROLLUP_LOCK_SQL,
520
+ args: [FIRST_PARTY_ANALYTICS_ROLLUP_LOCK_KEY],
521
+ });
522
+ }
509
523
  await tx.insert(schema.analyticsEvents).values(rows);
510
524
  await tx
511
525
  .update(schema.analyticsPublicKeys)
@@ -579,6 +593,233 @@ function stripSqlLiterals(sql: string): string {
579
593
  return out;
580
594
  }
581
595
 
596
+ interface AnalyticsSqlToken {
597
+ value: string;
598
+ quoted: boolean;
599
+ depth: number;
600
+ }
601
+
602
+ interface AnalyticsSqlSource {
603
+ ref: string;
604
+ quoted: boolean;
605
+ commaSeparated: boolean;
606
+ }
607
+
608
+ const SQL_SOURCE_CLAUSE_ENDS = new Set([
609
+ "where",
610
+ "group",
611
+ "order",
612
+ "limit",
613
+ "having",
614
+ "union",
615
+ "except",
616
+ "intersect",
617
+ "window",
618
+ "qualify",
619
+ "returning",
620
+ ]);
621
+
622
+ function tokenizeAnalyticsSql(sql: string): AnalyticsSqlToken[] {
623
+ const tokens: AnalyticsSqlToken[] = [];
624
+ let depth = 0;
625
+
626
+ for (let i = 0; i < sql.length; ) {
627
+ const ch = sql[i];
628
+ const next = sql[i + 1];
629
+
630
+ if (ch === "-" && next === "-") {
631
+ i += 2;
632
+ while (i < sql.length && sql[i] !== "\n") i++;
633
+ continue;
634
+ }
635
+ if (ch === "/" && next === "*") {
636
+ i += 2;
637
+ while (i < sql.length && !(sql[i] === "*" && sql[i + 1] === "/")) {
638
+ i++;
639
+ }
640
+ i += 2;
641
+ continue;
642
+ }
643
+ if (ch === "'") {
644
+ i++;
645
+ while (i < sql.length) {
646
+ if (sql[i] === "'" && sql[i + 1] === "'") {
647
+ i += 2;
648
+ continue;
649
+ }
650
+ if (sql[i] === "'") {
651
+ i++;
652
+ break;
653
+ }
654
+ i++;
655
+ }
656
+ continue;
657
+ }
658
+ if (ch === '"' || ch === "`" || ch === "[") {
659
+ const closing = ch === "[" ? "]" : ch;
660
+ let value = "";
661
+ i++;
662
+ while (i < sql.length) {
663
+ if (sql[i] === closing && sql[i + 1] === closing) {
664
+ value += closing;
665
+ i += 2;
666
+ continue;
667
+ }
668
+ if (sql[i] === closing) {
669
+ i++;
670
+ break;
671
+ }
672
+ value += sql[i++];
673
+ }
674
+ tokens.push({ value, quoted: true, depth });
675
+ continue;
676
+ }
677
+ if (/[A-Za-z_]/.test(ch)) {
678
+ const start = i;
679
+ i++;
680
+ while (i < sql.length && /[A-Za-z0-9_$]/.test(sql[i])) i++;
681
+ tokens.push({ value: sql.slice(start, i), quoted: false, depth });
682
+ continue;
683
+ }
684
+ if (ch === "(") {
685
+ tokens.push({ value: ch, quoted: false, depth });
686
+ depth++;
687
+ i++;
688
+ continue;
689
+ }
690
+ if (ch === ")") {
691
+ depth = Math.max(0, depth - 1);
692
+ tokens.push({ value: ch, quoted: false, depth });
693
+ i++;
694
+ continue;
695
+ }
696
+ if (ch === "." || ch === ",") {
697
+ tokens.push({ value: ch, quoted: false, depth });
698
+ }
699
+ i++;
700
+ }
701
+
702
+ return tokens;
703
+ }
704
+
705
+ function isAnalyticsSqlKeyword(
706
+ token: AnalyticsSqlToken | undefined,
707
+ keyword: string,
708
+ ): boolean {
709
+ return Boolean(
710
+ token && !token.quoted && token.value.toLowerCase() === keyword,
711
+ );
712
+ }
713
+
714
+ function readAnalyticsSqlSource(
715
+ tokens: AnalyticsSqlToken[],
716
+ start: number,
717
+ ): { source: Omit<AnalyticsSqlSource, "commaSeparated"> | null; next: number } {
718
+ let index = start;
719
+ while (
720
+ isAnalyticsSqlKeyword(tokens[index], "only") ||
721
+ isAnalyticsSqlKeyword(tokens[index], "lateral")
722
+ ) {
723
+ index++;
724
+ }
725
+
726
+ const first = tokens[index];
727
+ if (!first) return { source: null, next: index };
728
+ if (first.value === "(") {
729
+ const groupDepth = first.depth;
730
+ index++;
731
+ while (
732
+ index < tokens.length &&
733
+ !(tokens[index].value === ")" && tokens[index].depth === groupDepth)
734
+ ) {
735
+ index++;
736
+ }
737
+ return { source: null, next: Math.min(index + 1, tokens.length) };
738
+ }
739
+ if (!first.quoted && !/^[A-Za-z_][A-Za-z0-9_$]*$/.test(first.value)) {
740
+ return { source: null, next: index + 1 };
741
+ }
742
+
743
+ let ref = first.value;
744
+ let quoted = first.quoted;
745
+ if (
746
+ tokens[index + 1]?.value === "." &&
747
+ tokens[index + 2] &&
748
+ (tokens[index + 2].quoted ||
749
+ /^[A-Za-z_][A-Za-z0-9_$]*$/.test(tokens[index + 2].value))
750
+ ) {
751
+ ref += `.${tokens[index + 2].value}`;
752
+ quoted ||= tokens[index + 2].quoted;
753
+ index += 2;
754
+ }
755
+ return { source: { ref, quoted }, next: index + 1 };
756
+ }
757
+
758
+ function collectAnalyticsSqlSources(sql: string): {
759
+ cteNames: Set<string>;
760
+ sources: AnalyticsSqlSource[];
761
+ } {
762
+ const tokens = tokenizeAnalyticsSql(sql);
763
+ const cteNames = new Set<string>();
764
+ for (let i = 0; i + 2 < tokens.length; i++) {
765
+ if (
766
+ tokens[i].value &&
767
+ isAnalyticsSqlKeyword(tokens[i + 1], "as") &&
768
+ tokens[i + 2].value === "("
769
+ ) {
770
+ cteNames.add(tokens[i].value.toLowerCase());
771
+ }
772
+ }
773
+
774
+ const sources: AnalyticsSqlSource[] = [];
775
+ for (let i = 0; i < tokens.length; i++) {
776
+ if (!isAnalyticsSqlKeyword(tokens[i], "from")) continue;
777
+ const fromDepth = tokens[i].depth;
778
+ let expectSource = true;
779
+ let inJoinCondition = false;
780
+ let sourceAfterComma = false;
781
+
782
+ for (let j = i + 1; j < tokens.length; j++) {
783
+ const token = tokens[j];
784
+ if (token.depth < fromDepth) break;
785
+ if (token.depth > fromDepth) continue;
786
+
787
+ const word = token.quoted ? "" : token.value.toLowerCase();
788
+ if (SQL_SOURCE_CLAUSE_ENDS.has(word)) break;
789
+ if (word === "join") {
790
+ expectSource = true;
791
+ inJoinCondition = false;
792
+ sourceAfterComma = false;
793
+ continue;
794
+ }
795
+ if (word === "on" || word === "using") {
796
+ expectSource = false;
797
+ inJoinCondition = true;
798
+ continue;
799
+ }
800
+ if (token.value === "," && !inJoinCondition) {
801
+ expectSource = true;
802
+ sourceAfterComma = true;
803
+ continue;
804
+ }
805
+ if (!expectSource) continue;
806
+
807
+ const parsed = readAnalyticsSqlSource(tokens, j);
808
+ if (parsed.source) {
809
+ sources.push({
810
+ ...parsed.source,
811
+ commaSeparated: sourceAfterComma,
812
+ });
813
+ }
814
+ sourceAfterComma = false;
815
+ expectSource = false;
816
+ j = Math.max(j, parsed.next - 1);
817
+ }
818
+ }
819
+
820
+ return { cteNames, sources };
821
+ }
822
+
582
823
  export function validateFirstPartyAnalyticsSql(sql: string): void {
583
824
  const stripped = stripSqlLiterals(sql).trim();
584
825
  const lowered = stripped.toLowerCase();
@@ -603,30 +844,38 @@ export function validateFirstPartyAnalyticsSql(sql: string): void {
603
844
  if (/\$\d+\b/.test(stripped)) {
604
845
  throw new Error("Bind placeholders are not supported in dashboard SQL");
605
846
  }
847
+ if (/\bonly\b/i.test(stripped)) {
848
+ throw new Error(
849
+ "ONLY-qualified table sources are not supported in first-party analytics queries",
850
+ );
851
+ }
606
852
  if (/\bsession_replay_chunks\b/i.test(stripped)) {
607
853
  throw new Error(
608
854
  "First-party analytics queries cannot read session replay chunks",
609
855
  );
610
856
  }
611
857
 
612
- const cteNames = new Set<string>();
613
- const cteRe = /\b([a-zA-Z_][a-zA-Z0-9_]*)\s+as\s*\(/gi;
614
- for (const match of stripped.matchAll(cteRe)) {
615
- cteNames.add(match[1].toLowerCase());
616
- }
617
-
858
+ const { cteNames, sources } = collectAnalyticsSqlSources(sql);
618
859
  let usesAllowedTable = false;
619
- const tableRe =
620
- /\b(?:from|join)\s+([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)?)/gi;
621
- for (const match of stripped.matchAll(tableRe)) {
622
- const ref = match[1].toLowerCase();
860
+ for (const source of sources) {
861
+ const ref = source.ref.toLowerCase();
862
+ if (source.commaSeparated) {
863
+ throw new Error(
864
+ "Comma-separated table sources are not supported in first-party analytics queries; use an explicit JOIN",
865
+ );
866
+ }
623
867
  if (FIRST_PARTY_QUERY_TABLES.has(ref)) {
868
+ if (source.quoted) {
869
+ throw new Error(
870
+ "Quoted table identifiers are not supported in first-party analytics queries",
871
+ );
872
+ }
624
873
  usesAllowedTable = true;
625
874
  continue;
626
875
  }
627
876
  if (cteNames.has(ref)) continue;
628
877
  throw new Error(
629
- `First-party analytics queries can only read ${FIRST_PARTY_QUERY_TABLE_LIST} (found ${match[1]})`,
878
+ `First-party analytics queries can only read ${FIRST_PARTY_QUERY_TABLE_LIST} (found ${source.ref})`,
630
879
  );
631
880
  }
632
881
  if (!usesAllowedTable) {
@@ -17,6 +17,8 @@ const DEFAULT_API_BASE = "https://api.gong.io/v2";
17
17
  const MAX_GONG_SEARCH_PAGES = 50;
18
18
  const MAX_GONG_CALL_LIST_PAGES = 50;
19
19
  const MAX_GONG_EXHAUSTIVE_RECORDS = 500;
20
+ const GONG_API_REQUEST_TIMEOUT_MS = 30_000;
21
+ const MAX_GONG_EXHAUSTIVE_RUNTIME_MS = 60_000;
20
22
 
21
23
  const cache = new Map<string, { data: unknown; ts: number }>();
22
24
  const CACHE_TTL_MS = 10 * 60 * 1000;
@@ -38,7 +40,29 @@ async function getApiBase(): Promise<string> {
38
40
  return (configured || DEFAULT_API_BASE).replace(/\/+$/, "");
39
41
  }
40
42
 
41
- async function apiGet<T>(path: string, cacheKey?: string): Promise<T> {
43
+ export interface GongRequestOptions {
44
+ signal?: AbortSignal;
45
+ deadlineAt?: number;
46
+ }
47
+
48
+ function requestSignal(options?: GongRequestOptions): AbortSignal {
49
+ const remainingMs = options?.deadlineAt
50
+ ? Math.max(
51
+ 1,
52
+ Math.min(GONG_API_REQUEST_TIMEOUT_MS, options.deadlineAt - Date.now()),
53
+ )
54
+ : GONG_API_REQUEST_TIMEOUT_MS;
55
+ const timeoutSignal = AbortSignal.timeout(remainingMs);
56
+ return options?.signal
57
+ ? AbortSignal.any([options.signal, timeoutSignal])
58
+ : timeoutSignal;
59
+ }
60
+
61
+ async function apiGet<T>(
62
+ path: string,
63
+ cacheKey?: string,
64
+ options?: GongRequestOptions,
65
+ ): Promise<T> {
42
66
  const key = scopedCredentialCacheKey(cacheKey ?? path, "GONG_ACCESS_KEY");
43
67
  const cached = cache.get(key);
44
68
  if (cached && Date.now() - cached.ts < CACHE_TTL_MS) {
@@ -50,6 +74,7 @@ async function apiGet<T>(path: string, cacheKey?: string): Promise<T> {
50
74
  Authorization: await getAuthHeader(),
51
75
  "Content-Type": "application/json",
52
76
  },
77
+ signal: requestSignal(options),
53
78
  });
54
79
 
55
80
  if (!res.ok) {
@@ -72,6 +97,7 @@ async function apiPost<T>(
72
97
  path: string,
73
98
  body: unknown,
74
99
  cacheKey?: string,
100
+ options?: GongRequestOptions,
75
101
  ): Promise<T> {
76
102
  const key = scopedCredentialCacheKey(
77
103
  cacheKey ?? `POST:${path}:${JSON.stringify(body)}`,
@@ -89,6 +115,7 @@ async function apiPost<T>(
89
115
  "Content-Type": "application/json",
90
116
  },
91
117
  body: JSON.stringify(body),
118
+ signal: requestSignal(options),
92
119
  });
93
120
 
94
121
  if (!res.ok) {
@@ -159,11 +186,18 @@ export interface EnrichedTranscript {
159
186
  externalMonologues: EnrichedMonologue[];
160
187
  }
161
188
 
162
- export async function getCalls(filters?: {
163
- fromDateTime?: string;
164
- toDateTime?: string;
189
+ export async function getCalls(
190
+ filters?: {
191
+ fromDateTime?: string;
192
+ toDateTime?: string;
193
+ cursor?: string;
194
+ },
195
+ options?: GongRequestOptions,
196
+ ): Promise<{
197
+ calls: GongCall[];
165
198
  cursor?: string;
166
- }): Promise<{ calls: GongCall[]; cursor?: string; totalRecords?: number }> {
199
+ totalRecords?: number;
200
+ }> {
167
201
  const params = new URLSearchParams();
168
202
  if (filters?.fromDateTime) params.set("fromDateTime", filters.fromDateTime);
169
203
  if (filters?.toDateTime) params.set("toDateTime", filters.toDateTime);
@@ -174,7 +208,7 @@ export async function getCalls(filters?: {
174
208
  const data = await apiGet<{
175
209
  calls?: GongCall[];
176
210
  records?: { cursor?: string; totalRecords?: number };
177
- }>(path);
211
+ }>(path, undefined, options);
178
212
  return {
179
213
  calls: data.calls ?? [],
180
214
  cursor: data.records?.cursor,
@@ -184,41 +218,62 @@ export async function getCalls(filters?: {
184
218
  };
185
219
  }
186
220
 
187
- export async function getAllCalls(filters?: {
188
- fromDateTime?: string;
189
- toDateTime?: string;
190
- }): Promise<{
221
+ export async function getAllCalls(
222
+ filters?: {
223
+ fromDateTime?: string;
224
+ toDateTime?: string;
225
+ },
226
+ options: GongRequestOptions = {},
227
+ ): Promise<{
191
228
  calls: GongCall[];
192
229
  cursor?: string;
193
230
  pages: number;
194
231
  totalRecords?: number;
195
232
  }> {
233
+ const deadlineAt =
234
+ options.deadlineAt ?? Date.now() + MAX_GONG_EXHAUSTIVE_RUNTIME_MS;
196
235
  const calls: GongCall[] = [];
197
236
  let cursor: string | undefined;
198
237
  let pages = 0;
199
238
  let totalRecords: number | undefined;
200
239
 
201
240
  do {
202
- const page = await getCalls({
203
- ...filters,
204
- ...(cursor ? { cursor } : {}),
205
- });
241
+ if (Date.now() >= deadlineAt) {
242
+ throw new Error(
243
+ "Gong exhaustive call listing exceeded its 60-second runtime budget. " +
244
+ "Use provider-api-request with stageAs and pagination, followed by query-staged-dataset or a Data Program.",
245
+ );
246
+ }
247
+ const page = await getCalls(
248
+ {
249
+ ...filters,
250
+ ...(cursor ? { cursor } : {}),
251
+ },
252
+ { ...options, deadlineAt },
253
+ );
206
254
  pages += 1;
207
255
  if (typeof page.totalRecords === "number") {
208
256
  totalRecords = page.totalRecords;
209
257
  }
210
258
  if (
211
259
  typeof totalRecords === "number" &&
212
- totalRecords > MAX_GONG_EXHAUSTIVE_RECORDS
260
+ totalRecords >= MAX_GONG_EXHAUSTIVE_RECORDS
213
261
  ) {
214
262
  throw new Error(
215
- `Gong exhaustive call listing found ${totalRecords.toLocaleString()} records in the requested window. ` +
263
+ `Gong exhaustive call listing found ${totalRecords.toLocaleString()} records in the requested window; ` +
264
+ "Use provider-api-request with stageAs and pagination, followed by query-staged-dataset or a Data Program, instead of returning the full cohort through gong-calls.",
265
+ );
266
+ }
267
+ const nextCursor = page.cursor;
268
+ const nextCallCount = calls.length + page.calls.length;
269
+ if (nextCallCount >= MAX_GONG_EXHAUSTIVE_RECORDS) {
270
+ throw new Error(
271
+ `Gong exhaustive call listing reached ${MAX_GONG_EXHAUSTIVE_RECORDS.toLocaleString()} records before the cohort was safely returned. ` +
216
272
  "Use provider-api-request with stageAs and pagination, followed by query-staged-dataset or a Data Program, instead of returning the full cohort through gong-calls.",
217
273
  );
218
274
  }
219
275
  calls.push(...page.calls);
220
276
 
221
- const nextCursor = page.cursor;
222
277
  if (!nextCursor || nextCursor === cursor) {
223
278
  cursor = nextCursor;
224
279
  break;
@@ -229,18 +284,23 @@ export async function getAllCalls(filters?: {
229
284
  return { calls, cursor, pages, totalRecords };
230
285
  }
231
286
 
232
- export async function getCall(callId: string): Promise<GongCall | null> {
287
+ export async function getCall(
288
+ callId: string,
289
+ options?: GongRequestOptions,
290
+ ): Promise<GongCall | null> {
233
291
  const body = { filter: { callIds: [callId] } };
234
292
  const data = await apiPost<{ calls?: GongCall[] }>(
235
293
  "/calls",
236
294
  body,
237
295
  `call:${callId}`,
296
+ options,
238
297
  );
239
298
  return data.calls?.[0] ?? null;
240
299
  }
241
300
 
242
301
  export async function getCallDetail(
243
302
  callId: string,
303
+ options?: GongRequestOptions,
244
304
  ): Promise<GongCallDetail | null> {
245
305
  const body = {
246
306
  filter: { callIds: [callId] },
@@ -255,6 +315,7 @@ export async function getCallDetail(
255
315
  "/calls/extensive",
256
316
  body,
257
317
  `detail:${callId}`,
318
+ options,
258
319
  );
259
320
  const call = data.calls?.[0];
260
321
  if (!call) return null;
@@ -279,7 +340,10 @@ export async function getCallDetail(
279
340
  };
280
341
  }
281
342
 
282
- export async function getCallTranscripts(callIds: string[]): Promise<unknown> {
343
+ export async function getCallTranscripts(
344
+ callIds: string[],
345
+ options?: GongRequestOptions,
346
+ ): Promise<unknown> {
283
347
  const ids = Array.from(
284
348
  new Set(
285
349
  callIds
@@ -289,11 +353,19 @@ export async function getCallTranscripts(callIds: string[]): Promise<unknown> {
289
353
  );
290
354
  if (!ids.length) return { callTranscripts: [] };
291
355
  const body = { filter: { callIds: ids } };
292
- return apiPost("/calls/transcript", body, `transcripts:${ids.join(",")}`);
356
+ return apiPost(
357
+ "/calls/transcript",
358
+ body,
359
+ `transcripts:${ids.join(",")}`,
360
+ options,
361
+ );
293
362
  }
294
363
 
295
- export async function getCallTranscript(callId: string): Promise<unknown> {
296
- return getCallTranscripts([callId]);
364
+ export async function getCallTranscript(
365
+ callId: string,
366
+ options?: GongRequestOptions,
367
+ ): Promise<unknown> {
368
+ return getCallTranscripts([callId], options);
297
369
  }
298
370
 
299
371
  export async function getEnrichedTranscript(
@@ -502,6 +574,8 @@ export interface GongCallSearchOptions {
502
574
  fromDateTime?: string;
503
575
  toDateTime?: string;
504
576
  exhaustive?: boolean;
577
+ signal?: AbortSignal;
578
+ deadlineAt?: number;
505
579
  }
506
580
 
507
581
  function normalizedSearchQueries(queries: string[]): string[] {
@@ -591,32 +665,57 @@ export async function searchCallsForQueries(
591
665
 
592
666
  const matches = new Map<string, GongCall & { matchedQueries?: string[] }>();
593
667
  let searchedCallCount = 0;
668
+ let scannedCallCount = 0;
594
669
  let cursor: string | undefined;
595
670
  let pages = 0;
671
+ const deadlineAt =
672
+ options.deadlineAt ?? Date.now() + MAX_GONG_EXHAUSTIVE_RUNTIME_MS;
596
673
  do {
674
+ if (Date.now() >= deadlineAt) {
675
+ throw new Error(
676
+ "Gong call search exceeded its 60-second runtime budget. " +
677
+ "Use provider-api-request with stageAs and pagination, followed by query-staged-dataset or a Data Program.",
678
+ );
679
+ }
597
680
  const data = await apiPost<{
598
681
  calls?: unknown[];
599
682
  records?: { cursor?: string; totalRecords?: number };
600
- }>("/calls/extensive", {
601
- filter: {
602
- fromDateTime,
603
- ...(options.toDateTime ? { toDateTime: options.toDateTime } : {}),
683
+ }>(
684
+ "/calls/extensive",
685
+ {
686
+ filter: {
687
+ fromDateTime,
688
+ ...(options.toDateTime ? { toDateTime: options.toDateTime } : {}),
689
+ },
690
+ contentSelector: { exposedFields: { parties: true } },
691
+ ...(cursor ? { cursor } : {}),
604
692
  },
605
- contentSelector: { exposedFields: { parties: true } },
606
- ...(cursor ? { cursor } : {}),
607
- });
693
+ undefined,
694
+ {
695
+ signal: options.signal,
696
+ deadlineAt,
697
+ },
698
+ );
608
699
  pages += 1;
609
700
  if (
610
701
  options.exhaustive &&
611
702
  typeof data.records?.totalRecords === "number" &&
612
- data.records.totalRecords > MAX_GONG_EXHAUSTIVE_RECORDS
703
+ data.records.totalRecords >= MAX_GONG_EXHAUSTIVE_RECORDS
613
704
  ) {
614
705
  throw new Error(
615
- `Gong exhaustive search found ${data.records.totalRecords.toLocaleString()} records in the requested window. ` +
616
- "Use provider-corpus-job with staged call IDs for searches larger than 500 records so progress is checkpointed between batches.",
706
+ `Gong exhaustive search found ${data.records.totalRecords.toLocaleString()} records in the requested window; ` +
707
+ "Use provider-corpus-job with staged call IDs for searches with 500 or more records so progress is checkpointed between batches.",
708
+ );
709
+ }
710
+ const rawCalls = data.calls ?? [];
711
+ scannedCallCount += rawCalls.length;
712
+ if (options.exhaustive && scannedCallCount >= MAX_GONG_EXHAUSTIVE_RECORDS) {
713
+ throw new Error(
714
+ `Gong exhaustive search reached ${MAX_GONG_EXHAUSTIVE_RECORDS.toLocaleString()} records before the cohort was safely returned. ` +
715
+ "Use provider-corpus-job with staged call IDs for searches with 500 or more records so progress is checkpointed between batches.",
617
716
  );
618
717
  }
619
- const calls = (data.calls ?? [])
718
+ const calls = rawCalls
620
719
  .map(normalizeExtensiveCall)
621
720
  .filter((call): call is GongCall => Boolean(call))
622
721
  .filter(isExternalCall);
@@ -656,7 +656,7 @@ export function realDataFinalGuard(
656
656
  ) {
657
657
  return {
658
658
  retryMessage:
659
- "The user asked a coverage-sensitive provider question, but the draft only used bounded convenience data actions. Do not finalize an exhaustive, all-records, or absence-sensitive answer from shortcut actions alone. Use the broad provider API/MCP surface and a staged analysis workflow now: provider-api-catalog/provider-api-docs when needed; for Gong, use configured tracker results from /calls/extensive when they cover the term, otherwise use provider-api-request as raw ingestion with stageAs/saveToFile followed by query-staged-dataset or a Data Program; use provider-corpus-job for durable batched raw-transcript scans. Never loop per call from run-code or a delegated agent. For more than 500 Gong records, gong-calls is not the broad-search path. If full coverage is not possible in this turn, finalize with explicit partial-coverage wording, inspected counts, filters, and remaining gaps.",
659
+ "The user asked a coverage-sensitive provider question, but the draft only used bounded convenience data actions. Do not finalize an exhaustive, all-records, or absence-sensitive answer from shortcut actions alone. Use the broad provider API/MCP surface and a staged analysis workflow now: provider-api-catalog/provider-api-docs when needed; for Gong, use configured tracker results from /calls/extensive when they cover the term, otherwise use provider-api-request as raw ingestion with stageAs/saveToFile followed by query-staged-dataset or a Data Program; use provider-corpus-job for durable batched raw-transcript scans. Never loop per call from run-code or a delegated agent. For 500 or more Gong records, gong-calls is not the broad-search path. If full coverage is not possible in this turn, finalize with explicit partial-coverage wording, inspected counts, filters, and remaining gaps.",
660
660
  fallbackMessage:
661
661
  "I can't make a confident coverage-sensitive provider claim from bounded shortcut actions alone. I need a provider API/corpus workflow, or I need to label the answer as partial with exact inspected counts and gaps.",
662
662
  };
@@ -1,6 +1,10 @@
1
- import { createAuthPlugin } from "@agent-native/core/server";
1
+ import {
2
+ createAuthPlugin,
3
+ isInBackgroundFunctionRuntime,
4
+ markDefaultPluginProvided,
5
+ } from "@agent-native/core/server";
2
6
 
3
- export default createAuthPlugin({
7
+ const authPlugin = createAuthPlugin({
4
8
  publicPaths: [
5
9
  "/track",
6
10
  "/api/analytics/track",
@@ -23,3 +27,18 @@ export default createAuthPlugin({
23
27
  ],
24
28
  },
25
29
  });
30
+
31
+ export default async (nitroApp: any): Promise<void> => {
32
+ // Keep the custom slot marked so runtime discovery cannot mount a second
33
+ // default auth plugin while this worker is registering its internal routes.
34
+ markDefaultPluginProvided(nitroApp, "auth");
35
+ if (isInBackgroundFunctionRuntime()) {
36
+ // Background functions authenticate their signed processor routes locally;
37
+ // Better Auth would perform an unnecessary database initialization here.
38
+ console.info(
39
+ "[auth] Skipping Better Auth setup in durable background runtime",
40
+ );
41
+ return;
42
+ }
43
+ await authPlugin(nitroApp);
44
+ };