@agent-native/core 0.219.0 → 0.220.0-nightly-20261023212848

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 (97) hide show
  1. package/dist/action.d.ts +23 -3
  2. package/dist/action.js +58 -3
  3. package/dist/agent/abort-reasons.d.ts +6 -0
  4. package/dist/agent/abort-reasons.js +6 -0
  5. package/dist/agent/execution-fence.js +5 -1
  6. package/dist/agent/production-agent.d.ts +22 -6
  7. package/dist/agent/production-agent.js +136 -37
  8. package/dist/agent/run-manager.js +92 -44
  9. package/dist/agent/run-store.d.ts +1 -0
  10. package/dist/agent/run-store.js +260 -31
  11. package/dist/agent/run-terminal-tracking.d.ts +29 -0
  12. package/dist/agent/run-terminal-tracking.js +56 -0
  13. package/dist/agent/run-turn-id-header.d.ts +9 -0
  14. package/dist/agent/run-turn-id-header.js +13 -0
  15. package/dist/agent/types.d.ts +11 -0
  16. package/dist/chat-threads/store.d.ts +23 -6
  17. package/dist/chat-threads/store.js +119 -36
  18. package/dist/cli/amplify-stream.js +1 -1
  19. package/dist/client/agent-chat/index.d.ts +2 -1
  20. package/dist/client/agent-chat/index.js +2 -1
  21. package/dist/client/agent-chat.d.ts +2 -0
  22. package/dist/client/agent-chat.js +31 -5
  23. package/dist/client/chat/agentkit-agent-native.js +8 -1
  24. package/dist/client/chat/agentkit-protocol.js +7 -1
  25. package/dist/client/chat/run-outcome-telemetry.js +1 -0
  26. package/dist/client/chat/run-outcome.d.ts +5 -0
  27. package/dist/client/chat/runtime.d.ts +34 -1
  28. package/dist/client/chat/runtime.js +96 -12
  29. package/dist/client/chat/turn-submitted-telemetry.d.ts +4 -0
  30. package/dist/client/chat/turn-submitted-telemetry.js +39 -0
  31. package/dist/client/chat/turn-submitted.d.ts +39 -0
  32. package/dist/client/chat/turn-submitted.js +16 -0
  33. package/dist/client/route-chunk-recovery.d.ts +3 -8
  34. package/dist/client/route-chunk-recovery.js +11 -9
  35. package/dist/client/use-chat-thread-list.d.ts +2 -0
  36. package/dist/client/use-chat-thread-list.js +48 -2
  37. package/dist/client/use-chat-threads.d.ts +13 -0
  38. package/dist/client/use-chat-threads.js +59 -4
  39. package/dist/collab/routes.d.ts +2 -2
  40. package/dist/db/client.d.ts +1 -0
  41. package/dist/db/client.js +28 -18
  42. package/dist/db/schema-snapshot.generated.js +5 -0
  43. package/dist/identity/rekey.d.ts +2 -0
  44. package/dist/identity/rekey.js +19 -0
  45. package/dist/index.d.ts +1 -1
  46. package/dist/index.js +1 -1
  47. package/dist/mcp/agent-connected.d.ts +5 -0
  48. package/dist/mcp/agent-connected.js +10 -3
  49. package/dist/mcp/analytics.d.ts +14 -1
  50. package/dist/mcp/analytics.js +54 -2
  51. package/dist/mcp/embed-app.js +2 -1
  52. package/dist/mcp/oauth-route.js +56 -1
  53. package/dist/notifications/routes.d.ts +1 -1
  54. package/dist/observability/routes.d.ts +7 -7
  55. package/dist/observability/traces.d.ts +2 -0
  56. package/dist/observability/traces.js +11 -2
  57. package/dist/provider-api/actions/custom-provider-registration.d.ts +16 -16
  58. package/dist/provider-api/actions/provider-api.d.ts +13 -13
  59. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  60. package/dist/resources/handlers.d.ts +1 -1
  61. package/dist/review/index.d.ts +1 -1
  62. package/dist/review/index.js +1 -1
  63. package/dist/review/suggestions/actions.js +25 -17
  64. package/dist/review/suggestions/store.d.ts +14 -1
  65. package/dist/review/suggestions/store.js +16 -3
  66. package/dist/review/suggestions/types.d.ts +15 -1
  67. package/dist/scripts/chat/search-chats.js +14 -2
  68. package/dist/server/agent-chat/browser-team-tools.js +39 -6
  69. package/dist/server/agent-chat/script-entries.js +44 -9
  70. package/dist/server/agent-chat-plugin.d.ts +2 -0
  71. package/dist/server/agent-chat-plugin.js +66 -16
  72. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  73. package/dist/server/agent-engine-default-model-route.d.ts +1 -1
  74. package/dist/server/first-touch-route.d.ts +2 -2
  75. package/dist/shared/route-chunk-recovery-bootstrap.d.ts +9 -0
  76. package/dist/shared/route-chunk-recovery-bootstrap.js +25 -0
  77. package/dist/sharing/actions/list-resource-shares.d.ts +1 -1
  78. package/dist/templates/chat/.agents/skills/actions/SKILL.md +1 -0
  79. package/dist/templates/default/.agents/skills/actions/SKILL.md +1 -0
  80. package/dist/templates/factory/.agents/skills/review-prs/SKILL.md +27 -3
  81. package/dist/templates/headless/.agents/skills/actions/SKILL.md +1 -0
  82. package/dist/templates/workspace-core/.agents/skills/actions/SKILL.md +1 -0
  83. package/dist/templates/workspace-core/.agents/skills/external-agents/references/mcp-apps-embedding.md +12 -5
  84. package/dist/tracking/providers.js +3 -0
  85. package/dist/triggers/actions/manage-automation.d.ts +2 -2
  86. package/docs/content/external-agents.mdx +1 -1
  87. package/docs/content/mcp-apps.mdx +3 -3
  88. package/docs/content/mcp-protocol.mdx +1 -1
  89. package/docs/content/sharing.mdx +1 -0
  90. package/docs/content/template-assets-integrations.mdx +5 -3
  91. package/package.json +3 -3
  92. package/src/templates/chat/.agents/skills/actions/SKILL.md +1 -0
  93. package/src/templates/default/.agents/skills/actions/SKILL.md +1 -0
  94. package/src/templates/factory/.agents/skills/review-prs/SKILL.md +27 -3
  95. package/src/templates/headless/.agents/skills/actions/SKILL.md +1 -0
  96. package/src/templates/workspace-core/.agents/skills/actions/SKILL.md +1 -0
  97. package/src/templates/workspace-core/.agents/skills/external-agents/references/mcp-apps-embedding.md +12 -5
