@agent-native/core 0.161.1 → 0.161.2

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 (26) hide show
  1. package/corpus/templates/analytics/actions/compose-dashboard.ts +5 -2
  2. package/corpus/templates/analytics/actions/export-dashboard-panel-to-google-sheet.ts +11 -5
  3. package/corpus/templates/analytics/actions/migrate-first-party-analytics-to-bigquery.ts +89 -2
  4. package/corpus/templates/analytics/actions/update-dashboard.ts +14 -2
  5. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +6 -0
  6. package/corpus/templates/analytics/server/lib/dashboard-panel-query.ts +89 -41
  7. package/corpus/templates/analytics/server/lib/dashboard-panel-source-resolver.ts +8 -3
  8. package/corpus/templates/analytics/server/lib/error-capture.ts +6 -0
  9. package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +187 -44
  10. package/corpus/templates/analytics/server/lib/first-party-analytics.ts +44 -16
  11. package/dist/agent/engine/builder-engine.js +7 -4
  12. package/dist/agent/engine/types.d.ts +10 -0
  13. package/dist/agent/engine/types.js +3 -0
  14. package/dist/agent/production-agent.js +1 -0
  15. package/dist/agent/run-manager.js +8 -0
  16. package/dist/collab/struct-routes.d.ts +1 -1
  17. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  18. package/dist/observability/routes.d.ts +3 -3
  19. package/dist/provider-api/actions/custom-provider-registration.d.ts +2 -2
  20. package/dist/resources/handlers.d.ts +1 -1
  21. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  22. package/dist/server/release-migrations.js +4 -0
  23. package/dist/server/transcribe-voice.d.ts +1 -1
  24. package/dist/workspace-connections/migrations.d.ts +18 -0
  25. package/dist/workspace-connections/migrations.js +153 -0
  26. package/package.json +3 -3
@@ -34,6 +34,23 @@ export interface FirstPartyAnalyticsScope {
34
34
  orgId: string | null;
35
35
  }
36
36
 
