@rtrvr-ai/rover 1.1.0 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/rover.js CHANGED
@@ -11107,7 +11107,7 @@ var Bridge = class {
11107
11107
  this.runtimeId = opts.runtimeId;
11108
11108
  this.domainScopeMode = normalizeDomainScopeMode(opts.domainScopeMode);
11109
11109
  this.allowedDomains = normalizeAllowedDomains(opts.allowedDomains, window.location.hostname, this.domainScopeMode);
11110
- this.externalNavigationPolicy = normalizeExternalNavigationPolicy(opts.externalNavigationPolicy, opts.crossDomainPolicy);
11110
+ this.externalNavigationPolicy = normalizeExternalNavigationPolicy(opts.externalNavigationPolicy);
11111
11111
  this.registerOpenedTab = opts.registerOpenedTab;
11112
11112
  this.listKnownTabs = opts.listKnownTabs;
11113
11113
  this.switchToLogicalTab = opts.switchToLogicalTab;
@@ -11137,8 +11137,8 @@ var Bridge = class {
11137
11137
  if (options.allowedDomains) {
11138
11138
  this.allowedDomains = normalizeAllowedDomains(options.allowedDomains, window.location.hostname, this.domainScopeMode);
11139
11139
  }
11140
- if (options.externalNavigationPolicy || options.crossDomainPolicy) {
11141
- this.externalNavigationPolicy = normalizeExternalNavigationPolicy(options.externalNavigationPolicy, options.crossDomainPolicy);
11140
+ if (options.externalNavigationPolicy) {
11141
+ this.externalNavigationPolicy = normalizeExternalNavigationPolicy(options.externalNavigationPolicy);
11142
11142
  }
11143
11143
  }
11144
11144
  async getSnapshot() {
@@ -11465,7 +11465,8 @@ var Bridge = class {
11465
11465
  }
11466
11466
  async openUrlInNewTab(targetUrl, options) {
11467
11467
  const external = !isUrlAllowedByDomains(targetUrl, this.allowedDomains);
11468
- const popupAttempt = this.openVerifiedPopup(targetUrl);
11468
+ const knownTabIdsBeforeOpen = this.snapshotKnownTabIds();
11469
+ let popupAttempt = this.openVerifiedPopup(targetUrl);
11469
11470
  let logicalTabId;
11470
11471
  if (popupAttempt.opened && this.registerOpenedTab) {
11471
11472
  try {
@@ -11479,6 +11480,16 @@ var Bridge = class {
11479
11480
  } catch {
11480
11481
  }
11481
11482
  }
11483
+ if (popupAttempt.opened && !logicalTabId) {
11484
+ logicalTabId = await this.reconcileOpenedTab(targetUrl, knownTabIdsBeforeOpen);
11485
+ }
11486
+ if (!popupAttempt.opened) {
11487
+ const reconciledTabId = await this.reconcileOpenedTab(targetUrl, knownTabIdsBeforeOpen);
11488
+ if (reconciledTabId) {
11489
+ popupAttempt = { opened: true, verified: false };
11490
+ logicalTabId = logicalTabId || reconciledTabId;
11491
+ }
11492
+ }
11482
11493
  const registrationFailed = popupAttempt.opened && !logicalTabId && !!this.registerOpenedTab;
11483
11494
  if (!popupAttempt.opened) {
11484
11495
  const reason = "Browser popup settings blocked opening a new tab.";
@@ -11588,6 +11599,51 @@ var Bridge = class {
11588
11599
  return { opened: false, verified: false };
11589
11600
  }
11590
11601
  }
11602
+ snapshotKnownTabIds() {
11603
+ const ids = /* @__PURE__ */ new Set();
11604
+ if (!this.listKnownTabs)
11605
+ return ids;
11606
+ try {
11607
+ const tabs = this.listKnownTabs();
11608
+ for (const tab of tabs) {
11609
+ const logicalTabId = Number(tab?.logicalTabId);
11610
+ if (!Number.isFinite(logicalTabId) || logicalTabId <= 0)
11611
+ continue;
11612
+ ids.add(logicalTabId);
11613
+ }
11614
+ } catch {
11615
+ return ids;
11616
+ }
11617
+ return ids;
11618
+ }
11619
+ async reconcileOpenedTab(targetUrl, beforeIds) {
11620
+ if (!this.listKnownTabs)
11621
+ return void 0;
11622
+ const targetHost = extractHostname(targetUrl);
11623
+ const deadline = Date.now() + 2e3;
11624
+ while (Date.now() < deadline) {
11625
+ try {
11626
+ const knownTabs = this.listKnownTabs();
11627
+ for (const tab of knownTabs) {
11628
+ const logicalTabId = Number(tab?.logicalTabId);
11629
+ if (!Number.isFinite(logicalTabId) || logicalTabId <= 0)
11630
+ continue;
11631
+ if (beforeIds.has(logicalTabId))
11632
+ continue;
11633
+ const knownUrl = String(tab?.url || "").trim();
11634
+ if (!knownUrl)
11635
+ return logicalTabId;
11636
+ const knownHost = extractHostname(knownUrl);
11637
+ if (!targetHost || !knownHost || knownHost === targetHost) {
11638
+ return logicalTabId;
11639
+ }
11640
+ }
11641
+ } catch {
11642
+ }
11643
+ await new Promise((resolve) => setTimeout(resolve, 120));
11644
+ }
11645
+ return void 0;
11646
+ }
11591
11647
  async openElementInNewTab(args) {
11592
11648
  const elementId = args.element_id ?? args.source_element_id ?? args.target_element_id ?? args.center_element_id ?? null;
11593
11649
  if (!elementId) {
@@ -11721,18 +11777,11 @@ function normalizeToolName(name) {
11721
11777
  function normalizeDomainScopeMode(mode) {
11722
11778
  return mode === "host_only" ? "host_only" : "registrable_domain";
11723
11779
  }
11724
- function mapLegacyCrossDomainPolicy(policy) {
11725
- if (policy === "allow")
11726
- return "allow";
11727
- if (policy === "block_new_tab")
11728
- return "open_new_tab_notice";
11729
- return void 0;
11730
- }
11731
- function normalizeExternalNavigationPolicy(policy, legacy) {
11780
+ function normalizeExternalNavigationPolicy(policy) {
11732
11781
  if (policy === "allow" || policy === "block" || policy === "open_new_tab_notice") {
11733
11782
  return policy;
11734
11783
  }
11735
- return mapLegacyCrossDomainPolicy(legacy) || "open_new_tab_notice";
11784
+ return "open_new_tab_notice";
11736
11785
  }
11737
11786
  function normalizeAllowedDomains(input, currentHost, scopeMode) {
11738
11787
  const candidates = Array.isArray(input) ? input : [];
@@ -14639,6 +14688,14 @@ var SessionCoordinator = class _SessionCoordinator {
14639
14688
  const normalizedUrl = normalizeUrl2(payload.url);
14640
14689
  let logicalTabId = 0;
14641
14690
  this.mutate("local", (draft) => {
14691
+ const existing = draft.tabs.find((tab) => !tab.runtimeId && !!tab.external && tab.url === normalizedUrl);
14692
+ if (existing) {
14693
+ existing.title = payload.title || existing.title;
14694
+ existing.updatedAt = now();
14695
+ existing.openerRuntimeId = payload.openerRuntimeId || existing.openerRuntimeId;
14696
+ logicalTabId = existing.logicalTabId;
14697
+ return;
14698
+ }
14642
14699
  logicalTabId = draft.nextLogicalTabId++;
14643
14700
  draft.tabs.push({
14644
14701
  logicalTabId,
@@ -15045,7 +15102,7 @@ var SessionCoordinator = class _SessionCoordinator {
15045
15102
  };
15046
15103
 
15047
15104
  // dist/cloudCheckpoint.js
15048
- var DEFAULT_CLOUD_FUNCTIONS_BASE = "https://us-central1-rtrvr-extension-functions.cloudfunctions.net";
15105
+ var DEFAULT_EXTENSION_ROUTER_BASE = "https://extensionrouter.rtrvr.ai";
15049
15106
  function toFiniteNumber(value, fallback) {
15050
15107
  const n = Number(value);
15051
15108
  return Number.isFinite(n) ? n : fallback;
@@ -15071,8 +15128,22 @@ function buildRevisionKey(payload) {
15071
15128
  ].join(":");
15072
15129
  }
15073
15130
  function normalizeRouterEndpoint(apiBase) {
15074
- const base = (apiBase || DEFAULT_CLOUD_FUNCTIONS_BASE).replace(/\/$/, "");
15075
- return base.endsWith("/extensionRouter") ? base : `${base}/extensionRouter`;
15131
+ const fallback = DEFAULT_EXTENSION_ROUTER_BASE;
15132
+ const base = String(apiBase || fallback).trim().replace(/\/+$/, "");
15133
+ if (!base)
15134
+ return fallback;
15135
+ if (base.endsWith("/extensionRouter"))
15136
+ return base;
15137
+ try {
15138
+ const parsed = new URL(base);
15139
+ const pathname = parsed.pathname.replace(/\/+$/, "");
15140
+ if (pathname && pathname !== "/")
15141
+ return base;
15142
+ if (parsed.hostname.toLowerCase() === "extensionrouter.rtrvr.ai")
15143
+ return base;
15144
+ } catch {
15145
+ }
15146
+ return `${base}/extensionRouter`;
15076
15147
  }
15077
15148
  function toError(message, details) {
15078
15149
  const error = new Error(message);
@@ -15091,6 +15162,7 @@ var RoverCloudCheckpointClient = class {
15091
15162
  shouldWrite;
15092
15163
  buildCheckpoint;
15093
15164
  onCheckpoint;
15165
+ onStateChange;
15094
15166
  onError;
15095
15167
  started = false;
15096
15168
  dirty = false;
@@ -15102,8 +15174,9 @@ var RoverCloudCheckpointClient = class {
15102
15174
  lastAppliedRemoteUpdatedAt = 0;
15103
15175
  pushInFlight = false;
15104
15176
  pullInFlight = false;
15177
+ state = "active";
15105
15178
  constructor(options) {
15106
- const token = String(options.apiKey || options.authToken || "").trim();
15179
+ const token = String(options.authToken || options.apiKey || "").trim();
15107
15180
  if (!token) {
15108
15181
  throw toError("Rover cloud checkpoint requires apiKey or authToken.");
15109
15182
  }
@@ -15118,12 +15191,14 @@ var RoverCloudCheckpointClient = class {
15118
15191
  this.shouldWrite = options.shouldWrite || (() => true);
15119
15192
  this.buildCheckpoint = options.buildCheckpoint;
15120
15193
  this.onCheckpoint = options.onCheckpoint;
15194
+ this.onStateChange = options.onStateChange;
15121
15195
  this.onError = options.onError;
15122
15196
  }
15123
15197
  start() {
15124
15198
  if (this.started)
15125
15199
  return;
15126
15200
  this.started = true;
15201
+ this.setState("active");
15127
15202
  this.flushTimer = window.setInterval(() => {
15128
15203
  void this.flush(false);
15129
15204
  }, this.flushIntervalMs);
@@ -15156,6 +15231,8 @@ var RoverCloudCheckpointClient = class {
15156
15231
  async flush(force) {
15157
15232
  if (this.pushInFlight)
15158
15233
  return;
15234
+ if (this.state === "paused_auth")
15235
+ return;
15159
15236
  if (!this.dirty && !force)
15160
15237
  return;
15161
15238
  if (!this.shouldWrite())
@@ -15189,8 +15266,9 @@ var RoverCloudCheckpointClient = class {
15189
15266
  this.applyRemoteCheckpoint(response.checkpoint, "push_stale");
15190
15267
  this.dirty = false;
15191
15268
  }
15269
+ this.setState("active");
15192
15270
  } catch (error) {
15193
- this.onError?.(error);
15271
+ this.handleCheckpointError(error, "roverSessionCheckpointUpsert");
15194
15272
  } finally {
15195
15273
  this.pushInFlight = false;
15196
15274
  }
@@ -15198,6 +15276,8 @@ var RoverCloudCheckpointClient = class {
15198
15276
  async pull(force) {
15199
15277
  if (this.pullInFlight)
15200
15278
  return;
15279
+ if (this.state === "paused_auth")
15280
+ return;
15201
15281
  if (!force && Date.now() - this.lastPullAt < this.pullIntervalMs)
15202
15282
  return;
15203
15283
  this.pullInFlight = true;
@@ -15210,8 +15290,9 @@ var RoverCloudCheckpointClient = class {
15210
15290
  if (!response?.found || !response?.checkpoint || typeof response.checkpoint !== "object")
15211
15291
  return;
15212
15292
  this.applyRemoteCheckpoint(response.checkpoint, "pull");
15293
+ this.setState("active");
15213
15294
  } catch (error) {
15214
- this.onError?.(error);
15295
+ this.handleCheckpointError(error, "roverSessionCheckpointGet");
15215
15296
  } finally {
15216
15297
  this.pullInFlight = false;
15217
15298
  }
@@ -15240,14 +15321,68 @@ var RoverCloudCheckpointClient = class {
15240
15321
  const text = await response.text().catch(() => "");
15241
15322
  payload2 = text ? { error: truncateText(text, 2e3) } : void 0;
15242
15323
  }
15243
- throw toError(`Checkpoint HTTP ${response.status}`, payload2);
15324
+ throw toError(`Checkpoint HTTP ${response.status}`, {
15325
+ action,
15326
+ status: response.status,
15327
+ payload: payload2
15328
+ });
15244
15329
  }
15245
15330
  const payload = await response.json();
15246
15331
  if (payload?.success === false) {
15247
- throw toError("Checkpoint extensionRouter returned success=false", payload);
15332
+ throw toError("Checkpoint extensionRouter returned success=false", {
15333
+ action,
15334
+ status: response.status,
15335
+ payload
15336
+ });
15248
15337
  }
15249
15338
  return payload?.data;
15250
15339
  }
15340
+ setState(next, context) {
15341
+ if (this.state === next)
15342
+ return;
15343
+ this.state = next;
15344
+ this.onStateChange?.(next, context || {});
15345
+ }
15346
+ handleCheckpointError(error, action) {
15347
+ const details = this.normalizeCheckpointError(error);
15348
+ const isAuthFailure = this.isAuthFailure(details);
15349
+ if (isAuthFailure) {
15350
+ this.setState("paused_auth", {
15351
+ reason: "auth_failed",
15352
+ action,
15353
+ code: details.code,
15354
+ message: details.message
15355
+ });
15356
+ }
15357
+ this.onError?.(error, {
15358
+ action,
15359
+ state: this.state,
15360
+ code: details.code,
15361
+ message: details.message,
15362
+ status: details.status,
15363
+ paused: isAuthFailure
15364
+ });
15365
+ }
15366
+ normalizeCheckpointError(error) {
15367
+ const anyError = error;
15368
+ const details = anyError?.details;
15369
+ const payload = details?.payload || details?.response || details;
15370
+ const candidateCode = payload?.errorCode || payload?.errorDetails?.code || payload?.error?.code || payload?.code;
15371
+ const candidateMessage = payload?.error || payload?.errorDetails?.message || payload?.error?.message || anyError?.message || "Checkpoint request failed";
15372
+ const status = Number(details?.status);
15373
+ return {
15374
+ code: typeof candidateCode === "string" ? candidateCode : void 0,
15375
+ message: truncateText(candidateMessage, 1e3),
15376
+ status: Number.isFinite(status) ? status : void 0
15377
+ };
15378
+ }
15379
+ isAuthFailure(details) {
15380
+ const code = String(details.code || "").toUpperCase();
15381
+ if (code === "INVALID_API_KEY" || code === "MISSING_API_KEY" || code === "UNAUTHENTICATED" || code === "PERMISSION_DENIED") {
15382
+ return true;
15383
+ }
15384
+ return details.status === 401 || details.status === 403;
15385
+ }
15251
15386
  };
15252
15387
 
15253
15388
  // dist/runtimeStorage.js
@@ -15387,6 +15522,9 @@ var VISITOR_COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 365;
15387
15522
  var CHECKPOINT_PAYLOAD_VERSION = 1;
15388
15523
  var ACTIVE_PENDING_RUN_GRACE_MS = 3e3;
15389
15524
  var STALE_PENDING_RUN_MS = 9e4;
15525
+ var TELEMETRY_DEFAULT_FLUSH_INTERVAL_MS = 12e3;
15526
+ var TELEMETRY_DEFAULT_MAX_BATCH_SIZE = 30;
15527
+ var TELEMETRY_MAX_BUFFER_SIZE = 240;
15390
15528
  var instance = null;
15391
15529
  var bridge = null;
15392
15530
  var worker = null;
@@ -15398,6 +15536,11 @@ var runtimeStateStore = null;
15398
15536
  var runtimeId = "";
15399
15537
  var sessionCoordinator = null;
15400
15538
  var cloudCheckpointClient = null;
15539
+ var telemetryFlushTimer = null;
15540
+ var telemetryBuffer = [];
15541
+ var telemetryInFlight = false;
15542
+ var telemetryPausedAuth = false;
15543
+ var telemetrySeq = 0;
15401
15544
  var resolvedVisitorId = void 0;
15402
15545
  var suppressCheckpointSync = false;
15403
15546
  var currentMode = "controller";
@@ -15423,6 +15566,7 @@ var RUN_SCOPED_WORKER_MESSAGE_TYPES = /* @__PURE__ */ new Set([
15423
15566
  var pendingToolRegistrations = [];
15424
15567
  var eventHandlers = /* @__PURE__ */ new Map();
15425
15568
  function emit(event, payload) {
15569
+ recordTelemetryEvent(event, payload);
15426
15570
  const handlers = eventHandlers.get(event);
15427
15571
  if (!handlers)
15428
15572
  return;
@@ -15628,20 +15772,207 @@ function safeSerialize(value) {
15628
15772
  }
15629
15773
  }
15630
15774
  }
15631
- function resolveEmbeddedDomainLabel(allowedDomains) {
15632
- const raw = String((allowedDomains || [])[0] || "").trim();
15633
- if (!raw)
15634
- return "the configured domain";
15635
- return raw.replace(/^\*?\./, "").replace(/^=/, "");
15775
+ function normalizeTelemetryConfig(cfg) {
15776
+ const raw = cfg?.telemetry;
15777
+ const sampleRateRaw = Number(raw?.sampleRate);
15778
+ const sampleRate = Number.isFinite(sampleRateRaw) ? Math.min(1, Math.max(0, sampleRateRaw)) : 1;
15779
+ const flushRaw = Number(raw?.flushIntervalMs);
15780
+ const flushIntervalMs = Number.isFinite(flushRaw) ? Math.min(6e4, Math.max(2e3, Math.floor(flushRaw))) : TELEMETRY_DEFAULT_FLUSH_INTERVAL_MS;
15781
+ const batchRaw = Number(raw?.maxBatchSize);
15782
+ const maxBatchSize = Number.isFinite(batchRaw) ? Math.min(80, Math.max(1, Math.floor(batchRaw))) : TELEMETRY_DEFAULT_MAX_BATCH_SIZE;
15783
+ return {
15784
+ enabled: raw?.enabled !== false,
15785
+ sampleRate,
15786
+ flushIntervalMs,
15787
+ maxBatchSize,
15788
+ includePayloads: raw?.includePayloads === true
15789
+ };
15636
15790
  }
15637
- function buildInaccessibleTabPageData(cfg, tab, reason = "tab_not_accessible") {
15791
+ function canUseTelemetry(cfg) {
15792
+ if (!cfg)
15793
+ return false;
15794
+ const telemetry = normalizeTelemetryConfig(cfg);
15795
+ if (!telemetry.enabled)
15796
+ return false;
15797
+ if (!(cfg.authToken || cfg.apiKey))
15798
+ return false;
15799
+ return true;
15800
+ }
15801
+ function summarizeTelemetryPayload(payload) {
15802
+ if (payload == null)
15803
+ return void 0;
15804
+ if (typeof payload === "string")
15805
+ return truncateText2(payload, 260);
15806
+ if (typeof payload === "number" || typeof payload === "boolean")
15807
+ return payload;
15808
+ if (Array.isArray(payload)) {
15809
+ return { type: "array", length: payload.length };
15810
+ }
15811
+ if (typeof payload === "object") {
15812
+ const keys = Object.keys(payload).slice(0, 20);
15813
+ const summary = { type: "object", keys };
15814
+ const preferredKeys = ["code", "message", "stage", "status", "reason", "taskId", "runId", "policyAction"];
15815
+ for (const key of preferredKeys) {
15816
+ const value = payload?.[key];
15817
+ if (value == null)
15818
+ continue;
15819
+ if (typeof value === "string")
15820
+ summary[key] = truncateText2(value, 180);
15821
+ else if (typeof value === "number" || typeof value === "boolean")
15822
+ summary[key] = value;
15823
+ }
15824
+ return summary;
15825
+ }
15826
+ return void 0;
15827
+ }
15828
+ function buildTelemetryPayload(payload, includePayloads) {
15829
+ if (!includePayloads) {
15830
+ return summarizeTelemetryPayload(payload);
15831
+ }
15832
+ const cloned = cloneUnknown2(payload);
15833
+ if (cloned == null)
15834
+ return void 0;
15835
+ if (typeof cloned === "string")
15836
+ return truncateText2(cloned, 1e3);
15837
+ if (typeof cloned === "number" || typeof cloned === "boolean")
15838
+ return cloned;
15839
+ if (Array.isArray(cloned))
15840
+ return { type: "array", length: cloned.length };
15841
+ if (typeof cloned === "object")
15842
+ return cloned;
15843
+ return summarizeTelemetryPayload(payload);
15844
+ }
15845
+ function stopTelemetry() {
15846
+ if (telemetryFlushTimer) {
15847
+ clearInterval(telemetryFlushTimer);
15848
+ telemetryFlushTimer = null;
15849
+ }
15850
+ }
15851
+ var DEFAULT_EXTENSION_ROUTER_BASE2 = "https://extensionrouter.rtrvr.ai";
15852
+ function resolveExtensionRouterEndpoint(apiBase) {
15853
+ const fallback = DEFAULT_EXTENSION_ROUTER_BASE2;
15854
+ const base = String(apiBase || fallback).trim().replace(/\/+$/, "");
15855
+ if (!base)
15856
+ return fallback;
15857
+ if (base.endsWith("/extensionRouter"))
15858
+ return base;
15859
+ try {
15860
+ const parsed = new URL(base);
15861
+ const pathname = parsed.pathname.replace(/\/+$/, "");
15862
+ if (pathname && pathname !== "/")
15863
+ return base;
15864
+ if (parsed.hostname.toLowerCase() === "extensionrouter.rtrvr.ai")
15865
+ return base;
15866
+ } catch {
15867
+ }
15868
+ return `${base}/extensionRouter`;
15869
+ }
15870
+ function getTelemetryEndpoint(cfg) {
15871
+ return resolveExtensionRouterEndpoint(cfg.apiBase);
15872
+ }
15873
+ async function flushTelemetry(force = false) {
15874
+ if (telemetryInFlight)
15875
+ return;
15876
+ if (telemetryPausedAuth)
15877
+ return;
15878
+ if (!currentConfig || !canUseTelemetry(currentConfig)) {
15879
+ telemetryBuffer = [];
15880
+ return;
15881
+ }
15882
+ if (!telemetryBuffer.length)
15883
+ return;
15884
+ const telemetry = normalizeTelemetryConfig(currentConfig);
15885
+ const token = String(currentConfig.authToken || currentConfig.apiKey || "").trim();
15886
+ if (!token)
15887
+ return;
15888
+ const batch = telemetryBuffer.splice(0, telemetry.maxBatchSize);
15889
+ if (!batch.length)
15890
+ return;
15891
+ telemetryInFlight = true;
15892
+ try {
15893
+ const response = await fetch(getTelemetryEndpoint(currentConfig), {
15894
+ method: "POST",
15895
+ headers: {
15896
+ Authorization: `Bearer ${token}`,
15897
+ "Content-Type": "application/json"
15898
+ },
15899
+ body: JSON.stringify({
15900
+ action: "roverTelemetryIngest",
15901
+ data: {
15902
+ siteId: currentConfig.siteId,
15903
+ runtimeId,
15904
+ sessionId: runtimeState?.sessionId,
15905
+ visitorId: resolvedVisitorId,
15906
+ flushReason: force ? "manual" : "interval",
15907
+ sdkVersion: "rover_sdk_v1",
15908
+ pageUrl: window.location.href,
15909
+ userAgent: navigator.userAgent,
15910
+ sampleRate: telemetry.sampleRate,
15911
+ events: batch
15912
+ }
15913
+ })
15914
+ });
15915
+ if (!response.ok) {
15916
+ if (response.status === 401 || response.status === 403) {
15917
+ telemetryPausedAuth = true;
15918
+ } else {
15919
+ telemetryBuffer = [...batch, ...telemetryBuffer].slice(-TELEMETRY_MAX_BUFFER_SIZE);
15920
+ }
15921
+ return;
15922
+ }
15923
+ const payload = await response.json().catch(() => void 0);
15924
+ if (payload?.success === false) {
15925
+ const code = String(payload?.errorCode || payload?.errorDetails?.code || "").toUpperCase();
15926
+ if (code === "INVALID_API_KEY" || code === "MISSING_API_KEY" || code === "UNAUTHENTICATED" || code === "PERMISSION_DENIED") {
15927
+ telemetryPausedAuth = true;
15928
+ }
15929
+ return;
15930
+ }
15931
+ } catch {
15932
+ telemetryBuffer = [...batch, ...telemetryBuffer].slice(-TELEMETRY_MAX_BUFFER_SIZE);
15933
+ } finally {
15934
+ telemetryInFlight = false;
15935
+ }
15936
+ }
15937
+ function setupTelemetry(cfg) {
15938
+ stopTelemetry();
15939
+ telemetryPausedAuth = false;
15940
+ if (!canUseTelemetry(cfg)) {
15941
+ telemetryBuffer = [];
15942
+ return;
15943
+ }
15944
+ const telemetry = normalizeTelemetryConfig(cfg);
15945
+ telemetryFlushTimer = setInterval(() => {
15946
+ void flushTelemetry(false);
15947
+ }, telemetry.flushIntervalMs);
15948
+ }
15949
+ function recordTelemetryEvent(event, payload) {
15950
+ if (!canUseTelemetry(currentConfig) || telemetryPausedAuth)
15951
+ return;
15952
+ const telemetry = normalizeTelemetryConfig(currentConfig);
15953
+ if (telemetry.sampleRate < 1 && Math.random() > telemetry.sampleRate)
15954
+ return;
15955
+ const next = {
15956
+ name: event,
15957
+ ts: Date.now(),
15958
+ seq: ++telemetrySeq,
15959
+ payload: buildTelemetryPayload(payload, telemetry.includePayloads)
15960
+ };
15961
+ telemetryBuffer.push(next);
15962
+ if (telemetryBuffer.length > TELEMETRY_MAX_BUFFER_SIZE) {
15963
+ telemetryBuffer = telemetryBuffer.slice(-TELEMETRY_MAX_BUFFER_SIZE);
15964
+ }
15965
+ if (telemetryBuffer.length >= telemetry.maxBatchSize) {
15966
+ void flushTelemetry(false);
15967
+ }
15968
+ }
15969
+ function buildInaccessibleTabPageData(_cfg, tab, reason = "tab_not_accessible") {
15638
15970
  const logicalTabId = Number(tab?.logicalTabId) || void 0;
15639
15971
  const url = tab?.url || "";
15640
15972
  const title = tab?.title || (tab?.external ? "External Tab (Inaccessible)" : "Inactive Tab");
15641
- const embeddedDomain = resolveEmbeddedDomainLabel(cfg.allowedDomains);
15642
15973
  const normalizedReason = String(reason || "").trim();
15643
15974
  const reasonLine = normalizedReason ? ` Reason: ${normalizedReason}.` : "";
15644
- const content = tab?.external ? `Rover is embedded inside ${embeddedDomain}. This tab is outside direct DOM access and only virtual tab context is available.${reasonLine}` : `This tab is currently not attached to an active Rover runtime. Switch to a live tab or reopen it.${reasonLine}`;
15975
+ const content = tab?.external ? `This external tab is tracked in virtual mode only. Live DOM control and accessibility-tree access are unavailable here.${reasonLine}` : `This tab is currently not attached to an active Rover runtime. Switch to a live tab or reopen it.${reasonLine}`;
15645
15976
  return {
15646
15977
  url,
15647
15978
  title,
@@ -15659,8 +15990,7 @@ function buildInaccessibleTabPageData(cfg, tab, reason = "tab_not_accessible") {
15659
15990
  function buildTabAccessToolError(cfg, tab, reason = "tab_not_accessible") {
15660
15991
  const logicalTabId = Number(tab?.logicalTabId) || 0;
15661
15992
  const blockedUrl = tab?.url || "";
15662
- const embeddedDomain = resolveEmbeddedDomainLabel(cfg.allowedDomains);
15663
- const message = tab?.external ? `Tab ${logicalTabId} is outside Rover's embedded domain scope (${embeddedDomain}) and cannot be controlled directly.` : `Tab ${logicalTabId} is not attached to an active Rover runtime.`;
15993
+ const message = tab?.external ? `Tab ${logicalTabId} is external to the active runtime and cannot be controlled directly.` : `Tab ${logicalTabId} is not attached to an active Rover runtime.`;
15664
15994
  const code = tab?.external ? "DOMAIN_SCOPE_BLOCKED" : "TAB_NOT_ACCESSIBLE";
15665
15995
  return {
15666
15996
  success: false,
@@ -16010,6 +16340,8 @@ function ensureUnloadHandler() {
16010
16340
  sessionCoordinator?.releaseWorkflowLock(runtimeState.pendingRun.id);
16011
16341
  }
16012
16342
  persistRuntimeState();
16343
+ void flushTelemetry(true);
16344
+ stopTelemetry();
16013
16345
  cloudCheckpointClient?.markDirty();
16014
16346
  cloudCheckpointClient?.syncNow();
16015
16347
  cloudCheckpointClient?.stop();
@@ -16025,6 +16357,9 @@ function ensureUnloadHandler() {
16025
16357
  sessionCoordinator.claimLease(false);
16026
16358
  }
16027
16359
  autoResumeAttempted = false;
16360
+ if (currentConfig) {
16361
+ setupTelemetry(currentConfig);
16362
+ }
16028
16363
  };
16029
16364
  window.addEventListener("pageshow", onPageShow);
16030
16365
  }
@@ -16641,6 +16976,10 @@ function setupCloudCheckpointing(cfg) {
16641
16976
  if (!resolvedVisitorId)
16642
16977
  return;
16643
16978
  try {
16979
+ const emitCheckpointState = (payload) => {
16980
+ emit("checkpoint_state", payload);
16981
+ cfg.checkpointing?.onStateChange?.(payload);
16982
+ };
16644
16983
  cloudCheckpointClient = new RoverCloudCheckpointClient({
16645
16984
  apiBase: cfg.apiBase,
16646
16985
  apiKey: cfg.apiKey,
@@ -16656,7 +16995,36 @@ function setupCloudCheckpointing(cfg) {
16656
16995
  onCheckpoint: (payload) => {
16657
16996
  applyCloudCheckpointPayload(payload);
16658
16997
  },
16659
- onError: () => {
16998
+ onStateChange: (state, context) => {
16999
+ emitCheckpointState({
17000
+ state,
17001
+ reason: context.reason,
17002
+ action: context.action,
17003
+ code: context.code,
17004
+ message: context.message
17005
+ });
17006
+ if (state === "paused_auth") {
17007
+ emit("checkpoint_error", {
17008
+ action: context.action,
17009
+ code: context.code || "INVALID_API_KEY",
17010
+ message: context.message || "Checkpoint sync paused due to auth failure.",
17011
+ disabled: true,
17012
+ reason: context.reason || "auth_failed"
17013
+ });
17014
+ }
17015
+ },
17016
+ onError: (_error, context) => {
17017
+ cfg.checkpointing?.onError?.(context);
17018
+ if (!context.paused) {
17019
+ emit("checkpoint_error", {
17020
+ action: context.action,
17021
+ code: context.code,
17022
+ message: context.message,
17023
+ status: context.status,
17024
+ disabled: false,
17025
+ reason: "transient_failure"
17026
+ });
17027
+ }
16660
17028
  }
16661
17029
  });
16662
17030
  cloudCheckpointClient.start();
@@ -16965,6 +17333,10 @@ function handleWorkerMessage(msg) {
16965
17333
  const needsUserInput = completionState.needsUserInput;
16966
17334
  if (taskComplete) {
16967
17335
  markTaskCompleted("worker_task_complete");
17336
+ sessionCoordinator?.pruneTabs({
17337
+ dropRuntimeDetached: true,
17338
+ dropAllDetachedExternal: true
17339
+ });
16968
17340
  setUiStatus("Task completed");
16969
17341
  finalizeSuccessfulRunTimeline(typeof msg.runId === "string" ? msg.runId : void 0);
16970
17342
  } else {
@@ -17022,6 +17394,7 @@ function createRuntime(cfg) {
17022
17394
  }
17023
17395
  }
17024
17396
  setupCloudCheckpointing(cfg);
17397
+ setupTelemetry(cfg);
17025
17398
  const initialAllowActions = (cfg.allowActions ?? true) && (sessionCoordinator ? sessionCoordinator.isController() : true);
17026
17399
  bridge = new Bridge({
17027
17400
  allowActions: initialAllowActions,
@@ -17029,7 +17402,6 @@ function createRuntime(cfg) {
17029
17402
  allowedDomains: cfg.allowedDomains,
17030
17403
  domainScopeMode: cfg.domainScopeMode,
17031
17404
  externalNavigationPolicy: cfg.externalNavigationPolicy,
17032
- crossDomainPolicy: cfg.crossDomainPolicy,
17033
17405
  registerOpenedTab: (payload) => sessionCoordinator?.registerOpenedTab(payload),
17034
17406
  switchToLogicalTab: (logicalTabId) => sessionCoordinator?.switchToLogicalTab(logicalTabId) || { ok: false, reason: "No session coordinator" },
17035
17407
  listKnownTabs: () => (sessionCoordinator?.listTabs() || []).map((tab) => ({
@@ -17406,16 +17778,16 @@ function update(cfg) {
17406
17778
  setupSessionCoordinator(currentConfig);
17407
17779
  }
17408
17780
  setupCloudCheckpointing(currentConfig);
17781
+ setupTelemetry(currentConfig);
17409
17782
  if (bridge) {
17410
17783
  if (typeof cfg.allowActions === "boolean") {
17411
17784
  bridge.setAllowActions(cfg.allowActions && currentMode === "controller");
17412
17785
  }
17413
- if (cfg.allowedDomains || cfg.crossDomainPolicy || cfg.domainScopeMode || cfg.externalNavigationPolicy) {
17786
+ if (cfg.allowedDomains || cfg.domainScopeMode || cfg.externalNavigationPolicy) {
17414
17787
  bridge.setNavigationPolicy({
17415
17788
  allowedDomains: cfg.allowedDomains,
17416
17789
  domainScopeMode: cfg.domainScopeMode,
17417
- externalNavigationPolicy: cfg.externalNavigationPolicy,
17418
- crossDomainPolicy: cfg.crossDomainPolicy
17790
+ externalNavigationPolicy: cfg.externalNavigationPolicy
17419
17791
  });
17420
17792
  }
17421
17793
  }
@@ -17430,6 +17802,8 @@ function update(cfg) {
17430
17802
  function shutdown() {
17431
17803
  hideTaskSuggestion();
17432
17804
  persistRuntimeState();
17805
+ void flushTelemetry(true);
17806
+ stopTelemetry();
17433
17807
  cloudCheckpointClient?.markDirty();
17434
17808
  cloudCheckpointClient?.syncNow();
17435
17809
  cloudCheckpointClient?.stop();
@@ -17447,6 +17821,10 @@ function shutdown() {
17447
17821
  runtimeId = "";
17448
17822
  resolvedVisitorId = void 0;
17449
17823
  suppressCheckpointSync = false;
17824
+ telemetryInFlight = false;
17825
+ telemetryPausedAuth = false;
17826
+ telemetryBuffer = [];
17827
+ telemetrySeq = 0;
17450
17828
  currentMode = "controller";
17451
17829
  pendingTaskSuggestion = null;
17452
17830
  runtimeStateStore = null;