@agent-native/core 0.200.0-nightly-20261002124406 → 0.200.0

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 (48) hide show
  1. package/dist/agent/production-agent.d.ts +5 -1
  2. package/dist/agent/production-agent.js +36 -1
  3. package/dist/app-config/runtime.d.ts +1 -0
  4. package/dist/app-config/runtime.js +4 -0
  5. package/dist/app-config/schema.d.ts +1 -0
  6. package/dist/cli/design-connect.d.ts +1 -0
  7. package/dist/cli/design-connect.js +147 -7
  8. package/dist/client/analytics.js +24 -1
  9. package/dist/client/session-replay.d.ts +6 -0
  10. package/dist/client/session-replay.js +47 -5
  11. package/dist/collab/awareness.d.ts +2 -2
  12. package/dist/collab/routes.d.ts +1 -1
  13. package/dist/file-upload/builder.js +20 -2
  14. package/dist/mcp-client/app-api.d.ts +11 -1
  15. package/dist/mcp-client/app-api.js +17 -3
  16. package/dist/mcp-client/index.d.ts +1 -1
  17. package/dist/mcp-client/manager.d.ts +3 -1
  18. package/dist/mcp-client/manager.js +4 -0
  19. package/dist/notifications/routes.d.ts +3 -3
  20. package/dist/observability/metrics.js +14 -7
  21. package/dist/provider-api/actions/custom-provider-registration.d.ts +6 -6
  22. package/dist/provider-api/actions/provider-api.d.ts +15 -15
  23. package/dist/resource-changes/store.d.ts +166 -0
  24. package/dist/resource-changes/store.js +499 -0
  25. package/dist/search/index-store.d.ts +9 -0
  26. package/dist/search/index-store.js +50 -0
  27. package/dist/search/index.d.ts +5 -1
  28. package/dist/search/index.js +5 -0
  29. package/dist/search/indexer.d.ts +32 -0
  30. package/dist/search/indexer.js +651 -0
  31. package/dist/search/query-parser.d.ts +29 -0
  32. package/dist/search/query-parser.js +71 -0
  33. package/dist/search/query.d.ts +46 -0
  34. package/dist/search/query.js +149 -0
  35. package/dist/search/registry.d.ts +55 -0
  36. package/dist/search/registry.js +93 -0
  37. package/dist/search/tokenize.d.ts +103 -0
  38. package/dist/search/tokenize.js +367 -0
  39. package/dist/server/action-change-marker-write.js +5 -1
  40. package/dist/server/agent-chat/run-code-tools.d.ts +7 -0
  41. package/dist/server/agent-chat/run-code-tools.js +7 -0
  42. package/dist/server/agent-chat-plugin.d.ts +5 -1
  43. package/dist/server/agent-chat-plugin.js +18 -12
  44. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  45. package/dist/server/agent-engine-default-model-route.d.ts +2 -2
  46. package/dist/server/release-schema.js +8 -0
  47. package/dist/triggers/actions/manage-automation.d.ts +3 -3
  48. package/package.json +6 -9
@@ -10,6 +10,7 @@ import { type BuilderGatewayAuth } from "../server/credential-provider.js";
10
10
  import { fireInternalDispatch } from "../server/self-dispatch.js";
11
11
  import { type RefusedTurnRetryContext } from "../shared/agent-chat-run-not-started.js";
12
12
  import { type ReasoningEffort } from "../shared/reasoning-effort.js";
13
+ import type { TrackingMeta } from "../tracking/registry.js";
13
14
  import { type ResolveEngineConfig } from "./engine/index.js";
14
15
  import { PROVIDER_TO_ENV } from "./engine/provider-env-vars.js";
15
16
  import type { AgentEngine, EngineTool, EngineMessage, EngineContentPart } from "./engine/types.js";