@@ -53,6 +53,7 @@ export interface ChatThread {
53
53
  updatedAt: number;
54
54
  scope: ChatThreadScope | null;
55
55
  pinnedAt: number | null;
56
+ hiddenAt?: number | null;
56
57
  archivedAt: number | null;
57
58
  source: ChatThreadSource | null;
58
59
  orgId: string | null;
@@ -83,6 +84,7 @@ export interface ChatThreadSummary {
83
84
  updatedAt: number;
84
85
  scope: ChatThreadScope | null;
85
86
  pinnedAt: number | null;
87
+ hiddenAt?: number | null;
86
88
  archivedAt: number | null;
87
89
  source: ChatThreadSource | null;
88
90
  orgId: string | null;
@@ -164,6 +166,8 @@ export interface ListThreadsOptions {
164
166
  * (e.g. an "Archived" filter or restoring one via `setThreadArchived`).
165
167
  */
166
168
  includeArchived?: boolean;
169
+ /** Include chats hidden by this caller. Defaults to false. */
170
+ includeHidden?: boolean;
167
171
  /**
168
172
  * Include connected and other-app threads. The HTTP chat list defaults this
169
173
  * to false so each app shows its own local chats first; internal callers
@@ -184,6 +188,8 @@ export declare function searchThreads(ownerEmail: string, query: string, limit?:
184
188
  orgId?: string | null;
185
189
  /** See `ListThreadsOptions.includeArchived` — defaults to false. */
186
190
  includeArchived?: boolean;
191
+ /** Include chats hidden by this caller. Defaults to false. */
192
+ includeHidden?: boolean;
187
193
  /** See `ListThreadsOptions.includeExternal`. */
188
194
  includeExternal?: boolean;
189
195
  /** Current app id used by the local-only view. */
@@ -211,19 +217,30 @@ export declare function adoptThreadScopeIfUnscoped(id: string, ownerEmail: strin
211
217
  * `null` to clear the scope (chat becomes general).
212
218
  */
213
219
  export declare function setThreadScope(id: string, ownerEmail: string, scope: ChatThreadScope | null): Promise<void>;
214
- export declare function renameThread(id: string, title: string, options?: {
215
- ownerEmail?: string;
220
+ export declare function assertThreadOwner(thread: Pick<ChatThread, "ownerEmail">, ownerEmail: string): void;
221
+ export declare function renameThread(id: string, title: string, options: {
222
+ ownerEmail: string;
216
223
  }): Promise<boolean>;
217
- export declare function setThreadPinned(id: string, pinned: boolean, options?: {
218
- ownerEmail?: string;
224
+ export declare function setThreadPinned(id: string, pinned: boolean, options: {
225
+ ownerEmail: string;
226
+ orgId?: string | null;
219
227
  }): Promise<boolean>;
220
- export declare function setThreadArchived(id: string, archived: boolean, options?: {
221
- ownerEmail?: string;
228
+ export declare function setThreadHidden(id: string, hidden: boolean, options: {
229
+ ownerEmail: string;
230
+ orgId?: string | null;
231
+ }): Promise<boolean>;
232
+ export declare function setThreadArchived(id: string, archived: boolean, options: {
233
+ ownerEmail: string;
222
234
  }): Promise<boolean>;
223
235
  export interface UpdateThreadDataOptions {
224
236
  preserveExistingQueuedMessages?: boolean;
225
237
  preserveExistingTopLevelKeys?: boolean;
226
238
  preserveCurrentMetadata?: boolean;
239
+ /**
240
+ * Preserve the title and its override from each current CAS revision; a
241
+ * blank title still fills from the merged visible prompts.
242
+ */
243
+ preserveCurrentTitle?: boolean;
227
244
  preserveCurrentTitleAndPreview?: boolean;
228
245
  trustedRunSequenceResetIds?: ReadonlySet<string>;
229
246
  onAnnotationConflict?: (conflict: ThreadAnnotationSnapshotConflict) => void;
@@ -13,7 +13,7 @@ import { fromMigrationEntries } from "../db/versioned-store-migrations.js";
13
13
  import { widenIntColumnsToBigInt } from "../db/widen-columns.js";
14
14
  import { getRequestOrgId } from "../server/request-context.js";
15
15
  import { assertNoInlineImageBytes, stripInlineBytes, } from "../shared/inline-bytes.js";
16
- import { hasCurrentOrgMembership, resolveAccess, } from "../sharing/access.js";
16
+ import { ForbiddenError, hasCurrentOrgMembership, resolveAccess, } from "../sharing/access.js";
17
17
  import { registerShareableResource } from "../sharing/registry.js";
18
18
  import { roleSatisfies } from "../sharing/schema.js";
19
19
  import { emitChatThreadChange } from "./emitter.js";
@@ -161,6 +161,17 @@ export const chatThreadsStore = defineStore({
161
161
  },
162
162
  },
163
163
  ...fromMigrationEntries(CHAT_THREAD_SCHEMA_MIGRATIONS_TABLE, CHAT_THREAD_SCHEMA_MIGRATIONS, { data: [3] }),
164
+ {
165
+ name: "viewer-state",
166
+ sql: `CREATE TABLE IF NOT EXISTS chat_thread_viewer_state (
167
+ thread_id TEXT NOT NULL REFERENCES chat_threads(id) ON DELETE CASCADE,
168
+ user_email TEXT NOT NULL,
169
+ pinned_at BIGINT,
170
+ hidden_at BIGINT,
171
+ updated_at BIGINT NOT NULL,
172
+ PRIMARY KEY (thread_id, user_email)
173
+ )`,
174
+ },
164
175
  ],
165
176
  });
166
177
  function ensureTable() {
@@ -632,6 +643,7 @@ function rowToThread(r) {
632
643
  updatedAt: Number(r.updated_at),
633
644
  scope: readScope(r),
634
645
  pinnedAt: readNullableNumber(r.pinned_at),
646
+ hiddenAt: readNullableNumber(r.hidden_at),
635
647
  archivedAt: readNullableNumber(r.archived_at),
636
648
  source: readSource(r),
637
649
  orgId: r.org_id ?? null,
@@ -662,6 +674,7 @@ function rowToSummary(r) {
662
674
  updatedAt: Number(r.updated_at),
663
675
  scope: readScope(r),
664
676
  pinnedAt: readNullableNumber(r.pinned_at),
677
+ hiddenAt: readNullableNumber(r.hidden_at),
665
678
  archivedAt: readNullableNumber(r.archived_at),
666
679
  source: readSource(r),
667
680
  orgId: r.org_id ?? null,
@@ -765,7 +778,21 @@ export async function resolveThreadAccess(userEmail, threadId, minRole = "viewer
765
778
  return null;
766
779
  if (!(await hasThreadAccess(userEmail, threadId, minRole, ctx)))
767
780
  return null;
768
- return await getThread(threadId);
781
+ const thread = await getThread(threadId);
782
+ if (!thread)
783
+ return null;
784
+ if (normalizeAccessEmail(thread.ownerEmail) === normalizeAccessEmail(userEmail)) {
785
+ return { ...thread, hiddenAt: null };
786
+ }
787
+ const { rows } = await getDbExec().execute({
788
+ sql: `SELECT pinned_at, hidden_at FROM chat_thread_viewer_state WHERE thread_id = ? AND user_email = ?`,
789
+ args: [threadId, normalizeAccessEmail(userEmail)],
790
+ });
791
+ return {
792
+ ...thread,
793
+ pinnedAt: readNullableNumber(rows[0]?.pinned_at),
794
+ hiddenAt: readNullableNumber(rows[0]?.hidden_at),
795
+ };
769
796
  }
770
797
  export async function resolveThreadAccessScope(userEmail, threadId, minRole = "viewer", ctx = {}) {
771
798
  if (!userEmail || !threadId)
@@ -792,9 +819,10 @@ export async function resolveThreadsAccess(userEmail, threadIds, ctx = {}) {
792
819
  const access = await chatThreadAccessSql(userEmail, ctx.orgId);
793
820
  const client = getDbExec();
794
821
  const placeholders = ids.map(() => "?").join(", ");
822
+ const projection = chatThreadViewerSelect(THREAD_COLUMNS, userEmail);
795
823
  const { rows } = await client.execute({
796
- sql: `SELECT ${THREAD_COLUMNS} FROM chat_threads WHERE id IN (${placeholders}) AND ${access.sql}`,
797
- args: [...ids, ...access.args],
824
+ sql: `SELECT ${projection.sql} FROM chat_threads WHERE id IN (${placeholders}) AND ${access.sql}`,
825
+ args: [...projection.args, ...ids, ...access.args],
798
826
  });
799
827
  for (const row of rows) {
800
828
  const thread = rowToThread(row);
@@ -948,6 +976,14 @@ async function chatThreadAccessSql(userEmail, orgId) {
948
976
  function normalizeAccessEmail(userEmail) {
949
977
  return userEmail.trim().toLowerCase();
950
978
  }
979
+ function chatThreadViewerSelect(columns, userEmail) {
980
+ const email = normalizeAccessEmail(userEmail);
981
+ const viewerValue = (column) => `(SELECT ${column} FROM chat_thread_viewer_state WHERE thread_id = chat_threads.id AND user_email = ?)`;
982
+ return {
983
+ sql: `${columns.replace("pinned_at", `CASE WHEN LOWER(owner_email) = ? THEN pinned_at ELSE ${viewerValue("pinned_at")} END AS pinned_at`)}, CASE WHEN LOWER(owner_email) = ? THEN NULL ELSE ${viewerValue("hidden_at")} END AS hidden_at`,
984
+ args: [email, email, email, email],
985
+ };
986
+ }
951
987
  /**
952
988
  * Summary projection plus the caller's access basis, derived from the same
953
989
  * owner and per-user-share comparisons the ACL uses. Anything that is neither
@@ -955,9 +991,10 @@ function normalizeAccessEmail(userEmail) {
955
991
  */
956
992
  function chatThreadSummarySelect(userEmail) {
957
993
  const normalizedEmail = normalizeAccessEmail(userEmail);
994
+ const viewer = chatThreadViewerSelect(SUMMARY_COLUMNS, userEmail);
958
995
  return {
959
- sql: `${SUMMARY_COLUMNS}, CASE WHEN LOWER(owner_email) = ? THEN 'owner' WHEN EXISTS (SELECT 1 FROM chat_thread_shares WHERE chat_thread_shares.resource_id = chat_threads.id AND chat_thread_shares.principal_type = 'user' AND LOWER(chat_thread_shares.principal_id) = ?) THEN 'direct' ELSE 'organization' END AS access_basis`,
960
- args: [normalizedEmail, normalizedEmail],
996
+ sql: `${viewer.sql}, CASE WHEN LOWER(owner_email) = ? THEN 'owner' WHEN EXISTS (SELECT 1 FROM chat_thread_shares WHERE chat_thread_shares.resource_id = chat_threads.id AND chat_thread_shares.principal_type = 'user' AND LOWER(chat_thread_shares.principal_id) = ?) THEN 'direct' ELSE 'organization' END AS access_basis`,
997
+ args: [...viewer.args, normalizedEmail, normalizedEmail],
961
998
  };
962
999
  }
963
1000
  function chatThreadOwnershipSql(userEmail, ownership) {
@@ -996,6 +1033,10 @@ export async function listThreads(ownerEmail, options = {}, legacyOffset) {
996
1033
  filters.push(ownershipFilter.sql);
997
1034
  args.push(...ownershipFilter.args);
998
1035
  }
1036
+ if (!opts.includeHidden) {
1037
+ filters.push(`(LOWER(owner_email) = ? OR NOT EXISTS (SELECT 1 FROM chat_thread_viewer_state WHERE thread_id = chat_threads.id AND user_email = ? AND hidden_at IS NOT NULL))`);
1038
+ args.push(normalizeAccessEmail(ownerEmail), normalizeAccessEmail(ownerEmail));
1039
+ }
999
1040
  if (!opts.includeArchived) {
1000
1041
  filters.push(`archived_at IS NULL`);
1001
1042
  }
@@ -1015,7 +1056,7 @@ export async function listThreads(ownerEmail, options = {}, legacyOffset) {
1015
1056
  }
1016
1057
  args.push(limit, offset);
1017
1058
  const { rows } = await client.execute({
1018
- sql: `SELECT ${select.sql} FROM chat_threads WHERE ${filters.join(" AND ")} ORDER BY CASE WHEN pinned_at IS NULL THEN 1 ELSE 0 END, pinned_at DESC, updated_at DESC, id ASC LIMIT ? OFFSET ?`,
1059
+ sql: `SELECT ${select.sql} FROM chat_threads WHERE ${filters.join(" AND ")} ORDER BY pinned_at DESC NULLS LAST, updated_at DESC, id ASC LIMIT ? OFFSET ?`,
1019
1060
  args,
1020
1061
  });
1021
1062
  return rows
@@ -1051,6 +1092,10 @@ export async function searchThreads(ownerEmail, query, limit = 50, options = {})
1051
1092
  filters.push(ownershipFilter.sql);
1052
1093
  args.push(...ownershipFilter.args);
1053
1094
  }
1095
+ if (!options.includeHidden) {
1096
+ filters.push(`(LOWER(owner_email) = ? OR NOT EXISTS (SELECT 1 FROM chat_thread_viewer_state WHERE thread_id = chat_threads.id AND user_email = ? AND hidden_at IS NOT NULL))`);
1097
+ args.push(normalizeAccessEmail(ownerEmail), normalizeAccessEmail(ownerEmail));
1098
+ }
1054
1099
  if (!options.includeArchived) {
1055
1100
  filters.push(`archived_at IS NULL`);
1056
1101
  }
@@ -1067,7 +1112,7 @@ export async function searchThreads(ownerEmail, query, limit = 50, options = {})
1067
1112
  }
1068
1113
  args.push(limit, options.offset ?? 0);
1069
1114
  const { rows } = await client.execute({
1070
- sql: `SELECT ${select.sql} FROM chat_threads WHERE ${filters.join(" AND ")} ORDER BY CASE WHEN pinned_at IS NULL THEN 1 ELSE 0 END, pinned_at DESC, updated_at DESC, id ASC LIMIT ? OFFSET ?`,
1115
+ sql: `SELECT ${select.sql} FROM chat_threads WHERE ${filters.join(" AND ")} ORDER BY pinned_at DESC NULLS LAST, updated_at DESC, id ASC LIMIT ? OFFSET ?`,
1071
1116
  args,
1072
1117
  });
1073
1118
  return rows
@@ -1131,61 +1176,90 @@ export async function setThreadScope(id, ownerEmail, scope) {
1131
1176
  });
1132
1177
  emitChatThreadChange(id);
1133
1178
  }
1134
- export async function renameThread(id, title, options = {}) {
1179
+ export function assertThreadOwner(thread, ownerEmail) {
1180
+ if (!ownerEmail?.trim() ||
1181
+ thread.ownerEmail.toLowerCase() !== ownerEmail.trim().toLowerCase()) {
1182
+ throw new ForbiddenError();
1183
+ }
1184
+ }
1185
+ async function getOwnedThread(id, ownerEmail) {
1186
+ const thread = await getThread(id);
1187
+ if (thread)
1188
+ assertThreadOwner(thread, ownerEmail);
1189
+ return thread;
1190
+ }
1191
+ export async function renameThread(id, title, options) {
1135
1192
  const nextTitle = normalizeThreadTitle(title);
1136
1193
  if (!nextTitle)
1137
1194
  return false;
1138
1195
  return await withThreadDataLock(id, async () => {
1139
- const thread = await getThread(id);
1196
+ const thread = await getOwnedThread(id, options.ownerEmail);
1140
1197
  if (!thread)
1141
1198
  return false;
1142
- if (options.ownerEmail && thread.ownerEmail !== options.ownerEmail) {
1143
- return false;
1144
- }
1145
1199
  const repo = parseThreadData(thread.threadData);
1146
1200
  repo._titleOverride = nextTitle;
1147
- await updateThreadData(id, JSON.stringify(repo), nextTitle, thread.preview, thread.messageCount);
1148
- return true;
1201
+ return await updateThreadData(id, JSON.stringify(repo), nextTitle, thread.preview, thread.messageCount);
1149
1202
  });
1150
1203
  }
1151
- export async function setThreadPinned(id, pinned, options = {}) {
1204
+ async function setThreadViewerState(id, field, enabled, options) {
1205
+ const email = normalizeAccessEmail(options.ownerEmail);
1206
+ const { rows } = await getDbExec().execute({
1207
+ sql: `SELECT id, owner_email FROM chat_threads WHERE id = ?`,
1208
+ args: [id],
1209
+ });
1210
+ const thread = rows[0];
1211
+ if (!thread)
1212
+ return false;
1213
+ if (!email || normalizeAccessEmail(String(thread.owner_email)) === email) {
1214
+ throw new ForbiddenError();
1215
+ }
1216
+ if (!(await hasThreadAccess(email, id, "viewer", {
1217
+ orgId: (options.orgId === undefined ? getRequestOrgId() : options.orgId) ??
1218
+ undefined,
1219
+ }))) {
1220
+ throw new ForbiddenError();
1221
+ }
1222
+ const now = Math.max(Date.now(), 1);
1223
+ await getDbExec().execute({
1224
+ sql: `INSERT INTO chat_thread_viewer_state (thread_id, user_email, ${field}, updated_at)
1225
+ VALUES (?, ?, ?, ?)
1226
+ ON CONFLICT (thread_id, user_email) DO UPDATE SET ${field} = EXCLUDED.${field}, updated_at = EXCLUDED.updated_at`,
1227
+ args: [id, email, enabled ? now : null, now],
1228
+ });
1229
+ emitChatThreadChange(id);
1230
+ return true;
1231
+ }
1232
+ export async function setThreadPinned(id, pinned, options) {
1152
1233
  await ensureTable();
1153
1234
  const client = getDbExec();
1154
1235
  const now = Math.max(Date.now(), 1);
1155
- const args = [pinned ? now : null, id];
1156
- let ownerFilter = "";
1157
- if (options.ownerEmail) {
1158
- ownerFilter = " AND owner_email = ?";
1159
- args.push(options.ownerEmail);
1160
- }
1161
1236
  const result = await client.execute({
1162
- sql: `UPDATE chat_threads SET pinned_at = ? WHERE id = ?${ownerFilter}`,
1163
- args,
1237
+ sql: `UPDATE chat_threads SET pinned_at = ? WHERE id = ? AND LOWER(owner_email) = LOWER(?)`,
1238
+ args: [pinned ? now : null, id, options.ownerEmail.trim()],
1164
1239
  });
1165
1240
  if (result.rowsAffected > 0) {
1166
1241
  emitChatThreadChange(id);
1167
1242
  return true;
1168
1243
  }
1169
- return false;
1244
+ return setThreadViewerState(id, "pinned_at", pinned, options);
1170
1245
  }
1171
- export async function setThreadArchived(id, archived, options = {}) {
1246
+ export async function setThreadHidden(id, hidden, options) {
1247
+ await ensureTable();
1248
+ return setThreadViewerState(id, "hidden_at", hidden, options);
1249
+ }
1250
+ export async function setThreadArchived(id, archived, options) {
1172
1251
  await ensureTable();
1173
1252
  const client = getDbExec();
1174
1253
  const now = Math.max(Date.now(), 1);
1175
- const args = [archived ? now : null, id];
1176
- let ownerFilter = "";
1177
- if (options.ownerEmail) {
1178
- ownerFilter = " AND owner_email = ?";
1179
- args.push(options.ownerEmail);
1180
- }
1181
1254
  const result = await client.execute({
1182
- sql: `UPDATE chat_threads SET archived_at = ? WHERE id = ?${ownerFilter}`,
1183
- args,
1255
+ sql: `UPDATE chat_threads SET archived_at = ? WHERE id = ? AND LOWER(owner_email) = LOWER(?)`,
1256
+ args: [archived ? now : null, id, options.ownerEmail.trim()],
1184
1257
  });
1185
1258
  if (result.rowsAffected > 0) {
1186
1259
  emitChatThreadChange(id);
1187
1260
  return true;
1188
1261
  }
1262
+ await getOwnedThread(id, options.ownerEmail);
1189
1263
  return false;
1190
1264
  }
1191
1265
  export class InlineAttachmentDataNotPersistableError extends Error {
@@ -1238,6 +1312,14 @@ export async function updateThreadData(id, threadData, title, preview, messageCo
1238
1312
  trustedRunSequenceResetIds: options.trustedRunSequenceResetIds,
1239
1313
  onAnnotationConflict: (conflict) => annotationConflicts.push(conflict),
1240
1314
  });
1315
+ if (options.preserveCurrentTitle) {
1316
+ if (Object.prototype.hasOwnProperty.call(safeCurrentThreadData, "_titleOverride")) {
1317
+ merged._titleOverride = safeCurrentThreadData._titleOverride;
1318
+ }
1319
+ else {
1320
+ delete merged._titleOverride;
1321
+ }
1322
+ }
1241
1323
  // Client snapshots can predate their upload URL, so inline bytes become a
1242
1324
  // visible placeholder rather than failing the save; legacy rows are
1243
1325
  // scrubbed on their next write.
@@ -1251,13 +1333,14 @@ export async function updateThreadData(id, threadData, title, preview, messageCo
1251
1333
  const preserveCurrentMetadata = options.preserveCurrentMetadata;
1252
1334
  const preserveCurrentTitleAndPreview = options.preserveCurrentTitleAndPreview;
1253
1335
  const snapshotMetaForBlankFields = !options.preserveCurrentMetadata &&
1254
- options.preserveCurrentTitleAndPreview &&
1336
+ (options.preserveCurrentTitleAndPreview ||
1337
+ options.preserveCurrentTitle) &&
1255
1338
  (!current.title.trim() || !current.preview.trim())
1256
1339
  ? extractThreadMeta(merged)
1257
1340
  : undefined;
1258
1341
  const nextTitle = preserveCurrentMetadata
1259
1342
  ? current.title
1260
- : preserveCurrentTitleAndPreview
1343
+ : preserveCurrentTitleAndPreview || options.preserveCurrentTitle
1261
1344
  ? current.title.trim()
1262
1345
  ? current.title
1263
1346
  : snapshotMetaForBlankFields?.title || current.title
@@ -403,7 +403,7 @@ const FUNCTION_URL_CORS = JSON.stringify({
403
403
  ],
404
404
  AllowMethods: ["POST"],
405
405
  AllowOrigins: ["*"],
406
- ExposeHeaders: ["x-dispatch-mode", "x-run-id"],
406
+ ExposeHeaders: ["x-dispatch-mode", "x-run-id", "x-turn-id"],
407
407
  MaxAge: 600,
408
408
  });
409
409
  const FUNCTION_URL_PERMISSIONS = [
@@ -20,7 +20,7 @@ export { buildDynamicAgentSuggestions, dedupeSuggestions, mergeAgentSuggestions,
20
20
  export { clearChatStorage } from "../chat/storage.js";
21
21
  export { isAssistantChatHistoryVersion, coerceAssistantChatHistoryDate, type AssistantChatHistoryDate, type AssistantChatHistoryVersion, } from "../chat/assistant-chat-history-version.js";
22
22
  export { ChatThreadListConfigError, useChatThreadList, type UseChatThreadListOptions, type UseChatThreadListResult, type ChatThreadListRefreshOutcome, } from "../use-chat-thread-list.js";
23
- export { ChatThreadListError, fetchChatThreadListPage, notifyChatThreadsUpdated, useChatThreads, type ChatThreadAccessBasis, type ChatThreadListPage, type ChatThreadListRow, type ChatThreadListPageRequest, type ChatThreadOwnership, type ChatThreadVisibility, type ChatThreadScope, type ChatThreadSnapshot, type ChatThreadSummary, type ChatThreadData, type ChatThreadShareLink, type ChatThreadShareState, type UseChatThreadsOptions, } from "../use-chat-threads.js";
23
+ export { ChatThreadListError, ChatThreadMutationError, hideChatThread, fetchChatThreadListPage, notifyChatThreadsUpdated, useChatThreads, type ChatThreadAccessBasis, type ChatThreadListPage, type ChatThreadListRow, type ChatThreadListPageRequest, type ChatThreadOwnership, type ChatThreadVisibility, type ChatThreadScope, type ChatThreadSnapshot, type ChatThreadSummary, type ChatThreadData, type ChatThreadShareLink, type ChatThreadShareState, type UseChatThreadsOptions, } from "../use-chat-threads.js";
24
24
  export { type AgentCallProgress, type ContentPart, } from "../sse-event-processor.js";
25
25
  export { useRunStuckDetection, useAbortRun, type RunStuckState, type UseRunStuckDetectionOptions, } from "../use-run-stuck-detection.js";
26
26
  export { AGENT_CHAT_HOME_HANDOFF_TTL_MS, AGENT_CHAT_VIEW_TRANSITION_CLASS, AGENT_CHAT_VIEW_TRANSITION_NAME, consumeAgentChatHomeHandoff, getAgentChatViewTransitionStyle, isAgentChatHomeHandoffActive, markAgentChatHomeHandoff, navigateWithAgentChatViewTransition, startAgentChatViewTransition, supportsAgentChatViewTransition, type AgentChatHomeHandoffOptions, type AgentChatViewTransition, type AgentChatViewTransitionOptions, } from "../chat-view-transition.js";
@@ -28,6 +28,7 @@ export { useAgentChatHomeHandoff, useAgentChatHomeHandoffLinks, type UseAgentCha
28
28
  export { AGENT_SIDEBAR_DEFAULT_MAX_WIDTH, AGENT_SIDEBAR_MIN_WIDTH, AGENT_SIDEBAR_WIDE_WIDTH_RATIO, clampAgentSidebarWidth, consumeAgentSidebarUrlOpenOverride, getAgentSidebarMaxWidth, getAgentSidebarWideWidth, dispatchAgentSidebarStateChange, getInitialAgentSidebarOpen, requestAgentSidebarOpen, subscribeAgentSidebarUrlChanges, SIDEBAR_STATE_CHANGE_EVENT, setAgentSidebarOpenPreference, type AgentSidebarStateChangeDetail, type AgentSidebarStateMode, type AgentSidebarStateSource, } from "../agent-sidebar-state.js";
29
29
  export * from "../chat/agentkit-protocol.js";
30
30
  export { createAgentNativeAgentKitTransport, type CreateAgentNativeAgentKitTransportOptions, } from "../chat/agentkit-agent-native.js";
31
+ export { trackTurnSubmitted } from "../chat/turn-submitted-telemetry.js";
31
32
  export { AGENT_CHAT_RUNNING_EVENT, dispatchAgentChatRunning, resolveAgentChatRunningThreadId, useAgentChatRunningThreads, type AgentChatPresentationPhase, type AgentChatRunningEventDetail, type AgentChatRunningThreadsState, type UseAgentChatRunningThreadsOptions, } from "../use-agent-chat-running-threads.js";
32
33
  export { CHAT_FIRST_APP_LAYOUT_STORAGE_KEY, CHAT_FIRST_DEFAULT_APP_IDS, CHAT_FIRST_SURFACE_TABS_STORAGE_KEY, CHAT_FIRST_SURFACE_PANEL_STORAGE_KEY, CHAT_FIRST_SURFACE_WIDTH_DEFAULT, CHAT_FIRST_SURFACE_WIDTH_MIN, clampChatFirstSurfaceWidth, CHAT_FIRST_MODE_STORAGE_KEY, CHAT_FIRST_MODE_CHANGED_EVENT, CHAT_FIRST_OPEN_APP_EVENT, CHAT_FIRST_OPEN_BROWSER_EVENT, CHAT_FIRST_WATCH_SESSION_EVENT, CHAT_FIRST_SURFACE_CATALOG, chatFirstSurfaceTabId, closeChatFirstSessionWatch, emitChatFirstOpenApp, emitChatFirstOpenBrowser, emitChatFirstSessionWatch, getChatFirstSessionWatchStore, getChatFirstSurfaceTabsStore, getChatFirstSurfacePanelStore, orderChatFirstAppIds, readChatFirstAppLayout, resolveChatFirstAppTarget, resolveChatFirstBrowserTarget, resolveChatFirstSessionId, normalizeChatFirstSessionReference, readChatFirstMode, readChatFirstModeState, readChatFirstSurfaceWidth, subscribeChatFirstOpenApp, subscribeChatFirstOpenBrowser, subscribeChatFirstSessionWatch, useChatFirstSessionWatch, useChatFirstSurfaceResize, useChatFirstSurfacePanel, useChatFirstSurfaceTabs, writeChatFirstAppLayout, writeChatFirstMode, writeChatFirstSurfaceWidth, type ChatFirstAppLayoutPreference, type ChatFirstModeReadResult, type ChatFirstAppRegistration, type ChatFirstAppResolution, type ChatFirstAppTarget, type ChatFirstAppSurfacePlacement, type ChatFirstAgentActivity, type ChatFirstAgentActivityStatus, type ChatFirstOpenAppDelivery, type ChatFirstOpenAppDetail, type ChatFirstOpenBrowserDetail, type ChatFirstBrowserResolution, type ChatFirstSessionKind, type ChatFirstSessionReference, type ChatFirstSessionWatchDelivery, type ChatFirstSessionWatchState, type ChatFirstSessionWatchStore, type ChatFirstSurfaceKind, type ChatFirstSurfaceTab, type ChatFirstSurfacePanelState, type ChatFirstSurfacePanelStore, type ChatFirstSurfaceTabsState, type ChatFirstSurfaceTabsStore, } from "../chat-first-state.js";
33
34
  export { useVoiceProviderStatus } from "../voice-provider-status.js";
@@ -20,13 +20,14 @@ export { buildDynamicAgentSuggestions, dedupeSuggestions, mergeAgentSuggestions,
20
20
  export { clearChatStorage } from "../chat/storage.js";
21
21
  export { isAssistantChatHistoryVersion, coerceAssistantChatHistoryDate, } from "../chat/assistant-chat-history-version.js";
22
22
  export { ChatThreadListConfigError, useChatThreadList, } from "../use-chat-thread-list.js";
23
- export { ChatThreadListError, fetchChatThreadListPage, notifyChatThreadsUpdated, useChatThreads, } from "../use-chat-threads.js";
23
+ export { ChatThreadListError, ChatThreadMutationError, hideChatThread, fetchChatThreadListPage, notifyChatThreadsUpdated, useChatThreads, } from "../use-chat-threads.js";
24
24
  export { useRunStuckDetection, useAbortRun, } from "../use-run-stuck-detection.js";
25
25
  export { AGENT_CHAT_HOME_HANDOFF_TTL_MS, AGENT_CHAT_VIEW_TRANSITION_CLASS, AGENT_CHAT_VIEW_TRANSITION_NAME, consumeAgentChatHomeHandoff, getAgentChatViewTransitionStyle, isAgentChatHomeHandoffActive, markAgentChatHomeHandoff, navigateWithAgentChatViewTransition, startAgentChatViewTransition, supportsAgentChatViewTransition, } from "../chat-view-transition.js";
26
26
  export { useAgentChatHomeHandoff, useAgentChatHomeHandoffLinks, } from "../use-agent-chat-home-handoff.js";
27
27
  export { AGENT_SIDEBAR_DEFAULT_MAX_WIDTH, AGENT_SIDEBAR_MIN_WIDTH, AGENT_SIDEBAR_WIDE_WIDTH_RATIO, clampAgentSidebarWidth, consumeAgentSidebarUrlOpenOverride, getAgentSidebarMaxWidth, getAgentSidebarWideWidth, dispatchAgentSidebarStateChange, getInitialAgentSidebarOpen, requestAgentSidebarOpen, subscribeAgentSidebarUrlChanges, SIDEBAR_STATE_CHANGE_EVENT, setAgentSidebarOpenPreference, } from "../agent-sidebar-state.js";
28
28
  export * from "../chat/agentkit-protocol.js";
29
29
  export { createAgentNativeAgentKitTransport, } from "../chat/agentkit-agent-native.js";
30
+ export { trackTurnSubmitted } from "../chat/turn-submitted-telemetry.js";
30
31
  export { AGENT_CHAT_RUNNING_EVENT, dispatchAgentChatRunning, resolveAgentChatRunningThreadId, useAgentChatRunningThreads, } from "../use-agent-chat-running-threads.js";
31
32
  export { CHAT_FIRST_APP_LAYOUT_STORAGE_KEY, CHAT_FIRST_DEFAULT_APP_IDS, CHAT_FIRST_SURFACE_TABS_STORAGE_KEY, CHAT_FIRST_SURFACE_PANEL_STORAGE_KEY, CHAT_FIRST_SURFACE_WIDTH_DEFAULT, CHAT_FIRST_SURFACE_WIDTH_MIN, clampChatFirstSurfaceWidth, CHAT_FIRST_MODE_STORAGE_KEY, CHAT_FIRST_MODE_CHANGED_EVENT, CHAT_FIRST_OPEN_APP_EVENT, CHAT_FIRST_OPEN_BROWSER_EVENT, CHAT_FIRST_WATCH_SESSION_EVENT, CHAT_FIRST_SURFACE_CATALOG, chatFirstSurfaceTabId, closeChatFirstSessionWatch, emitChatFirstOpenApp, emitChatFirstOpenBrowser, emitChatFirstSessionWatch, getChatFirstSessionWatchStore, getChatFirstSurfaceTabsStore, getChatFirstSurfacePanelStore, orderChatFirstAppIds, readChatFirstAppLayout, resolveChatFirstAppTarget, resolveChatFirstBrowserTarget, resolveChatFirstSessionId, normalizeChatFirstSessionReference, readChatFirstMode, readChatFirstModeState, readChatFirstSurfaceWidth, subscribeChatFirstOpenApp, subscribeChatFirstOpenBrowser, subscribeChatFirstSessionWatch, useChatFirstSessionWatch, useChatFirstSurfaceResize, useChatFirstSurfacePanel, useChatFirstSurfaceTabs, writeChatFirstAppLayout, writeChatFirstMode, writeChatFirstSurfaceWidth, } from "../chat-first-state.js";
32
33
  export { useVoiceProviderStatus } from "../voice-provider-status.js";
@@ -182,6 +182,8 @@ export interface ParsedSubmitChat {
182
182
  submissionExpiresAt?: number;
183
183
  usageLabel?: string;
184
184
  approvedToolCalls?: string[];
185
+ /** The sender's `preset`, reported as the turn's analytics surface. */
186
+ surface?: string;
185
187
  }
186
188
  export declare function parseSubmitChatMessage(event: MessageEvent): ParsedSubmitChat | null;
187
189
  export declare function routesToCodeFrame(opts: Pick<AgentChatMessage, "type" | "requiresCode" | "approvedToolCalls">): boolean;
@@ -4,6 +4,8 @@ import { trackEvent } from "./analytics.js";
4
4
  import { agentNativePath } from "./api-path.js";
5
5
  import { readClientAppState, writeClientAppState, } from "./application-state.js";
6
6
  import { isInBuilderFrame, isTrustedBuilderMessage, sendToBuilderChat, } from "./builder-frame.js";
7
+ import { trackRoutedTurnSubmitted } from "./chat/turn-submitted-telemetry.js";
8
+ import { DEFAULT_TURN_SURFACE } from "./chat/turn-submitted.js";
7
9
  import { isEmbedAuthActive, isEmbedMcpChatBridgeActive, markEmbedMcpChatBridgeActive, readEmbedMcpChatBridgeFlagFromUrl, } from "./embed-auth.js";
8
10
  import { getFramePostMessageTargetOrigin, isTrustedFrameMessage, } from "./frame.js";
9
11
  import { isOpenAiMcpAppHost, sendMcpAppHostMessage, } from "./mcp-app-host.js";
@@ -782,6 +784,7 @@ export function parseSubmitChatMessage(event) {
782
784
  : {}),
783
785
  usageLabel: nonEmptyString(raw.usageLabel),
784
786
  approvedToolCalls: parseSubmitChatApprovedToolCalls(raw.approvedToolCalls),
787
+ surface: nonEmptyString(raw.preset),
785
788
  };
786
789
  }
787
790
  function normalizeStoredAgentChatExecMode(value) {
@@ -826,6 +829,22 @@ export function routesToCodeFrame(opts) {
826
829
  return false;
827
830
  return !keepsApprovalInAppChat(opts);
828
831
  }
832
+ /** A send that delivers a non-blank prompt; only these count as a turn. */
833
+ function submitsMessage(opts) {
834
+ return opts.submit !== false && !!opts.message.trim();
835
+ }
836
+ /** A submitted prompt that leaves this page for Builder or an MCP host. */
837
+ function trackRoutedSend(opts, routedTo, submitMessageId, requestMode) {
838
+ // Typed as a string, but JS callers reach this too; a tracker never throws.
839
+ if (typeof opts.message !== "string" || !submitsMessage(opts))
840
+ return;
841
+ trackRoutedTurnSubmitted({
842
+ routedTo,
843
+ surface: opts.preset ?? DEFAULT_TURN_SURFACE,
844
+ submitMessageId,
845
+ requestMode,
846
+ });
847
+ }
829
848
  function sendToAgentChatInternal(opts, forceMcpAppWrapperRelay = false) {
830
849
  const tabId = opts.tabId ?? generateTabId();
831
850
  const actionScope = opts.actionScope === undefined
@@ -841,7 +860,7 @@ function sendToAgentChatInternal(opts, forceMcpAppWrapperRelay = false) {
841
860
  (mcpAppLocalPayload && !routesLocalToChatGpt);
842
861
  const requestMode = normalizeAgentChatRequestMode(opts.requestMode ?? opts.mode) ??
843
862
  readStoredAgentChatRequestMode();
844
- if (opts.submit !== false && opts.message.trim()) {
863
+ if (submitsMessage(opts)) {
845
864
  trackEvent("app.first_action", {
846
865
  action: "chat_submit",
847
866
  surface: opts.preset ?? "chat",
@@ -851,6 +870,7 @@ function sendToAgentChatInternal(opts, forceMcpAppWrapperRelay = false) {
851
870
  });
852
871
  }
853
872
  if (isCodeRequest && isInBuilderFrame()) {
873
+ trackRoutedSend(opts, "builder", opts.submitMessageId, requestMode);
854
874
  sendToBuilderChat({
855
875
  message: opts.message,
856
876
  context: opts.context,
@@ -872,10 +892,12 @@ function sendToAgentChatInternal(opts, forceMcpAppWrapperRelay = false) {
872
892
  },
873
893
  };
874
894
  if (forceMcpAppWrapperRelay && mcpBridgeEnabled) {
895
+ trackRoutedSend(opts, "mcp_host", submitMessageId, requestMode);
875
896
  window.parent.postMessage(payload, getFramePostMessageTargetOrigin() || "*");
876
897
  return tabId;
877
898
  }
878
899
  if (opts.submit !== false && !localChatTarget && mcpBridgeEnabled) {
900
+ trackRoutedSend(opts, "mcp_host", submitMessageId, requestMode);
879
901
  if (opts.targetTabId) {
880
902
  window.parent.postMessage(payload, getFramePostMessageTargetOrigin() || "*");
881
903
  return tabId;
@@ -1004,13 +1026,17 @@ export function sendToAgentChatAndConfirm(opts, options) {
1004
1026
  return confirmAgentChatSubmit(opts, options, tabId, (submission) => sendToAgentChatInternal(submission, true));
1005
1027
  }
1006
1028
  return Promise.resolve(hostDelivery)
1007
- .then((delivered) => delivered === false
1008
- ? confirmAgentChatSubmit(opts, options, tabId, (submission) => sendToAgentChatInternal(submission, true))
1009
- : {
1029
+ .then((delivered) => {
1030
+ if (delivered === false) {
1031
+ return confirmAgentChatSubmit(opts, options, tabId, (submission) => sendToAgentChatInternal(submission, true));
1032
+ }
1033
+ trackRoutedSend(opts, "mcp_host", options?.submitMessageId, requestMode);
1034
+ return {
1010
1035
  tabId,
1011
1036
  delivered: delivered === true,
1012
1037
  ...(delivered === true ? {} : { reason: "host-unconfirmed" }),
1013
- })
1038
+ };
1039
+ })
1014
1040
  .catch(() => ({ tabId, delivered: false, reason: "host-rejected" }));
1015
1041
  }
1016
1042
  return confirmAgentChatSubmit(opts, options, tabId, sendToAgentChat);
@@ -14,6 +14,7 @@ import { AGENT_NATIVE_PROTOCOL_METADATA_KEY, createAgentKitProtocolAdapter, } fr
14
14
  import { trackRunFeedback, trackRunOutcome } from "./run-outcome-telemetry.js";
15
15
  import { runOutcomeForCode } from "./run-outcome.js";
16
16
  import { createAgentNativeChatRuntime, isAgentNativeChatRuntime, } from "./runtime.js";
17
+ import { trackTurnSubmitted } from "./turn-submitted-telemetry.js";
17
18
  const RUN_SLOT_POLL_INTERVAL_MS = 150;
18
19
  const RUN_SLOT_STABLE_POLLS = 2;
19
20
  const SAFE_BOUNDARY_POLL_INTERVAL_MS = 500;
@@ -3386,7 +3387,10 @@ export function createAgentNativeAgentKitTransport(options = {}) {
3386
3387
  Object.assign(error, { code: "queue_item_busy", retryable: true });
3387
3388
  throw error;
3388
3389
  }
3389
- const runtime = options.runtime ?? createAgentNativeChatRuntime(options);
3390
+ const runtime = options.runtime ??
3391
+ createAgentNativeChatRuntime(options, {
3392
+ onTurnSubmitted: trackTurnSubmitted,
3393
+ });
3390
3394
  const feedbackUrl = options.feedbackUrl ??
3391
3395
  agentNativePath("/_agent-native/observability/feedback");
3392
3396
  const onRunOutcome = options.adapter?.onRunOutcome ?? trackRunOutcome;
@@ -3827,6 +3831,9 @@ export function createAgentNativeAgentKitTransport(options = {}) {
3827
3831
  onRunOutcome({
3828
3832
  runId: record.turnId,
3829
3833
  threadId,
3834
+ ...(typeof record.requestTurnId === "string"
3835
+ ? { turnId: record.requestTurnId }
3836
+ : {}),
3830
3837
  outcome: runOutcomeForCode(code),
3831
3838
  ...(code ? { code } : {}),
3832
3839
  ...(error instanceof Error && error.message
@@ -1804,6 +1804,7 @@ export function createAgentKitProtocolAdapter(runtime, options = {}) {
1804
1804
  options.onRunOutcome({
1805
1805
  runId: run.turn.runId ?? run.runId,
1806
1806
  threadId: run.threadId,
1807
+ ...(run.turn.requestTurnId ? { turnId: run.turn.requestTurnId } : {}),
1807
1808
  outcome: event.type === "run.completed"
1808
1809
  ? "succeeded"
1809
1810
  : event.type === "run.cancelled"
@@ -3005,7 +3006,12 @@ export function createAgentKitProtocolAdapter(runtime, options = {}) {
3005
3006
  run.quietAuthorityReads = 0;
3006
3007
  run.successorWaitStartedAtMs = undefined;
3007
3008
  }
3008
- run.turn = resumed;
3009
+ // A resumed stream is the same turn; only the response that started it
3010
+ // carries the turn the server stored the run under.
3011
+ run.turn =
3012
+ resumed.requestTurnId || !run.turn.requestTurnId
3013
+ ? resumed
3014
+ : { ...resumed, requestTurnId: run.turn.requestTurnId };
3009
3015
  indexRun(run, resumed.runId);
3010
3016
  }
3011
3017
  function ensurePump(run) {
@@ -80,6 +80,7 @@ export function trackRunOutcome(report, send = trackEvent, random = Math.random)
80
80
  drain_attempts: report.drainAttempts,
81
81
  run_id: report.runId,
82
82
  thread_id: report.threadId,
83
+ ...(report.turnId ? { turn_id: report.turnId } : {}),
83
84
  sample_rate: rate,
84
85
  sample_weight: 1 / rate,
85
86
  });
@@ -41,6 +41,11 @@ export type RunTerminalSource = "stream" | "authority" | "pipe" | "local";
41
41
  export interface RunOutcomeReport {
42
42
  readonly runId: string;
43
43
  readonly threadId: string;
44
+ /**
45
+ * The turn the server stored the run under; absent when this page did not
46
+ * start it, or for a replay whose turn the server did not name.
47
+ */
48
+ readonly turnId?: string;
44
49
  readonly outcome: RunOutcome;
45
50
  /** The legacy code that named this outcome one incident at a time. */
46
51
  readonly code?: string;
@@ -3,6 +3,7 @@ import type { ActionChatUIConfig } from "../../action-ui.js";
3
3
  import type { AgentMcpAppPayload } from "../../mcp-client/app-result.js";
4
4
  import type { ReasoningEffort } from "../../shared/reasoning-effort.js";
5
5
  import { type ServerRunState } from "./run-outcome.js";
6
+ import { type TurnSubmittedReport } from "./turn-submitted.js";
6
7
  export type { ServerRunState } from "./run-outcome.js";
7
8
  export type AgentChatRuntimeId = string;
8
9
  export type AgentChatRuntimeSessionId = string;
@@ -549,6 +550,13 @@ export type AgentChatRuntimeKnownEvent = AgentChatRuntimeMessageStartEvent | Age
549
550
  export type AgentChatRuntimeEvent<TCustomEvent extends AgentChatRuntimeCustomEvent = never> = AgentChatRuntimeKnownEvent | TCustomEvent;
550
551
  export interface AgentChatRuntimeTurn<TEvent extends AgentChatRuntimeEventBase = AgentChatRuntimeKnownEvent> {
551
552
  readonly id?: AgentChatRuntimeTurnId;
553
+ /**
554
+ * The turn the server stored the run under (its `X-Turn-Id`), else the one
555
+ * the request carried; absent for a replay the server did not name the turn
556
+ * of. A continuation sends the turn it continues, and an approval resume
557
+ * runs under its pending approval's turn, so this is not always `id`.
558
+ */
559
+ readonly requestTurnId?: AgentChatRuntimeTurnId;
552
560
  readonly sessionId: AgentChatRuntimeSessionId;
553
561
  readonly runId?: string;
554
562
  readonly metadata?: AgentChatRuntimeMetadata;
@@ -636,7 +644,25 @@ export interface CreateHttpAgentChatRuntimeOptions<TEvent extends AgentChatRunti
636
644
  session: AgentChatRuntimeSessionSummary;
637
645
  turn: AgentChatRuntimeTurnInput;
638
646
  turnId: AgentChatRuntimeTurnId;
647
+ requestTurnId: AgentChatRuntimeTurnId;
639
648
  }) => unknown;
649
+ /** The turn id a turn's request carries; the runtime's own id by default. */
650
+ readonly requestTurnId?: (input: {
651
+ turn: AgentChatRuntimeTurnInput;
652
+ turnId: AgentChatRuntimeTurnId;
653
+ }) => AgentChatRuntimeTurnId;
654
+ /**
655
+ * Told once the endpoint accepts a turn, before its events are read. It is
656
+ * not awaited and anything it throws is ignored.
657
+ */
658
+ readonly onTurnAccepted?: (input: {
659
+ session: AgentChatRuntimeSessionSummary;
660
+ turn: AgentChatRuntimeTurnInput;
661
+ turnId: AgentChatRuntimeTurnId;
662
+ /** The turn the server stored the run under, as on the returned turn. */
663
+ requestTurnId: AgentChatRuntimeTurnId;
664
+ runId: string;
665
+ }) => void;
640
666
  /** Called at the final client boundary before a new turn reaches the endpoint. */
641
667
  readonly beforeStartTurn?: (input: {
642
668
  session: AgentChatRuntimeSessionSummary;
@@ -682,5 +708,12 @@ export interface CreateAgentNativeChatRuntimeOptions {
682
708
  export declare function createHttpAgentChatRuntime<TEvent extends AgentChatRuntimeEventBase = AgentChatRuntimeKnownEvent>(options: CreateHttpAgentChatRuntimeOptions<TEvent>): AgentChatRuntime<TEvent>;
683
709
  export declare function loadedSkillSlugsFromMessages(messages: readonly AgentChatRuntimeMessage[] | undefined): string[];
684
710
  export declare const AGENT_CALL_FAILURE_REASON_KEYS: readonly string[];
685
- export declare function createAgentNativeChatRuntime(options?: CreateAgentNativeChatRuntimeOptions): AgentChatRuntime<AgentChatRuntimeKnownEvent>;
711
+ /**
712
+ * Where the runtime reports telemetry. It is injected rather than imported so
713
+ * the runtime stays loadable without the browser analytics module.
714
+ */
715
+ export interface AgentNativeChatRuntimeTelemetry {
716
+ readonly onTurnSubmitted?: (report: TurnSubmittedReport) => void;
717
+ }
718
+ export declare function createAgentNativeChatRuntime(options?: CreateAgentNativeChatRuntimeOptions, telemetry?: AgentNativeChatRuntimeTelemetry): AgentChatRuntime<AgentChatRuntimeKnownEvent>;
686
719
  export declare function isAgentNativeChatRuntime(runtime: AgentChatRuntime | undefined): boolean;