@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
@@ -122,6 +122,8 @@ async function requestChatThreadListPage(request) {
122
122
  params.set("includeExternal", "1");
123
123
  if (request.includeArchived)
124
124
  params.set("includeArchived", "1");
125
+ if (request.includeHidden)
126
+ params.set("includeHidden", "1");
125
127
  appendChatThreadScopeParams(params, request.scope);
126
128
  const search = params.toString();
127
129
  const res = await fetch(`${apiUrl}/threads${search ? `?${search}` : ""}`);
@@ -155,7 +157,27 @@ export async function fetchChatThreadListPage(request = {}) {
155
157
  hasMore: page.hasMore,
156
158
  };
157
159
  }
158
- async function fetchThreadListPage(apiUrl, offset, includeExternal, scope, ownership) {
160
+ export class ChatThreadMutationError extends Error {
161
+ status;
162
+ constructor(status) {
163
+ super(`Chat thread mutation failed (HTTP ${status}).`);
164
+ this.status = status;
165
+ this.name = "ChatThreadMutationError";
166
+ }
167
+ }
168
+ export async function hideChatThread(threadId, hidden, options = {}) {
169
+ const apiUrl = options.apiUrl ?? agentNativePath("/_agent-native/agent-chat");
170
+ const response = await fetch(withChatThreadScope(`${apiUrl}/threads/${encodeURIComponent(threadId)}/hide`, options.scope), {
171
+ method: "POST",
172
+ headers: { "Content-Type": "application/json" },
173
+ body: JSON.stringify({ hidden }),
174
+ });
175
+ if (!response.ok)
176
+ throw new ChatThreadMutationError(response.status);
177
+ notifyChatThreadsUpdated();
178
+ return true;
179
+ }
180
+ async function fetchThreadListPage(apiUrl, offset, includeExternal, scope, ownership, includeHidden = false) {
159
181
  // The lifecycle list keeps optimistic local drafts beside server rows, so it
160
182
  // tolerates summaries without ownership metadata; the ownership-aware lists
161
183
  // go through `fetchChatThreadListPage`, which requires it.
@@ -166,6 +188,7 @@ async function fetchThreadListPage(apiUrl, offset, includeExternal, scope, owner
166
188
  includeExternal,
167
189
  scope,
168
190
  ownership,
191
+ includeHidden,
169
192
  });
170
193
  return page.threads;
171
194
  }
@@ -355,6 +378,7 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
355
378
  const autoCreate = options?.autoCreate !== false;
356
379
  const restoreActiveThread = options?.restoreActiveThread !== false;
357
380
  const includeExternal = options?.includeExternal === true;
381
+ const includeHidden = options?.includeHidden === true;
358
382
  const ownership = options?.ownership;
359
383
  const isolateHistoryByScope = options?.isolateHistoryByScope === true;
360
384
  const browserTabId = options?.browserTabId ??
@@ -774,7 +798,7 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
774
798
  let loadedThreads;
775
799
  try {
776
800
  const offset = options?.append ? nextThreadsOffsetRef.current : 0;
777
- const loaded = await fetchThreadListPage(apiUrl, offset, includeExternal, historyScope, ownership);
801
+ const loaded = await fetchThreadListPage(apiUrl, offset, includeExternal, historyScope, ownership, includeHidden);
778
802
  if (requestId !== latestFetchRequestRef.current)
779
803
  return undefined;
780
804
  if (!loaded) {
@@ -958,6 +982,7 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
958
982
  historyScope,
959
983
  historyScopeKey,
960
984
  includeExternal,
985
+ includeHidden,
961
986
  isolateHistory,
962
987
  ownership,
963
988
  routeControlsActiveThread,
@@ -1232,6 +1257,25 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
1232
1257
  return false;
1233
1258
  }
1234
1259
  }, [apiUrl, fetchThreads, historyScope]);
