@rtrvr-ai/rover 1.0.3 → 1.1.1

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,9 +11465,10 @@ var Bridge = class {
11465
11465
  }
11466
11466
  async openUrlInNewTab(targetUrl, options) {
11467
11467
  const external = !isUrlAllowedByDomains(targetUrl, this.allowedDomains);
11468
- const popup = window.open(targetUrl, "_blank", "noopener,noreferrer");
11468
+ const knownTabIdsBeforeOpen = this.snapshotKnownTabIds();
11469
+ let popupAttempt = this.openVerifiedPopup(targetUrl);
11469
11470
  let logicalTabId;
11470
- if (this.registerOpenedTab) {
11471
+ if (popupAttempt.opened && this.registerOpenedTab) {
11471
11472
  try {
11472
11473
  const registered = await this.registerOpenedTab({
11473
11474
  url: targetUrl,
@@ -11479,8 +11480,18 @@ var Bridge = class {
11479
11480
  } catch {
11480
11481
  }
11481
11482
  }
11482
- const registrationFailed = !logicalTabId && !!this.registerOpenedTab;
11483
- if (!popup) {
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
+ }
11493
+ const registrationFailed = popupAttempt.opened && !logicalTabId && !!this.registerOpenedTab;
11494
+ if (!popupAttempt.opened) {
11484
11495
  const reason = "Browser popup settings blocked opening a new tab.";
11485
11496
  if (options?.policyBlocked) {
11486
11497
  this.emitNavigationGuardrail({
@@ -11516,6 +11527,13 @@ var Bridge = class {
11516
11527
  openedInNewTab: true
11517
11528
  });
11518
11529
  }
11530
+ const warningMessages = [];
11531
+ if (registrationFailed) {
11532
+ warningMessages.push("Tab opened but registration failed; tab may not be targetable.");
11533
+ }
11534
+ if (!popupAttempt.verified) {
11535
+ warningMessages.push("Tab open was triggered, but browser did not return a popup handle.");
11536
+ }
11519
11537
  return {
11520
11538
  success: true,
11521
11539
  output: {
@@ -11523,13 +11541,109 @@ var Bridge = class {
11523
11541
  external,
11524
11542
  logicalTabId,
11525
11543
  openedInNewTab: true,
11544
+ openVerification: popupAttempt.verified ? "verified" : "unverified",
11526
11545
  policyBlocked: !!options?.policyBlocked,
11527
11546
  message,
11528
- ...registrationFailed && { warning: "Tab opened but registration failed \u2014 tab may not be targetable" }
11547
+ ...warningMessages.length ? { warning: warningMessages.join(" ") } : {}
11529
11548
  },
11530
11549
  allowFallback: true
11531
11550
  };
11532
11551
  }
11552
+ openVerifiedPopup(targetUrl) {
11553
+ const popup = window.open("about:blank", "_blank");
11554
+ if (popup) {
11555
+ try {
11556
+ popup.opener = null;
11557
+ } catch {
11558
+ }
11559
+ try {
11560
+ popup.location.href = targetUrl;
11561
+ } catch {
11562
+ }
11563
+ return { opened: true, verified: true };
11564
+ }
11565
+ try {
11566
+ const directPopup = window.open(targetUrl, "_blank");
11567
+ if (directPopup) {
11568
+ try {
11569
+ directPopup.opener = null;
11570
+ } catch {
11571
+ }
11572
+ return { opened: true, verified: true };
11573
+ }
11574
+ } catch {
11575
+ }
11576
+ try {
11577
+ const noOpenerPopup = window.open(targetUrl, "_blank", "noopener,noreferrer");
11578
+ if (noOpenerPopup) {
11579
+ try {
11580
+ noOpenerPopup.opener = null;
11581
+ } catch {
11582
+ }
11583
+ return { opened: true, verified: true };
11584
+ }
11585
+ return { opened: true, verified: false };
11586
+ } catch {
11587
+ }
11588
+ try {
11589
+ const anchor = document.createElement("a");
11590
+ anchor.href = targetUrl;
11591
+ anchor.target = "_blank";
11592
+ anchor.rel = "noopener noreferrer";
11593
+ anchor.style.display = "none";
11594
+ document.body.appendChild(anchor);
11595
+ anchor.click();
11596
+ anchor.remove();
11597
+ return { opened: true, verified: false };
11598
+ } catch {
11599
+ return { opened: false, verified: false };
11600
+ }
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
+ }
11533
11647
  async openElementInNewTab(args) {
11534
11648
  const elementId = args.element_id ?? args.source_element_id ?? args.target_element_id ?? args.center_element_id ?? null;
11535
11649
  if (!elementId) {
@@ -11663,18 +11777,11 @@ function normalizeToolName(name) {
11663
11777
  function normalizeDomainScopeMode(mode) {
11664
11778
  return mode === "host_only" ? "host_only" : "registrable_domain";
11665
11779
  }
11666
- function mapLegacyCrossDomainPolicy(policy) {
11667
- if (policy === "allow")
11668
- return "allow";
11669
- if (policy === "block_new_tab")
11670
- return "open_new_tab_notice";
11671
- return void 0;
11672
- }
11673
- function normalizeExternalNavigationPolicy(policy, legacy) {
11780
+ function normalizeExternalNavigationPolicy(policy) {
11674
11781
  if (policy === "allow" || policy === "block" || policy === "open_new_tab_notice") {
11675
11782
  return policy;
11676
11783
  }
11677
- return mapLegacyCrossDomainPolicy(legacy) || "open_new_tab_notice";
11784
+ return "open_new_tab_notice";
11678
11785
  }
11679
11786
  function normalizeAllowedDomains(input, currentHost, scopeMode) {
11680
11787
  const candidates = Array.isArray(input) ? input : [];
@@ -11950,6 +12057,7 @@ function bindRpc(port, handlers) {
11950
12057
  }
11951
12058
 
11952
12059
  // ../ui/dist/mount.js
12060
+ var DEFAULT_AGENT_NAME = "Rover";
11953
12061
  var DEFAULT_MASCOT_MP4 = "https://www.rtrvr.ai/rover/mascot.mp4";
11954
12062
  var DEFAULT_MASCOT_WEBM = "https://www.rtrvr.ai/rover/mascot.webm";
11955
12063
  var EXPAND_THRESHOLD_OUTPUT = 280;
@@ -11999,6 +12107,22 @@ function deriveTraceKey(event) {
11999
12107
  function sanitizeText(text) {
12000
12108
  return String(text || "").trim();
12001
12109
  }
12110
+ function resolveAgentName(input) {
12111
+ const normalized = String(input || "").trim();
12112
+ if (!normalized)
12113
+ return DEFAULT_AGENT_NAME;
12114
+ return normalized.slice(0, 64);
12115
+ }
12116
+ function deriveAgentInitial(name) {
12117
+ const normalized = String(name || "").trim();
12118
+ if (!normalized)
12119
+ return "R";
12120
+ return normalized[0].toUpperCase();
12121
+ }
12122
+ function deriveLauncherToken(name) {
12123
+ const compact = String(name || "").replace(/[^a-zA-Z0-9]/g, "").slice(0, 3).toUpperCase();
12124
+ return compact || "RVR";
12125
+ }
12002
12126
  function parseStageFromTitle(title) {
12003
12127
  const clean = sanitizeText(title);
12004
12128
  const match = /^(Analyze|Route|Execute|Verify|Complete):\s*(.*)$/i.exec(clean);
@@ -12174,6 +12298,9 @@ function createExpandableRichContent(text, threshold) {
12174
12298
  return wrapper;
12175
12299
  }
12176
12300
  function mountWidget(opts) {
12301
+ const agentName = resolveAgentName(opts.agent?.name);
12302
+ const agentInitial = deriveAgentInitial(agentName);
12303
+ const launcherToken = deriveLauncherToken(agentName);
12177
12304
  const host = document.createElement("div");
12178
12305
  host.id = "rover-widget-root";
12179
12306
  document.documentElement.appendChild(host);
@@ -13220,7 +13347,7 @@ function mountWidget(opts) {
13220
13347
  wrapper.dataset.mood = "idle";
13221
13348
  const launcher = document.createElement("button");
13222
13349
  launcher.className = "launcher";
13223
- launcher.setAttribute("aria-label", "Open Rover assistant");
13350
+ launcher.setAttribute("aria-label", `Open ${agentName} assistant`);
13224
13351
  const mascotDisabled = opts.mascot?.disabled === true;
13225
13352
  let launcherVideo = null;
13226
13353
  if (!mascotDisabled) {
@@ -13242,7 +13369,7 @@ function mountWidget(opts) {
13242
13369
  }
13243
13370
  const launcherFallback = document.createElement("span");
13244
13371
  launcherFallback.className = "launcherFallback";
13245
- launcherFallback.textContent = "RVR";
13372
+ launcherFallback.textContent = launcherToken;
13246
13373
  const launcherShine = document.createElement("div");
13247
13374
  launcherShine.className = "launcherShine";
13248
13375
  launcher.appendChild(launcherFallback);
@@ -13260,13 +13387,13 @@ function mountWidget(opts) {
13260
13387
  }
13261
13388
  const avatarFallback = document.createElement("span");
13262
13389
  avatarFallback.className = "avatarFallback";
13263
- avatarFallback.textContent = "R";
13390
+ avatarFallback.textContent = agentInitial;
13264
13391
  avatar.appendChild(avatarFallback);
13265
13392
  const meta = document.createElement("div");
13266
13393
  meta.className = "meta";
13267
13394
  const titleEl = document.createElement("div");
13268
13395
  titleEl.className = "title";
13269
- titleEl.textContent = "Rover";
13396
+ titleEl.textContent = agentName;
13270
13397
  const statusEl = document.createElement("div");
13271
13398
  statusEl.className = "status";
13272
13399
  const statusDot = document.createElement("span");
@@ -13399,7 +13526,7 @@ function mountWidget(opts) {
13399
13526
  composer.className = "composer";
13400
13527
  const composerTextarea = document.createElement("textarea");
13401
13528
  composerTextarea.rows = 1;
13402
- composerTextarea.placeholder = "Ask Rover to act on this website...";
13529
+ composerTextarea.placeholder = `Ask ${agentName} to act on this website...`;
13403
13530
  composer.appendChild(composerTextarea);
13404
13531
  const sendButton = document.createElement("button");
13405
13532
  sendButton.type = "submit";
@@ -13707,7 +13834,7 @@ function mountWidget(opts) {
13707
13834
  if (mode === "controller") {
13708
13835
  modeBadge.textContent = "active";
13709
13836
  menuTakeControl.style.display = "none";
13710
- inputEl.placeholder = "Ask Rover to act on this website...";
13837
+ inputEl.placeholder = `Ask ${agentName} to act on this website...`;
13711
13838
  } else {
13712
13839
  modeBadge.textContent = "observer";
13713
13840
  if (executionMeta?.canTakeControl !== false) {
@@ -13720,7 +13847,7 @@ function mountWidget(opts) {
13720
13847
  } else if (canComposeInObserver) {
13721
13848
  inputEl.placeholder = "Send to take control and run here.";
13722
13849
  } else if (executionMeta?.activeLogicalTabId && executionMeta?.localLogicalTabId && executionMeta.activeLogicalTabId !== executionMeta.localLogicalTabId) {
13723
- inputEl.placeholder = `Observing: Rover is acting in tab #${executionMeta.activeLogicalTabId}`;
13850
+ inputEl.placeholder = `Observing: ${agentName} is acting in tab #${executionMeta.activeLogicalTabId}`;
13724
13851
  } else {
13725
13852
  inputEl.placeholder = "Observer mode. Take control to run actions here.";
13726
13853
  }
@@ -13920,6 +14047,9 @@ function mountWidget(opts) {
13920
14047
  var SHARED_VERSION = 2;
13921
14048
  var SHARED_KEY_PREFIX = "rover:shared:";
13922
14049
  var SHARED_CHANNEL_PREFIX = "rover:channel:";
14050
+ var STALE_DETACHED_EXTERNAL_TAB_MS = 2 * 6e4;
14051
+ var STALE_DETACHED_TAB_MS = 10 * 6e4;
14052
+ var STALE_RUNTIME_TAB_MS = 45e3;
13923
14053
  function now() {
13924
14054
  return Date.now();
13925
14055
  }
@@ -14125,6 +14255,36 @@ var SessionCoordinator = class _SessionCoordinator {
14125
14255
  lastNotifiedRole;
14126
14256
  roleChangeTimer = null;
14127
14257
  static ROLE_CHANGE_DEBOUNCE_MS = 200;
14258
+ pruneDetachedTabs(draft, options) {
14259
+ const dropRuntimeDetached = !!options?.dropRuntimeDetached;
14260
+ const dropAllDetachedExternal = !!options?.dropAllDetachedExternal;
14261
+ const nowMs2 = now();
14262
+ const before = draft.tabs.length;
14263
+ draft.tabs = draft.tabs.filter((tab) => {
14264
+ if (tab.runtimeId) {
14265
+ if (tab.runtimeId === this.runtimeId)
14266
+ return true;
14267
+ return nowMs2 - Math.max(tab.updatedAt || 0, tab.openedAt || 0) <= STALE_RUNTIME_TAB_MS;
14268
+ }
14269
+ if (dropRuntimeDetached && tab.openerRuntimeId === this.runtimeId) {
14270
+ return false;
14271
+ }
14272
+ if (tab.external) {
14273
+ if (dropAllDetachedExternal)
14274
+ return false;
14275
+ return nowMs2 - Math.max(tab.updatedAt || 0, tab.openedAt || 0) <= STALE_DETACHED_EXTERNAL_TAB_MS;
14276
+ }
14277
+ return nowMs2 - Math.max(tab.updatedAt || 0, tab.openedAt || 0) <= STALE_DETACHED_TAB_MS;
14278
+ });
14279
+ if (before !== draft.tabs.length) {
14280
+ if (draft.activeLogicalTabId && !draft.tabs.some((tab) => tab.logicalTabId === draft.activeLogicalTabId)) {
14281
+ draft.activeLogicalTabId = this.localLogicalTabId || draft.tabs[0]?.logicalTabId;
14282
+ }
14283
+ if (draft.nextLogicalTabId <= (draft.tabs.at(-1)?.logicalTabId ?? 0)) {
14284
+ draft.nextLogicalTabId = draft.tabs.reduce((max, tab) => Math.max(max, tab.logicalTabId), 0) + 1;
14285
+ }
14286
+ }
14287
+ }
14128
14288
  constructor(options) {
14129
14289
  this.siteId = options.siteId;
14130
14290
  this.sessionId = options.sessionId;
@@ -14144,6 +14304,9 @@ var SessionCoordinator = class _SessionCoordinator {
14144
14304
  if (this.started)
14145
14305
  return;
14146
14306
  this.started = true;
14307
+ this.mutate("local", (draft) => {
14308
+ this.pruneDetachedTabs(draft, { dropRuntimeDetached: true, dropAllDetachedExternal: true });
14309
+ });
14147
14310
  this.registerCurrentTab(window.location.href, document.title || void 0);
14148
14311
  this.claimLease(false);
14149
14312
  if (!this.state.task) {
@@ -14278,10 +14441,27 @@ var SessionCoordinator = class _SessionCoordinator {
14278
14441
  }
14279
14442
  hydrateExternalState(raw) {
14280
14443
  const incoming = sanitizeSharedState(raw, this.siteId, this.sessionId);
14444
+ this.pruneDetachedTabs(incoming, { dropRuntimeDetached: true, dropAllDetachedExternal: true });
14281
14445
  const beforeSeq = this.state.seq;
14282
14446
  const beforeUpdatedAt = this.state.updatedAt;
14283
14447
  this.applyIncomingState(incoming);
14284
- const changed = this.state.seq !== beforeSeq || this.state.updatedAt !== beforeUpdatedAt;
14448
+ let changed = this.state.seq !== beforeSeq || this.state.updatedAt !== beforeUpdatedAt;
14449
+ if (!changed) {
14450
+ const hasDetachedCandidates = this.state.tabs.some((tab) => {
14451
+ if (tab.runtimeId)
14452
+ return false;
14453
+ if (tab.external)
14454
+ return true;
14455
+ return tab.openerRuntimeId === this.runtimeId;
14456
+ });
14457
+ if (hasDetachedCandidates) {
14458
+ const beforeTabCount = this.state.tabs.length;
14459
+ this.mutate("local", (draft) => {
14460
+ this.pruneDetachedTabs(draft, { dropRuntimeDetached: true, dropAllDetachedExternal: true });
14461
+ });
14462
+ changed = this.state.tabs.length !== beforeTabCount;
14463
+ }
14464
+ }
14285
14465
  if (!changed)
14286
14466
  return false;
14287
14467
  this.persistState();
@@ -14291,7 +14471,23 @@ var SessionCoordinator = class _SessionCoordinator {
14291
14471
  return true;
14292
14472
  }
14293
14473
  listTabs() {
14294
- return [...this.state.tabs];
14474
+ const nowMs2 = now();
14475
+ return this.state.tabs.filter((tab) => {
14476
+ if (tab.runtimeId) {
14477
+ if (tab.runtimeId === this.runtimeId)
14478
+ return true;
14479
+ return nowMs2 - Math.max(tab.updatedAt || 0, tab.openedAt || 0) <= STALE_RUNTIME_TAB_MS;
14480
+ }
14481
+ if (tab.external) {
14482
+ return nowMs2 - Math.max(tab.updatedAt || 0, tab.openedAt || 0) <= STALE_DETACHED_EXTERNAL_TAB_MS;
14483
+ }
14484
+ return nowMs2 - Math.max(tab.updatedAt || 0, tab.openedAt || 0) <= STALE_DETACHED_TAB_MS;
14485
+ });
14486
+ }
14487
+ pruneTabs(options) {
14488
+ this.mutate("local", (draft) => {
14489
+ this.pruneDetachedTabs(draft, options);
14490
+ });
14295
14491
  }
14296
14492
  startNewTask(task) {
14297
14493
  const startedAt = Number(task.startedAt) || now();
@@ -14312,6 +14508,7 @@ var SessionCoordinator = class _SessionCoordinator {
14312
14508
  draft.uiStatus = void 0;
14313
14509
  draft.activeRun = void 0;
14314
14510
  draft.workerContext = void 0;
14511
+ this.pruneDetachedTabs(draft, { dropAllDetachedExternal: true });
14315
14512
  });
14316
14513
  return nextTask;
14317
14514
  }
@@ -14426,9 +14623,6 @@ var SessionCoordinator = class _SessionCoordinator {
14426
14623
  this.mutate("local", (draft) => {
14427
14624
  if (!activeRun) {
14428
14625
  draft.activeRun = void 0;
14429
- if (draft.task && draft.task.status === "running") {
14430
- draft.task.status = "completed";
14431
- }
14432
14626
  return;
14433
14627
  }
14434
14628
  draft.activeRun = {
@@ -14494,6 +14688,14 @@ var SessionCoordinator = class _SessionCoordinator {
14494
14688
  const normalizedUrl = normalizeUrl2(payload.url);
14495
14689
  let logicalTabId = 0;
14496
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
+ }
14497
14699
  logicalTabId = draft.nextLogicalTabId++;
14498
14700
  draft.tabs.push({
14499
14701
  logicalTabId,
@@ -14781,6 +14983,7 @@ var SessionCoordinator = class _SessionCoordinator {
14781
14983
  if (draft.workflowLock?.runtimeId === this.runtimeId) {
14782
14984
  draft.workflowLock.expiresAt = now() + this.leaseMs * 5;
14783
14985
  }
14986
+ this.pruneDetachedTabs(draft);
14784
14987
  });
14785
14988
  this.notifyRoleChange();
14786
14989
  }
@@ -14872,6 +15075,7 @@ var SessionCoordinator = class _SessionCoordinator {
14872
15075
  return;
14873
15076
  if (incoming.seq === this.state.seq && incoming.updatedAt <= this.state.updatedAt)
14874
15077
  return;
15078
+ this.pruneDetachedTabs(incoming);
14875
15079
  this.state = incoming;
14876
15080
  this.syncLocalLogicalTabId();
14877
15081
  this.onStateChange?.(this.state, "remote");
@@ -14944,6 +15148,7 @@ var RoverCloudCheckpointClient = class {
14944
15148
  shouldWrite;
14945
15149
  buildCheckpoint;
14946
15150
  onCheckpoint;
15151
+ onStateChange;
14947
15152
  onError;
14948
15153
  started = false;
14949
15154
  dirty = false;
@@ -14955,8 +15160,9 @@ var RoverCloudCheckpointClient = class {
14955
15160
  lastAppliedRemoteUpdatedAt = 0;
14956
15161
  pushInFlight = false;
14957
15162
  pullInFlight = false;
15163
+ state = "active";
14958
15164
  constructor(options) {
14959
- const token = String(options.apiKey || options.authToken || "").trim();
15165
+ const token = String(options.authToken || options.apiKey || "").trim();
14960
15166
  if (!token) {
14961
15167
  throw toError("Rover cloud checkpoint requires apiKey or authToken.");
14962
15168
  }
@@ -14971,12 +15177,14 @@ var RoverCloudCheckpointClient = class {
14971
15177
  this.shouldWrite = options.shouldWrite || (() => true);
14972
15178
  this.buildCheckpoint = options.buildCheckpoint;
14973
15179
  this.onCheckpoint = options.onCheckpoint;
15180
+ this.onStateChange = options.onStateChange;
14974
15181
  this.onError = options.onError;
14975
15182
  }
14976
15183
  start() {
14977
15184
  if (this.started)
14978
15185
  return;
14979
15186
  this.started = true;
15187
+ this.setState("active");
14980
15188
  this.flushTimer = window.setInterval(() => {
14981
15189
  void this.flush(false);
14982
15190
  }, this.flushIntervalMs);
@@ -15009,6 +15217,8 @@ var RoverCloudCheckpointClient = class {
15009
15217
  async flush(force) {
15010
15218
  if (this.pushInFlight)
15011
15219
  return;
15220
+ if (this.state === "paused_auth")
15221
+ return;
15012
15222
  if (!this.dirty && !force)
15013
15223
  return;
15014
15224
  if (!this.shouldWrite())
@@ -15042,8 +15252,9 @@ var RoverCloudCheckpointClient = class {
15042
15252
  this.applyRemoteCheckpoint(response.checkpoint, "push_stale");
15043
15253
  this.dirty = false;
15044
15254
  }
15255
+ this.setState("active");
15045
15256
  } catch (error) {
15046
- this.onError?.(error);
15257
+ this.handleCheckpointError(error, "roverSessionCheckpointUpsert");
15047
15258
  } finally {
15048
15259
  this.pushInFlight = false;
15049
15260
  }
@@ -15051,6 +15262,8 @@ var RoverCloudCheckpointClient = class {
15051
15262
  async pull(force) {
15052
15263
  if (this.pullInFlight)
15053
15264
  return;
15265
+ if (this.state === "paused_auth")
15266
+ return;
15054
15267
  if (!force && Date.now() - this.lastPullAt < this.pullIntervalMs)
15055
15268
  return;
15056
15269
  this.pullInFlight = true;
@@ -15063,8 +15276,9 @@ var RoverCloudCheckpointClient = class {
15063
15276
  if (!response?.found || !response?.checkpoint || typeof response.checkpoint !== "object")
15064
15277
  return;
15065
15278
  this.applyRemoteCheckpoint(response.checkpoint, "pull");
15279
+ this.setState("active");
15066
15280
  } catch (error) {
15067
- this.onError?.(error);
15281
+ this.handleCheckpointError(error, "roverSessionCheckpointGet");
15068
15282
  } finally {
15069
15283
  this.pullInFlight = false;
15070
15284
  }
@@ -15093,14 +15307,68 @@ var RoverCloudCheckpointClient = class {
15093
15307
  const text = await response.text().catch(() => "");
15094
15308
  payload2 = text ? { error: truncateText(text, 2e3) } : void 0;
15095
15309
  }
15096
- throw toError(`Checkpoint HTTP ${response.status}`, payload2);
15310
+ throw toError(`Checkpoint HTTP ${response.status}`, {
15311
+ action,
15312
+ status: response.status,
15313
+ payload: payload2
15314
+ });
15097
15315
  }
15098
15316
  const payload = await response.json();
15099
15317
  if (payload?.success === false) {
15100
- throw toError("Checkpoint extensionRouter returned success=false", payload);
15318
+ throw toError("Checkpoint extensionRouter returned success=false", {
15319
+ action,
15320
+ status: response.status,
15321
+ payload
15322
+ });
15101
15323
  }
15102
15324
  return payload?.data;
15103
15325
  }
15326
+ setState(next, context) {
15327
+ if (this.state === next)
15328
+ return;
15329
+ this.state = next;
15330
+ this.onStateChange?.(next, context || {});
15331
+ }
15332
+ handleCheckpointError(error, action) {
15333
+ const details = this.normalizeCheckpointError(error);
15334
+ const isAuthFailure = this.isAuthFailure(details);
15335
+ if (isAuthFailure) {
15336
+ this.setState("paused_auth", {
15337
+ reason: "auth_failed",
15338
+ action,
15339
+ code: details.code,
15340
+ message: details.message
15341
+ });
15342
+ }
15343
+ this.onError?.(error, {
15344
+ action,
15345
+ state: this.state,
15346
+ code: details.code,
15347
+ message: details.message,
15348
+ status: details.status,
15349
+ paused: isAuthFailure
15350
+ });
15351
+ }
15352
+ normalizeCheckpointError(error) {
15353
+ const anyError = error;
15354
+ const details = anyError?.details;
15355
+ const payload = details?.payload || details?.response || details;
15356
+ const candidateCode = payload?.errorCode || payload?.errorDetails?.code || payload?.error?.code || payload?.code;
15357
+ const candidateMessage = payload?.error || payload?.errorDetails?.message || payload?.error?.message || anyError?.message || "Checkpoint request failed";
15358
+ const status = Number(details?.status);
15359
+ return {
15360
+ code: typeof candidateCode === "string" ? candidateCode : void 0,
15361
+ message: truncateText(candidateMessage, 1e3),
15362
+ status: Number.isFinite(status) ? status : void 0
15363
+ };
15364
+ }
15365
+ isAuthFailure(details) {
15366
+ const code = String(details.code || "").toUpperCase();
15367
+ if (code === "INVALID_API_KEY" || code === "MISSING_API_KEY" || code === "UNAUTHENTICATED" || code === "PERMISSION_DENIED") {
15368
+ return true;
15369
+ }
15370
+ return details.status === 401 || details.status === 403;
15371
+ }
15104
15372
  };
15105
15373
 
15106
15374
  // dist/runtimeStorage.js
@@ -15238,9 +15506,11 @@ var MAX_AUTO_RESUME_AGE_MS = 15 * 6e4;
15238
15506
  var MAX_AUTO_RESUME_ATTEMPTS = 12;
15239
15507
  var VISITOR_COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 365;
15240
15508
  var CHECKPOINT_PAYLOAD_VERSION = 1;
15241
- var DEFAULT_TASK_INACTIVITY_MS = 5 * 6e4;
15242
- var DEFAULT_SEMANTIC_SIMILARITY_THRESHOLD = 0.18;
15243
- var MAX_RECENT_USER_MESSAGES_FOR_TASK_HEURISTIC = 3;
15509
+ var ACTIVE_PENDING_RUN_GRACE_MS = 3e3;
15510
+ var STALE_PENDING_RUN_MS = 9e4;
15511
+ var TELEMETRY_DEFAULT_FLUSH_INTERVAL_MS = 12e3;
15512
+ var TELEMETRY_DEFAULT_MAX_BATCH_SIZE = 30;
15513
+ var TELEMETRY_MAX_BUFFER_SIZE = 240;
15244
15514
  var instance = null;
15245
15515
  var bridge = null;
15246
15516
  var worker = null;
@@ -15252,6 +15522,11 @@ var runtimeStateStore = null;
15252
15522
  var runtimeId = "";
15253
15523
  var sessionCoordinator = null;
15254
15524
  var cloudCheckpointClient = null;
15525
+ var telemetryFlushTimer = null;
15526
+ var telemetryBuffer = [];
15527
+ var telemetryInFlight = false;
15528
+ var telemetryPausedAuth = false;
15529
+ var telemetrySeq = 0;
15255
15530
  var resolvedVisitorId = void 0;
15256
15531
  var suppressCheckpointSync = false;
15257
15532
  var currentMode = "controller";
@@ -15277,6 +15552,7 @@ var RUN_SCOPED_WORKER_MESSAGE_TYPES = /* @__PURE__ */ new Set([
15277
15552
  var pendingToolRegistrations = [];
15278
15553
  var eventHandlers = /* @__PURE__ */ new Map();
15279
15554
  function emit(event, payload) {
15555
+ recordTelemetryEvent(event, payload);
15280
15556
  const handlers = eventHandlers.get(event);
15281
15557
  if (!handlers)
15282
15558
  return;
@@ -15482,6 +15758,240 @@ function safeSerialize(value) {
15482
15758
  }
15483
15759
  }
15484
15760
  }
15761
+ function normalizeTelemetryConfig(cfg) {
15762
+ const raw = cfg?.telemetry;
15763
+ const sampleRateRaw = Number(raw?.sampleRate);
15764
+ const sampleRate = Number.isFinite(sampleRateRaw) ? Math.min(1, Math.max(0, sampleRateRaw)) : 1;
15765
+ const flushRaw = Number(raw?.flushIntervalMs);
15766
+ const flushIntervalMs = Number.isFinite(flushRaw) ? Math.min(6e4, Math.max(2e3, Math.floor(flushRaw))) : TELEMETRY_DEFAULT_FLUSH_INTERVAL_MS;
15767
+ const batchRaw = Number(raw?.maxBatchSize);
15768
+ const maxBatchSize = Number.isFinite(batchRaw) ? Math.min(80, Math.max(1, Math.floor(batchRaw))) : TELEMETRY_DEFAULT_MAX_BATCH_SIZE;
15769
+ return {
15770
+ enabled: raw?.enabled !== false,
15771
+ sampleRate,
15772
+ flushIntervalMs,
15773
+ maxBatchSize,
15774
+ includePayloads: raw?.includePayloads === true
15775
+ };
15776
+ }
15777
+ function canUseTelemetry(cfg) {
15778
+ if (!cfg)
15779
+ return false;
15780
+ const telemetry = normalizeTelemetryConfig(cfg);
15781
+ if (!telemetry.enabled)
15782
+ return false;
15783
+ if (!(cfg.authToken || cfg.apiKey))
15784
+ return false;
15785
+ return true;
15786
+ }
15787
+ function summarizeTelemetryPayload(payload) {
15788
+ if (payload == null)
15789
+ return void 0;
15790
+ if (typeof payload === "string")
15791
+ return truncateText2(payload, 260);
15792
+ if (typeof payload === "number" || typeof payload === "boolean")
15793
+ return payload;
15794
+ if (Array.isArray(payload)) {
15795
+ return { type: "array", length: payload.length };
15796
+ }
15797
+ if (typeof payload === "object") {
15798
+ const keys = Object.keys(payload).slice(0, 20);
15799
+ const summary = { type: "object", keys };
15800
+ const preferredKeys = ["code", "message", "stage", "status", "reason", "taskId", "runId", "policyAction"];
15801
+ for (const key of preferredKeys) {
15802
+ const value = payload?.[key];
15803
+ if (value == null)
15804
+ continue;
15805
+ if (typeof value === "string")
15806
+ summary[key] = truncateText2(value, 180);
15807
+ else if (typeof value === "number" || typeof value === "boolean")
15808
+ summary[key] = value;
15809
+ }
15810
+ return summary;
15811
+ }
15812
+ return void 0;
15813
+ }
15814
+ function buildTelemetryPayload(payload, includePayloads) {
15815
+ if (!includePayloads) {
15816
+ return summarizeTelemetryPayload(payload);
15817
+ }
15818
+ const cloned = cloneUnknown2(payload);
15819
+ if (cloned == null)
15820
+ return void 0;
15821
+ if (typeof cloned === "string")
15822
+ return truncateText2(cloned, 1e3);
15823
+ if (typeof cloned === "number" || typeof cloned === "boolean")
15824
+ return cloned;
15825
+ if (Array.isArray(cloned))
15826
+ return { type: "array", length: cloned.length };
15827
+ if (typeof cloned === "object")
15828
+ return cloned;
15829
+ return summarizeTelemetryPayload(payload);
15830
+ }
15831
+ function stopTelemetry() {
15832
+ if (telemetryFlushTimer) {
15833
+ clearInterval(telemetryFlushTimer);
15834
+ telemetryFlushTimer = null;
15835
+ }
15836
+ }
15837
+ function getTelemetryEndpoint(cfg) {
15838
+ const base = (cfg.apiBase || "https://us-central1-rtrvr-extension-functions.cloudfunctions.net").replace(/\/$/, "");
15839
+ return base.endsWith("/extensionRouter") ? base : `${base}/extensionRouter`;
15840
+ }
15841
+ async function flushTelemetry(force = false) {
15842
+ if (telemetryInFlight)
15843
+ return;
15844
+ if (telemetryPausedAuth)
15845
+ return;
15846
+ if (!currentConfig || !canUseTelemetry(currentConfig)) {
15847
+ telemetryBuffer = [];
15848
+ return;
15849
+ }
15850
+ if (!telemetryBuffer.length)
15851
+ return;
15852
+ const telemetry = normalizeTelemetryConfig(currentConfig);
15853
+ const token = String(currentConfig.authToken || currentConfig.apiKey || "").trim();
15854
+ if (!token)
15855
+ return;
15856
+ const batch = telemetryBuffer.splice(0, telemetry.maxBatchSize);
15857
+ if (!batch.length)
15858
+ return;
15859
+ telemetryInFlight = true;
15860
+ try {
15861
+ const response = await fetch(getTelemetryEndpoint(currentConfig), {
15862
+ method: "POST",
15863
+ headers: {
15864
+ Authorization: `Bearer ${token}`,
15865
+ "Content-Type": "application/json"
15866
+ },
15867
+ body: JSON.stringify({
15868
+ action: "roverTelemetryIngest",
15869
+ data: {
15870
+ siteId: currentConfig.siteId,
15871
+ runtimeId,
15872
+ sessionId: runtimeState?.sessionId,
15873
+ visitorId: resolvedVisitorId,
15874
+ flushReason: force ? "manual" : "interval",
15875
+ sdkVersion: "rover_sdk_v1",
15876
+ pageUrl: window.location.href,
15877
+ userAgent: navigator.userAgent,
15878
+ sampleRate: telemetry.sampleRate,
15879
+ events: batch
15880
+ }
15881
+ })
15882
+ });
15883
+ if (!response.ok) {
15884
+ if (response.status === 401 || response.status === 403) {
15885
+ telemetryPausedAuth = true;
15886
+ } else {
15887
+ telemetryBuffer = [...batch, ...telemetryBuffer].slice(-TELEMETRY_MAX_BUFFER_SIZE);
15888
+ }
15889
+ return;
15890
+ }
15891
+ const payload = await response.json().catch(() => void 0);
15892
+ if (payload?.success === false) {
15893
+ const code = String(payload?.errorCode || payload?.errorDetails?.code || "").toUpperCase();
15894
+ if (code === "INVALID_API_KEY" || code === "MISSING_API_KEY" || code === "UNAUTHENTICATED" || code === "PERMISSION_DENIED") {
15895
+ telemetryPausedAuth = true;
15896
+ }
15897
+ return;
15898
+ }
15899
+ } catch {
15900
+ telemetryBuffer = [...batch, ...telemetryBuffer].slice(-TELEMETRY_MAX_BUFFER_SIZE);
15901
+ } finally {
15902
+ telemetryInFlight = false;
15903
+ }
15904
+ }
15905
+ function setupTelemetry(cfg) {
15906
+ stopTelemetry();
15907
+ telemetryPausedAuth = false;
15908
+ if (!canUseTelemetry(cfg)) {
15909
+ telemetryBuffer = [];
15910
+ return;
15911
+ }
15912
+ const telemetry = normalizeTelemetryConfig(cfg);
15913
+ telemetryFlushTimer = setInterval(() => {
15914
+ void flushTelemetry(false);
15915
+ }, telemetry.flushIntervalMs);
15916
+ }
15917
+ function recordTelemetryEvent(event, payload) {
15918
+ if (!canUseTelemetry(currentConfig) || telemetryPausedAuth)
15919
+ return;
15920
+ const telemetry = normalizeTelemetryConfig(currentConfig);
15921
+ if (telemetry.sampleRate < 1 && Math.random() > telemetry.sampleRate)
15922
+ return;
15923
+ const next = {
15924
+ name: event,
15925
+ ts: Date.now(),
15926
+ seq: ++telemetrySeq,
15927
+ payload: buildTelemetryPayload(payload, telemetry.includePayloads)
15928
+ };
15929
+ telemetryBuffer.push(next);
15930
+ if (telemetryBuffer.length > TELEMETRY_MAX_BUFFER_SIZE) {
15931
+ telemetryBuffer = telemetryBuffer.slice(-TELEMETRY_MAX_BUFFER_SIZE);
15932
+ }
15933
+ if (telemetryBuffer.length >= telemetry.maxBatchSize) {
15934
+ void flushTelemetry(false);
15935
+ }
15936
+ }
15937
+ function buildInaccessibleTabPageData(_cfg, tab, reason = "tab_not_accessible") {
15938
+ const logicalTabId = Number(tab?.logicalTabId) || void 0;
15939
+ const url = tab?.url || "";
15940
+ const title = tab?.title || (tab?.external ? "External Tab (Inaccessible)" : "Inactive Tab");
15941
+ const normalizedReason = String(reason || "").trim();
15942
+ const reasonLine = normalizedReason ? ` Reason: ${normalizedReason}.` : "";
15943
+ 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}`;
15944
+ return {
15945
+ url,
15946
+ title,
15947
+ contentType: "text/html",
15948
+ content,
15949
+ metadata: {
15950
+ inaccessible: true,
15951
+ external: !!tab?.external,
15952
+ accessMode: tab?.external ? "external_placeholder" : "inactive_tab",
15953
+ reason,
15954
+ logicalTabId
15955
+ }
15956
+ };
15957
+ }
15958
+ function buildTabAccessToolError(cfg, tab, reason = "tab_not_accessible") {
15959
+ const logicalTabId = Number(tab?.logicalTabId) || 0;
15960
+ const blockedUrl = tab?.url || "";
15961
+ 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.`;
15962
+ const code = tab?.external ? "DOMAIN_SCOPE_BLOCKED" : "TAB_NOT_ACCESSIBLE";
15963
+ return {
15964
+ success: false,
15965
+ error: message,
15966
+ allowFallback: true,
15967
+ output: {
15968
+ success: false,
15969
+ error: {
15970
+ code,
15971
+ message,
15972
+ missing: [],
15973
+ next_action: tab?.external ? "Use open_new_tab for external context or continue on an in-scope tab." : "Switch to an active tab and retry.",
15974
+ retryable: false
15975
+ },
15976
+ blocked_url: blockedUrl || void 0,
15977
+ logical_tab_id: logicalTabId || void 0,
15978
+ external: !!tab?.external,
15979
+ policy_action: tab?.external ? cfg.externalNavigationPolicy || "open_new_tab_notice" : void 0,
15980
+ reason
15981
+ },
15982
+ errorDetails: {
15983
+ code,
15984
+ message,
15985
+ retryable: false,
15986
+ details: {
15987
+ logicalTabId,
15988
+ blockedUrl,
15989
+ external: !!tab?.external,
15990
+ reason
15991
+ }
15992
+ }
15993
+ };
15994
+ }
15485
15995
  function normalizeStatusStage(input) {
15486
15996
  if (input === "analyze" || input === "route" || input === "execute" || input === "verify" || input === "complete") {
15487
15997
  return input;
@@ -15557,14 +16067,26 @@ function shouldIgnoreRunScopedWorkerMessage(msg) {
15557
16067
  return false;
15558
16068
  }
15559
16069
  if (type === "run_completed") {
15560
- if (!pendingRunId)
16070
+ if (!pendingRunId) {
16071
+ const sharedRunId = sessionCoordinator?.getState()?.activeRun?.runId;
16072
+ if (sharedRunId && sharedRunId === messageRunId)
16073
+ return false;
15561
16074
  return true;
16075
+ }
15562
16076
  return pendingRunId !== messageRunId;
15563
16077
  }
15564
16078
  if (!pendingRunId)
15565
16079
  return true;
15566
16080
  return pendingRunId !== messageRunId;
15567
16081
  }
16082
+ function normalizeRunCompletionState(msg) {
16083
+ if (!msg || typeof msg !== "object") {
16084
+ return { taskComplete: false, needsUserInput: false };
16085
+ }
16086
+ const needsUserInput = msg.needsUserInput === true;
16087
+ const taskComplete = msg.taskComplete === true && !needsUserInput;
16088
+ return { taskComplete, needsUserInput };
16089
+ }
15568
16090
  function getLatestAssistantText(runId) {
15569
16091
  if (runId && latestAssistantByRunId.has(runId)) {
15570
16092
  return latestAssistantByRunId.get(runId);
@@ -15786,6 +16308,8 @@ function ensureUnloadHandler() {
15786
16308
  sessionCoordinator?.releaseWorkflowLock(runtimeState.pendingRun.id);
15787
16309
  }
15788
16310
  persistRuntimeState();
16311
+ void flushTelemetry(true);
16312
+ stopTelemetry();
15789
16313
  cloudCheckpointClient?.markDirty();
15790
16314
  cloudCheckpointClient?.syncNow();
15791
16315
  cloudCheckpointClient?.stop();
@@ -15801,6 +16325,9 @@ function ensureUnloadHandler() {
15801
16325
  sessionCoordinator.claimLease(false);
15802
16326
  }
15803
16327
  autoResumeAttempted = false;
16328
+ if (currentConfig) {
16329
+ setupTelemetry(currentConfig);
16330
+ }
15804
16331
  };
15805
16332
  window.addEventListener("pageshow", onPageShow);
15806
16333
  }
@@ -15828,6 +16355,33 @@ function markTaskActivity(role, timestamp = Date.now()) {
15828
16355
  task.endedAt = void 0;
15829
16356
  }
15830
16357
  }
16358
+ function markTaskRunning(reason = "worker_task_active", timestamp = Date.now()) {
16359
+ if (!runtimeState)
16360
+ return;
16361
+ const task = ensureActiveTask(reason);
16362
+ if (!task)
16363
+ return;
16364
+ task.status = "running";
16365
+ task.endedAt = void 0;
16366
+ task.boundaryReason = reason;
16367
+ if (!task.lastUserAt && !task.lastAssistantAt) {
16368
+ task.lastAssistantAt = timestamp;
16369
+ }
16370
+ sessionCoordinator?.syncTask({ ...task }, runtimeState.taskEpoch);
16371
+ persistRuntimeState();
16372
+ }
16373
+ function markTaskCompleted(reason = "worker_task_complete", timestamp = Date.now()) {
16374
+ if (!runtimeState)
16375
+ return;
16376
+ const task = ensureActiveTask(reason);
16377
+ if (!task)
16378
+ return;
16379
+ task.status = "completed";
16380
+ task.endedAt = timestamp;
16381
+ task.boundaryReason = reason;
16382
+ sessionCoordinator?.syncTask({ ...task }, runtimeState.taskEpoch);
16383
+ persistRuntimeState();
16384
+ }
15831
16385
  function hideTaskSuggestion() {
15832
16386
  pendingTaskSuggestion = null;
15833
16387
  ui?.setTaskSuggestion({ visible: false });
@@ -15837,53 +16391,37 @@ function clearTaskUiState() {
15837
16391
  ui?.clearTimeline();
15838
16392
  hideTaskSuggestion();
15839
16393
  }
15840
- function collectRecentUserTextsForHeuristic() {
15841
- if (!runtimeState)
15842
- return [];
15843
- const userMessages = runtimeState.uiMessages.filter((message) => message.role === "user");
15844
- return userMessages.slice(-MAX_RECENT_USER_MESSAGES_FOR_TASK_HEURISTIC).map((message) => message.text.trim()).filter(Boolean);
15845
- }
15846
- function tokenizeForSimilarity(input) {
15847
- return new Set(String(input || "").toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).map((token) => token.trim()).filter((token) => token.length > 2));
15848
- }
15849
- function computeSemanticOverlap(a, b) {
15850
- const ta = tokenizeForSimilarity(a);
15851
- const tb = tokenizeForSimilarity(b);
15852
- if (!ta.size || !tb.size)
15853
- return 0;
15854
- let intersection = 0;
15855
- for (const token of ta) {
15856
- if (tb.has(token))
15857
- intersection += 1;
16394
+ function isPendingRunLikelyActive() {
16395
+ const pending = runtimeState?.pendingRun;
16396
+ if (!pending)
16397
+ return false;
16398
+ if (!sessionCoordinator)
16399
+ return true;
16400
+ const sharedActiveRun = sessionCoordinator?.getState()?.activeRun;
16401
+ if (sharedActiveRun?.runId && sharedActiveRun.runId === pending.id) {
16402
+ return true;
16403
+ }
16404
+ if (sharedActiveRun?.runtimeId && sharedActiveRun.runtimeId !== runtimeId) {
16405
+ return true;
15858
16406
  }
15859
- return intersection / Math.max(ta.size, tb.size);
16407
+ const ageMs = Date.now() - Number(pending.startedAt || 0);
16408
+ return ageMs >= 0 && ageMs <= ACTIVE_PENDING_RUN_GRACE_MS;
15860
16409
  }
15861
- function shouldSuggestResetForPrompt(text) {
15862
- if (!runtimeState || !currentConfig)
15863
- return { suggest: false };
15864
- const suggestEnabled = currentConfig.taskContext?.suggestReset !== false;
15865
- if (!suggestEnabled)
15866
- return { suggest: false };
15867
- if (runtimeState.pendingRun)
15868
- return { suggest: false };
15869
- const task = ensureActiveTask("implicit");
15870
- if (!task || task.status === "ended")
15871
- return { suggest: false };
15872
- const inactivityMs = Math.max(6e4, Number(currentConfig.taskContext?.inactivityMs) || DEFAULT_TASK_INACTIVITY_MS);
15873
- const now2 = Date.now();
15874
- const latestActivity = Math.max(task.lastUserAt || 0, task.lastAssistantAt || 0, task.startedAt || 0);
15875
- if (!latestActivity || now2 - latestActivity < inactivityMs)
15876
- return { suggest: false };
15877
- const recent = collectRecentUserTextsForHeuristic();
15878
- if (!recent.length)
15879
- return { suggest: true, reason: "inactivity_only" };
15880
- const baseline = recent.join(" ");
15881
- const overlap = computeSemanticOverlap(baseline, text);
15882
- const threshold = Math.max(0.05, Math.min(0.9, Number(currentConfig.taskContext?.semanticSimilarityThreshold) || DEFAULT_SEMANTIC_SIMILARITY_THRESHOLD));
15883
- if (overlap < threshold) {
15884
- return { suggest: true, reason: `low_similarity_${overlap.toFixed(2)}` };
15885
- }
15886
- return { suggest: false };
16410
+ function maybeClearStalePendingRun() {
16411
+ if (!runtimeState?.pendingRun)
16412
+ return;
16413
+ if (isPendingRunLikelyActive())
16414
+ return;
16415
+ if (!sessionCoordinator)
16416
+ return;
16417
+ const pending = runtimeState.pendingRun;
16418
+ const ageMs = Date.now() - Number(pending.startedAt || 0);
16419
+ if (!Number.isFinite(ageMs) || ageMs < STALE_PENDING_RUN_MS)
16420
+ return;
16421
+ setPendingRun(void 0);
16422
+ sessionCoordinator?.clearActiveRunRuntimeId(pending.id);
16423
+ sessionCoordinator?.releaseWorkflowLock(pending.id);
16424
+ sessionCoordinator?.setActiveRun(void 0);
15887
16425
  }
15888
16426
  function appendUiMessage(role, text, persist = true, options) {
15889
16427
  const clean = truncateText2(String(text || ""));
@@ -16068,28 +16606,16 @@ function dispatchUserPrompt(text, options) {
16068
16606
  const trimmed = String(text || "").trim();
16069
16607
  if (!trimmed)
16070
16608
  return;
16071
- if (options?.startNewTask) {
16072
- newTask({ reason: options.reason || "suggested_new_task", clearUi: true });
16073
- } else if (!options?.bypassSuggestion) {
16074
- const decision = shouldSuggestResetForPrompt(trimmed);
16075
- if (decision.suggest) {
16076
- pendingTaskSuggestion = {
16077
- text: trimmed,
16078
- reason: decision.reason || "heuristic",
16079
- createdAt: Date.now()
16080
- };
16081
- ui?.setTaskSuggestion({
16082
- visible: true,
16083
- text: "This looks like a different request. Start a new task or continue this one?",
16084
- primaryLabel: "Start new",
16085
- secondaryLabel: "Continue"
16086
- });
16087
- emit("task_suggested_reset", {
16088
- text: trimmed,
16089
- reason: pendingTaskSuggestion.reason
16090
- });
16091
- return;
16092
- }
16609
+ maybeClearStalePendingRun();
16610
+ const activeTaskStatus = runtimeState?.activeTask?.status;
16611
+ const shouldStartFreshTask = !!options?.startNewTask || activeTaskStatus === "completed" || activeTaskStatus === "ended";
16612
+ sessionCoordinator?.pruneTabs({
16613
+ dropRuntimeDetached: true,
16614
+ dropAllDetachedExternal: shouldStartFreshTask
16615
+ });
16616
+ if (shouldStartFreshTask) {
16617
+ const autoReason = options?.reason || (activeTaskStatus === "completed" ? "auto_after_task_complete" : "auto_after_task_end");
16618
+ newTask({ reason: autoReason, clearUi: true });
16093
16619
  }
16094
16620
  hideTaskSuggestion();
16095
16621
  postRun(trimmed, { appendUserMessage: true, resume: false, autoResume: true });
@@ -16418,6 +16944,10 @@ function setupCloudCheckpointing(cfg) {
16418
16944
  if (!resolvedVisitorId)
16419
16945
  return;
16420
16946
  try {
16947
+ const emitCheckpointState = (payload) => {
16948
+ emit("checkpoint_state", payload);
16949
+ cfg.checkpointing?.onStateChange?.(payload);
16950
+ };
16421
16951
  cloudCheckpointClient = new RoverCloudCheckpointClient({
16422
16952
  apiBase: cfg.apiBase,
16423
16953
  apiKey: cfg.apiKey,
@@ -16433,7 +16963,36 @@ function setupCloudCheckpointing(cfg) {
16433
16963
  onCheckpoint: (payload) => {
16434
16964
  applyCloudCheckpointPayload(payload);
16435
16965
  },
16436
- onError: () => {
16966
+ onStateChange: (state, context) => {
16967
+ emitCheckpointState({
16968
+ state,
16969
+ reason: context.reason,
16970
+ action: context.action,
16971
+ code: context.code,
16972
+ message: context.message
16973
+ });
16974
+ if (state === "paused_auth") {
16975
+ emit("checkpoint_error", {
16976
+ action: context.action,
16977
+ code: context.code || "INVALID_API_KEY",
16978
+ message: context.message || "Checkpoint sync paused due to auth failure.",
16979
+ disabled: true,
16980
+ reason: context.reason || "auth_failed"
16981
+ });
16982
+ }
16983
+ },
16984
+ onError: (_error, context) => {
16985
+ cfg.checkpointing?.onError?.(context);
16986
+ if (!context.paused) {
16987
+ emit("checkpoint_error", {
16988
+ action: context.action,
16989
+ code: context.code,
16990
+ message: context.message,
16991
+ status: context.status,
16992
+ disabled: false,
16993
+ reason: "transient_failure"
16994
+ });
16995
+ }
16437
16996
  }
16438
16997
  });
16439
16998
  cloudCheckpointClient.start();
@@ -16727,6 +17286,7 @@ function handleWorkerMessage(msg) {
16727
17286
  }
16728
17287
  sessionCoordinator?.setActiveRun(void 0);
16729
17288
  if (!msg.ok && msg.error) {
17289
+ markTaskRunning("worker_run_failed");
16730
17290
  setUiStatus(`Task failed: ${String(msg.error)}`);
16731
17291
  latestAssistantByRunId.delete(String(msg.runId || ""));
16732
17292
  appendTimelineEvent({
@@ -16736,8 +17296,27 @@ function handleWorkerMessage(msg) {
16736
17296
  status: "error"
16737
17297
  });
16738
17298
  } else if (msg.ok) {
16739
- setUiStatus("Execution completed");
16740
- finalizeSuccessfulRunTimeline(typeof msg.runId === "string" ? msg.runId : void 0);
17299
+ const completionState = normalizeRunCompletionState(msg);
17300
+ const taskComplete = completionState.taskComplete;
17301
+ const needsUserInput = completionState.needsUserInput;
17302
+ if (taskComplete) {
17303
+ markTaskCompleted("worker_task_complete");
17304
+ sessionCoordinator?.pruneTabs({
17305
+ dropRuntimeDetached: true,
17306
+ dropAllDetachedExternal: true
17307
+ });
17308
+ setUiStatus("Task completed");
17309
+ finalizeSuccessfulRunTimeline(typeof msg.runId === "string" ? msg.runId : void 0);
17310
+ } else {
17311
+ markTaskRunning(needsUserInput ? "worker_waiting_for_input" : "worker_continuation");
17312
+ setUiStatus(needsUserInput ? "Need more input to continue" : "Execution finished. Continue when ready.");
17313
+ appendTimelineEvent({
17314
+ kind: "status",
17315
+ title: needsUserInput ? "Waiting for your input" : "Continuation available",
17316
+ detail: needsUserInput ? "Planner requested more information before marking the task complete." : "Task is still active and will continue with your next message.",
17317
+ status: "info"
17318
+ });
17319
+ }
16741
17320
  if (typeof msg.runId === "string" && msg.runId) {
16742
17321
  latestAssistantByRunId.delete(msg.runId);
16743
17322
  }
@@ -16783,6 +17362,7 @@ function createRuntime(cfg) {
16783
17362
  }
16784
17363
  }
16785
17364
  setupCloudCheckpointing(cfg);
17365
+ setupTelemetry(cfg);
16786
17366
  const initialAllowActions = (cfg.allowActions ?? true) && (sessionCoordinator ? sessionCoordinator.isController() : true);
16787
17367
  bridge = new Bridge({
16788
17368
  allowActions: initialAllowActions,
@@ -16790,7 +17370,6 @@ function createRuntime(cfg) {
16790
17370
  allowedDomains: cfg.allowedDomains,
16791
17371
  domainScopeMode: cfg.domainScopeMode,
16792
17372
  externalNavigationPolicy: cfg.externalNavigationPolicy,
16793
- crossDomainPolicy: cfg.crossDomainPolicy,
16794
17373
  registerOpenedTab: (payload) => sessionCoordinator?.registerOpenedTab(payload),
16795
17374
  switchToLogicalTab: (logicalTabId) => sessionCoordinator?.switchToLogicalTab(logicalTabId) || { ok: false, reason: "No session coordinator" },
16796
17375
  listKnownTabs: () => (sessionCoordinator?.listTabs() || []).map((tab) => ({
@@ -16815,26 +17394,34 @@ function createRuntime(cfg) {
16815
17394
  bindRpc(channel.port1, {
16816
17395
  getSnapshot: () => bridge.getSnapshot(),
16817
17396
  getPageData: async (params) => {
16818
- const tabId = params?.tabId;
17397
+ const runtimeCfg = currentConfig || cfg;
17398
+ const tabId = Number(params?.tabId);
16819
17399
  const localTabId = sessionCoordinator?.getLocalLogicalTabId();
16820
- if (!tabId || tabId === localTabId || !sessionCoordinator) {
17400
+ if (!Number.isFinite(tabId) || tabId <= 0 || tabId === localTabId || !sessionCoordinator) {
16821
17401
  return bridge.getPageData(params);
16822
17402
  }
16823
17403
  const tabs = sessionCoordinator.listTabs();
16824
17404
  const targetTab = tabs.find((t) => t.logicalTabId === tabId);
16825
- if (!targetTab || targetTab.runtimeId === runtimeId) {
17405
+ if (!targetTab) {
17406
+ return buildInaccessibleTabPageData(runtimeCfg, { logicalTabId: tabId, external: true }, "target_tab_missing");
17407
+ }
17408
+ if (targetTab.runtimeId === runtimeId) {
16826
17409
  return bridge.getPageData(params);
16827
17410
  }
17411
+ if (targetTab.external && runtimeCfg.externalNavigationPolicy !== "allow") {
17412
+ return buildInaccessibleTabPageData(runtimeCfg, targetTab, "external_domain_inaccessible");
17413
+ }
16828
17414
  if (!targetTab.runtimeId || !sessionCoordinator.isTabAlive(tabId)) {
16829
- return bridge.getPageData(params);
17415
+ return buildInaccessibleTabPageData(runtimeCfg, targetTab, "target_tab_inactive");
16830
17416
  }
16831
17417
  try {
16832
17418
  return await sessionCoordinator.sendCrossTabRpc(targetTab.runtimeId, "getPageData", params, 15e3);
16833
17419
  } catch {
16834
- return bridge.getPageData(params);
17420
+ return buildInaccessibleTabPageData(runtimeCfg, targetTab, "cross_tab_rpc_failed");
16835
17421
  }
16836
17422
  },
16837
17423
  executeTool: async (params) => {
17424
+ const runtimeCfg = currentConfig || cfg;
16838
17425
  const activeTabId = sessionCoordinator?.getActiveLogicalTabId();
16839
17426
  const localTabId = sessionCoordinator?.getLocalLogicalTabId();
16840
17427
  if (!activeTabId || activeTabId === localTabId || !sessionCoordinator) {
@@ -16842,16 +17429,22 @@ function createRuntime(cfg) {
16842
17429
  }
16843
17430
  const tabs = sessionCoordinator.listTabs();
16844
17431
  const targetTab = tabs.find((t) => t.logicalTabId === activeTabId);
16845
- if (!targetTab?.runtimeId || targetTab.runtimeId === runtimeId) {
16846
- return bridge.executeTool(params.call, params.payload);
17432
+ if (!targetTab) {
17433
+ return buildTabAccessToolError(runtimeCfg, { logicalTabId: activeTabId, external: true }, "target_tab_missing");
16847
17434
  }
16848
- if (!sessionCoordinator.isTabAlive(activeTabId)) {
17435
+ if (targetTab.external && runtimeCfg.externalNavigationPolicy !== "allow") {
17436
+ return buildTabAccessToolError(runtimeCfg, targetTab, "external_tab_action_blocked");
17437
+ }
17438
+ if (targetTab.runtimeId === runtimeId) {
16849
17439
  return bridge.executeTool(params.call, params.payload);
16850
17440
  }
17441
+ if (!targetTab.runtimeId || !sessionCoordinator.isTabAlive(activeTabId)) {
17442
+ return buildTabAccessToolError(runtimeCfg, targetTab, "target_tab_inactive");
17443
+ }
16851
17444
  try {
16852
17445
  return await sessionCoordinator.sendCrossTabRpc(targetTab.runtimeId, "executeTool", params, 2e4);
16853
17446
  } catch {
16854
- return bridge.executeTool(params.call, params.payload);
17447
+ return buildTabAccessToolError(runtimeCfg, targetTab, "cross_tab_execute_failed");
16855
17448
  }
16856
17449
  },
16857
17450
  executeClientTool: (params) => bridge.executeClientTool(params.name, params.args),
@@ -16938,6 +17531,9 @@ function createRuntime(cfg) {
16938
17531
  },
16939
17532
  showTaskControls: cfg.ui?.showTaskControls !== false,
16940
17533
  muted: cfg.ui?.muted,
17534
+ agent: {
17535
+ name: cfg.ui?.agent?.name
17536
+ },
16941
17537
  mascot: {
16942
17538
  disabled: cfg.ui?.mascot?.disabled,
16943
17539
  mp4Url: cfg.ui?.mascot?.mp4Url,
@@ -17052,9 +17648,7 @@ function boot(cfg) {
17052
17648
  plannerOnActError: cfg.taskRouting?.plannerOnActError
17053
17649
  },
17054
17650
  taskContext: {
17055
- inactivityMs: cfg.taskContext?.inactivityMs ?? DEFAULT_TASK_INACTIVITY_MS,
17056
- suggestReset: cfg.taskContext?.suggestReset ?? true,
17057
- semanticSimilarityThreshold: cfg.taskContext?.semanticSimilarityThreshold ?? DEFAULT_SEMANTIC_SIMILARITY_THRESHOLD
17651
+ ...cfg.taskContext
17058
17652
  }
17059
17653
  };
17060
17654
  runtimeState.sessionId = currentConfig.sessionId;
@@ -17115,17 +17709,29 @@ function update(cfg) {
17115
17709
  ...cfg.taskRouting
17116
17710
  },
17117
17711
  taskContext: {
17118
- inactivityMs: cfg.taskContext?.inactivityMs ?? currentConfig.taskContext?.inactivityMs ?? DEFAULT_TASK_INACTIVITY_MS,
17119
- suggestReset: cfg.taskContext?.suggestReset ?? currentConfig.taskContext?.suggestReset ?? true,
17120
- semanticSimilarityThreshold: cfg.taskContext?.semanticSimilarityThreshold ?? currentConfig.taskContext?.semanticSimilarityThreshold ?? DEFAULT_SEMANTIC_SIMILARITY_THRESHOLD
17712
+ ...currentConfig.taskContext,
17713
+ ...cfg.taskContext
17121
17714
  },
17122
17715
  ui: {
17123
17716
  ...currentConfig.ui,
17124
17717
  ...cfg.ui,
17718
+ agent: {
17719
+ ...currentConfig.ui?.agent,
17720
+ ...cfg.ui?.agent
17721
+ },
17125
17722
  panel: {
17126
17723
  ...currentConfig.ui?.panel,
17127
17724
  ...cfg.ui?.panel
17128
17725
  }
17726
+ },
17727
+ tools: {
17728
+ ...currentConfig.tools,
17729
+ ...cfg.tools,
17730
+ client: cfg.tools?.client ?? currentConfig.tools?.client,
17731
+ web: {
17732
+ ...currentConfig.tools?.web,
17733
+ ...cfg.tools?.web
17734
+ }
17129
17735
  }
17130
17736
  };
17131
17737
  resolvedVisitorId = resolveVisitorId(currentConfig);
@@ -17140,16 +17746,16 @@ function update(cfg) {
17140
17746
  setupSessionCoordinator(currentConfig);
17141
17747
  }
17142
17748
  setupCloudCheckpointing(currentConfig);
17749
+ setupTelemetry(currentConfig);
17143
17750
  if (bridge) {
17144
17751
  if (typeof cfg.allowActions === "boolean") {
17145
17752
  bridge.setAllowActions(cfg.allowActions && currentMode === "controller");
17146
17753
  }
17147
- if (cfg.allowedDomains || cfg.crossDomainPolicy || cfg.domainScopeMode || cfg.externalNavigationPolicy) {
17754
+ if (cfg.allowedDomains || cfg.domainScopeMode || cfg.externalNavigationPolicy) {
17148
17755
  bridge.setNavigationPolicy({
17149
17756
  allowedDomains: cfg.allowedDomains,
17150
17757
  domainScopeMode: cfg.domainScopeMode,
17151
- externalNavigationPolicy: cfg.externalNavigationPolicy,
17152
- crossDomainPolicy: cfg.crossDomainPolicy
17758
+ externalNavigationPolicy: cfg.externalNavigationPolicy
17153
17759
  });
17154
17760
  }
17155
17761
  }
@@ -17164,6 +17770,8 @@ function update(cfg) {
17164
17770
  function shutdown() {
17165
17771
  hideTaskSuggestion();
17166
17772
  persistRuntimeState();
17773
+ void flushTelemetry(true);
17774
+ stopTelemetry();
17167
17775
  cloudCheckpointClient?.markDirty();
17168
17776
  cloudCheckpointClient?.syncNow();
17169
17777
  cloudCheckpointClient?.stop();
@@ -17181,6 +17789,10 @@ function shutdown() {
17181
17789
  runtimeId = "";
17182
17790
  resolvedVisitorId = void 0;
17183
17791
  suppressCheckpointSync = false;
17792
+ telemetryInFlight = false;
17793
+ telemetryPausedAuth = false;
17794
+ telemetryBuffer = [];
17795
+ telemetrySeq = 0;
17184
17796
  currentMode = "controller";
17185
17797
  pendingTaskSuggestion = null;
17186
17798
  runtimeStateStore = null;