37
+ /**
38
+ * Stored panel SQL is valid PostgreSQL but has no BigQuery equivalent. This is
39
+ * not a query failure: the panel cannot run for this scope until its SQL or the
40
+ * scope's sink changes, so retrying is pointless and callers render an
41
+ * explanatory state instead. `construct` names the exact syntax that has no
42
+ * mapping, and is the only part safe to show a user.
43
+ */
44
+ export class FirstPartyAnalyticsUnsupportedSqlError extends Error {
45
+ readonly construct: string;
46
+
47
+ constructor(construct: string, message: string) {
48
+ super(message);
49
+ this.name = "FirstPartyAnalyticsUnsupportedSqlError";
50
+ this.construct = construct;
51
+ }
52
+ }
53
+
37
54
  interface FirstPartyAnalyticsBackendSetting {
38
55
  sink?: unknown;
39
56
  table?: unknown;
@@ -572,6 +589,59 @@ function rewriteOutsideSqlLiterals(
572
589
  return result;
573
590
  }
574
591
 
592
+ const SQL_LITERAL_PLACEHOLDER_PREFIX = "_fpa_lit_";
593
+
594
+ /**
595
+ * Same intent as `rewriteOutsideSqlLiterals`, but `rewrite` sees one string
596
+ * with each literal stood in for by an identifier-shaped placeholder rather
597
+ * than a sequence of fragments split at every quote. A cast operand routinely
598
+ * sits on the far side of a literal — `'2026-08-01'::date`,
599
+ * `(COALESCE(properties, '{}'))::text` — and the fragment view cuts that
600
+ * operand in half, which is why both read as "invalid PostgreSQL cast".
601
+ */
602
+ function rewriteWithMaskedSqlLiterals(
603
+ sql: string,
604
+ rewrite: (code: string) => string,
605
+ ): string {
606
+ if (sql.includes(SQL_LITERAL_PLACEHOLDER_PREFIX)) {
607
+ throw new FirstPartyAnalyticsUnsupportedSqlError(
608
+ `an identifier reserved for query translation (${SQL_LITERAL_PLACEHOLDER_PREFIX}*)`,
609
+ `First-party BigQuery query cannot use identifiers starting with ${SQL_LITERAL_PLACEHOLDER_PREFIX}`,
610
+ );
611
+ }
612
+ const literals: string[] = [];
613
+ let masked = "";
614
+ let cursor = 0;
615
+ while (cursor < sql.length) {
616
+ const literalStart = sql.indexOf("'", cursor);
617
+ if (literalStart === -1) {
618
+ masked += sql.slice(cursor);
619
+ break;
620
+ }
621
+ masked += sql.slice(cursor, literalStart);
622
+ let literalEnd = literalStart + 1;
623
+ while (literalEnd < sql.length) {
624
+ if (sql[literalEnd] !== "'") {
625
+ literalEnd++;
626
+ continue;
627
+ }
628
+ if (sql[literalEnd + 1] === "'") {
629
+ literalEnd += 2;
630
+ continue;
631
+ }
632
+ literalEnd++;
633
+ break;
634
+ }
635
+ masked += `${SQL_LITERAL_PLACEHOLDER_PREFIX}${literals.length}_`;
636
+ literals.push(sql.slice(literalStart, literalEnd));
637
+ cursor = literalEnd;
638
+ }
639
+ return rewrite(masked).replace(
640
+ new RegExp(`${SQL_LITERAL_PLACEHOLDER_PREFIX}(\\d+)_`, "g"),
641
+ (whole, index: string) => literals[Number(index)] ?? whole,
642
+ );
643
+ }
644
+
575
645
  function findMatchingSqlParen(sql: string, openIndex: number): number {
576
646
  let depth = 0;
577
647
  let inLiteral = false;
@@ -697,6 +767,46 @@ function coerceDateComparisonOperands(sql: string): string {
697
767
  );
698
768
  }
699
769
 
770
+ /**
771
+ * Extent of the operand a `::` cast at `castIndex` applies to.
772
+ *
773
+ * The function-call case is the trap: stopping at the matching `(` leaves the
774
+ * function name outside the rewritten CAST, so `sum(x)::numeric` became
775
+ * `sumCAST((x) AS NUMERIC)` and BigQuery answered `Function not found: SUMCAST`.
776
+ */
777
+ function postgresCastOperandBounds(
778
+ code: string,
779
+ castIndex: number,
780
+ ): { start: number; end: number } {
781
+ let end = castIndex;
782
+ while (end > 0 && /\s/.test(code[end - 1] ?? "")) {
783
+ end--;
784
+ }
785
+ let start = end;
786
+ if (code[end - 1] === ")") {
787
+ let depth = 0;
788
+ for (let index = end - 1; index >= 0; index--) {
789
+ if (code[index] === ")") depth++;
790
+ if (code[index] !== "(") continue;
791
+ depth--;
792
+ if (depth === 0) {
793
+ start = index;
794
+ break;
795
+ }
796
+ }
797
+ }
798
+ while (start > 0 && /[A-Za-z0-9_.$]/.test(code[start - 1] ?? "")) {
799
+ start--;
800
+ }
801
+ if (start === end) {
802
+ throw new FirstPartyAnalyticsUnsupportedSqlError(
803
+ "a PostgreSQL cast with no readable operand",
804
+ "First-party BigQuery query has an invalid PostgreSQL cast",
805
+ );
806
+ }
807
+ return { start, end };
808
+ }
809
+
700
810
  function replacePostgresCastsInCode(code: string): string {
701
811
  const castType = new RegExp(
702
812
  "::\\s*(date|timestamp|timestamptz|int|int2|int4|int8|integer|float|float4|float8|double\\s+precision|numeric|text|varchar|boolean|bool|json|jsonb)\\b",
@@ -728,40 +838,16 @@ function replacePostgresCastsInCode(code: string): string {
728
838
  };
729
839
  const targetType = mappedType[normalizedType];
730
840
  if (!targetType) {
731
- throw new Error(
841
+ throw new FirstPartyAnalyticsUnsupportedSqlError(
842
+ `a PostgreSQL ${normalizedType} cast`,
732
843
  `First-party BigQuery query does not support PostgreSQL ${normalizedType} casts`,
733
844
  );
734
845
  }
735
846
 
736
- let operandEnd = match.index;
737
- while (operandEnd > 0 && /\s/.test(result[operandEnd - 1] ?? "")) {
738
- operandEnd--;
739
- }
740
- let operandStart = operandEnd;
741
- if (result[operandEnd - 1] === ")") {
742
- let depth = 0;
743
- for (let index = operandEnd - 1; index >= 0; index--) {
744
- if (result[index] === ")") depth++;
745
- if (result[index] !== "(") continue;
746
- depth--;
747
- if (depth === 0) {
748
- operandStart = index;
749
- break;
750
- }
751
- }
752
- } else {
753
- while (
754
- operandStart > 0 &&
755
- /[A-Za-z0-9_.$]/.test(result[operandStart - 1] ?? "")
756
- ) {
757
- operandStart--;
758
- }
759
- }
760
- if (operandStart === operandEnd) {
761
- throw new Error(
762
- "First-party BigQuery query has an invalid PostgreSQL cast",
763
- );
764
- }
847
+ const { start: operandStart, end: operandEnd } = postgresCastOperandBounds(
848
+ result,
849
+ match.index,
850
+ );
765
851
  const operand = result.slice(operandStart, operandEnd).trim();
766
852
  const castEnd = match.index + match[0].length;
767
853
  result = `${result.slice(0, operandStart)}CAST(${operand} AS ${targetType})${result.slice(castEnd)}`;
@@ -810,12 +896,45 @@ function replaceBigQueryDateArithmetic(code: string): string {
810
896
  return translated;
811
897
  }
812
898
 
899
+ /**
900
+ * PostgreSQL truncates to the start of the ISO week (Monday); BigQuery's bare
901
+ * `WEEK` starts on Sunday, so the week mapping must name the weekday or the
902
+ * same query silently buckets differently on each backend.
903
+ */
904
+ const BIGQUERY_DATE_TRUNC_PARTS: Record<string, string> = {
905
+ day: "DAY",
906
+ week: "WEEK(MONDAY)",
907
+ month: "MONTH",
908
+ quarter: "QUARTER",
909
+ year: "YEAR",
910
+ };
911
+
912
+ /**
913
+ * BigQuery JSONPath field names are always quoted here rather than only when
914
+ * they look unusual: an unquoted `$.$ai_model` is rejected outright, and an
915
+ * unquoted `$.page.title` silently reads a nested field the caller never asked
916
+ * for.
917
+ */
918
+ function bigQueryJsonPath(key: string): string {
919
+ return `'$."${key.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"'`;
920
+ }
921
+
813
922
  function translatePostgresJsonOperators(sql: string): string {
814
- return sql.replace(
815
- /\b([A-Za-z_][A-Za-z0-9_.]*)\s*::\s*jsonb\s*->>\s*'([^']+)'/gi,
816
- (_match, expression: string, key: string) =>
817
- `JSON_VALUE(${expression}, '$.${key}')`,
818
- );
923
+ const jsonExtract = /\s*::\s*jsonb?\s*->>\s*'([^']*)'/gi;
924
+ let result = sql;
925
+ let match = jsonExtract.exec(result);
926
+ while (match) {
927
+ const { start, end } = postgresCastOperandBounds(result, match.index);
928
+ const operand = result.slice(start, end).trim();
929
+ const replacement = `JSON_VALUE(${operand}, ${bigQueryJsonPath(match[1] ?? "")})`;
930
+ result =
931
+ result.slice(0, start) +
932
+ replacement +
933
+ result.slice(match.index + match[0].length);
934
+ jsonExtract.lastIndex = start + replacement.length;
935
+ match = jsonExtract.exec(result);
936
+ }
937
+ return result;
819
938
  }
