Skip to content

[Bug]: Duplicate "BrowserSkill" tab groups stack up in the same Agent Window after the MV3 service worker restarts | 同一 Agent Window 在 Service Worker 重启后会叠加多个同名分组 #534

Open

Description

@Yiki0613

Prerequisites

  • I have searched existing open and closed issues to ensure this is not a duplicate.
  • I have verified that all sensitive data (passwords, tokens, API keys, private URLs, personal info) have been redacted from logs and snippets.

BrowserSkill Version or Commit

0.4.0 (protocol 1.7)

Operating System

macOS (Apple Silicon)

OS Details and Architecture

macOS 27.0.1 arm64 (Apple M-series)

Installation Method

Other — binary at ~/.local/bin/bsk, installed and upgraded by the built-in auto-updater (0.3.3 → 0.4.0)

Browser and Browser Extension Version

Google Chrome 156.0.8078.79 / Extension 0.4.0 (MV3)

Exact Command or API Call

bsk session start --json
bsk navigate "https://example.com/chat/<id>" --session <session>
bsk session stop <session>

Minimal Reproduction Steps

  1. bsk session start → an Agent Window opens and its tabs are placed into an orange tab group titled BrowserSkill. This part works as intended.
  2. Force the extension's service worker to be recycled: chrome://extensions → BrowserSkill → Service worker → Terminate. (Waiting for the MV3 idle timeout, or restarting Chrome, has the same effect.)
  3. In the same Agent Window, run any further action that opens or navigates a tab, e.g. bsk navigate "https://example.com/..." --session <session>.
  4. Inspect the tab strip.
  5. Repeat step 2–3 a few times.

Expected Behavior

One BrowserSkill group per Agent Window. After the service worker restarts, reconcile() should discover the existing group in that window and reuse it.

Actual Behavior

A second orange BrowserSkill group appears in the same window. The first group keeps its tab(s) (about:blank), while the new group only receives tabs whose groupId === -1, so the two groups coexist. Repeating step 2–3 stacks more of them.

Observed: two BrowserSkill groups side by side in a single window, each holding a single tab.

Two secondary symptoms of the same design:

  • Groups outlive the session. When bsk session stop takes the window_released path (the Agent Window still holds a user tab), the window and its BrowserSkill group label are deliberately preserved, but nothing ever ungroups or reclaims the label.
  • Groups come back after a crash. Chrome session restore revives the saved groups (profile.exit_type = "Crashed"), so a single crash permanently multiplies the labels.

Relevant Logs / Diagnostics Output

No error is surfaced — reconcile() swallows failures:

catch (e) { console.debug("[bsk] Agent Window tab group update failed", e) }

Local evidence from Chrome's SNSS session files: group metadata records carry a title field that is exactly BrowserSkill — 28 such records in one Tabs_* snapshot and 6 in a later one, with the group tokens of the later file fully contained in the earlier one. That is consistent with the same logical groups being re-created and restored rather than replaced.

Is this a regression?

No / Not sure

Additional Context

Root cause in the extension's background.js (0.4.0). The group-id cache is in-memory only and the recovery path never looks the group up by window:

// constructor — cache does not survive a service-worker restart
groups = new Map;

async reconcile(e) {
  let t = this.manager.findByWindowId(e),
      n = this.groups.get(e),
      r = n === void 0 ? void 0
        : (await this.api.tabGroups.query({})).find(g => g.id === n);   // (1)
  if (!r) this.groups.delete(e), n = void 0;                            // (2)
  ...
  n = await this.api.tabs.group({
        tabIds: c,
        ...(n === void 0 ? { createProperties: { windowId: e } } : { groupId: n })
      });                                                               // (3)
  n !== void 0 && (r?.title !== 'BrowserSkill' || r.color !== 'orange')
    && await this.api.tabGroups.update(n, { title: 'BrowserSkill', color: 'orange' });
}
  1. tabGroups.query({}) is unfiltered and its result is matched only against the cached id — it never asks "does this window already have a group titled BrowserSkill?".
  2. After a service-worker restart the Map is empty, so n is undefined.
  3. An undefined n selects createProperties: { windowId }, which creates a brand-new group instead of reusing the one already on screen.

