@agent-native/core 0.161.1 → 0.161.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 (44) 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/corpus/templates/clips/actions/save-browser-transcript.ts +20 -4
  12. package/corpus/templates/clips/app/components/meetings/transcript-bubbles.tsx +167 -35
  13. package/corpus/templates/clips/desktop/src/lib/transcription-capture.ts +8 -1
  14. package/corpus/templates/clips/desktop/src/lib/transcription-engine.ts +39 -2
  15. package/corpus/templates/slides/actions/list-decks.ts +36 -1
  16. package/corpus/templates/slides/app/context/DeckContext.tsx +17 -6
  17. package/dist/agent/engine/builder-engine.js +37 -19
  18. package/dist/agent/engine/types.d.ts +29 -0
  19. package/dist/agent/engine/types.js +6 -0
  20. package/dist/agent/production-agent.js +2 -0
  21. package/dist/agent/run-manager.d.ts +6 -0
  22. package/dist/agent/run-manager.js +27 -0
  23. package/dist/agent/run-store.d.ts +5 -5
  24. package/dist/agent/run-store.js +52 -15
  25. package/dist/client/AssistantChat.d.ts +1 -0
  26. package/dist/client/AssistantChat.js +10 -1
  27. package/dist/db/client.js +10 -2
  28. package/dist/db/create-get-db.js +42 -0
  29. package/dist/observability/routes.d.ts +3 -3
  30. package/dist/provider-api/actions/custom-provider-registration.d.ts +2 -2
  31. package/dist/resources/handlers.d.ts +1 -1
  32. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  33. package/dist/server/onboarding-html.js +22 -77
  34. package/dist/server/poll.d.ts +5 -5
  35. package/dist/server/poll.js +19 -26
  36. package/dist/server/release-migrations.js +4 -0
  37. package/dist/server/transcribe-voice.d.ts +1 -1
  38. package/dist/shared/auth-copy.d.ts +7 -0
  39. package/dist/shared/auth-copy.js +77 -0
  40. package/dist/shared/mcp-embed-headers.js +8 -4
  41. package/dist/vite/client.js +94 -3
  42. package/dist/workspace-connections/migrations.d.ts +18 -0
  43. package/dist/workspace-connections/migrations.js +153 -0
  44. package/package.json +1 -1
@@ -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}`;
@@ -26,8 +26,19 @@ import { buildCaptionSegmentsFromText } from "../shared/transcript-segments.js";
26
26
  import { booleanParam } from "./lib/cli-params.js";
27
27
  import { isAutoTitleReplaceable } from "./lib/title-source.js";
28
28
 
29
- function nativeSegmentsJson(fullText: string): string {
30
- return JSON.stringify(buildCaptionSegmentsFromText(fullText));
29
+ // web-speech and macos-native are both mic-only engines — see
30
+ // transcription-engine.ts's file header. When a caller sends fullText with
31
+ // no segments (word-level timings were never captured), there's no
32
+ // per-line source to preserve, but for these two engines there's also no
33
+ // ambiguity: every word came from the mic. Leaving source undefined here
34
+ // falls through to resolveSpeaker's default and renders the whole thing as
35
+ // "Them". Whisper mixes mic + system, so it has no safe single-speaker guess.
36
+ function nativeSegmentsJson(
37
+ fullText: string,
38
+ engineSource?: "web-speech" | "macos-native" | "whisper",
39
+ ): string {
40
+ const source = engineSource && engineSource !== "whisper" ? "mic" : undefined;
41
+ return JSON.stringify(buildCaptionSegmentsFromText(fullText, null, source));
31
42
  }
32
43
 
33
44
  // Real transcript segments supplied by a caller that already has accurate
@@ -45,6 +56,11 @@ const segmentSchema = z
45
56
  text: z.string(),
46
57
  // Stream the segment came from; the transcript UI maps mic→"Me", system→"Them".
47
58
  source: z.enum(["mic", "system"]).optional(),
59
+ // Diarized speaker for this segment, when the provider identifies one.
60
+ // Declared so zod keeps it: an undeclared key is stripped before the array
61
+ // is serialized, which would drop a provider's speaker labels on save and
62
+ // leave the transcript unable to tell its speakers apart on reload.
63
+ speaker: z.string().nullable().optional(),
48
64
  })
49
65
  .transform((s) => {
50
66
  if (s.endMs < s.startMs) {
@@ -74,7 +90,7 @@ export default defineAction({
74
90
  .array(segmentSchema)
75
91
  .optional()
76
92
  .describe(
77
- "Real transcript segments with accurate timestamps (ms). When provided, stored verbatim instead of synthesizing timings from fullText.",
93
+ "Transcript segments with per-segment timings (ms) and the `mic`/`system` stream each came from. Stored verbatim when provided, instead of synthesizing timings from fullText. Timings are the engine's own where it reported them; the mic-only engines report none, so callers may send estimates to keep each segment's speaker.",
78
94
  ),
79
95
  overwriteReady: booleanParam
80
96
  .default(false)
@@ -97,7 +113,7 @@ export default defineAction({
97
113
  const segmentsJson =
98
114
  args.segments && args.segments.length > 0
99
115
  ? JSON.stringify(args.segments)
100
- : nativeSegmentsJson(fullText);
116
+ : nativeSegmentsJson(fullText, args.source);
101
117
 
102
118
  const [current] = await db
103
119
  .select({