820
939
 
821
940
  function translateFirstPartyAnalyticsBigQuerySql(sql: string): string {
@@ -837,16 +956,20 @@ function translateFirstPartyAnalyticsBigQuerySql(sql: string): string {
837
956
  `INTERVAL ${amount} ${unit.replace(/s$/i, "").toUpperCase()}`,
838
957
  );
839
958
  translated = rewriteSqlFunctionCalls(translated, "date_trunc", (args) => {
840
- if (args.length !== 2 || !/^'week'$/i.test(args[0] ?? "")) {
841
- throw new Error(
842
- "First-party BigQuery query only supports PostgreSQL date_trunc('week', ...) expressions",
959
+ const unit = /^'([a-z]+)'$/i.exec(args[0] ?? "")?.[1]?.toLowerCase();
960
+ const datePart = unit ? BIGQUERY_DATE_TRUNC_PARTS[unit] : undefined;
961
+ if (args.length !== 2 || !datePart) {
962
+ throw new FirstPartyAnalyticsUnsupportedSqlError(
963
+ `date_trunc(${args[0] ?? "?"}, ...)`,
964
+ `First-party BigQuery query supports PostgreSQL date_trunc with ${Object.keys(BIGQUERY_DATE_TRUNC_PARTS).join(", ")}`,
843
965
  );
844
966
  }
845
- return `DATE_TRUNC(CAST(${translatePostgresDateExpression(args[1] ?? "")} AS DATE), WEEK(MONDAY))`;
967
+ return `DATE_TRUNC(CAST(${translatePostgresDateExpression(args[1] ?? "")} AS DATE), ${datePart})`;
846
968
  });
847
969
  translated = rewriteSqlFunctionCalls(translated, "to_char", (args) => {
848
970
  if (args.length !== 2 || !/^'YYYY-MM-DD'$/i.test(args[1] ?? "")) {
849
- throw new Error(
971
+ throw new FirstPartyAnalyticsUnsupportedSqlError(
972
+ `to_char(..., ${args[1] ?? "?"})`,
850
973
  "First-party BigQuery query only supports PostgreSQL to_char(..., 'YYYY-MM-DD') expressions",
851
974
  );
852
975
  }
@@ -854,7 +977,10 @@ function translateFirstPartyAnalyticsBigQuerySql(sql: string): string {
854
977
  });
855
978
  translated = rewriteSqlFunctionCalls(translated, "chr", (args) => {
856
979
  if (args.length !== 1) {
857
- throw new Error("First-party BigQuery query has an invalid chr call");
980
+ throw new FirstPartyAnalyticsUnsupportedSqlError(
981
+ "a multi-argument chr(...) call",
982
+ "First-party BigQuery query has an invalid chr call",
983
+ );
858
984
  }
859
985
  return `CHR(${args[0]})`;
860
986
  });
@@ -864,14 +990,15 @@ function translateFirstPartyAnalyticsBigQuerySql(sql: string): string {
864
990
  translated = rewriteSqlFunctionCalls(translated, "split_part", (args) => {
865
991
  const index = Number(args[2]);
866
992
  if (args.length !== 3 || !Number.isInteger(index) || index < 1) {
867
- throw new Error(
993
+ throw new FirstPartyAnalyticsUnsupportedSqlError(
994
+ "split_part(...) with a non-literal or non-positive index",
868
995
  "First-party BigQuery query only supports split_part with a positive integer index",
869
996
  );
870
997
  }
871
998
  return `SPLIT(${args[0]}, ${args[1]})[SAFE_OFFSET(${index - 1})]`;
872
999
  });
873
1000
  }
874
- translated = rewriteOutsideSqlLiterals(translated, (code) => {
1001
+ translated = rewriteWithMaskedSqlLiterals(translated, (code) => {
875
1002
  let rewritten = replacePostgresCastsInCode(code);
876
1003
  rewritten = rewritten
877
1004
  .replace(/\bnow\s*\(\s*\)/gi, "CURRENT_TIMESTAMP()")
@@ -893,10 +1020,16 @@ function translateFirstPartyAnalyticsBigQuerySql(sql: string): string {
893
1020
  [/\bINTERVAL\s*'/i, "PostgreSQL interval literals"],
894
1021
  [/\bAT\s+TIME\s+ZONE\b/i, "AT TIME ZONE"],
895
1022
  [/\bFILTER\s*\(\s*WHERE\b/i, "FILTER (WHERE ...)"],
1023
+ [/\bDISTINCT\s+ON\b/i, "SELECT DISTINCT ON"],
1024
+ // Anything the JSON translation above could not consume. BigQuery has no
1025
+ // such operators, so leaving it through buys a provider 400 instead of a
1026
+ // rendered explanation.
1027
+ [/->>|->|#>>|@>/, "PostgreSQL JSON operators"],
896
1028
  ];
897
1029
  const incompatible = unsupported.find(([pattern]) => pattern.test(code));
898
1030
  if (incompatible) {
899
- throw new Error(
1031
+ throw new FirstPartyAnalyticsUnsupportedSqlError(
1032
+ incompatible[1],
900
1033
  `First-party analytics query uses unsupported PostgreSQL syntax (${incompatible[1]}) after BigQuery translation`,
901
1034
  );
902
1035
  }
@@ -977,6 +1110,16 @@ function addPartitionPrunedEventDeduplication(
977
1110
  return result;
978
1111
  }
979
1112
 
1113
+ /**
1114
+ * Throws `FirstPartyAnalyticsUnsupportedSqlError` when this SQL has no BigQuery
1115
+ * translation. Save-time validation runs on the panel's own (unscoped) SQL,
1116
+ * which is a subset of what the read path translates, so a pass here cannot
1117
+ * pass a construct through that the read path would then reject.
1118
+ */
1119
+ export function assertFirstPartyAnalyticsBigQuerySql(sql: string): void {
1120
+ translateFirstPartyAnalyticsBigQuerySql(sql);
1121
+ }
1122
+
980
1123
  export function renderFirstPartyAnalyticsBigQuerySql(
981
1124
  scopedSql: string,
982
1125
  args: Array<string | null>,
@@ -10,6 +10,8 @@ import {
10
10
  type DerivedExceptionFields,
11
11
  } from "./error-capture.js";
12
12
  import {
13
+ assertFirstPartyAnalyticsBigQuerySql,
14
+ type FirstPartyAnalyticsSink,
13
15
  getFirstPartyAnalyticsBackend,
14
16
  getFirstPartyAnalyticsTable,
15
17
  insertFirstPartyAnalyticsRows,
@@ -1088,6 +1090,44 @@ function inferSchema(rows: Record<string, unknown>[]): {
1088
1090
  }));
1089
1091
  }
1090
1092
 
1093
+ /**
1094
+ * Which store this query actually runs against under a given sink. Save-time
1095
+ * validation and the read path both route through here: a panel validated for
1096
+ * one store and executed against the other is the whole bug class.
1097
+ */
1098
+ function firstPartyAnalyticsQueryTarget(
1099
+ sql: string,
1100
+ sink: FirstPartyAnalyticsSink,
1101
+ ): "sql-store" | "bigquery" {
1102
+ if (sink !== "bigquery") return "sql-store";
1103
+ const usesSessionRecordings = /\bsession_recordings\b/i.test(sql);
1104
+ const usesEventTables =
1105
+ /\banalytics_events\b|\banalytics_event_daily_rollups\b|\banalytics_user_days\b/i.test(
1106
+ sql,
1107
+ );
1108
+ if (usesSessionRecordings && usesEventTables) {
1109
+ throw new Error(
1110
+ "Cross-backend joins are not supported; query first-party event tables in BigQuery and session_recordings in the Analytics SQL store separately.",
1111
+ );
1112
+ }
1113
+ return usesSessionRecordings ? "sql-store" : "bigquery";
1114
+ }
1115
+
1116
+ /**
1117
+ * Save-time counterpart to `queryFirstPartyAnalytics`. `sink` is a mutable
1118
+ * per-scope setting, so generic PostgreSQL validation alone accepts panels the
1119
+ * live backend cannot execute.
1120
+ */
1121
+ export async function validateFirstPartyAnalyticsSqlForScope(
1122
+ sql: string,
1123
+ scope: AnalyticsScope,
1124
+ ): Promise<void> {
1125
+ validateFirstPartyAnalyticsSql(sql);
1126
+ const backend = await getFirstPartyAnalyticsBackend(scope);
1127
+ if (firstPartyAnalyticsQueryTarget(sql, backend.sink) !== "bigquery") return;
1128
+ assertFirstPartyAnalyticsBigQuerySql(sql);
1129
+ }
1130
+
1091
1131
  export async function queryFirstPartyAnalytics(
1092
1132
  sql: string,
1093
1133
  scope: AnalyticsScope,
@@ -1095,22 +1135,10 @@ export async function queryFirstPartyAnalytics(
1095
1135
  ): Promise<AnalyticsQueryResult> {
1096
1136
  validateFirstPartyAnalyticsSql(sql);
1097
1137
  const backend = await getFirstPartyAnalyticsBackend(scope);
1098
- if (backend.sink === "bigquery") {
1099
- const usesSessionRecordings = /\bsession_recordings\b/i.test(sql);
1100
- const usesEventTables =
1101
- /\banalytics_events\b|\banalytics_event_daily_rollups\b|\banalytics_user_days\b/i.test(
1102
- sql,
1103
- );
1104
- if (usesSessionRecordings && usesEventTables) {
1105
- throw new Error(
1106
- "Cross-backend joins are not supported; query first-party event tables in BigQuery and session_recordings in the Analytics SQL store separately.",
1107
- );
1108
- }
1109
- if (!usesSessionRecordings) {
1110
- const table = await getFirstPartyAnalyticsTable(backend.table);
1111
- const scoped = scopedAnalyticsSql(sql, scope);
1112
- return queryFirstPartyAnalyticsInBigQuery(scoped.sql, scoped.args, table);
1113
- }
1138
+ if (firstPartyAnalyticsQueryTarget(sql, backend.sink) === "bigquery") {
1139
+ const table = await getFirstPartyAnalyticsTable(backend.table);
1140
+ const scoped = scopedAnalyticsSql(sql, scope);
1141
+ return queryFirstPartyAnalyticsInBigQuery(scoped.sql, scoped.args, table);
1114
1142
  }
1115
1143
  const scoped = scopedAnalyticsSql(sql, scope);
1116
1144
  const wrappedSql = `SELECT * FROM (${scoped.sql}) AS first_party_analytics_query LIMIT ${MAX_QUERY_ROWS}`;
@@ -655,6 +655,7 @@ async function* parseJsonlStream(reader, model, captureContext = {}) {
655
655
  const explicitErrMsg = event.error || event.message || event.detail;
656
656
  const errMsg = explicitErrMsg ??
657
657
  `Gateway error (no detail; raw event: ${JSON.stringify(event)})`;
658
+ const gatewayRequestId = typeof event.requestId === "string" ? event.requestId : undefined;
658
659
  const gatewayErrCode = event.errorCode ?? event.code;
659
660
  // The gateway already authenticated this request before streaming,
660
661
  // so a bare "Unauthorized" here means the account cannot use this
@@ -684,7 +685,7 @@ async function* parseJsonlStream(reader, model, captureContext = {}) {
684
685
  // would otherwise do it downstream on the visitor line and
685
686
  // record `unknown` on the credits lane alone.
686
687
  classifyTerminalErrorCode(String(errMsg))));
687
- console.error(`[builder-engine] stop reason=error model=${model} code=${errCode ?? "(none)"} error=${errMsg}`);
688
+ console.error(`[builder-engine] stop reason=error model=${model} code=${errCode ?? "(none)"} requestId=${gatewayRequestId ?? "(none)"} error=${errMsg}`);
688
689
  if (isCredentialAuthError) {
689
690
  await recordBuilderGatewayAuthFailure({
690
691
  code: typeof gatewayErrCode === "string" ? gatewayErrCode : errCode,
@@ -699,9 +700,7 @@ async function* parseJsonlStream(reader, model, captureContext = {}) {
699
700
  // it's thrown, but without these tags.
700
701
  if (!explicitErrMsg) {
701
702
  captureBuilderGatewayNoDetailError({
702
- requestId: typeof event.requestId === "string"
703
- ? event.requestId
704
- : undefined,
703
+ requestId: gatewayRequestId,
705
704
  model,
706
705
  gatewayUrl: captureContext.gatewayUrl,
707
706
  rawEvent: event,
@@ -716,6 +715,10 @@ async function* parseJsonlStream(reader, model, captureContext = {}) {
716
715
  ...(isTransientGatewayFailure(String(errMsg))
717
716
  ? { providerRetryable: true }
718
717
  : {}),
718
+ // requestId rides the stop event whether or not the gateway sent a
719
+ // message: a message like "...ERROR ID: <hex>" is as opaque as no
720
+ // message at all, and this is the only key that reaches upstream.
721
+ ...(gatewayRequestId ? { requestId: gatewayRequestId } : {}),
719
722
  });
720
723
  }
721
724
  else if (reason === "end_turn" ||
@@ -26,6 +26,8 @@ export declare class EngineError extends Error {
26
26
  readonly statusCode?: number;
27
27
  /** Whether the provider explicitly marked this error as retryable. */
28
28
  readonly providerRetryable?: boolean;
29
+ /** Upstream request id, when the provider/gateway supplied one. */
30
+ readonly requestId?: string;
29
31
  /**
30
32
  * Whether the request exceeded the model's context window. Set by engines that
31
33
  * classified the provider's own reply, because the delivered message may not
@@ -39,6 +41,7 @@ export declare class EngineError extends Error {
39
41
  upgradeUrl?: string;
40
42
  statusCode?: number;
41
43
  providerRetryable?: boolean;
44
+ requestId?: string;
42
45
  contextOverflow?: boolean;
43
46
  });
44
47
  }
@@ -196,6 +199,13 @@ export type EngineEvent = {
196
199
  * should retry even if status code / message patterns don't match.
197
200
  */
198
201
  providerRetryable?: boolean;
202
+ /**
203
+ * Upstream request id, when the provider/gateway supplies one. This is
204
+ * the only key that ties a user-facing error back to the upstream log,
205
+ * so it must survive to the capture even when the error also carries a
206
+ * message — an opaque message is not a diagnostic.
207
+ */
208
+ requestId?: string;
199
209
  /**
200
210
  * The request exceeded the model's context window. Carried structurally
201
211
  * for the same reason as `providerRetryable`: `error` is visitor copy on a
@@ -25,6 +25,8 @@ export class EngineError extends Error {
25
25
  statusCode;
26
26
  /** Whether the provider explicitly marked this error as retryable. */
27
27
  providerRetryable;
28
+ /** Upstream request id, when the provider/gateway supplied one. */
29
+ requestId;
28
30
  /**
29
31
  * Whether the request exceeded the model's context window. Set by engines that
30
32
  * classified the provider's own reply, because the delivered message may not
@@ -40,6 +42,7 @@ export class EngineError extends Error {
40
42
  this.upgradeUrl = opts?.upgradeUrl;
41
43
  this.statusCode = opts?.statusCode;
42
44
  this.providerRetryable = opts?.providerRetryable;
45
+ this.requestId = opts?.requestId;
43
46
  this.contextOverflow = opts?.contextOverflow;
44
47
  }
45
48
  }
@@ -3752,6 +3752,7 @@ export async function runAgentLoop(opts) {
3752
3752
  statusCode: event.statusCode,
3753
3753
  providerRetryable: event.providerRetryable,
3754
3754
  contextOverflow: event.contextOverflow,
3755
+ requestId: event.requestId,
3755
3756
  });
3756
3757
  }
3757
3758
  }
@@ -1060,6 +1060,10 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
1060
1060
  let pendingTerminalEvent = null;
1061
1061
  const captureRunError = (error, phase) => {
1062
1062
  const errorCode = getRunErrorCode(error);
1063
+ // A gateway error often arrives as one opaque user-facing sentence, so the
1064
+ // structured fields EngineError already carries are the whole diagnostic.
1065
+ // Dropping them here left operators with an error id and nothing to join on.
1066
+ const engineError = error instanceof EngineError ? error : null;
1063
1067
  captureError(error, {
1064
1068
  route: "/_agent-native/agent-chat",
1065
1069
  aiTraceId: runId,
@@ -1070,6 +1074,10 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
1070
1074
  softTimedOut: softTimedOut ? "true" : "false",
1071
1075
  abortReason: run.abortReason,
1072
1076
  errorCode,
1077
+ gatewayRequestId: engineError?.requestId,
1078
+ statusCode: engineError?.statusCode != null
1079
+ ? String(engineError.statusCode)
1080
+ : undefined,
1073
1081
  },
1074
1082
  extra: {
1075
1083
  runId,
@@ -13,8 +13,8 @@
13
13
  * Body: { json: any, fieldName?: string, type?: "map"|"array", requestSource?: string }
14
14
  */
15
15
  export declare const postCollabJson: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
16
- error: string;
17
16
  ok?: undefined;
17
+ error: string;
18
18
  } | {
19
19
  error?: undefined;
20
20
  ok: boolean;
@@ -17,11 +17,11 @@ declare const _default: import("../../action.js").ActionDefinition<{
17
17
  id?: undefined;
18
18
  provider?: undefined;
19
19
  } | {
20
+ error?: undefined;
20
21
  configured?: undefined;
21
22
  connectPath?: undefined;
22
23
  url: string;
23
24
  id: string;
24
25
  provider: string;
25
- error?: undefined;
26
26
  }>;
27
27
  export default _default;
@@ -41,16 +41,16 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
41
41
  thumbsUpRate: number;
42
42
  avgEvalScore: number;
43
43
  } | {
44
+ error?: undefined;
44
45
  summary: import("./types.js").TraceSummary;
45
46
  spans: import("./types.js").TraceSpan[];
46
47
  id?: undefined;
47
- error?: undefined;
48
48
  ok?: undefined;
49
49
  } | {
50
+ error?: undefined;
50
51
  summary?: undefined;
51
52
  spans?: undefined;
52
53
  id: string;
53
- error?: undefined;
54
54
  ok?: undefined;
55
55
  } | {
56
56
  summary?: undefined;
@@ -59,9 +59,9 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
59
59
  error: any;
60
60
  ok?: undefined;
61
61
  } | {
62
+ error?: undefined;
62
63
  summary?: undefined;
63
64
  spans?: undefined;
64
65
  id?: undefined;
65
66
  ok: boolean;
66
- error?: undefined;
67
67
  }>>;
@@ -75,8 +75,8 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
75
75
  user: "user";
76
76
  }>>;
77
77
  }, z.core.$strip>>, {
78
- id?: undefined;
79
78
  message?: undefined;
79
+ id?: undefined;
80
80
  deleted?: undefined;
81
81
  providers: {
82
82
  id: string;
@@ -93,9 +93,9 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
93
93
  registered?: undefined;
94
94
  label?: undefined;
95
95
  } | {
96
- id?: undefined;
97
96
  message?: undefined;
98
97
  count?: undefined;
98
+ id?: undefined;
99
99
  deleted?: undefined;
100
100
  providers?: undefined;
101
101
  found: boolean;
@@ -48,8 +48,8 @@ export declare function handleUpdateResource(event: any): Promise<import("./stor
48
48
  }>;
49
49
  /** DELETE /_agent-native/resources/:id — delete a resource */
50
50
  export declare function handleDeleteResource(event: any): Promise<{
51
- ok?: undefined;
52
51
  error: string;
52
+ ok?: undefined;
53
53
  } | {
54
54
  error?: undefined;
55
55
  ok: boolean;
@@ -27,10 +27,10 @@ export declare function resolveAgentEngineApiKeyWriteTarget(event: H3Event, scop
27
27
  export declare function createAgentEngineApiKeyHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
28
28
  error: any;
29
29
  } | {
30
+ error?: undefined;
30
31
  ok: boolean;
31
32
  key: string;
32
33
  baseUrlKey?: string;
33
34
  scope: AgentEngineApiKeyScope;
34
- error?: undefined;
35
35
  }>>;
36
36
  export {};
@@ -11,6 +11,7 @@ import { runAutomationSchedulerHealthMigrations } from "../jobs/scheduler-health
11
11
  import { OAUTH_TOKEN_MIGRATIONS, OAUTH_TOKEN_MIGRATIONS_TABLE, } from "../oauth-tokens/migrations.js";
12
12
  import { ORG_MIGRATIONS } from "../org/migrations.js";
13
13
  import { USAGE_ALERT_MIGRATIONS, USAGE_ALERT_MIGRATIONS_TABLE, } from "../usage/migrations.js";
14
+ import { WORKSPACE_CONNECTIONS_MIGRATIONS, WORKSPACE_CONNECTIONS_MIGRATIONS_TABLE, } from "../workspace-connections/migrations.js";
14
15
  import { runBetterAuthMigrations } from "./better-auth-migrations.js";
15
16
  import { IDENTITY_SSO_MIGRATIONS } from "./identity-sso-migrations.js";
16
17
  /**
@@ -53,6 +54,9 @@ export async function runFrameworkReleaseMigrations(nitroApp) {
53
54
  await runMigrations(OBSERVATIONAL_MEMORY_MIGRATIONS, {
54
55
  table: "_observational_memory_migrations",
55
56
  })(nitroApp);
57
+ await runMigrations(WORKSPACE_CONNECTIONS_MIGRATIONS, {
58
+ table: WORKSPACE_CONNECTIONS_MIGRATIONS_TABLE,
59
+ })(nitroApp);
56
60
  await runAutomationRunMigrations(nitroApp);
57
61
  await runAutomationSchedulerHealthMigrations(nitroApp);
58
62
  }
@@ -20,6 +20,6 @@ export declare function createTranscribeVoiceHandler(): import("h3").EventHandle
20
20
  error: string;
21
21
  text?: undefined;
22
22
  } | {
23
- error?: undefined;
24
23
  text: string;
24
+ error?: undefined;
25
25
  }>>;
@@ -0,0 +1,18 @@
1
+ import type { MigrationEntry } from "../db/migrations.js";
2
+ export declare const WORKSPACE_CONNECTIONS_MIGRATIONS_TABLE = "_workspace_connections_migrations";
3
+ /**
4
+ * Deploy-time schema for workspace connections, grants, and user groups.
5
+ *
6
+ * These three tables were shipped with only their runtime `ensureTable`
7
+ * helpers in `store.ts` / `groups.ts`. That is enough locally and on a
8
+ * long-lived server, but `schemaEnsureDisabled()` makes every probe report
9
+ * "present" on a production serverless runtime, so the ensure path issues no
10
+ * DDL there at all. A table with no entry here therefore never gets created in
11
+ * production, and the first read fails with `relation ... does not exist` —
12
+ * which is exactly what `workspace_user_groups` did from the day after it
13
+ * shipped. Runtime ensure covers dev; this list is the production contract.
14
+ *
15
+ * `created_at` / `updated_at` must be BIGINT on Postgres: they store epoch
16
+ * milliseconds, which overflow int4.
17
+ */
18
+ export declare const WORKSPACE_CONNECTIONS_MIGRATIONS: MigrationEntry[];