Suggested fix, in order of preference:

  1. Look the group up before creating it:
    const [existing] = await chrome.tabGroups.query({ windowId: e, title: 'BrowserSkill' });
    if (existing) n = existing.id;
  2. Or persist the cache so it survives SW restarts: chrome.storage.session, keyed by windowId.
  3. On the window_released release path, chrome.tabs.ungroup() the session's tabs so the label does not outlive the session.
  4. Optionally expose an opt-out. The Automation settings panel currently offers only confirmTabBorrow and requestHelpEnabled, and groups = new Map / title: 'BrowserSkill' are hard-coded with no user-facing switch.

Screenshots of the duplicated groups are available on request.


中文说明

版本与环境

  • BrowserSkill 0.4.0(protocol 1.7),扩展 0.4.0(MV3)
  • macOS 27.0.1 arm64(Apple 芯片)
  • Google Chrome 156.0.8078.79
  • 安装方式:二进制位于 ~/.local/bin/bsk,由内置自动更新从 0.3.3 升到 0.4.0

复现步骤

  1. bsk session start → 打开 Agent Window,其标签被放入一个橙色、名为 BrowserSkill 的标签页分组。这一步符合预期。
  2. 强制回收扩展的 service worker:chrome://extensions → BrowserSkill → Service worker → 终止。(等 MV3 空闲超时、或直接重启 Chrome,效果相同。)
  3. 在同一个 Agent Window 里再做一次会打开/跳转标签页的动作,例如 bsk navigate "https://example.com/..." --session <session>。
  4. 观察标签栏。
  5. 重复第 2–3 步若干次。

期望行为

每个 Agent Window 只应有一个 BrowserSkill 分组。service worker 重启后,reconcile() 应当发现该窗口里已存在的分组并复用。

实际行为

同一窗口里出现了第二个橙色 BrowserSkill 分组。旧分组保留原有标签(about:blank),新分组只收走 groupId === -1 的标签,于是两组并存。重复第 2–3 步会继续叠加。

实测现象:一个窗口里并排两个 BrowserSkill 分组,各自只装着一个标签。

同一设计导致的另外两个现象:

  • 分组比会话活得久。bsk session stop 走 window_released 分支时(Agent Window 里还留着用户标签页),窗口和 BrowserSkill 组名会被刻意保留,但之后没有任何逻辑去取消分组或回收这个组名。
  • 崩溃后分组会复活。Chrome 的会话恢复会把已保存的分组重新拉起来(profile.exit_type = "Crashed"),一次崩溃即永久多出若干组名。

日志 / 诊断

不会报错——reconcile() 把异常吞掉了:

catch (e) { console.debug("[bsk] Agent Window tab group update failed", e) }

本地证据:Chrome 的 SNSS 会话文件里存在标题字段精确等于 BrowserSkill 的分组元数据记录——较旧的一个 Tabs_* 快照里有 28 条,较新的一个里有 6 条,且后者的分组 token 全部包含在前者之中。这说明是同一批逻辑分组被反复重建/恢复,而不是被替换。

是否为回归

不确定(仅在 0.4.0 上验证)

根因与建议

根因在扩展 background.js(0.4.0):分组 id 只缓存在内存里,且恢复路径从不按窗口去查已有分组。

  1. tabGroups.query({}) 没有按 windowId 过滤,其结果只与缓存 id 比对——从不询问"这个窗口里是不是已经有一个叫 BrowserSkill 的分组"。
  2. service worker 重启后 Map 为空,n 变成 undefined。
  3. n 为 undefined 时走 createProperties: { windowId },于是新建一个分组,而不是复用屏幕上已有的那个。

建议按优先级修复:

  1. 建组前先查:const [existing] = await chrome.tabGroups.query({ windowId: e, title: 'BrowserSkill' }); if (existing) n = existing.id;
  2. 或把缓存持久化到 chrome.storage.session(以 windowId 为键),使其能跨 SW 重启存活。
  3. 在 window_released 释放路径上对会话标签执行 chrome.tabs.ungroup(),避免组名比会话活得更久。
  4. 可选:提供一个关闭该行为的开关。目前 Automation settings 面板只有 confirmTabBorrow 与 requestHelpEnabled,而 groups = new Map 与 title: 'BrowserSkill' 都是硬编码,没有任何面向用户的开关。

需要截图的话可以提供。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions