diff --git a/apps/extension/src/entrypoints/popup/App.test.tsx b/apps/extension/src/entrypoints/popup/App.test.tsx
index f32dacd..5800104 100644
--- a/apps/extension/src/entrypoints/popup/App.test.tsx
+++ b/apps/extension/src/entrypoints/popup/App.test.tsx
@@ -100,6 +100,24 @@ describe("App", () => {
expect(screen.queryByText("录制你的操作,供 Agent 参考")).toBeNull();
});
+ it("renders localized copy for a version failure instead of protocol details", () => {
+ mockUseConnectionState.mockReturnValue({
+ snapshot: {
+ ...baseSnapshot,
+ lastError:
+ "[handshake] daemon rejected handshake: version_too_old — peer protocol 1.0 is below local min_compatible_protocol 1.1",
+ },
+ statusState: "disconnected",
+ setLabel,
+ setConnectionEnabled,
+ });
+
+ render();
+
+ const error = screen.getByText(/bsk 与 BrowserSkill 扩展版本不兼容/);
+ expect(error.textContent).not.toContain("min_compatible_protocol");
+ });
+
it("shows single-line compact metadata and copies the instance id", async () => {
mockUseConnectionState.mockReturnValue({
snapshot: {
diff --git a/apps/extension/src/entrypoints/popup/App.tsx b/apps/extension/src/entrypoints/popup/App.tsx
index 3092a06..c8be08c 100644
--- a/apps/extension/src/entrypoints/popup/App.tsx
+++ b/apps/extension/src/entrypoints/popup/App.tsx
@@ -83,6 +83,9 @@ export function App() {
const daemonProtocol = snapshot.handshake?.protocol_version ?? "—";
const extensionVersion = snapshot.extensionVersion || "—";
const instanceId = snapshot.instanceId || "—";
+ const errorMessage = snapshot.lastError?.includes("version_too_old")
+ ? t("popup.versionIncompatibleError")
+ : snapshot.lastError;
const copyInstanceId = async () => {
if (!snapshot.instanceId || !navigator.clipboard?.writeText) return;
@@ -252,12 +255,12 @@ export function App() {
- {snapshot.lastError && (
+ {errorMessage && (
- {snapshot.lastError}
+ {errorMessage}
)}
diff --git a/apps/extension/src/lib/__tests__/connection-controller.test.ts b/apps/extension/src/lib/__tests__/connection-controller.test.ts
index 6d5b578..6790916 100644
--- a/apps/extension/src/lib/__tests__/connection-controller.test.ts
+++ b/apps/extension/src/lib/__tests__/connection-controller.test.ts
@@ -27,19 +27,19 @@ function handshake(
describe("computeConnectedState (protocol-based compat)", () => {
it("returns connected when protocol strings match", () => {
- expect(computeConnectedState(handshake("1.0", "1.0"), MIN_COMPATIBLE_PROTOCOL)).toEqual({
+ expect(computeConnectedState(handshake("1.1", "1.1"), MIN_COMPATIBLE_PROTOCOL)).toEqual({
kind: "connected",
});
});
it("returns version_skew when daemon protocol minor is newer", () => {
- expect(computeConnectedState(handshake("1.1", "1.0"))).toEqual({
+ expect(computeConnectedState(handshake("1.2", "1.1"))).toEqual({
kind: "version_skew",
});
});
it("returns version_skew when daemon protocol string differs but floor is satisfied", () => {
- expect(computeConnectedState(handshake("1", "1.0"))).toEqual({
+ expect(computeConnectedState(handshake("1.1.0", "1.1"))).toEqual({
kind: "version_skew",
});
});
@@ -48,16 +48,19 @@ describe("computeConnectedState (protocol-based compat)", () => {
const result = computeConnectedState(handshake("2.0", "1.0"));
expect(result.kind).toBe("rejected");
if (result.kind === "rejected") {
- expect(result.reason).toContain("protocol-major mismatch");
+ expect(result.reason).toContain("bsk CLI");
+ expect(result.reason).toContain("BrowserSkill extension");
+ expect(result.reason).not.toContain("protocol");
}
});
it("rejects when extension is below daemon min_compatible_protocol", () => {
- const result = computeConnectedState(handshake("1.0", "1.5"));
+ const result = computeConnectedState(handshake("1.1", "1.5"));
expect(result.kind).toBe("rejected");
if (result.kind === "rejected") {
- expect(result.reason).toContain("min_compatible_protocol");
- expect(result.reason).toContain("1.5");
+ expect(result.reason).toContain("BrowserSkill extension is out of date");
+ expect(result.reason).toContain("Update and reload the extension");
+ expect(result.reason).not.toContain("min_compatible_protocol");
}
});
@@ -65,7 +68,7 @@ describe("computeConnectedState (protocol-based compat)", () => {
const result = computeConnectedState({
server: "browser-skill-daemon",
version: "0.1.0",
- protocol_version: "1.0",
+ protocol_version: "1.1",
min_compatible_peer: "0.1.0",
});
expect(result).toEqual({ kind: "connected" });
@@ -75,16 +78,23 @@ describe("computeConnectedState (protocol-based compat)", () => {
const result = computeConnectedState(handshake("1.0", "1.0"), "1.1");
expect(result.kind).toBe("rejected");
if (result.kind === "rejected") {
- expect(result.reason).toContain("below extension min_compatible_protocol");
+ expect(result.reason).toContain("bsk CLI is out of date");
+ expect(result.reason).toContain("bsk daemon restart");
+ expect(result.reason).not.toContain("min_compatible_protocol");
}
});
+ it("rejects a pre-Trace-v3 daemon using the configured extension floor", () => {
+ const result = computeConnectedState(handshake("1.0", "1.0"));
+ expect(result.kind).toBe("rejected");
+ });
+
it("rejects malformed daemon min_compatible_protocol with a daemon-floor reason", () => {
- const result = computeConnectedState(handshake("1.0", "not-a-protocol"));
+ const result = computeConnectedState(handshake("1.1", "not-a-protocol"));
expect(result.kind).toBe("rejected");
if (result.kind === "rejected") {
- expect(result.reason).toContain("daemon min_compatible_protocol");
- expect(result.reason).toContain("not-a-protocol");
+ expect(result.reason).toContain("could not verify version compatibility");
+ expect(result.reason).not.toContain("min_compatible_protocol");
}
});
@@ -242,11 +252,11 @@ describe("ConnectionController connectionEnabled", () => {
const second = transport.send.mock.calls[1]?.[0] as { id: string };
expect(second.id).not.toBe(first.id);
- transport.emitMessage({ id: first.id, result: handshake("1.0", "1.0") });
+ transport.emitMessage({ id: first.id, result: handshake("1.1", "1.1") });
await Promise.resolve();
expect(controller.snapshot().state).not.toBe("connected");
- transport.emitMessage({ id: second.id, result: handshake("1.0", "1.0") });
+ transport.emitMessage({ id: second.id, result: handshake("1.1", "1.1") });
await vi.waitFor(() => expect(controller.snapshot().state).toBe("connected"));
});
});
diff --git a/apps/extension/src/lib/connection-controller.ts b/apps/extension/src/lib/connection-controller.ts
index 687fe8a..8da78a3 100644
--- a/apps/extension/src/lib/connection-controller.ts
+++ b/apps/extension/src/lib/connection-controller.ts
@@ -313,6 +313,15 @@ export type HandshakeVerdict =
| { kind: "version_skew" }
| { kind: "rejected"; reason: string };
+const UPDATE_BOTH_VERSION_MESSAGE =
+ "The installed bsk CLI and BrowserSkill extension are incompatible. Update both, reload the extension, then run `bsk daemon restart`.";
+const UPDATE_BSK_VERSION_MESSAGE =
+ "The installed bsk CLI is out of date for this BrowserSkill extension. Update bsk, then run `bsk daemon restart`.";
+const UPDATE_EXTENSION_VERSION_MESSAGE =
+ "This BrowserSkill extension is out of date for the installed bsk CLI. Update and reload the extension, then try again.";
+const INVALID_VERSION_MESSAGE =
+ "BrowserSkill could not verify version compatibility. Update bsk and the BrowserSkill extension, reload the extension, then run `bsk daemon restart`.";
+
/**
* Symmetric extension-side **protocol** compat check.
*
@@ -339,19 +348,19 @@ export function computeConnectedState(
if (daemonMajor === null || ourMajor === null || daemonMajor !== ourMajor) {
return {
kind: "rejected",
- reason: `protocol-major mismatch (daemon protocol v${handshake.protocol_version}, extension protocol v${PROTOCOL_VERSION})`,
+ reason: UPDATE_BOTH_VERSION_MESSAGE,
};
}
if (compareProtocol(handshake.protocol_version, localMinCompatibleProtocol) === null) {
return {
kind: "rejected",
- reason: `daemon protocol v${handshake.protocol_version} is unparseable`,
+ reason: INVALID_VERSION_MESSAGE,
};
}
if (compareProtocol(handshake.protocol_version, localMinCompatibleProtocol)! < 0) {
return {
kind: "rejected",
- reason: `daemon protocol v${handshake.protocol_version} below extension min_compatible_protocol ${localMinCompatibleProtocol}`,
+ reason: UPDATE_BSK_VERSION_MESSAGE,
};
}
const peerFloor = handshake.min_compatible_protocol;
@@ -359,13 +368,13 @@ export function computeConnectedState(
if (compareProtocol(PROTOCOL_VERSION, peerFloor) === null) {
return {
kind: "rejected",
- reason: `daemon min_compatible_protocol ${peerFloor} is unparseable`,
+ reason: INVALID_VERSION_MESSAGE,
};
}
if (compareProtocol(PROTOCOL_VERSION, peerFloor)! < 0) {
return {
kind: "rejected",
- reason: `extension protocol v${PROTOCOL_VERSION} below daemon min_compatible_protocol ${peerFloor}`,
+ reason: UPDATE_EXTENSION_VERSION_MESSAGE,
};
}
}
diff --git a/crates/bsk-cli/skill/SKILL.md b/crates/bsk-cli/skill/SKILL.md
index e1cffb9..8546a15 100644
--- a/crates/bsk-cli/skill/SKILL.md
+++ b/crates/bsk-cli/skill/SKILL.md
@@ -265,19 +265,26 @@ retry — complete the task autonomously or stop gracefully.
### Recording — `bsk record`
-Capture the user's own actions in the Agent Window to a `trace.json`, for later LLM-driven automation:
+Capture the user's own actions in the Agent Window to a **trace v3 bundle**, for later LLM-driven automation:
```bash
-bsk record start --browser [--url https://…] [--purpose "publish a wiki doc"] [--output trace.json]
+bsk record start --browser \
+ [--url https://…] [--purpose "publish a wiki doc"] \
+ [--max-page-tokens 3000] [--redact-values] \
+ [--output trace]
# `--url` is optional; default https://example.com/ when omitted (must be http(s)).
-# Blocks until the user clicks Finish in the recording panel, then writes ./trace.json and closes the window.
+# Blocks until the user clicks Finish in the recording panel, then writes:
+# trace/trace.json — action chain + state index (version 3)
+# trace/pages/ — one `.vom.txt` observe snapshot per settled page state
-bsk record stop [--output trace.json] # terminal fallback if the browser panel is unavailable
+bsk record stop [--output trace] # terminal fallback if the browser panel is unavailable
```
-- The trace is a **record-only action log** (a `pages[]` dictionary + `navigate`/`click`/`fill`/`select`/`press` steps with `target` descriptors). It records *what the user did*; deciding which inputs are variable is left to the executing agent.
+- **Bundle layout:** `--output` is a directory (default `./trace`) containing `trace.json` and `pages/`. `trace.json` lists `states[]` (page observation ids) and `steps[]` (each step binds `state` = observe snapshot *before* the action and `result.state` = snapshot *after* settle). Page bodies live in `pages/sN.vom.txt` using the same VOM format as `bsk observe`.
+- Each `states[]` entry is one **settled page observation** (captured after recording start, navigation landing, or action settle — not on a timer).
+- `target.ref` values like `@e12` exist **only inside** the bundle for disambiguation; do **not** copy `@eN` refs into SKILL.md or agent runbooks — use visible names from the observation text instead.
- `--purpose` is optional context metadata; it does **not** change what gets captured.
-- There is **no** `bsk replay` — to redo a flow, read the trace and reuse the existing `session` / `snapshot` / `@eN` / `click` / `fill` tools. Follow **Stop when the goal is met**.
+- `--redact-values` masks all form values in page files as `[filled]` / `[empty]`.
- Do **not** record on banking/SSO/password-manager pages; passwords are redacted but traces may still contain sensitive text.
## Error handling
diff --git a/packages/i18n/src/locales/en-US/extension.json b/packages/i18n/src/locales/en-US/extension.json
index 68c1a3e..4ef0956 100644
--- a/packages/i18n/src/locales/en-US/extension.json
+++ b/packages/i18n/src/locales/en-US/extension.json
@@ -6,20 +6,20 @@
"disconnected": "Disconnected",
"connecting": "Connecting…",
"connected": "Connected",
- "version_skew": "Protocol mismatch",
+ "version_skew": "Version mismatch",
"disabled": "Connection off"
},
"stateDetail": {
"disconnected": "Open BrowserSkill to connect.",
"connecting": "Connecting to BrowserSkill…",
"connected": "Ready for automation tasks.",
- "version_skew": "Some features may be unavailable."
+ "version_skew": "bsk and the browser extension differ in version. You can continue using them, but updating is recommended."
},
"stateBadge": {
"disconnected": "Standby",
"connecting": "Igniting",
"connected": "Ready",
- "version_skew": "Action needed",
+ "version_skew": "Update recommended",
"disabled": "Off"
},
"connectionToggleTitle": "BrowserSkill connection",
@@ -27,6 +27,7 @@
"controlHintsToggleHint": "Show the status pill and orange glow while the Agent controls a page.",
"controlHintsInfoLabel": "About control hints",
"versionSkewWarning": "Extension protocol v{{extensionProtocol}}, daemon v{{daemonProtocol}}.",
+ "versionIncompatibleError": "bsk and the BrowserSkill extension are incompatible. Update both, reload the extension, then run bsk daemon restart.",
"upgradeAvailable": "Upgradable",
"launcher": {
"title": "Quick actions"
diff --git a/packages/i18n/src/locales/zh-CN/extension.json b/packages/i18n/src/locales/zh-CN/extension.json
index 520b791..331e173 100644
--- a/packages/i18n/src/locales/zh-CN/extension.json
+++ b/packages/i18n/src/locales/zh-CN/extension.json
@@ -6,20 +6,20 @@
"disconnected": "未连接",
"connecting": "连接中…",
"connected": "已连接",
- "version_skew": "协议不一致",
+ "version_skew": "版本不一致",
"disabled": "连接已关闭"
},
"stateDetail": {
"disconnected": "请先打开 BrowserSkill。",
"connecting": "正在连接 BrowserSkill…",
"connected": "可接收自动化任务。",
- "version_skew": "部分能力可能不可用。"
+ "version_skew": "bsk 与浏览器扩展版本不同,当前仍可使用,建议更新至最新版本。"
},
"stateBadge": {
"disconnected": "Standby",
"connecting": "Igniting",
"connected": "Ready",
- "version_skew": "Action needed",
+ "version_skew": "建议更新",
"disabled": "Off"
},
"connectionToggleTitle": "BrowserSkill 连接",
@@ -27,6 +27,7 @@
"controlHintsToggleHint": "Agent 控制页面时显示提示条和橙色闪光。",
"controlHintsInfoLabel": "控制提示说明",
"versionSkewWarning": "扩展协议 v{{extensionProtocol}},daemon 协议 v{{daemonProtocol}}。",
+ "versionIncompatibleError": "bsk 与 BrowserSkill 扩展版本不兼容。请将两者更新至最新版本,重新加载扩展后,再运行 bsk daemon restart。",
"upgradeAvailable": "可升级",
"launcher": {
"title": "快捷功能"
diff --git a/skill/SKILL.md b/skill/SKILL.md
index e1cffb9..8546a15 100644
--- a/skill/SKILL.md
+++ b/skill/SKILL.md
@@ -265,19 +265,26 @@ retry — complete the task autonomously or stop gracefully.
### Recording — `bsk record`
-Capture the user's own actions in the Agent Window to a `trace.json`, for later LLM-driven automation:
+Capture the user's own actions in the Agent Window to a **trace v3 bundle**, for later LLM-driven automation:
```bash
-bsk record start --browser [--url https://…] [--purpose "publish a wiki doc"] [--output trace.json]
+bsk record start --browser \
+ [--url https://…] [--purpose "publish a wiki doc"] \
+ [--max-page-tokens 3000] [--redact-values] \
+ [--output trace]
# `--url` is optional; default https://example.com/ when omitted (must be http(s)).
-# Blocks until the user clicks Finish in the recording panel, then writes ./trace.json and closes the window.
+# Blocks until the user clicks Finish in the recording panel, then writes:
+# trace/trace.json — action chain + state index (version 3)
+# trace/pages/ — one `.vom.txt` observe snapshot per settled page state
-bsk record stop [--output trace.json] # terminal fallback if the browser panel is unavailable
+bsk record stop [--output trace] # terminal fallback if the browser panel is unavailable
```
-- The trace is a **record-only action log** (a `pages[]` dictionary + `navigate`/`click`/`fill`/`select`/`press` steps with `target` descriptors). It records *what the user did*; deciding which inputs are variable is left to the executing agent.
+- **Bundle layout:** `--output` is a directory (default `./trace`) containing `trace.json` and `pages/`. `trace.json` lists `states[]` (page observation ids) and `steps[]` (each step binds `state` = observe snapshot *before* the action and `result.state` = snapshot *after* settle). Page bodies live in `pages/sN.vom.txt` using the same VOM format as `bsk observe`.
+- Each `states[]` entry is one **settled page observation** (captured after recording start, navigation landing, or action settle — not on a timer).
+- `target.ref` values like `@e12` exist **only inside** the bundle for disambiguation; do **not** copy `@eN` refs into SKILL.md or agent runbooks — use visible names from the observation text instead.
- `--purpose` is optional context metadata; it does **not** change what gets captured.
-- There is **no** `bsk replay` — to redo a flow, read the trace and reuse the existing `session` / `snapshot` / `@eN` / `click` / `fill` tools. Follow **Stop when the goal is met**.
+- `--redact-values` masks all form values in page files as `[filled]` / `[empty]`.
- Do **not** record on banking/SSO/password-manager pages; passwords are redacted but traces may still contain sensitive text.
## Error handling