Repository navigation
[Bug]: Duplicate "BrowserSkill" tab groups stack up in the same Agent Window after the MV3 service worker restarts | 同一 Agent Window 在 Service Worker 重启后会叠加多个同名分组 #534
Description
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
bsk session start→ an Agent Window opens and its tabs are placed into an orange tab group titledBrowserSkill. This part works as intended.- 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.) - In the same Agent Window, run any further action that opens or navigates a tab, e.g.
bsk navigate "https://example.com/..." --session <session>. - Inspect the tab strip.
- 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 stoptakes thewindow_releasedpath (the Agent Window still holds a user tab), the window and itsBrowserSkillgroup 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' });
}tabGroups.query({})is unfiltered and its result is matched only against the cached id — it never asks "does this window already have a group titledBrowserSkill?".- After a service-worker restart the Map is empty, so
nisundefined. - An
undefinednselectscreateProperties: { windowId }, which creates a brand-new group instead of reusing the one already on screen.
Suggested fix, in order of preference:
- Look the group up before creating it:
const [existing] = await chrome.tabGroups.query({ windowId: e, title: 'BrowserSkill' }); if (existing) n = existing.id;
- Or persist the cache so it survives SW restarts:
chrome.storage.session, keyed bywindowId. - On the
window_releasedrelease path,chrome.tabs.ungroup()the session's tabs so the label does not outlive the session. - Optionally expose an opt-out. The Automation settings panel currently offers only
confirmTabBorrowandrequestHelpEnabled, andgroups = 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
复现步骤
bsk session start→ 打开 Agent Window,其标签被放入一个橙色、名为BrowserSkill的标签页分组。这一步符合预期。- 强制回收扩展的 service worker:
chrome://extensions→ BrowserSkill → Service worker → 终止。(等 MV3 空闲超时、或直接重启 Chrome,效果相同。) - 在同一个 Agent Window 里再做一次会打开/跳转标签页的动作,例如
bsk navigate "https://example.com/..." --session <session>。 - 观察标签栏。
- 重复第 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 只缓存在内存里,且恢复路径从不按窗口去查已有分组。
tabGroups.query({})没有按windowId过滤,其结果只与缓存 id 比对——从不询问"这个窗口里是不是已经有一个叫BrowserSkill的分组"。- service worker 重启后 Map 为空,
n变成undefined。 n为undefined时走createProperties: { windowId },于是新建一个分组,而不是复用屏幕上已有的那个。
建议按优先级修复:
- 建组前先查:
const [existing] = await chrome.tabGroups.query({ windowId: e, title: 'BrowserSkill' }); if (existing) n = existing.id; - 或把缓存持久化到
chrome.storage.session(以windowId为键),使其能跨 SW 重启存活。 - 在
window_released释放路径上对会话标签执行chrome.tabs.ungroup(),避免组名比会话活得更久。 - 可选:提供一个关闭该行为的开关。目前 Automation settings 面板只有
confirmTabBorrow与requestHelpEnabled,而groups = new Map与title: 'BrowserSkill'都是硬编码,没有任何面向用户的开关。
需要截图的话可以提供。
Activity