@@ -232,6 +233,9 @@ export declare function filterActionsByAllowedNames(actions: Record<string, Acti
232
233
  export declare const PLAN_MODE_SYSTEM_PROMPT = "## Plan Mode Active\n\nYou are in Plan mode. This turn is for research, clarification, and a proposed approach only.\n\nHard rules:\n- Use only read-only tools. Do not edit files, write resources, run mutating bash commands, mutate SQL rows, navigate the UI, send notifications, create jobs, create tools, send messages or tasks to external agents, or change external systems.\n- If a needed detail is unclear, ask a concise clarifying question before proposing a plan.\n- When ready, present a concrete plan with the files/tools you expect to touch, the intended changes, validation steps, and notable risks.\n- Do not treat approval as implicit while Plan mode is still active. Tell the user to switch to Act mode with the mode selector or /act before implementation.";
233
234
  export declare function isPlanModeToolCallAllowed(name: string, input: unknown, entry: ActionEntry): boolean;
234
235
  export declare function createPlanModeActionRegistry(actions: Record<string, ActionEntry>): Record<string, ActionEntry>;
236
+ type AgentRunTrackingSource = Pick<TrackingMeta, "userId" | "authUserId" | "anonymousId" | "sessionId"> & {
237
+ isSyntheticTraffic?: boolean;
238
+ };
235
239
  export interface ProductionAgentOptions {
236
240
  actions?: Record<string, ActionEntry>;
237
241
  /** @deprecated Use `actions` instead */
@@ -252,7 +256,7 @@ export interface ProductionAgentOptions {
252
256
  hostedHarnessConfig?: AgentNativeHarnessSetting;
253
257
  reasoningEffort?: ReasoningEffort;
254
258
  providerOptions?: EngineMessage extends never ? never : any;
255
- onRunComplete?: (run: ActiveRun, threadId: string | undefined) => void;
259
+ onRunComplete?: (run: ActiveRun, threadId: string | undefined, trackingSource?: AgentRunTrackingSource) => void;
256
260
  onRunPrepared?: (details: {
257
261
  runId: string;
258
262
  turnId: string;
@@ -958,6 +958,35 @@ export async function resolveAgentOwnerEmail(options, event) {
958
958
  }
959
959
  return ownerEmail ?? getRequestUserEmail() ?? null;
960
960
  }
961
+ function snapshotAgentRunTrackingSource() {
962
+ const requestContext = getRequestContext();
963
+ if (!requestContext)
964
+ return undefined;
965
+ const source = requestContext.agentRunAnonymous
966
+ ? {
967
+ ...(requestContext.userEmail
968
+ ? { anonymousId: requestContext.userEmail }
969
+ : {}),
970
+ ...(requestContext.browserSessionId
971
+ ? { sessionId: requestContext.browserSessionId }
972
+ : {}),
973
+ }
974
+ : {
975
+ ...(requestContext.userEmail
976
+ ? { userId: requestContext.userEmail }
977
+ : {}),
978
+ ...(requestContext.authUserId
979
+ ? { authUserId: requestContext.authUserId }
980
+ : {}),
981
+ ...(requestContext.browserSessionId
982
+ ? { sessionId: requestContext.browserSessionId }
983
+ : {}),
984
+ };
985
+ const isSyntheticTraffic = requestContext.isSyntheticTraffic === true;
986
+ return Object.keys(source).length > 0 || isSyntheticTraffic
987
+ ? { ...source, ...(isSyntheticTraffic ? { isSyntheticTraffic: true } : {}) }
988
+ : undefined;
989
+ }
961
990
  const MAX_RETRIES = 3;
962
991
  const COMPLETION_DATABASE_RETRY_DELAYS_MS = [250, 750, 1_500];
963
992
  export async function runCompletionCallbackWithDatabaseRetry(callback, options) {
@@ -6336,6 +6365,9 @@ export function createProductionAgentHandler(options) {
6336
6365
  };
6337
6366
  const getEngineTools = (actions) => actionsToEngineTools(getRequestActions(actions));
6338
6367
  return defineEventHandler(async (event) => {
6368
+ let completionTrackingSource = options.onRunComplete
6369
+ ? snapshotAgentRunTrackingSource()
6370
+ : undefined;
6339
6371
  const setupT0 = Date.now();
6340
6372
  const setupMarks = {};
6341
6373
  const setupMark = (k) => {
@@ -7634,12 +7666,15 @@ export function createProductionAgentHandler(options) {
7634
7666
  const baseHandleRunComplete = options.onRunComplete || trackedProgressRunId
7635
7667
  ? async (run) => {
7636
7668
  try {
7637
- await runCompletionCallbackWithDatabaseRetry(() => options.onRunComplete?.(run, threadId));
7669
+ await runCompletionCallbackWithDatabaseRetry(() => options.onRunComplete?.(run, threadId, completionTrackingSource));
7638
7670
  }
7639
7671
  catch (err) {
7640
7672
  await completeTrackedProgressRun(run, err);
7641
7673
  throw err;
7642
7674
  }
7675
+ finally {
7676
+ completionTrackingSource = undefined;
7677
+ }
7643
7678
  await completeTrackedProgressRun(run);
7644
7679
  }
7645
7680
  : undefined;
@@ -2,6 +2,7 @@ import { z } from "zod";
2
2
  export declare const runtimeConfig: z.ZodObject<{
3
3
  backgroundJobsEnabled: z.ZodOptional<z.ZodBoolean>;
4
4
  databasePoolMax: z.ZodOptional<z.ZodNumber>;
5
+ searchDrainBudgetMs: z.ZodDefault<z.ZodNumber>;
5
6
  agentChatStreaming: z.ZodDefault<z.ZodBoolean>;
6
7
  vercelBranchUrl: z.ZodOptional<z.ZodString>;
7
8
  databaseUrlUnpooled: z.ZodOptional<z.ZodString>;
@@ -8,6 +8,10 @@ export const runtimeConfig = z.object({
8
8
  env: "AGENT_NATIVE_DB_POOL_MAX",
9
9
  doc: "Maximum connections per database pool. Serverless runtimes always use one connection; local runtimes default to twenty.",
10
10
  }),
11
+ searchDrainBudgetMs: z.number().int().min(0).default(100).meta({
12
+ env: "AGENT_NATIVE_SEARCH_DRAIN_BUDGET_MS",
13
+ doc: "Milliseconds a search spends processing pending search index changes before answering; if changes remain, that search uses the app's fallback. Zero processes none, leaving them to the drains that follow writes and the recurring sweep.",
14
+ }),
11
15
  agentChatStreaming: z.boolean().default(false).meta({
12
16
  env: "AGENT_NATIVE_AGENT_CHAT_STREAM_RUNTIME",
13
17
  doc: "Run the dedicated Nitro agent-chat response-streaming route used by an AWS Lambda Function URL.",
@@ -131,6 +131,7 @@ export declare const appConfigSchema: z.ZodObject<{
131
131
  runtime: z.ZodPrefault<z.ZodObject<{
132
132
  backgroundJobsEnabled: z.ZodOptional<z.ZodBoolean>;
133
133
  databasePoolMax: z.ZodOptional<z.ZodNumber>;
134
+ searchDrainBudgetMs: z.ZodDefault<z.ZodNumber>;
134
135
  agentChatStreaming: z.ZodDefault<z.ZodBoolean>;
135
136
  vercelBranchUrl: z.ZodOptional<z.ZodString>;
136
137
  databaseUrlUnpooled: z.ZodOptional<z.ZodString>;
@@ -51,6 +51,7 @@ export interface DesignConnectManifest {
51
51
  }
52
52
  export interface DesignConnectBridge {
53
53
  server: Server;
54
+ close: () => Promise<void>;
54
55
  manifest: DesignConnectManifest;
55
56
  /** Per-rootPath bridge token. Kept in-process only; never serialised into
56
57
  * the manifest JSON so it is not exposed over the network via GET /manifest.
@@ -5,6 +5,7 @@ import fs from "node:fs/promises";
5
5
  import http from "node:http";
6
6
  import https from "node:https";
7
7
  import path from "node:path";
8
+ import { WebSocket, WebSocketServer } from "ws";
8
9
  import { injectDocumentMarkup } from "../shared/html-document.js";
9
10
  const DEFAULT_BRIDGE_PORT = 7331;
10
11
  const ROUTE_MANIFEST_FILE = path.join(".agent-native", "design-routes.json");
@@ -1200,6 +1201,53 @@ function addPreviewTokenToResourceUrl(resourceUrl, previewToken) {
1200
1201
  const withoutStaleToken = stripQueryPair(search, "previewToken");
1201
1202
  return `${pathname}${appendQueryPair(withoutStaleToken, "previewToken", previewToken)}${hash}`;
1202
1203
  }
1204
+ const MAX_PREVIEW_HMR_MESSAGE_BYTES = 1024 * 1024;
1205
+ function rewriteViteHmrUpdate(payload, previewToken) {
1206
+ if (payload.length > MAX_PREVIEW_HMR_MESSAGE_BYTES)
1207
+ return undefined;
1208
+ let message;
1209
+ try {
1210
+ message = JSON.parse(payload.toString("utf8"));
1211
+ }
1212
+ catch {
1213
+ // coercion-ok: non-JSON Vite control frames are relayed unchanged.
1214
+ return undefined;
1215
+ }
1216
+ if (!message ||
1217
+ typeof message !== "object" ||
1218
+ !("type" in message) ||
1219
+ message.type !== "update" ||
1220
+ !("updates" in message) ||
1221
+ !Array.isArray(message.updates)) {
1222
+ return undefined;
1223
+ }
1224
+ let changed = false;
1225
+ for (const candidate of message.updates) {
1226
+ if (!candidate ||
1227
+ typeof candidate !== "object" ||
1228
+ !("type" in candidate) ||
1229
+ candidate.type !== "js-update" ||
1230
+ !("acceptedPath" in candidate) ||
1231
+ typeof candidate.acceptedPath !== "string" ||
1232
+ !("timestamp" in candidate) ||
1233
+ typeof candidate.timestamp !== "number" ||
1234
+ !Number.isSafeInteger(candidate.timestamp)) {
1235
+ continue;
1236
+ }
1237
+ // Vite compares path and acceptedPath as module identities before it imports
1238
+ // the update, so carry proxy auth in its cache-busting query instead.
1239
+ candidate.timestamp = `${candidate.timestamp}&previewToken=${encodeURIComponent(previewToken)}`;
1240
+ changed = true;
1241
+ }
1242
+ return changed ? Buffer.from(JSON.stringify(message)) : undefined;
1243
+ }
1244
+ function webSocketDataBuffer(data) {
1245
+ if (Buffer.isBuffer(data))
1246
+ return data;
1247
+ if (Array.isArray(data))
1248
+ return Buffer.concat(data);
1249
+ return Buffer.from(data);
1250
+ }
1203
1251
  function addOpaqueFrameCredentials(html, previewToken) {
1204
1252
  const withCredentialedResources = html.replace(/<(script|link|img|audio|video|source|track|iframe)\b([^>]*?)>/gi, (fullTag, tagName, attributes) => {
1205
1253
  const resourceMatch = attributes.match(/\b(src|href)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/i);
@@ -1243,9 +1291,12 @@ function addOpaqueFrameCredentials(html, previewToken) {
1243
1291
  },
1244
1292
  });
1245
1293
  const withImportMap = injectDocumentMarkup(withHmrToken, `<script type="importmap" data-agent-native-opaque-preview-imports>${previewImportMap}</script>`, { target: "head" });
1246
- return injectDocumentMarkup(withImportMap,
1294
+ const withViteHmrStyleAuth = injectDocumentMarkup(withImportMap,
1295
+ // coercion-ok: malformed Vite-generated CSS URLs stay unchanged in the frame.
1296
+ `<script data-agent-native-vite-css-auth>(function(t){var p=Element.prototype.after;Element.prototype.after=function(){for(var i=0;i<arguments.length;i++){var n=arguments[i];if(!n||n.nodeType!==1||n.tagName!=="LINK")continue;try{var b=new URL(document.baseURI),u=new URL(n.getAttribute("href"),b);if(u.origin===b.origin&&!u.searchParams.has("previewToken")){u.searchParams.set("previewToken",t);n.setAttribute("href",u.toString())}}catch(_){}}return p.apply(this,arguments)}})(${previewTokenLiteral});</script>`, { target: "head" });
1297
+ return injectDocumentMarkup(withViteHmrStyleAuth,
1247
1298
  // coercion-ok: invalid browser-owned URLs stay unchanged in the frame.
1248
- String.raw `<script data-agent-native-opaque-preview-auth>(function(){var t=${previewTokenLiteral},o,h;try{var b=new URL(document.baseURI);o=b.origin;h=b.host}catch(_){return}function u(v){try{var a=new URL(String(v),document.baseURI),same=a.origin===o||(h===a.host&&(a.protocol==="ws:"||a.protocol==="wss:"));if(!same||a.searchParams.has("previewToken"))return null;a.searchParams.set("previewToken",t);return a.toString()}catch(_){return null}}function c(v){return String(v).replace(/url\(\s*(["']?)([^"'()]+)\1\s*\)/gi,function(h,q,v){var s=u(v);return s?"url("+q+s+q+")":h})}var f=window.fetch.bind(window);window.fetch=function(i,n){var v=i instanceof Request?i.url:i,s=u(v);return s?f(i instanceof Request?new Request(s,i):s,n):f(i,n)};var x=XMLHttpRequest.prototype.open;XMLHttpRequest.prototype.open=function(m,v){var s=u(v);return x.call(this,m,s||v,...Array.prototype.slice.call(arguments,2))};var e=window.EventSource;if(e){var E=function(v,n){return new e(u(v)||v,n);};E.prototype=e.prototype;window.EventSource=E}var W=window.WebSocket;if(W){var S=function(v,p){var s=u(v);return p===undefined?new W(s||v):new W(s||v,p)};S.prototype=W.prototype;S.CONNECTING=W.CONNECTING;S.OPEN=W.OPEN;S.CLOSING=W.CLOSING;S.CLOSED=W.CLOSED;window.WebSocket=S}var p=Node.prototype.appendChild;Node.prototype.appendChild=function(n){if(n&&n.nodeType===1){var a=n.tagName==="SCRIPT"?"src":n.tagName==="LINK"?"href":n.tagName==="IMG"?"src":n.tagName==="IFRAME"?"src":null;if(a){var v=n.getAttribute(a),s=u(v);if(s)n.setAttribute(a,s)}else if(n.tagName==="STYLE"&&n.textContent){n.textContent=c(n.textContent)}}return p.call(this,n)}})();</script>`, { target: "head" });
1299
+ String.raw `<script data-agent-native-opaque-preview-auth>(function(){var t=${previewTokenLiteral},o,h;try{var b=new URL(document.baseURI);o=b.origin;h=b.host}catch(_){return}function u(v){try{var a=new URL(String(v),document.baseURI),same=a.origin===o||(h===a.host&&(a.protocol==="ws:"||a.protocol==="wss:"));if(!same||a.searchParams.has("previewToken"))return null;a.searchParams.set("previewToken",t);return a.toString()}catch(_){return null}}function c(v){return String(v).replace(/url\(\s*(["']?)([^"'()]+)\1\s*\)/gi,function(h,q,v){var s=u(v);return s?"url("+q+s+q+")":h})}var f=window.fetch.bind(window);window.fetch=function(i,n){var v=i instanceof Request?i.url:i,s=u(v);return s?f(i instanceof Request?new Request(s,i):s,n):f(i,n)};var x=XMLHttpRequest.prototype.open;XMLHttpRequest.prototype.open=function(m,v){var s=u(v);return x.call(this,m,s||v,...Array.prototype.slice.call(arguments,2))};var e=window.EventSource;if(e){var E=function(v,n){return new e(u(v)||v,n);};E.prototype=e.prototype;window.EventSource=E}var W=window.WebSocket;if(W){var S=function(v,p){var s=u(v);return p===undefined?new W(s||v):new W(s||v,p)};S.prototype=W.prototype;S.CONNECTING=W.CONNECTING;S.OPEN=W.OPEN;S.CLOSING=W.CLOSING;S.CLOSED=W.CLOSED;window.WebSocket=S}function d(n){if(n&&n.nodeType===1){var a=n.tagName==="SCRIPT"?"src":n.tagName==="LINK"?"href":n.tagName==="IMG"?"src":n.tagName==="IFRAME"?"src":null;if(a){var v=n.getAttribute(a),s=u(v);if(s)n.setAttribute(a,s)}else if(n.tagName==="STYLE"&&n.textContent){n.textContent=c(n.textContent)}}return n}var p=Node.prototype.appendChild;Node.prototype.appendChild=function(n){return p.call(this,d(n))};var i=Node.prototype.insertBefore;Node.prototype.insertBefore=function(n,r){return i.call(this,d(n),r)}})();</script>`, { target: "head" });
1249
1300
  }
1250
1301
  function addOpaqueFrameResourceTokens(css, previewToken) {
1251
1302
  const withImportTokens = css.replace(/(@import\s+)(["'])([^"']+)\2/gi, (_full, prefix, quote, resourceUrl) => `${prefix}${quote}${addPreviewTokenToResourceUrl(resourceUrl, previewToken)}${quote}`);
@@ -2107,6 +2158,11 @@ export async function startDesignConnectBridge(manifest, seedOrOptions) {
2107
2158
  const isValidLiveEditRegistrationCapability = (req, designId) => constantTimeTokenMatches(readHeader(req, "x-agent-native-live-edit-registration-capability"), deriveDesignScopedLiveEditRegistrationCapability(bridgeToken, designId));
2108
2159
  const bridgeInstanceId = crypto.randomBytes(16).toString("hex");
2109
2160
  const previewSessionCookies = new PreviewSessionCookieJar(manifest.devServerUrl);
2161
+ const hmrWebSocketServer = new WebSocketServer({
2162
+ noServer: true,
2163
+ perMessageDeflate: false,
2164
+ handleProtocols: (protocols) => protocols.has("vite-hmr") ? "vite-hmr" : false,
2165
+ });
2110
2166
  const server = http.createServer((req, res) => {
2111
2167
  const requestUrl = new URL(req.url ?? "/", manifest.bridgeUrl);
2112
2168
  const pathname = requestUrl.pathname;
@@ -2326,6 +2382,7 @@ export async function startDesignConnectBridge(manifest, seedOrOptions) {
2326
2382
  sendJson(res, 200, {
2327
2383
  ok: true,
2328
2384
  pending: pendingVisualEditPayloads.get(designId)?.pending ?? null,
2385
+ revision: pendingVisualEditRevisionHighWaterMarks.get(designId) ?? 0,
2329
2386
  });
2330
2387
  return;
2331
2388
  }
@@ -2406,7 +2463,7 @@ export async function startDesignConnectBridge(manifest, seedOrOptions) {
2406
2463
  : stored === "stale"
2407
2464
  ? "stale pending publication revision"
2408
2465
  : "conflicting pending publication revision",
2409
- revision: existing?.revision,
2466
+ revision: pendingVisualEditRevisionHighWaterMarks.get(pendingDesignId) ?? 0,
2410
2467
  });
2411
2468
  return;
2412
2469
  }
@@ -2472,7 +2529,7 @@ export async function startDesignConnectBridge(manifest, seedOrOptions) {
2472
2529
  : stored === "stale"
2473
2530
  ? "stale pending publication revision"
2474
2531
  : "conflicting pending publication revision",
2475
- revision: existing?.revision,
2532
+ revision: pendingVisualEditRevisionHighWaterMarks.get(candidate.designId) ?? 0,
2476
2533
  });
2477
2534
  return;
2478
2535
  }
@@ -2889,6 +2946,16 @@ export async function startDesignConnectBridge(manifest, seedOrOptions) {
2889
2946
  }
2890
2947
  sendJson(res, 404, { ok: false, error: "not found" });
2891
2948
  });
2949
+ const terminateHmrClients = () => {
2950
+ for (const client of hmrWebSocketServer.clients)
2951
+ client.terminate();
2952
+ };
2953
+ // noServer upgrades are not closed by http.Server.close().
2954
+ const closeHttpServer = server.close.bind(server);
2955
+ server.close = (callback) => {
2956
+ terminateHmrClients();
2957
+ return closeHttpServer(callback);
2958
+ };
2892
2959
  // Vite's proxied /@vite/client derives its HMR socket from the document's
2893
2960
  // bridge origin. Tunnel WebSocket upgrades to the one connected dev-server
2894
2961
  // origin so Fast Refresh remains live inside URL-backed screens. The target
@@ -2903,6 +2970,9 @@ export async function startDesignConnectBridge(manifest, seedOrOptions) {
2903
2970
  "";
2904
2971
  const browserOrigin = readHeader(req, "origin");
2905
2972
  const sameBridgeOrigin = browserOrigin === new URL(manifest.bridgeUrl).origin;
2973
+ const isViteHmrSocket = readHeader(req, "sec-websocket-protocol")
2974
+ .split(",")
2975
+ .some((protocol) => protocol.trim() === "vite-hmr");
2906
2976
  if (!constantTimeTokenMatches(providedPreviewToken, previewToken)) {
2907
2977
  clientSocket.end("HTTP/1.1 401 Unauthorized\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
2908
2978
  return;
@@ -2921,11 +2991,60 @@ export async function startDesignConnectBridge(manifest, seedOrOptions) {
2921
2991
  delete upstreamHeaders["x-agent-native-live-edit-capability"];
2922
2992
  delete upstreamHeaders["authorization"];
2923
2993
  delete upstreamHeaders["cookie"];
2994
+ if (isViteHmrSocket) {
2995
+ delete upstreamHeaders["connection"];
2996
+ delete upstreamHeaders["upgrade"];
2997
+ delete upstreamHeaders["sec-websocket-key"];
2998
+ delete upstreamHeaders["sec-websocket-version"];
2999
+ delete upstreamHeaders["sec-websocket-protocol"];
3000
+ delete upstreamHeaders["sec-websocket-extensions"];
3001
+ }
2924
3002
  upstreamHeaders.host = targetUrl.host;
2925
3003
  upstreamHeaders.origin = new URL(manifest.devServerUrl).origin;
2926
3004
  const cookie = mergePreviewCookieHeaders(previewSessionCookies.headerFor(targetUrl.toString()), sameBridgeOrigin ? readHeader(req, "cookie") : "");
2927
3005
  if (cookie)
2928
3006
  upstreamHeaders.cookie = cookie;
3007
+ if (isViteHmrSocket) {
3008
+ const hmrUrl = new URL(targetUrl);
3009
+ hmrUrl.protocol = targetUrl.protocol === "https:" ? "wss:" : "ws:";
3010
+ hmrWebSocketServer.handleUpgrade(req, clientSocket, clientHead, (browserSocket) => {
3011
+ const upstreamSocket = new WebSocket(hmrUrl, "vite-hmr", {
3012
+ headers: upstreamHeaders,
3013
+ perMessageDeflate: false,
3014
+ });
3015
+ const queuedMessages = [];
3016
+ browserSocket.on("message", (data, isBinary) => {
3017
+ if (upstreamSocket.readyState === WebSocket.OPEN) {
3018
+ upstreamSocket.send(data, { binary: isBinary });
3019
+ }
3020
+ else if (upstreamSocket.readyState === WebSocket.CONNECTING) {
3021
+ queuedMessages.push({ data, isBinary });
3022
+ }
3023
+ });
3024
+ upstreamSocket.once("open", () => {
3025
+ for (const message of queuedMessages) {
3026
+ upstreamSocket.send(message.data, { binary: message.isBinary });
3027
+ }
3028
+ queuedMessages.length = 0;
3029
+ });
3030
+ upstreamSocket.on("message", (data, isBinary) => {
3031
+ const payload = webSocketDataBuffer(data);
3032
+ const rewritten = isBinary
3033
+ ? undefined
3034
+ : rewriteViteHmrUpdate(payload, previewToken);
3035
+ browserSocket.send(rewritten ?? payload, { binary: isBinary });
3036
+ });
3037
+ browserSocket.once("close", () => upstreamSocket.terminate());
3038
+ upstreamSocket.once("close", () => browserSocket.close());
3039
+ upstreamSocket.on("error", () => {
3040
+ if (browserSocket.readyState === WebSocket.OPEN) {
3041
+ browserSocket.close(1011, "Dev server HMR connection failed");
3042
+ }
3043
+ });
3044
+ browserSocket.on("error", () => upstreamSocket.terminate());
3045
+ });
3046
+ return;
3047
+ }
2929
3048
  const requestImpl = targetUrl.protocol === "https:" ? https : http;
2930
3049
  let upgraded = false;
2931
3050
  const upstreamRequest = requestImpl.request({
@@ -2972,6 +3091,17 @@ export async function startDesignConnectBridge(manifest, seedOrOptions) {
2972
3091
  });
2973
3092
  upstreamRequest.end();
2974
3093
  });
3094
+ let closePromise;
3095
+ const close = () => {
3096
+ closePromise ??= (async () => {
3097
+ terminateHmrClients();
3098
+ await new Promise((resolve) => hmrWebSocketServer.close(resolve));
3099
+ await new Promise((resolve, reject) => {
3100
+ server.close((error) => (error ? reject(error) : resolve()));
3101
+ });
3102
+ })();
3103
+ return closePromise;
3104
+ };
2975
3105
  await new Promise((resolve, reject) => {
2976
3106
  server.once("error", reject);
2977
3107
  server.listen(new URL(manifest.bridgeUrl).port, "127.0.0.1", () => {
@@ -2984,11 +3114,18 @@ export async function startDesignConnectBridge(manifest, seedOrOptions) {
2984
3114
  await persistBridgeToken(manifest.rootPath, bridgeToken);
2985
3115
  }
2986
3116
  catch (error) {
2987
- await new Promise((resolve) => server.close(() => resolve()));
3117
+ await close();
2988
3118
  throw error;
2989
3119
  }
2990
3120
  }
2991
- return { server, manifest, bridgeToken, previewToken, bridgeInstanceId };
3121
+ return {
3122
+ close,
3123
+ server,
3124
+ manifest,
3125
+ bridgeToken,
3126
+ previewToken,
3127
+ bridgeInstanceId,
3128
+ };
2992
3129
  }
2993
3130
  export function resolveAppUrl(explicit) {
2994
3131
  const raw = explicit ||
@@ -3338,7 +3475,10 @@ export async function runDesign(argv) {
3338
3475
  }
3339
3476
  return await new Promise((resolve) => {
3340
3477
  const stop = () => {
3341
- bridge.server.close(() => resolve(0));
3478
+ void bridge.close().then(() => resolve(0), (error) => {
3479
+ console.error("[design connect] bridge shutdown failed", error);
3480
+ resolve(1);
3481
+ });
3342
3482
  };
3343
3483
  for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
3344
3484
  process.once(signal, stop);
@@ -1739,7 +1739,27 @@ function emitBrowserTrackingEvent(name, props, options = {}) {
1739
1739
  const authUserId = getTrackingAuthUserId();
1740
1740
  sendAgentNativeAnalytics(name, authUserId ? { ...props, auth_user_id: authUserId } : props);
1741
1741
  }
1742
+ // Browser events that are telemetry or already have their own replay marker.
1743
+ const REPLAY_UNMARKED_EVENT_NAMES = new Set([
1744
+ "pageview",
1745
+ "session status",
1746
+ "session_status",
1747
+ "action.response",
1748
+ "agent_chat_lifecycle",
1749
+ "session_replay_started",
1750
+ "session replay upload rejected",
1751
+ "session_replay_upload_rejected",
1752
+ AGENT_NATIVE_EXCEPTION_EVENT_NAME,
1753
+ ]);
1754
+ function markTrackedEventInSessionReplay(name) {
1755
+ if (REPLAY_UNMARKED_EVENT_NAMES.has(name))
1756
+ return;
1757
+ _sessionReplayModuleForCapture?.emitSessionReplayAnalyticsEvent?.(name);
1758
+ }
1742
1759
  export function trackEvent(name, params) {
1760
+ trackBrowserEvent(name, params, true);
1761
+ }
1762
+ function trackBrowserEvent(name, params, markInReplay) {
1743
1763
  if (typeof window === "undefined")
1744
1764
  return;
1745
1765
  if (isSyntheticBrowserTraffic())
@@ -1758,10 +1778,13 @@ export function trackEvent(name, params) {
1758
1778
  sendGtag: !gtagNameMatchesCanonical,
1759
1779
  });
1760
1780
  }
1781
+ if (markInReplay)
1782
+ markTrackedEventInSessionReplay(canonical?.name ?? name);
1761
1783
  void recordTrackingEvent(name, props, "client");
1762
1784
  const lifecycle = legacyLifecycleEvent(name, props);
1785
+ // The alias describes the same moment, so it gets no second replay marker.
1763
1786
  if (lifecycle)
1764
- trackEvent(lifecycle.name, lifecycle.properties);
1787
+ trackBrowserEvent(lifecycle.name, lifecycle.properties, false);
1765
1788
  }
1766
1789
  export function trackAnonymousEvent(name, properties) {
1767
1790
  if (typeof window === "undefined" ||
@@ -71,6 +71,7 @@ export interface SessionReplayStartResult {
71
71
  export declare const SESSION_REPLAY_CONSOLE_EVENT_TAG = "agent-native.console";
72
72
  export declare const SESSION_REPLAY_NETWORK_EVENT_TAG = "agent-native.network";
73
73
  export declare const SESSION_REPLAY_AGENT_CHAT_EVENT_TAG = "agent-native.chat";
74
+ export declare const SESSION_REPLAY_ANALYTICS_EVENT_TAG = "agent-native.event";
74
75
  export declare function getSessionReplaySamplingScore(sessionId: string, salt?: string): number;
75
76
  export declare function shouldSampleSessionReplay(sessionId: string, sampleRate?: number, salt?: string): boolean;
76
77
  export declare function flushSessionReplay(reason?: string): Promise<void>;
@@ -88,6 +89,11 @@ export declare function emitSessionReplayException(input: {
88
89
  stack?: string;
89
90
  url?: string;
90
91
  }): void;
92
+ /**
93
+ * Mark a tracked analytics event on the replay timeline. Only the event name
94
+ * is recorded; event properties stay out of the replay.
95
+ */
96
+ export declare function emitSessionReplayAnalyticsEvent(name: string): void;
91
97
  export type SessionReplayAgentChatEvent = {
92
98
  phase: "surface-mounted" | "run-observed" | "run-stopped";
93
99
  surface: string | number;
@@ -70,10 +70,13 @@ const SESSION_REPLAY_CLAIM_TIMEOUT_MS = 150;
70
70
  export const SESSION_REPLAY_CONSOLE_EVENT_TAG = "agent-native.console";
71
71
  export const SESSION_REPLAY_NETWORK_EVENT_TAG = "agent-native.network";
72
72
  export const SESSION_REPLAY_AGENT_CHAT_EVENT_TAG = "agent-native.chat";
73
+ export const SESSION_REPLAY_ANALYTICS_EVENT_TAG = "agent-native.event";
73
74
  const SESSION_REPLAY_LIFECYCLE_EVENT_TAG = "agent-native.session_replay";
74
75
  const DEFAULT_MAX_CONSOLE_EVENTS = 1000;
75
76
  const DEFAULT_MAX_NETWORK_EVENTS = 2000;
76
77
  const MAX_CONSOLE_MESSAGE_LENGTH = 500;
78
+ const MAX_ANALYTICS_EVENT_NAME_LENGTH = 120;
79
+ const MAX_ANALYTICS_EVENTS_PER_REPLAY = 1000;
77
80
  const MAX_CONSOLE_ARGS = 10;
78
81
  const MAX_CONSOLE_STACK_LENGTH = 2000;
79
82
  const MAX_CONSOLE_SERIALIZE_DEPTH = 4;
@@ -136,6 +139,8 @@ function getState() {
136
139
  removeLifecycleListeners: null,
137
140
  restoreIframeBridge: null,
138
141
  addCustomEvent: null,
142
+ analyticsEventCount: 0,
143
+ analyticsEventReplayId: null,
139
144
  takeFullSnapshot: null,
140
145
  restoreCaptures: null,
141
146
  options: null,
@@ -255,11 +260,7 @@ function removeStoredReplaySession(replayId) {
255
260
  function getOrCreateReplaySession(sessionId, linkBaseUrl) {
256
261
  clearLegacyLocalStorageReplaySession();
257
262
  const parsed = readStoredReplaySession();
258
- const parsedSequence = typeof parsed?.sequence === "number" &&
259
- Number.isFinite(parsed.sequence) &&
260
- parsed.sequence >= 0
261
- ? Math.floor(parsed.sequence)
262
- : 0;
263
+ const parsedSequence = storedCount(parsed?.sequence);
263
264
  if (parsed?.sessionId === sessionId &&
264
265
  parsed.replayId &&
265
266
  parsedSequence < MAX_REPLAY_CHUNKS_PER_RECORDING - 1) {
@@ -278,6 +279,7 @@ function getOrCreateReplaySession(sessionId, linkBaseUrl) {
278
279
  startedAtMs,
279
280
  sequence,
280
281
  ...(resolvedLinkBaseUrl ? { linkBaseUrl: resolvedLinkBaseUrl } : {}),
282
+ analyticsEventCount: storedCount(parsed.analyticsEventCount),
281
283
  resumed: true,
282
284
  };
283
285
  }
@@ -295,9 +297,15 @@ function getOrCreateReplaySession(sessionId, linkBaseUrl) {
295
297
  startedAtMs,
296
298
  sequence: 0,
297
299
  ...(linkBaseUrl ? { linkBaseUrl } : {}),
300
+ analyticsEventCount: 0,
298
301
  resumed: false,
299
302
  };
300
303
  }
304
+ function storedCount(value) {
305
+ return typeof value === "number" && Number.isFinite(value) && value >= 0
306
+ ? Math.floor(value)
307
+ : 0;
308
+ }
301
309
  function openReplayBroadcastChannel() {
302
310
  if (typeof BroadcastChannel === "undefined")
303
311
  return null;
@@ -392,6 +400,9 @@ function persistReplaySequence(sessionId, replayId, startedAtMs, sequence, linkB
392
400
  ...(linkBaseUrl || existing?.linkBaseUrl
393
401
  ? { linkBaseUrl: linkBaseUrl ?? existing?.linkBaseUrl }
394
402
  : {}),
403
+ ...(existing?.replayId === replayId && existing.analyticsEventCount
404
+ ? { analyticsEventCount: existing.analyticsEventCount }
405
+ : {}),
395
406
  });
396
407
  }
397
408
  function clampSamplingRate(value) {
@@ -2510,6 +2521,7 @@ async function startSessionReplayRecorder(state, normalized, sessionId, sampled,
2510
2521
  ...(normalized.linkBaseUrl
2511
2522
  ? { linkBaseUrl: normalized.linkBaseUrl }
2512
2523
  : {}),
2524
+ analyticsEventCount: 0,
2513
2525
  resumed: false,
2514
2526
  };
2515
2527
  }
@@ -2548,6 +2560,11 @@ async function startSessionReplayRecorder(state, normalized, sessionId, sampled,
2548
2560
  state.lastAuthenticatedProperties = replayUserEmail(initialProperties)
2549
2561
  ? { ...initialProperties }
2550
2562
  : null;
2563
+ // A resumed replay keeps its marker count, across page reloads too.
2564
+ if (state.analyticsEventReplayId !== state.replayId) {
2565
+ state.analyticsEventReplayId = state.replayId;
2566
+ state.analyticsEventCount = replaySession.analyticsEventCount;
2567
+ }
2551
2568
  state.active = true;
2552
2569
  try {
2553
2570
  const stopRecorder = rrweb.record({
@@ -2743,6 +2760,31 @@ export function emitSessionReplayException(input) {
2743
2760
  ...(input.url ? { url: input.url } : {}),
2744
2761
  });
2745
2762
  }
2763
+ /**
2764
+ * Mark a tracked analytics event on the replay timeline. Only the event name
2765
+ * is recorded; event properties stay out of the replay.
2766
+ */
2767
+ export function emitSessionReplayAnalyticsEvent(name) {
2768
+ const state = getState();
2769
+ if (!state.active || !state.addCustomEvent)
2770
+ return;
2771
+ if (state.analyticsEventCount >= MAX_ANALYTICS_EVENTS_PER_REPLAY)
2772
+ return;
2773
+ const bounded = name.trim().slice(0, MAX_ANALYTICS_EVENT_NAME_LENGTH);
2774
+ if (!bounded)
2775
+ return;
2776
+ state.analyticsEventCount += 1;
2777
+ const stored = readStoredReplaySession();
2778
+ if (stored?.replayId === state.replayId) {
2779
+ writeStoredReplaySession({
2780
+ ...stored,
2781
+ analyticsEventCount: state.analyticsEventCount,
2782
+ });
2783
+ }
2784
+ emitReplayCustomEvent(state, SESSION_REPLAY_ANALYTICS_EVENT_TAG, {
2785
+ name: bounded,
2786
+ });
2787
+ }
2746
2788
  export function emitSessionReplayAgentChatEvent(input) {
2747
2789
  const state = getState();
2748
2790
  if (!state.active || !state.addCustomEvent)
@@ -38,19 +38,19 @@ export declare const postAwareness: import("h3").EventHandlerWithFetch<import("h
38
38
  error: string;
39
39
  states?: undefined;
40
40
  } | {
41
+ error?: undefined;
41
42
  states: {
42
43
  clientId: number;
43
44
  state: string;
44
45
  }[];
45
- error?: undefined;
46
46
  }>>;
47
47
  export declare const getActiveUsers: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
48
48
  error: string;
49
49
  users?: undefined;
50
50
  } | {
51
+ error?: undefined;
51
52
  users: {
52
53
  clientId: number;
53
54
  lastSeen: number;
54
55
  }[];
55
- error?: undefined;
56
56
  }>>;
@@ -8,8 +8,8 @@ export declare const getCollabState: import("h3").EventHandlerWithFetch<import("
8
8
  state: string;
9
9
  }>>;
10
10
  export declare const postCollabUpdate: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
11
- error: string;
12
11
  ok?: undefined;
12
+ error: string;
13
13
  } | {
14
14
  error?: undefined;
15
15
  ok: boolean;
@@ -34,6 +34,13 @@ async function assertOk(res, label) {
34
34
  throw Object.assign(new Error(`${label} (${res.status}): ${body || res.statusText}`), { status: res.status });
35
35
  }
36
36
  }
37
+ function confirmedBuilderAuthorizationFailure(error) {
38
+ const cause = error instanceof Error ? error : new Error(String(error));
39
+ return Object.assign(new Error(cause.message, { cause }), {
40
+ status: 401,
41
+ errorCode: "builder_credentials_rejected",
42
+ });
43
+ }
37
44
  /**
38
45
  * Run a Builder asset API call, and on a 401 refresh the OAuth access token
39
46
  * once and retry. A token can be revoked or rotated before its stated
@@ -51,10 +58,21 @@ async function withAssetAuthorization(run, held = {}) {
51
58
  const refreshed = await assetAuthorization({ forceRefresh: true });
52
59
  // The same token again (no refresh token, a key credential) would only
53
60
  // repeat the 401; report the original refusal instead.
54
- if (refreshed.authorization === auth.authorization)
61
+ if (refreshed.authorization === auth.authorization) {
62
+ if (auth.apiKey)
63
+ throw confirmedBuilderAuthorizationFailure(error);
55
64
  throw error;
65
+ }
56
66
  held.auth = refreshed;
57
- return run(refreshed);
67
+ try {
68
+ return await run(refreshed);
69
+ }
70
+ catch (retryError) {
71
+ if (retryError?.status !== 401) {
72
+ throw retryError;
73
+ }
74
+ throw confirmedBuilderAuthorizationFailure(retryError);
75
+ }
58
76
  }
59
77
  }
60
78
  async function uploadLargeFileViaSignedUrl(input, held, bareMimeType, bytes) {