1260
+ const hideThread = useCallback(async (threadId, hidden) => {
1261
+ const previous = threadsRef.current.find((thread) => thread.id === threadId);
1262
+ setThreads((threads) => threads.map((thread) => thread.id === threadId
1263
+ ? { ...thread, hiddenAt: hidden ? Date.now() : null }
1264
+ : thread));
1265
+ try {
1266
+ return await hideChatThread(threadId, hidden, {
1267
+ apiUrl,
1268
+ scope: historyScope,
1269
+ });
1270
+ }
1271
+ catch (error) {
1272
+ if (previous)
1273
+ setThreads((threads) => threads.map((thread) => thread.id === threadId
1274
+ ? { ...thread, hiddenAt: previous.hiddenAt }
1275
+ : thread));
1276
+ throw error;
1277
+ }
1278
+ }, [apiUrl, historyScope]);
1235
1279
  const archiveThread = useCallback(async (threadId) => {
1236
1280
  const previousArchivedAt = threadsRef.current.find((t) => t.id === threadId)?.archivedAt ?? null;
1237
1281
  const previousUpdatedAt = threadsRef.current.find((t) => t.id === threadId)?.updatedAt ?? null;
@@ -1642,6 +1686,8 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
1642
1686
  const params = new URLSearchParams({ q: query });
1643
1687
  if (includeExternal)
1644
1688
  params.set("includeExternal", "1");
1689
+ if (includeHidden)
1690
+ params.set("includeHidden", "1");
1645
1691
  if (ownership && ownership !== "all") {
1646
1692
  params.set("ownership", ownership);
1647
1693
  }
@@ -1656,7 +1702,14 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
1656
1702
  catch {
1657
1703
  return [];
1658
1704
  }
1659
- }, [apiUrl, historyScope, includeExternal, isolateHistory, ownership]);
1705
+ }, [
1706
+ apiUrl,
1707
+ historyScope,
1708
+ includeExternal,
1709
+ includeHidden,
1710
+ isolateHistory,
1711
+ ownership,
1712
+ ]);
1660
1713
  const getThreadShareState = useCallback(async (threadId) => {
1661
1714
  try {
1662
1715
  const res = await fetch(withChatThreadScope(`${apiUrl}/threads/${encodeURIComponent(threadId)}/share`, historyScope));
@@ -1698,8 +1751,9 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
1698
1751
  const refreshThreads = useCallback(() => {
1699
1752
  void fetchThreads();
1700
1753
  }, [fetchThreads]);
1754
+ const visibleThreads = useMemo(() => includeHidden ? threads : threads.filter((thread) => !thread.hiddenAt), [includeHidden, threads]);
1701
1755
  return {
1702
- threads,
1756
+ threads: visibleThreads,
1703
1757
  activeThreadId,
1704
1758
  isLoading,
1705
1759
  createThread,
@@ -1708,6 +1762,7 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
1708
1762
  deleteThread: removeThread,
1709
1763
  detachThread,
1710
1764
  pinThread,
1765
+ hideThread,
1711
1766
  archiveThread,
1712
1767
  renameThread,
1713
1768
  forkThread,
@@ -13,12 +13,12 @@ export declare const getCollabState: import("h3").EventHandlerWithFetch<import("
13
13
  };
14
14
  }>>;
15
15
  export declare const postCollabUpdate: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
16
- error: string;
17
16
  ok?: undefined;
17
+ error: string;
18
18
  } | {
19
+ error?: undefined;
19
20
  liveCopy: "derived" | "not_derived" | "unchanged";
20
21
  liveCopyReason?: string;
21
- error?: undefined;
22
22
  ok: boolean;
23
23
  } | {
24
24
  error?: undefined;
@@ -165,6 +165,7 @@ export declare function createDbExec(config?: DbExecConfig): Promise<DbExec>;
165
165
  export declare function annotateMissingTable(err: unknown, sql: unknown): unknown;
166
166
  export declare function withDbExec<T>(exec: DbExec, run: () => T): T;
167
167
  export declare function getScopedDbExec(): DbExec | undefined;
168
+ export declare function getScopedTransactionDbExec(): DbExec | undefined;
168
169
  /**
169
170
  * True while a `getDbExec().transaction()` callback runs. That callback leaves
170
171
  * `getScopedDbExec()` unset, so code that must not run inside a caller's
package/dist/db/client.js CHANGED
@@ -1722,11 +1722,23 @@ export function annotateMissingTable(err, sql) {
1722
1722
  const scopedDbExec = new AsyncLocalStorage();
1723
1723
  const transactionCallbacks = new AsyncLocalStorage();
1724
1724
  export function withDbExec(exec, run) {
1725
- return scopedDbExec.run(guardSchemaMutations(exec), run);
1725
+ const guarded = guardSchemaMutations(exec);
1726
+ const callback = transactionCallbacks.getStore();
1727
+ if (callback?.active && callback.executors.has(exec)) {
1728
+ callback.executors.add(guarded);
1729
+ }
1730
+ return scopedDbExec.run(guarded, run);
1726
1731
  }
1727
1732
  export function getScopedDbExec() {
1728
1733
  return scopedDbExec.getStore();
1729
1734
  }
1735
+ export function getScopedTransactionDbExec() {
1736
+ const scoped = getScopedDbExec();
1737
+ const callback = transactionCallbacks.getStore();
1738
+ return scoped && callback?.active && callback.executors.has(scoped)
1739
+ ? scoped
1740
+ : undefined;
1741
+ }
1730
1742
  /**
1731
1743
  * True while a `getDbExec().transaction()` callback runs. That callback leaves
1732
1744
  * `getScopedDbExec()` unset, so code that must not run inside a caller's
@@ -1735,8 +1747,8 @@ export function getScopedDbExec() {
1735
1747
  export function insideDbTransactionCallback() {
1736
1748
  return transactionCallbacks.getStore()?.active === true;
1737
1749
  }
1738
- async function inTransactionCallback(run) {
1739
- const callback = { active: true };
1750
+ async function inTransactionCallback(exec, run) {
1751
+ const callback = { active: true, executors: new WeakSet([exec]) };
1740
1752
  try {
1741
1753
  return await transactionCallbacks.run(callback, run);
1742
1754
  }
@@ -1804,17 +1816,21 @@ export function getDbExec() {
1804
1816
  if (!currentExec.transaction) {
1805
1817
  throw new Error("This database does not support transactions.");
1806
1818
  }
1807
- return currentExec.transaction((tx) => inTransactionCallback(() => fn({
1808
- execute: (s) => {
1809
- assertSchemaMutationAllowed(s);
1810
- return tx.execute(sanitize(s));
1811
- },
1812
- transaction: tx.transaction?.bind(tx),
1813
- })));
1819
+ return currentExec.transaction((tx) => runTransactionCallback(tx, fn));
1814
1820
  }
1815
1821
  : undefined,
1816
1822
  };
1817
1823
  }
1824
+ function runTransactionCallback(tx, fn) {
1825
+ const exec = {
1826
+ execute: (s) => {
1827
+ assertSchemaMutationAllowed(s);
1828
+ return tx.execute(sanitize(s));
1829
+ },
1830
+ transaction: tx.transaction?.bind(tx),
1831
+ };
1832
+ return inTransactionCallback(exec, () => fn(exec));
1833
+ }
1818
1834
  const proxy = {
1819
1835
  async execute(sql) {
1820
1836
  assertSchemaMutationAllowed(sql);
@@ -1826,19 +1842,13 @@ export function getDbExec() {
1826
1842
  const exec = await initializeProxy();
1827
1843
  Object.assign(proxy, createInitializedProxy(exec));
1828
1844
  if (exec.transaction) {
1829
- return exec.transaction((tx) => inTransactionCallback(() => fn({
1830
- execute: (s) => {
1831
- assertSchemaMutationAllowed(s);
1832
- return tx.execute(sanitize(s));
1833
- },
1834
- transaction: tx.transaction?.bind(tx),
1835
- })));
1845
+ return exec.transaction((tx) => runTransactionCallback(tx, fn));
1836
1846
  }
1837
1847
  if (exec.atomicBatch) {
1838
1848
  throw new Error("This database supports atomic batches, not interactive transactions.");
1839
1849
  }
1840
1850
  const wrapper = createInitializedProxy(exec);
1841
- return explicitTransaction(wrapper.execute.bind(wrapper))((tx) => inTransactionCallback(() => fn(tx)));
1851
+ return explicitTransaction(wrapper.execute.bind(wrapper))((tx) => inTransactionCallback(tx, () => fn(tx)));
1842
1852
  },
1843
1853
  async atomicBatch(statements) {
1844
1854
  for (const statement of statements) {
@@ -73,6 +73,7 @@ export const SCHEMA_SNAPSHOT = {
73
73
  'CREATE TABLE "automation_trigger_event_queue_sweep_state" (\n "scope_key" text NOT NULL,\n "last_trigger_id" text,\n "purge_after" bigint DEFAULT 0 NOT NULL,\n "updated_at" bigint NOT NULL\n);',
74
74
  'CREATE TABLE "automation_webhook_tokens" (\n "token_hash" text NOT NULL,\n "automation_id" text NOT NULL,\n "owner" text NOT NULL,\n "path" text NOT NULL,\n "secret_scope" text NOT NULL,\n "secret_scope_id" text NOT NULL,\n "secret_key" text NOT NULL,\n "created_at" bigint NOT NULL,\n "updated_at" bigint NOT NULL\n);',
75
75
  'CREATE TABLE "chat_thread_shares" (\n "id" text NOT NULL,\n "resource_id" text NOT NULL,\n "principal_type" text NOT NULL,\n "principal_id" text NOT NULL,\n "role" text DEFAULT \'viewer\'::text NOT NULL,\n "created_by" text NOT NULL,\n "created_at" text DEFAULT now() NOT NULL,\n "notified_at" text\n);',
76
+ 'CREATE TABLE "chat_thread_viewer_state" (\n "thread_id" text NOT NULL,\n "user_email" text NOT NULL,\n "pinned_at" bigint,\n "hidden_at" bigint,\n "updated_at" bigint NOT NULL\n);',
76
77
  'CREATE TABLE "chat_threads" (\n "id" text NOT NULL,\n "owner_email" text NOT NULL,\n "title" text DEFAULT \'\'::text NOT NULL,\n "preview" text DEFAULT \'\'::text NOT NULL,\n "thread_data" text DEFAULT \'{}\'::text NOT NULL,\n "message_count" bigint DEFAULT 0 NOT NULL,\n "created_at" bigint NOT NULL,\n "updated_at" bigint NOT NULL,\n "scope_type" text,\n "scope_id" text,\n "scope_label" text,\n "pinned_at" bigint,\n "archived_at" bigint,\n "share_token_hash" text,\n "source_platform" text,\n "source_app_id" text,\n "source_url" text,\n "org_id" text,\n "visibility" text DEFAULT \'private\'::text NOT NULL\n);',
77
78
  'CREATE TABLE "context_directives" (\n "id" text NOT NULL,\n "thread_id" text NOT NULL,\n "segment_id" text NOT NULL,\n "action" text NOT NULL,\n "summary_text" text,\n "created_by" text DEFAULT \'user\'::text NOT NULL,\n "active" integer DEFAULT 1 NOT NULL,\n "origin_turn" text,\n "created_at" integer NOT NULL,\n "updated_at" integer NOT NULL,\n "owner_email" text DEFAULT \'local@localhost\'::text NOT NULL,\n "org_id" text,\n "visibility" text DEFAULT \'private\'::text NOT NULL\n);',
78
79
  'CREATE TABLE "custom_api_providers" (\n "id" text NOT NULL,\n "scope" text NOT NULL,\n "scope_id" text NOT NULL,\n "label" text NOT NULL,\n "base_url" text NOT NULL,\n "auth_json" text NOT NULL,\n "docs_urls_json" text NOT NULL,\n "allowed_host_suffixes_json" text NOT NULL,\n "default_headers_json" text NOT NULL,\n "notes" text DEFAULT \'\'::text NOT NULL,\n "created_at" bigint NOT NULL,\n "updated_at" bigint NOT NULL\n);',
@@ -251,6 +252,7 @@ export const SCHEMA_SNAPSHOT = {
251
252
  'ALTER TABLE "automation_webhook_tokens" ADD CONSTRAINT "automation_webhook_tokens_automation_id_key" UNIQUE (automation_id);',
252
253
  'ALTER TABLE "automation_webhook_tokens" ADD CONSTRAINT "automation_webhook_tokens_pkey" PRIMARY KEY (token_hash);',
253
254
  'ALTER TABLE "chat_thread_shares" ADD CONSTRAINT "chat_thread_shares_pkey" PRIMARY KEY (id);',
255
+ 'ALTER TABLE "chat_thread_viewer_state" ADD CONSTRAINT "chat_thread_viewer_state_pkey" PRIMARY KEY (thread_id, user_email);',
254
256
  'ALTER TABLE "chat_threads" ADD CONSTRAINT "chat_threads_pkey" PRIMARY KEY (id);',
255
257
  'ALTER TABLE "context_directives" ADD CONSTRAINT "context_directives_pkey" PRIMARY KEY (id);',
256
258
  'ALTER TABLE "custom_api_providers" ADD CONSTRAINT "custom_api_providers_pkey" PRIMARY KEY (scope, scope_id, id);',
@@ -377,6 +379,7 @@ export const SCHEMA_SNAPSHOT = {
377
379
  'ALTER TABLE "workspace_connection_grants" ADD CONSTRAINT "workspace_connection_grants_pkey" PRIMARY KEY (id);',
378
380
  'ALTER TABLE "workspace_connections" ADD CONSTRAINT "workspace_connections_pkey" PRIMARY KEY (id);',
379
381
  'ALTER TABLE "workspace_user_groups" ADD CONSTRAINT "workspace_user_groups_pkey" PRIMARY KEY (id);',
382
+ 'ALTER TABLE "chat_thread_viewer_state" ADD CONSTRAINT "chat_thread_viewer_state_thread_id_fkey" FOREIGN KEY (thread_id) REFERENCES chat_threads(id) ON DELETE CASCADE;',
380
383
  "CREATE UNIQUE INDEX idx_a2a_tasks_org_scope_idempotency ON public.a2a_tasks USING btree (owner_scope, idempotency_key) WHERE ((owner_email IS NULL) AND (idempotency_key IS NOT NULL));",
381
384
  "CREATE UNIQUE INDEX idx_a2a_tasks_owner_scope_idempotency ON public.a2a_tasks USING btree (owner_email, owner_scope, idempotency_key);",
382
385
  "CREATE INDEX idx_a2a_tasks_recovery_created ON public.a2a_tasks USING btree (created_at) WHERE (status_state = ANY (ARRAY['submitted'::text, 'working'::text, 'processing'::text]));",
@@ -696,6 +699,7 @@ export const SCHEMA_SNAPSHOT = {
696
699
  "automation_trigger_event_queue_sweep_state",
697
700
  "automation_webhook_tokens",
698
701
  "chat_thread_shares",
702
+ "chat_thread_viewer_state",
699
703
  "chat_threads",
700
704
  "context_directives",
701
705
  "custom_api_providers",
@@ -974,6 +978,7 @@ export const SCHEMA_SNAPSHOT = {
974
978
  "76d071b18224f89b",
975
979
  ],
976
980
  ["chat_threads", "indexes", null],
981
+ ["chat_threads", "viewer-state", "12d60e0fc4587fc5"],
977
982
  ["chat_threads", "widen-bigint", null],
978
983
  ["checkpoints", "baseline", null],
979
984
  ["collab_awareness", "baseline", null],
@@ -10,6 +10,8 @@ export type IdentityColumn = {
10
10
  mode?: "email" | "user-scope" | "secret-scope" | "user-share" | "group-json" | "state-session" | "email-user-id" | "viewer-consent" | "owner" | "typed-scope" | "custom-scope" | "scope-key" | "unsupported-oauth";
11
11
  /** Email-change behavior. Omitted means rewrite the value to the new address. */
12
12
  emailChange?: IdentityEmailChange;
13
+ /** The other key columns. An email change refuses when the new address already holds the same key. */
14
+ uniqueWith?: readonly string[];
13
15
  /** Offboarding behavior. Omitted means offboard.ts derives it from `mode`. */
14
16
  offboard?: IdentityOffboard;
15
17
  /** How an organization-scoped offboard finds this organization's rows. Omitted means an `org_id` column. */
@@ -53,6 +53,17 @@ export const IDENTITY_REKEY_COLUMNS = [
53
53
  },
54
54
  { table: "agent_turn_initiators", column: "principal_email" },
55
55
  { table: "chat_threads", column: "owner_email" },
56
+ {
57
+ table: "chat_thread_viewer_state",
58
+ column: "user_email",
59
+ emailChange: "rekey",
60
+ uniqueWith: ["thread_id"],
61
+ offboard: "delete",
62
+ orgScope: {
63
+ column: "thread_id",
64
+ references: { table: "chat_threads", column: "id", orgColumn: "org_id" },
65
+ },
66
+ },
56
67
  { table: "chat_thread_shares", column: "principal_id", mode: "user-share" },
57
68
  { table: "chat_thread_shares", column: "created_by" },
58
69
  { table: "data_programs", column: "owner_email" },
@@ -1016,6 +1027,14 @@ export async function rekeyIdentity(db, oldEmailInput, newEmailInput, options =
1016
1027
  await db.unsafe(`UPDATE ${quote(entry.table)} SET ${quote(entry.column)} = CASE WHEN LOWER(${quote(entry.column)}) = LOWER($1) THEN $3 ELSE 'user:' || $3 END WHERE ${where}`, [...args, newEmail]);
1017
1028
  }
1018
1029
  else {
1030
+ if (entry.uniqueWith?.length) {
1031
+ const sameKey = entry.uniqueWith
1032
+ .map((column) => `target.${quote(column)} = source.${quote(column)}`)
1033
+ .join(" AND ");
1034
+ const collisionRows = await db.unsafe(`SELECT 1 FROM ${quote(entry.table)} source JOIN ${quote(entry.table)} target ON ${sameKey} AND LOWER(target.${quote(entry.column)}) = LOWER($2) WHERE LOWER(source.${quote(entry.column)}) = LOWER($1) LIMIT 1`, [oldEmail, newEmail]);
1035
+ if (collisionRows.length)
1036
+ throw new Error(`${entry.table} key collision detected; no identity data was changed.`);
1037
+ }
1019
1038
  if (!options.dryRun) {
1020
1039
  const update = await db.unsafe(`UPDATE ${quote(entry.table)} SET ${quote(entry.column)} = $${args.length + 1} WHERE ${where}`, [...args, newEmail]);
1021
1040
  counts[`${entry.table}.${entry.column}`] = update.count ?? count;
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export * from "./config.js";
2
2
  export * from "./authorization/index.js";
3
3
  export { createProductionAgentHandler, type ActionEntry, type ScriptEntry, type ProductionAgentOptions, type AgentActionSurface, type DefaultAgentActionSurface, type AgentActionSurfaceResolution, type AgentActionSurfaceDetails, type ActionTool, type ScriptTool, type AgentMessage, type AgentChatRequest, type AgentChatEvent, type AgentChatAttachment, DEFAULT_MODEL, TripWire, type Processor, type ProcessorState, type ProcessorAbort, } from "./agent/index.js";
4
- export { defineAction, fail, type FailOptions, ActionContractError, isActionContractError, type ActionContractErrorOptions, ActionOutputContractError, isActionOutputContractError, type ActionOutputContractErrorOptions, type ActionOutputEffect, type ActionMcpOutputContract, AgentActionStopError, isAgentActionStopError, type ActionDefinition, type ActionHttpConfig, type AgentActionStopOptions, type PublicAgentActionConfig, type ActionDeepLink, type ActionLinkBuilder, MCP_APP_EXTENSION_ID, MCP_APP_MIME_TYPE, MCP_APP_RESOURCE_URI_META_KEY, type ActionMcpAppConfig, type ActionMcpAppCsp, type ActionMcpAppCspBuilder, type ActionMcpAppHtmlBuilder, type ActionMcpAppPermissions, type ActionMcpAppResourceConfig, type ActionMcpAppResourceMeta, } from "./action.js";
4
+ export { defineAction, fail, type FailOptions, ActionContractError, isActionContractError, type ActionContractErrorOptions, NoWriteError, isNoWriteError, ActionOutputContractError, isActionOutputContractError, type ActionOutputContractErrorOptions, type ActionOutputEffect, type ActionMcpOutputContract, AgentActionStopError, isAgentActionStopError, type ActionDefinition, type ActionHttpConfig, type AgentActionStopOptions, type PublicAgentActionConfig, type ActionDeepLink, type ActionLinkBuilder, MCP_APP_EXTENSION_ID, MCP_APP_MIME_TYPE, MCP_APP_RESOURCE_URI_META_KEY, type ActionMcpAppConfig, type ActionMcpAppCsp, type ActionMcpAppCspBuilder, type ActionMcpAppHtmlBuilder, type ActionMcpAppPermissions, type ActionMcpAppResourceConfig, type ActionMcpAppResourceMeta, } from "./action.js";
5
5
  export * from "./user-profile/index.js";
6
6
  export { embedApp, MCP_APP_REQUEST_ORIGIN_CSP_SOURCE, type EmbedAppOptions, } from "./mcp/embed-app.js";
7
7
  export { embedRoute, type EmbedRouteContext, type EmbedRouteOptions, type EmbedRoutePathBuilder, type EmbedRouteResult, } from "./mcp/embed-route.js";
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  export * from "./config.js";
2
2
  export * from "./authorization/index.js";
3
3
  export { createProductionAgentHandler, DEFAULT_MODEL, TripWire, } from "./agent/index.js";
4
- export { defineAction, fail, ActionContractError, isActionContractError, ActionOutputContractError, isActionOutputContractError, AgentActionStopError, isAgentActionStopError, MCP_APP_EXTENSION_ID, MCP_APP_MIME_TYPE, MCP_APP_RESOURCE_URI_META_KEY, } from "./action.js";
4
+ export { defineAction, fail, ActionContractError, isActionContractError, NoWriteError, isNoWriteError, ActionOutputContractError, isActionOutputContractError, AgentActionStopError, isAgentActionStopError, MCP_APP_EXTENSION_ID, MCP_APP_MIME_TYPE, MCP_APP_RESOURCE_URI_META_KEY, } from "./action.js";
5
5
  export * from "./user-profile/index.js";
6
6
  export { embedApp, MCP_APP_REQUEST_ORIGIN_CSP_SOURCE, } from "./mcp/embed-app.js";
7
7
  export { embedRoute, } from "./mcp/embed-route.js";
@@ -4,6 +4,11 @@ export declare function connectionApp(origin: string, appId?: string): string;
4
4
  * Any other device client value, such as "Example, Inc.", is one name.
5
5
  */
6
6
  export declare function deviceFlowClients(client: string | null): string | string[] | null;
7
+ /**
8
+ * The client key `agent_connected` reports. OAuth funnel events carry the same
9
+ * key, so a client's starts and finished connections join on it.
10
+ */
11
+ export declare function connectionClientKey(name: string): string;
7
12
  export declare function trackAgentConnected(input: {
8
13
  email: string;
9
14
  origin: string;
@@ -18,6 +18,15 @@ export function deviceFlowClients(client) {
18
18
  const ids = client?.split(",");
19
19
  return ids?.every((id) => CLIENTS.includes(id)) ? ids : client;
20
20
  }
21
+ /**
22
+ * The client key `agent_connected` reports. OAuth funnel events carry the same
23
+ * key, so a client's starts and finished connections join on it.
24
+ */
25
+ export function connectionClientKey(name) {
26
+ return (resolveMcpConnectHostId(name) ??
27
+ detectVendorClient(name) ??
28
+ (name.trim().toLowerCase().slice(0, 120) || "unknown"));
29
+ }
21
30
  export async function trackAgentConnected(input) {
22
31
  // Everything runs inside the try: the connection is already committed, so a
23
32
  // tracking or configuration failure must not turn it into a failed response.
@@ -30,9 +39,7 @@ export async function trackAgentConnected(input) {
30
39
  return "disabled";
31
40
  const app = connectionApp(input.origin, input.appId);
32
41
  const raw = input.client;
33
- const clients = new Set((typeof raw === "string" ? [raw] : (raw ?? ["unknown"])).map((name) => resolveMcpConnectHostId(name) ??
34
- detectVendorClient(name) ??
35
- (name.trim().toLowerCase().slice(0, 120) || "unknown")));
42
+ const clients = new Set((typeof raw === "string" ? [raw] : (raw ?? ["unknown"])).map((name) => connectionClientKey(name)));
36
43
  let emitted = false;
37
44
  for (const client of clients) {
38
45
  const email = input.email.trim().toLowerCase();
@@ -4,15 +4,19 @@ export declare const MCP_ANALYTICS_EVENTS: {
4
4
  readonly toolCall: "$mcp_tool_call";
5
5
  readonly resourcesList: "$mcp_resources_list";
6
6
  readonly resourceRead: "$mcp_resource_read";
7
+ readonly oauthAuthorizeOpened: "$mcp_oauth_authorize_opened";
8
+ readonly oauthLoginWallShown: "$mcp_oauth_login_wall_shown";
7
9
  };
8
10
  export interface McpAnalyticsContext {
9
11
  source: "http" | "stdio";
10
12
  serverName: string;
11
- serverVersion: string;
13
+ serverVersion?: string;
12
14
  appId?: string;
13
15
  clientName?: string;
14
16
  clientVersion?: string;
15
17
  clientUserAgent?: string;
18
+ clientKey?: string;
19
+ redirectHost?: string;
16
20
  protocolVersion?: string;
17
21
  userId?: string;
18
22
  sessionId?: string;
@@ -24,6 +28,7 @@ interface McpRequestLike {
24
28
  _meta?: Record<string, unknown> | undefined;
25
29
  }
26
30
  export declare function detectVendorClient(...candidates: Array<string | undefined>): string | undefined;
31
+ export declare function vendorForRedirectHost(host: string | undefined): string | undefined;
27
32
  export declare function readClientInfoFromRequest(request: McpRequestLike | undefined): {
28
33
  clientName?: string;
29
34
  clientVersion?: string;
@@ -56,6 +61,14 @@ export declare function trackMcpResourceRead(ctx: McpAnalyticsContext, args: {
56
61
  errorType?: string;
57
62
  errorMessage?: string;
58
63
  }): void;
64
+ export type McpOAuthRoute = "directory" | "mcp";
65
+ export declare function trackMcpOAuthAuthorizeOpened(ctx: McpAnalyticsContext, args: {
66
+ route: McpOAuthRoute;
67
+ signedIn: boolean;
68
+ }): void;
69
+ export declare function trackMcpOAuthLoginWallShown(ctx: McpAnalyticsContext, args: {
70
+ route: McpOAuthRoute;
71
+ }): void;
59
72
  export declare function describeMcpError(err: unknown): {
60
73
  errorType: string;
61
74
  errorMessage: string;
@@ -9,6 +9,8 @@ export const MCP_ANALYTICS_EVENTS = {
9
9
  toolCall: "$mcp_tool_call",
10
10
  resourcesList: "$mcp_resources_list",
11
11
  resourceRead: "$mcp_resource_read",
12
+ oauthAuthorizeOpened: "$mcp_oauth_authorize_opened",
13
+ oauthLoginWallShown: "$mcp_oauth_login_wall_shown",
12
14
  };
13
15
  const VENDOR_CLIENT_PATTERNS = [
14
16
  [/claude[\s._-]?code/, "claude-code"],
@@ -41,6 +43,22 @@ export function detectVendorClient(...candidates) {
41
43
  }
42
44
  return undefined;
43
45
  }
46
+ // A known redirect host labels a vendor when the client's name does not (a
47
+ // client that points its callback at a domain it does not control cannot use
48
+ // the code). It is still the client's own registration, so it does not prove
49
+ // who the client is. Match the host itself or a subdomain, never a substring:
50
+ // that would credit `openai.evil.example`.
51
+ const REDIRECT_HOST_VENDORS = [
52
+ ["chatgpt.com", "openai"],
53
+ ["openai.com", "openai"],
54
+ ["claude.ai", "claude"],
55
+ ];
56
+ export function vendorForRedirectHost(host) {
57
+ const name = host?.toLowerCase();
58
+ if (!name)
59
+ return undefined;
60
+ return REDIRECT_HOST_VENDORS.find(([domain]) => name === domain || name.endsWith(`.${domain}`))?.[1];
61
+ }
44
62
  export function readClientInfoFromRequest(request) {
45
63
  const meta = request?.params?._meta ?? request?._meta;
46
64
  if (!meta || typeof meta !== "object")
@@ -65,18 +83,31 @@ function captureParameters() {
65
83
  return getAppConfig().observability.mcpCaptureParameters;
66
84
  }
67
85
  function contextProperties(ctx) {
68
- const vendor = detectVendorClient(ctx.clientName, ctx.clientUserAgent);
86
+ // The client's name or user agent labels the vendor as it does on every other
87
+ // MCP event, so the dimension stays consistent; a known redirect host labels
88
+ // it only when they do not. On OAuth events (the ones with a redirect host)
89
+ // `$mcp_vendor_source` says whether the host gave the same label. Neither
90
+ // proves who the client is.
91
+ const hostVendor = vendorForRedirectHost(ctx.redirectHost);
92
+ const vendor = detectVendorClient(ctx.clientName, ctx.clientUserAgent) ?? hostVendor;
69
93
  return {
70
94
  $mcp_source: ctx.source,
71
95
  $mcp_server_name: ctx.serverName,
72
- $mcp_server_version: ctx.serverVersion,
96
+ ...(ctx.serverVersion ? { $mcp_server_version: ctx.serverVersion } : {}),
73
97
  ...(ctx.appId ? { $mcp_app_id: ctx.appId } : {}),
74
98
  ...(ctx.clientName ? { $mcp_client_name: ctx.clientName } : {}),
75
99
  ...(ctx.clientVersion ? { $mcp_client_version: ctx.clientVersion } : {}),
76
100
  ...(ctx.clientUserAgent
77
101
  ? { $mcp_client_user_agent: ctx.clientUserAgent }
78
102
  : {}),
103
+ ...(ctx.clientKey ? { $mcp_client_key: ctx.clientKey } : {}),
104
+ ...(ctx.redirectHost ? { $mcp_redirect_host: ctx.redirectHost } : {}),
79
105
  ...(vendor ? { $mcp_vendor_client: vendor } : {}),
106
+ ...(vendor && ctx.redirectHost
107
+ ? {
108
+ $mcp_vendor_source: hostVendor === vendor ? "redirect_host" : "client_name",
109
+ }
110
+ : {}),
80
111
  ...(ctx.protocolVersion
81
112
  ? { $mcp_protocol_version: ctx.protocolVersion }
82
113
  : {}),
@@ -142,6 +173,27 @@ export function trackMcpResourceRead(ctx, args) {
142
173
  ...(args.errorMessage ? { $mcp_error_message: args.errorMessage } : {}),
143
174
  });
144
175
  }
176
+ // A new user's attempt serves two pages: the sign-in page (signedIn false, with
177
+ // a login wall) and, after sign-in, the consent page (signedIn true). A
178
+ // returning signed-in user sees only the consent page. So signedIn false counts
179
+ // signed-out arrivals and signedIn true counts everyone who reached consent,
180
+ // new and returning together; neither is "starts" on its own, and adding them
181
+ // double-counts new users.
182
+ export function trackMcpOAuthAuthorizeOpened(ctx, args) {
183
+ if (!analyticsEnabled())
184
+ return;
185
+ emit(MCP_ANALYTICS_EVENTS.oauthAuthorizeOpened, ctx, {
186
+ $mcp_oauth_route: args.route,
187
+ $mcp_oauth_signed_in: args.signedIn,
188
+ });
189
+ }
190
+ export function trackMcpOAuthLoginWallShown(ctx, args) {
191
+ if (!analyticsEnabled())
192
+ return;
193
+ emit(MCP_ANALYTICS_EVENTS.oauthLoginWallShown, ctx, {
194
+ $mcp_oauth_route: args.route,
195
+ });
196
+ }
145
197
  export function describeMcpError(err) {
146
198
  if (err && typeof err === "object") {
147
199
  const record = err;
@@ -1,5 +1,6 @@
1
1
  import { MCP_APP_CHAT_BRIDGE_QUERY_PARAM } from "../shared/embed-auth.js";
2
2
  import { MCP_APP_HOST_FILL_ATTRIBUTE, MCP_APP_PANE_FILL_MAX_HEIGHT, } from "../shared/mcp-app-display.js";
3
+ import { EXTERNAL_EMBED_WINDOW_KEY } from "../shared/route-chunk-recovery-bootstrap.js";
3
4
  const MCP_APP_IMPORT = "https://esm.sh/@modelcontextprotocol/ext-apps@1.7.5/app-with-deps";
4
5
  export const MCP_APP_REQUEST_ORIGIN_CSP_SOURCE = "$requestOrigin";
5
6
  const MCP_APP_WRAPPER_CHROME_HEIGHT = 44;
@@ -714,7 +715,7 @@ export function embedApp(options = {}) {
714
715
  }
715
716
 
716
717
  function installExternalEmbedRuntime(config) {
717
- window.__AGENT_NATIVE_EXTERNAL_EMBED = config;
718
+ window[${JSON.stringify(EXTERNAL_EMBED_WINDOW_KEY)}] = config;
718
719
  installReactRefreshPreambleFallback();
719
720
  try {
720
721
  if (config.target) {
@@ -8,7 +8,8 @@ import { getAuthSecret } from "../server/better-auth-instance.js";
8
8
  import { CREDENTIAL_MEMBERSHIP_UNAVAILABLE_MESSAGE } from "../server/credential-membership-unavailable.js";
9
9
  import { getOrigin } from "../server/google-oauth.js";
10
10
  import { readBody } from "../server/h3-helpers.js";
11
- import { trackAgentConnected } from "./agent-connected.js";
11
+ import { connectionApp, connectionClientKey, trackAgentConnected, } from "./agent-connected.js";
12
+ import { trackMcpOAuthAuthorizeOpened, trackMcpOAuthLoginWallShown, } from "./analytics.js";
12
13
  import { McpCredentialIssuanceError, withMcpCredentialIssuance, } from "./credential-issuance.js";
13
14
  import { applicationTypeForRedirectUris, isAllowedOAuthRedirectUri, isUrlBasedOAuthClientId, matchesRegisteredRedirectUri, resolveOAuthClientMetadataDocument, } from "./oauth-client-metadata.js";
14
15
  import { createOAuthCode, createOAuthRefreshToken, consumeOAuthCode, ensureOAuthTables, generateOpaqueToken, getOAuthClient, getOAuthCode, getOAuthRefreshToken, registerOAuthClient, revokeOAuthRefreshToken, touchOAuthRefreshToken, } from "./oauth-store.js";
@@ -595,6 +596,41 @@ async function readOAuthParams(event) {
595
596
  }
596
597
  return {};
597
598
  }
599
+ function trackAuthorizeStage(stage, input) {
600
+ // The authorize page is the only way into a connection, so a tracking fault
601
+ // must not turn a sign-in into a failure.
602
+ try {
603
+ const ctx = {
604
+ source: "http",
605
+ serverName: input.options.appName || input.options.appId || "Agent-Native",
606
+ appId: connectionApp(input.issuer, input.options.appId),
607
+ ...(input.clientName ? { clientName: input.clientName } : {}),
608
+ // The key agent_connected reports, so a client's starts and finished
609
+ // connections join on it.
610
+ clientKey: connectionClientKey(input.clientName ?? "unknown"),
611
+ // Host only: the redirect URI's path and query can carry per-attempt values.
612
+ ...(URL.canParse(input.redirectUri)
613
+ ? { redirectHost: new URL(input.redirectUri).hostname }
614
+ : {}),
615
+ ...(input.userId ? { userId: input.userId } : {}),
616
+ };
617
+ const route = input.resource.endsWith(MCP_DIRECTORY_ROUTE_PREFIX)
618
+ ? "directory"
619
+ : "mcp";
620
+ if (stage === "opened") {
621
+ trackMcpOAuthAuthorizeOpened(ctx, {
622
+ route,
623
+ signedIn: Boolean(input.userId),
624
+ });
625
+ }
626
+ else {
627
+ trackMcpOAuthLoginWallShown(ctx, { route });
628
+ }
629
+ }
630
+ catch (error) {
631
+ console.error("[mcp-oauth-analytics] Failed to record authorize:", error);
632
+ }
633
+ }
598
634
  async function handleAuthorize(event, options) {
599
635
  const method = getMethod(event);
600
636
  if (method !== "GET" && method !== "POST") {
@@ -634,6 +670,13 @@ async function handleAuthorize(event, options) {
634
670
  return oauthError("invalid_client", "Unknown client or redirect_uri");
635
671
  }
636
672
  const session = await getSession(event);
673
+ const analyticsInput = {
674
+ issuer,
675
+ options,
676
+ clientName: client.clientName,
677
+ redirectUri,
678
+ resource,
679
+ };
637
680
  if (!session?.email) {
638
681
  if (params.prompt === "none") {
639
682
  return redirectWithOAuthError({
@@ -644,6 +687,14 @@ async function handleAuthorize(event, options) {
644
687
  });
645
688
  }
646
689
  const loginPage = getConfiguredLoginHtml(event);
690
+ // Only the real sign-in page (200) is a login wall; 503 is the deploy's
691
+ // setup-required page. The consent POST is the same visit continuing, not
692
+ // a second open.
693
+ if (loginPage?.status === 200) {
694
+ if (method === "GET")
695
+ trackAuthorizeStage("opened", analyticsInput);
696
+ trackAuthorizeStage("login_wall", analyticsInput);
697
+ }
647
698
  return loginPage
648
699
  ? html(loginPage.html, loginPage.status)
649
700
  : oauthError("login_required", "Sign in required", 401);
@@ -661,6 +712,10 @@ async function handleAuthorize(event, options) {
661
712
  ? params.organization_id || null
662
713
  : undefined);
663
714
  if (method === "GET") {
715
+ trackAuthorizeStage("opened", {
716
+ ...analyticsInput,
717
+ userId: session.email,
718
+ });
664
719
  return html(renderConsentPage({
665
720
  appName: options.appName || options.appId || "Agent-Native",
666
721
  email: session.email,