|r~$w2oQ;`8_6hlZZ&5=+z8uOTjVGvxakpUrBQT
zyhx85@B;Zx1V5Z1$dL}f4{xLa^Gg2Elc8AjAwb8{!a_b_Kt=)y5~>CvRu_E>$fPuA
z1qi_Ey;uNB_czJ?#}D(+YDnkDxNL+S9Z{YY6f&c58Losr(n3{!B6{rD{)h5-mIFBP
zqNd4z9L3w3Bo`cvcGIjkwyBHJT6=06*cOpKS$my^mW0N1uGNIgg;Gv&+Ng|MQ$~L_
z+)fJXm}g}Pp>@)GoAxs9k}BQymd;-}(MzlKvGGLCQ_Zu5k0V!QnxvHS$RCuuB`+!!
zNxowo6rc1^IrBmYzFV33o&eZrKGM8?!J(NqD+-El1rc9SI{8W(qzJ#ZPtqYti<^7H
zSPDvS092iVoJ3SoZ^`dC9o2Xph{|^>z!wvMbwQpu)`t92i-H=JV+N4lz#?jI^$_SE
zFyU2nW7~tFLv&FV(9T3s9Dq$A9v`=lkvI-6dF%nc2rM)XtXa`OLFBQ3$_*_xen(pU
zh4vOAl$tV;3>*{cud|r}m$%Ps%=1MWR9vPub~@`(H;u`Aj~$K-t2*4RUzML*ElscJ
zf0Ss)@Nl=lhj4p$!nO~C&gzXR$UplsslB9EUxfRzZD1y$|LQV@*@ehwlu6|PsUl=a
z0H`Zx^gYb4PquB9CeDheYq+V5otViJq&cgOMf|A(btiu~vD=yON~iVex=X|{S09Az
zsVH^f_OwE#^S)V?M!=%rsDLcT>!ElsNNadyER6(J1_BiM(^CXMmi2Bc=$4rp{;xP>
zM7s`x?M3IcvN{}r{O|z|elxPvpj;ru7FS#Rr?Mnv!LIk}HwQ!7P*dtO#I(kMm8(SM
zxGt@#?$>WSW1?(`qBV(bXe7TRG?8!~Kf&3!gl7~!CGM@5QgBLH2+BjumLas>%Y>Si
z`30~`Iv=AS5^yEq|
z;qH0U8M4UdnSJq%Xby_Ozsmg2Ltyw6LChFHLkcS&?4RgkA9-h|`t#iCe?=OpEeMRD
z^ZpeFYa%iA8(X-yW46g5jou$}i%mDoK;=t22GuEi_H^WY9SimcVUF#4-mXY&9x@Ay`K!!j}$b}5gH9OB19HC7^-(6KgwjCKaF
zqJM6om24V5`7qvPzq5?#XdU29L4Gg1NZC-Up<8Gxv%2<_knfxG@%qr6eFeQsDG0yQ
zb+w07Pk5KFI5|DQK9}4nv1kc8+@Drko}7qAv-8VHN!8lld*YV$t@bJ0^`#iad)ct(
zFIE)*N~F+v{LcGkG^GOxI!m`hn}5?(3zgoR+A?}o{oCg*iL>?Lyq90#Z?)+Nwct;e
z$@x=zaPi~f5}s%YM0Vad9>Sm<4w(dctmgY=?+b5q}O2kEK|WA4<8e{z}g}ECPUKE3nb{
z@lXtR`RdjCX94mq%ud!!mGD5pli`l;AGTIU-qJW24cMX@mJWLb5IWlCGiq3{E%x16
z^qLmtVXOg`|3R7%uhy#dH8&qe{nn8000A`<9v}!I4sY?N2;(mfRcOuMs7ut7;Plxt
z_0=BT&S)f8kNSR_FuOJ|yURbOxNH
zmn!`X%+q;D>+}i7bb#I81@L9?G2|AdC(IJp@A!$EEOon1qvFpPB_lmOUWg^R~Yn8P<9JKt9?EGnSTtK(D))gBSaE&?f9h&8sL
zE|_ajXfJvekV+lOze)ka;_l>Ff$@1*=o#M{B#^=T!Hy=hl;T!0v!bhjgzt|NQ?2Wt-;dnH8=Puow@!Z{M
zowiuJQ!+sew32`$`M_A-3are6<)gm`jAcz-gOdhl4yd^
zx2`24fojhif;-N4FLPavz(yf6
z#m8r!%NVwvv>yKHe#HM4Pq_#v#V7l>=vctyIRAYo=55^}VpDJT+PwC^$^-@KP5vE&
zHHFk{RM1U}sjGz~ecwPL!d{(R3K#FIZ4*8KO8e`yOAz^R2F0EU#%-
zk~Lq%{vxO^t4Mf@xEpT;eFJHf0shQ0BpQ?01PP3?!aXFz*R7#r1QNIhAW>C|*4P_40)Qc%#|i-|UZENrkt_(jKLN<0gY8+7
zkJf$T3xgD$XZb!XZcH)qM)Z3C+n{8lfIuBlo9vJYw9n>oc&Cr7OrQKyb0-O3dKRH6ODXT+=Pq{3;e^bAK^u15I}cfMbgz!ml^~9@0|~9=eS%F);o`)ahsUm1UoK(H@#B{nV
zZt3Kfwr$0>^Myaw_PL*;U91bIjy|D6F9qLv1m}Xi2nsdvpyScW0}UjrP@(-*<;ENw
zxbj${b_SS5S$|?Jsi3nyCzG;@3l<`s4yU55orn(y9|C-QG{skp
zx)c9$Zd~LsI9}z1MZuRL)iTwVq-7Sy)TH?5_TmUL
zCQ0LJX!HHfcX|S{tsTrcu_hUj|G#=gJ7wsx_SiNKuByDO=5
z)JcB5YTIS?;Bi3lwY4%EDIMhArby}glQlkJ?$rsp+SZj0R(wMK8qcp;m?oD_5Z{d{
zJY;9AEIVtw5!#&o95!oeE>%o2Z!|lq3jU}sV9mSk`FX4W!1=&VOY7NEE&)3p*N3b2
zIxqtR=alSz0m{M{yvY-?gj11YenhT4Dqjl;G#A%urM2Xb&p_6b>1-6vA`gdld!PBnk28TI1q2We{iH;?E3FWn
zyHB7?V4?}L8fkA4VxUJ~N#300>B|Rq()N^_5t%w>F$c8kGZ~71@ga0{BQ$=FLwh_2
z7}^_$2ah8^EnL6<{4~|m&+ty-`n~MYL)C}^#zokz@`8oimV?*w3&y#i^BuqA$mFn5
zq8lPJg-xxA)-G3V+pxOYfJn(T1;
z5{D#doC~xEc_PSgAG8KrXb3q~)ib3k1JI?4LMg1?sn3rJV2<{+{j!%vhk9SS=viW%
zBaECY@3=cKq&M;Nb}2uN00S+{tuO4Cfz=I{+SNTdI>?^=xy3ayu30RIpNIlTR)ONN}5^sabt*
z&oKknNN9ai2<>1Gv!OFCibSS=ib#!HDWc6IGme}2Sco)O=)U|uS}cl9V?6&_O$tYY
zbC+C@Wuv=OeZ<+xQOk#n@&fv<;{wImn?lrP9X_q-Mair*{eRzoMo1T
z-cy%w9bN~Bg3Y-6l9fHP35_R1DEaM9t66(LKSVvAw3bk+4f}JLbo=VzWa8nEfSVu2
z@Lua`+tG!=i-T!R|J2OWF(#^hCpXUkS9=F{<>H?(=t_uo?tszaLW>A|G=??$*kKIK&Pj{(P
z3afM$!f6haKWtn)c{!I>8Q8&`pXUJ(Aa}aAgC?HLs^G6|sGHv}HNz5?-bP0V1l^kJ
z%)OVvbi>$6eUTYAb^}6(EGh8E&tss(&}7L_l>*K){|W!|)Er|sHdCHl5Ln4N5%K>3tM=&!
literal 0
HcmV?d00001
diff --git a/evals/browser/cases/regression/cooperation-video/run.mjs b/evals/browser/cases/regression/cooperation-video/run.mjs
new file mode 100644
index 00000000..e39973f6
--- /dev/null
+++ b/evals/browser/cases/regression/cooperation-video/run.mjs
@@ -0,0 +1,476 @@
+import assert from "node:assert/strict";
+import { execFile, spawn } from "node:child_process";
+import { once } from "node:events";
+import { appendFile, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
+import { createServer } from "node:http";
+import { tmpdir } from "node:os";
+import { dirname, join, resolve } from "node:path";
+import { fileURLToPath } from "node:url";
+import { promisify } from "node:util";
+import { withChrome } from "../snapshot-coordinates/chrome.mjs";
+
+const directory = dirname(fileURLToPath(import.meta.url));
+const cli = resolve(process.env.BSK_COOP_CLI ?? "target/debug/bsk");
+const executable = process.env.BSK_COOP_CHROME;
+assert(executable, "Set BSK_COOP_CHROME to an isolated Chromium executable");
+const port = Number(process.env.BSK_COOP_PORT ?? 52931);
+assert(Number.isInteger(port) && port > 0 && port !== 52800);
+const extensionPath = resolve(process.env.BSK_COOP_EXTENSION ?? "apps/extension/dist/chrome-mv3");
+assert(
+ (await readFile(join(extensionPath, "background.js"), "utf8")).includes(`ws://127.0.0.1:${port}`),
+ "Build the test extension with BSK_DAEMON_WS_URL matching BSK_COOP_PORT",
+);
+const artifacts = resolve(
+ process.env.BSK_COOP_ARTIFACTS ?? "evals/browser/results/cooperation-video",
+);
+await mkdir(artifacts, { recursive: true });
+const runtime = await mkdtemp(join(tmpdir(), "bsk-video-live-"));
+const env = { ...process.env, BSK_HOME: runtime, BSK_AUTO_START: "0", BSK_AUTO_UPDATE: "off" };
+const exec = promisify(execFile);
+const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
+const commands = [];
+const results = [];
+let daemon;
+let daemonOutput = "";
+let browserVersion;
+const bsk = async (args) => {
+ const start = performance.now();
+ let output, stderr, exitCode;
+ try {
+ const result = await exec(cli, [...args, "--json"], {
+ env,
+ timeout: 35000,
+ maxBuffer: 2 ** 22,
+ });
+ output = result.stdout;
+ stderr = result.stderr;
+ exitCode = 0;
+ } catch (error) {
+ output = error.stdout || "";
+ stderr = error.stderr || error.message;
+ exitCode = error.code;
+ }
+ let result;
+ try {
+ result = JSON.parse(output);
+ } catch {
+ result = { stdout: output, stderr };
+ }
+ const entry = {
+ args,
+ exitCode,
+ durationMs: Math.round(performance.now() - start),
+ result,
+ stderr,
+ };
+ commands.push(entry);
+ await appendFile(join(artifacts, "commands.jsonl"), JSON.stringify(entry) + "\n");
+ return entry;
+};
+const ok = async (args) => {
+ const reply = await bsk(args);
+ assert.equal(reply.exitCode, 0, JSON.stringify(reply));
+ return reply.result;
+};
+const poll = async (read, label, timeout = 15000) => {
+ const until = performance.now() + timeout;
+ while (performance.now() < until) {
+ const value = await read();
+ if (value) return value;
+ await delay(100);
+ }
+ throw new Error("Timed out: " + label);
+};
+const html = await readFile(join(directory, "fixture.html"));
+const clip = await readFile(join(directory, "preview.webm"));
+const server = createServer((req, res) => {
+ if (req.url === "/preview.webm") {
+ res.setHeader("Content-Type", "video/webm");
+ res.setHeader("Content-Length", clip.length);
+ res.end(clip);
+ return;
+ }
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
+ res.end(html);
+});
+const cases = [
+ { name: "video_pixels_only", variant: "pixels", target: false, dependencies: false, allow: true },
+ {
+ name: "timer_text_untargeted_space",
+ variant: "text",
+ target: false,
+ dependencies: false,
+ allow: false,
+ },
+ {
+ name: "timer_text_targeted_scoped_space",
+ variant: "text",
+ target: true,
+ dependencies: true,
+ allow: true,
+ },
+ {
+ name: "timer_text_dependencies_without_target",
+ variant: "text",
+ target: false,
+ dependencies: true,
+ allow: true,
+ },
+ {
+ name: "progress_range_targeted_scoped_space",
+ variant: "range",
+ target: true,
+ dependencies: true,
+ allow: true,
+ },
+ {
+ name: "cosmetic_class_untargeted_space",
+ variant: "cosmetic",
+ target: false,
+ dependencies: false,
+ allow: true,
+ },
+ {
+ name: "continuous_progress_sync_and_space",
+ variant: "range",
+ target: true,
+ dependencies: true,
+ allow: true,
+ continuous: true,
+ },
+ {
+ name: "progress_dependencies_without_target",
+ variant: "range",
+ target: false,
+ dependencies: true,
+ allow: true,
+ },
+ {
+ name: "business_range_still_blocked",
+ variant: "business-range",
+ target: true,
+ dependencies: true,
+ allow: false,
+ },
+ {
+ name: "submitted_timeline_still_blocked",
+ variant: "submitted-range",
+ target: true,
+ dependencies: true,
+ allow: false,
+ },
+ {
+ name: "form_timeline_still_blocked",
+ variant: "form-range",
+ target: true,
+ dependencies: true,
+ allow: false,
+ },
+ {
+ name: "progress_and_business_field_still_blocked",
+ variant: "combined",
+ target: true,
+ dependencies: true,
+ allow: false,
+ },
+ {
+ name: "programmatic_focus_still_blocked",
+ variant: "focus",
+ target: false,
+ dependencies: true,
+ allow: false,
+ focus: true,
+ },
+ {
+ name: "business_field_targeted_scoped_space",
+ variant: "field",
+ target: true,
+ dependencies: true,
+ allow: false,
+ },
+ {
+ name: "learned_price_current_target_blocked",
+ variant: "learned-price",
+ learn: true,
+ target: true,
+ targetSelector: "#buy",
+ allow: false,
+ },
+ {
+ name: "learned_price_declared_dependency_blocked",
+ variant: "learned-price",
+ learn: true,
+ target: true,
+ dependencies: ["#buy"],
+ allow: false,
+ },
+ {
+ name: "learned_price_bare_key_blocked",
+ variant: "learned-price",
+ learn: true,
+ target: false,
+ allow: false,
+ },
+ {
+ name: "learned_price_untargeted_dependency_blocked",
+ variant: "learned-price",
+ learn: true,
+ target: false,
+ dependencies: ["#buy"],
+ allow: false,
+ },
+ {
+ name: "refused_context_requires_sync",
+ variant: "learned-price",
+ learn: true,
+ retryRefused: true,
+ target: true,
+ allow: true,
+ },
+ {
+ name: "explicit_timeline_dependency_still_blocked",
+ variant: "range",
+ target: true,
+ dependencies: ["#progress"],
+ allow: false,
+ },
+];
+try {
+ await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
+ const base = `http://127.0.0.1:${server.address().port}`;
+ daemon = spawn(cli, ["daemon", "start", "--foreground", "--port", String(port)], {
+ env,
+ stdio: ["ignore", "pipe", "pipe"],
+ });
+ daemon.stdout.on("data", (chunk) => {
+ daemonOutput += chunk;
+ });
+ daemon.stderr.on("data", (chunk) => {
+ daemonOutput += chunk;
+ });
+ await poll(async () => (await bsk(["status"])).exitCode === 0, "isolated daemon");
+ await withChrome(
+ {
+ executable,
+ extensionPath,
+ extensionViaCdp: process.env.BSK_COOP_LOAD_CDP !== "0",
+ deviceScale: 1,
+ zoom: 1,
+ headless: true,
+ softwareRendering: true,
+ startupTimeout: 30000,
+ extraArgs: ["--autoplay-policy=no-user-gesture-required"],
+ },
+ async (send) => {
+ browserVersion = await send("Browser.getVersion");
+ const browsers = await poll(async () => {
+ const reply = await ok(["browsers"]);
+ return reply.length && reply;
+ }, "test extension connection");
+ assert.equal(browsers.length, 1, "Only the isolated test browser should use this daemon");
+ for (const scenario of cases) {
+ console.log("CASE " + scenario.name);
+ const { session_id: session } = await ok([
+ "session",
+ "start",
+ "--browser",
+ browsers[0].instance_id,
+ ]);
+ const scope = ["--session", session];
+ let oracle;
+ try {
+ const url = `${base}/?variant=${scenario.variant}&case=${scenario.name}&interval=${scenario.continuous ? 10 : 80}`;
+ await ok(["navigate", url, ...scope]);
+ const target = (await send("Target.getTargets")).targetInfos.find(
+ (target) => target.url === url,
+ );
+ assert(target, "Test tab must exist");
+ ({ sessionId: oracle } = await send("Target.attachToTarget", {
+ targetId: target.targetId,
+ flatten: true,
+ }));
+ await send("Runtime.enable", {}, oracle);
+ await send("Page.bringToFront", {}, oracle);
+ const read = async (expression) => {
+ const reply = await send(
+ "Runtime.evaluate",
+ { expression, returnByValue: true, awaitPromise: true },
+ oracle,
+ );
+ assert(!reply.exceptionDetails, JSON.stringify(reply.exceptionDetails));
+ return reply.result.value;
+ };
+ let playback;
+ try {
+ await poll(async () => {
+ playback = await read(
+ "({ready:window.fixtureReady,error:window.fixtureError,report:window.report?.()})",
+ );
+ assert(!playback.error, playback.error);
+ return playback.ready && playback.report.decodedFrames >= 3;
+ }, "actual video playback");
+ } catch (error) {
+ await writeFile(
+ join(artifacts, scenario.name + ".playback-error.json"),
+ JSON.stringify(playback, null, 2),
+ );
+ throw error;
+ }
+ let learning;
+ if (scenario.learn) {
+ const initial = await ok(["cooperate", "sync", ...scope]);
+ // Keep node identities stable and tick exactly once per observation.
+ await read("window.tickNoise(); true");
+ const learnArgs = [
+ "press",
+ "Space",
+ ...scope,
+ "--context",
+ initial.context_id,
+ "--selector",
+ "#pause",
+ ];
+ const refused = await bsk(learnArgs);
+ assert.notEqual(refused.exitCode, 0);
+ assert.match(refused.result.message, /changed by itself/);
+ assert.equal(refused.result.data?.effect_state, "none");
+ learning = { refused };
+ if (scenario.retryRefused) {
+ learning.retry = await bsk(learnArgs);
+ assert.notEqual(learning.retry.exitCode, 0);
+ assert.equal(
+ learning.retry.result.message,
+ "This action requires the context returned by the latest cooperate sync",
+ );
+ assert.equal(learning.retry.result.data?.effect_state, "none");
+ }
+ const report = await read("window.report()");
+ assert.equal(report.keydowns, 0);
+ assert.equal(report.paused, false);
+ }
+ if (scenario.continuous) {
+ await read("window.startNoise(); true");
+ await delay(100);
+ }
+ let sync = await ok(["cooperate", "sync", ...scope]);
+ if (scenario.continuous) {
+ for (let i = 0; i < 2; i++) {
+ await delay(100);
+ sync = await ok(["cooperate", "sync", ...scope]);
+ }
+ }
+ const before = await read("window.report()");
+ assert.equal(before.paused, false);
+ // Start page-origin updates after the observation, before the proposed input.
+ await read(scenario.learn ? "window.tickNoise(); true" : "window.startNoise(); true");
+ await delay(320);
+ const atAction = await read("window.report()");
+ assert(
+ atAction.decodedFrames > before.decodedFrames,
+ "Video must actually decode frames",
+ );
+ assert(atAction.ticks > before.ticks, "Page updates must run before the input");
+ if (atAction.progress !== undefined) assert.notEqual(atAction.progress, before.progress);
+ if (scenario.variant === "text") assert.notEqual(atAction.clock, before.clock);
+ if (["field", "combined"].includes(scenario.variant))
+ assert.notEqual(atAction.order, before.order);
+ if (scenario.learn) assert.notEqual(atAction.price, before.price);
+ if (scenario.focus) assert.notEqual(atAction.focused, before.focused);
+ const wait = await ok(["cooperate", "wait", ...scope, "--timeout-ms", "0"]);
+ const action = await bsk([
+ "press",
+ "Space",
+ ...scope,
+ "--context",
+ sync.context_id,
+ ...(scenario.target ? ["--selector", scenario.targetSelector ?? "#pause"] : []),
+ ...(scenario.dependencies
+ ? (Array.isArray(scenario.dependencies) ? scenario.dependencies : ["#pause"]).flatMap(
+ (dependency) => ["--depends-on", dependency],
+ )
+ : []),
+ ]);
+ const after = await read("window.report()");
+ const evidence = {
+ ...scenario,
+ session,
+ learning,
+ before,
+ atAction,
+ coordinationReady: wait.ready,
+ action,
+ after,
+ };
+ results.push(evidence);
+ await writeFile(
+ join(artifacts, scenario.name + ".json"),
+ JSON.stringify(evidence, null, 2),
+ );
+ if (scenario.name === "timer_text_untargeted_space") {
+ const screenshot = await send("Page.captureScreenshot", { format: "png" }, oracle);
+ await writeFile(
+ join(artifacts, "blocked-preview.png"),
+ Buffer.from(screenshot.data, "base64"),
+ );
+ }
+ assert.equal(wait.ready, true, "No person should be holding control");
+ if (scenario.allow) {
+ assert.equal(action.exitCode, 0, JSON.stringify(action));
+ assert.equal(after.keydowns, 1);
+ assert.equal(after.paused, true);
+ } else {
+ assert.notEqual(action.exitCode, 0, JSON.stringify(action));
+ assert.equal(
+ action.result.message,
+ scenario.focus
+ ? "The keyboard focus changed; synchronize before pressing a key"
+ : "The action's observed content changed; synchronize and replan",
+ );
+ assert.equal(action.result.data?.effect_state, "none");
+ assert.equal(after.keydowns, 0);
+ assert.equal(after.pauseCalls, 0);
+ assert.equal(after.paused, false);
+ }
+ console.log(
+ JSON.stringify({
+ name: scenario.name,
+ exit: action.exitCode,
+ keydowns: after.keydowns,
+ paused: after.paused,
+ elapsedMs: action.durationMs,
+ }),
+ );
+ } finally {
+ if (oracle) await send("Target.detachFromTarget", { sessionId: oracle }).catch(() => {});
+ await ok(["session", "stop", session]);
+ }
+ }
+ },
+ );
+ await writeFile(
+ join(artifacts, "result.json"),
+ JSON.stringify({ passed: true, browserVersion, cases: results }, null, 2),
+ );
+ console.log(`PASS ${results.length} real-browser cases; artifacts=${artifacts}`);
+} catch (error) {
+ await writeFile(
+ join(artifacts, "result.json"),
+ JSON.stringify(
+ { passed: false, browserVersion, error: String(error.stack || error), cases: results },
+ null,
+ 2,
+ ),
+ );
+ throw error;
+} finally {
+ await writeFile(join(artifacts, "daemon.log"), daemonOutput);
+ if (daemon && daemon.exitCode === null && daemon.signalCode === null) {
+ const exited = once(daemon, "exit");
+ daemon.kill("SIGTERM");
+ const force = setTimeout(() => daemon.kill("SIGKILL"), 5000);
+ await exited;
+ clearTimeout(force);
+ }
+ await new Promise((resolve) => server.close(resolve));
+ await rm(runtime, { recursive: true, force: true });
+}
diff --git a/packages/dsh-plugin-browserskill/skill/references/cooperation.md b/packages/dsh-plugin-browserskill/skill/references/cooperation.md
index ad368b99..2f5f1609 100644
--- a/packages/dsh-plugin-browserskill/skill/references/cooperation.md
+++ b/packages/dsh-plugin-browserskill/skill/references/cooperation.md
@@ -34,7 +34,8 @@ set is uncertain, omit it and retain whole-page checking. Text that changes by i
sync and action (a clock, a counter) is reported once with the before/after text. After you sync
and replan, further changes to that text no longer block actions on a ref or selector unless it
shares a form, list, dialog, panel or other container with the target. Actions without a ref or
-selector, such as a bare key press, always check the whole page exactly. Added, removed or
+selector never learn incidental text; a bare key press checks the whole page exactly unless
+explicit dependencies narrow its decision regions. Added, removed or
re-rendered elements and text without its own accessible node (such as `aria-hidden` content)
keep blocking. If the decision relies on such text, declare it with `--depends-on`, which stays
exact. Dependencies must come from the current observation; they do not authorize replaying an
@@ -46,6 +47,14 @@ A regular coordination result is not a failed browser attempt. Use wait then syn
the next action from the actual result. Use request-help only for an actual human requirement
such as login or a captcha. Explicit user stop always ends the task.
+On continuously updating pages, declare the observed decision regions instead of repeatedly
+retrying a whole-page action. `press` honors dependencies even without a focus target; specify
+a target when the key must be delivered there. Untargeted keys reject a changed focus. A
+clearly labelled media timeline outside all declared regions may advance without revoking the
+action when it is unambiguously associated with a video/audio element. Submitted inputs, form members,
+ordinary sliders, and other business values remain protected; declaring the timeline itself
+as a dependency protects its value too. Unknown media controls retain the ordinary checks.
+
## Scripts and file transfers
Scripts, upload/download, active hover observation, full-page screenshots, device emulation
From d3b260d0647d629a3b715f216cd1e4e0e7d3e25f Mon Sep 17 00:00:00 2001
From: Zhang GH <5351707+iuyo5678@users.noreply.github.com>
Date: Tue, 27 Oct 2026 15:49:53 +0800
Subject: [PATCH 2/3] fix(cooperation): protect implicit focus and media source
changes
---
.../__tests__/cooperation-capture.test.ts | 60 +++++-
.../src/content/cooperation-capture.ts | 60 +++++-
.../src/cooperation/controller.test.ts | 197 ++++++++++++++++++
apps/extension/src/cooperation/controller.ts | 73 ++++++-
.../cooperation/observation-evidence.test.ts | 31 +++
.../src/cooperation/observation-evidence.ts | 26 ++-
apps/extension/src/cooperation/types.ts | 13 ++
.../vom/__tests__/semantic-graph.test.ts | 31 +++
.../src/tools/vom/semantic-graph/resolve.ts | 3 +
.../bsk-cli/skill/references/cooperation.md | 13 +-
.../regression/cooperation-video/README.md | 22 +-
.../regression/cooperation-video/fixture.html | 42 +++-
.../regression/cooperation-video/run.mjs | 187 ++++++++++++++++-
.../skill/references/cooperation.md | 13 +-
packages/vom/src/types.ts | 2 +
15 files changed, 722 insertions(+), 51 deletions(-)
diff --git a/apps/extension/src/content/__tests__/cooperation-capture.test.ts b/apps/extension/src/content/__tests__/cooperation-capture.test.ts
index abb981ad..8f0b3e21 100644
--- a/apps/extension/src/content/__tests__/cooperation-capture.test.ts
+++ b/apps/extension/src/content/__tests__/cooperation-capture.test.ts
@@ -20,9 +20,10 @@ afterEach(() => {
vi.restoreAllMocks();
});
-function collector() {
+function collector(shadowRoot?: (element: HTMLElement) => ShadowRoot | null) {
let receive!: (message: unknown, sender: unknown, reply: (result: unknown) => void) => void;
vi.stubGlobal("chrome", {
+ dom: shadowRoot ? { openOrClosedShadowRoot: shadowRoot } : undefined,
runtime: {
sendMessage: vi.fn(async () => {}),
onMessage: {
@@ -453,3 +454,60 @@ it("reports programmatic focus changes even without a human event", () => {
expect(current.focus).not.toBe(first);
expect(current.human).toBe(0);
});
+
+it("records media sources independently of playback time, including runtime-only source changes", () => {
+ document.body.innerHTML = '';
+ const video = document.querySelector("video")!;
+ const source = video.querySelector("source")!;
+ const read = collector();
+ const first = read()!;
+ video.currentTime = 1;
+ expect(read()!.media).toEqual(first.media);
+ source.src = "two.webm";
+ const changed = read()!;
+ expect(changed.media).not.toEqual(first.media);
+ expect(changed.revision).toBeGreaterThan(first.revision);
+ Object.defineProperty(video, "currentSrc", { configurable: true, value: "selected.webm" });
+ const selected = read()!;
+ expect(selected.media![0].currentSrc).toBe("selected.webm");
+ expect(selected.revision).toBeGreaterThan(changed.revision);
+ Object.defineProperty(video, "srcObject", { configurable: true, value: {} });
+ const stream = read()!;
+ expect(stream.media![0].srcObject).toEqual(expect.any(String));
+ Object.defineProperty(video, "srcObject", { configurable: true, value: {} });
+ expect(read()!.media![0].srcObject).not.toBe(stream.media![0].srcObject);
+});
+
+it("collects media and deep focus in open shadow roots", () => {
+ document.body.innerHTML = '';
+ const root = document.querySelector("#host")!.attachShadow({ mode: "open" });
+ root.innerHTML = '';
+ const read = collector();
+ expect(read()!.focusIsDocument).toBe(true);
+ root.querySelector("button")!.focus();
+ const focused = read()!;
+ expect(focused.focusIsDocument).toBe(false);
+ expect(focused.media).toHaveLength(1);
+ root.querySelector("source")!.src = "two.ogg";
+ expect(read()!.media).not.toEqual(focused.media);
+});
+
+it("collects source and recipient changes inside closed shadow roots", () => {
+ document.body.innerHTML = '';
+ const host = document.querySelector("#host")!;
+ const root = host.attachShadow({ mode: "closed" });
+ root.innerHTML =
+ '';
+ const read = collector((element) => (element === host ? root : null));
+ const buttons = root.querySelectorAll("button");
+ buttons[0].focus();
+ const first = read()!;
+ expect(host.shadowRoot).toBeNull();
+ expect(first.media).toHaveLength(1);
+ buttons[1].focus();
+ root.querySelector("source")!.src = "two.webm";
+ const changed = read()!;
+ expect(changed.focus).not.toBe(first.focus);
+ expect(changed.media).not.toEqual(first.media);
+ expect(changed.revision).toBeGreaterThan(first.revision);
+});
diff --git a/apps/extension/src/content/cooperation-capture.ts b/apps/extension/src/content/cooperation-capture.ts
index 9bf8aa18..98fa1a81 100644
--- a/apps/extension/src/content/cooperation-capture.ts
+++ b/apps/extension/src/content/cooperation-capture.ts
@@ -43,9 +43,9 @@ export function attachCooperationCapture() {
const keys = new Set();
const fields = new Map();
const sampledValues = new WeakMap();
- const ids = new WeakMap();
+ const ids = new WeakMap |