fix(dsh-plugin-browserskill): serialize per-session commands (observation vs tool calls)

The daemon accepts only one unfinished command per session; the
thumbnail loop racing a model tool call produced 'session already has
an unfinished command' failures on both sides (found by the Phase 1-C
e2e: navigate/snapshot/click all errored while captures kept running).
Add a KeyedExecutor: every command for a session — tool calls and
observation captures alike — runs FIFO; queued tasks reject early on
abort, running tasks keep the runner's signal-driven kill.
This commit is contained in:
BB-fat
2026-08-14 06:41:49 +00:00
parent 2955260d11
commit bc5d9610a3
6 changed files with 93 additions and 9 deletions
+13 -5
View File
@@ -14,6 +14,7 @@ import type { ContentBlock } from "@deepseek-ai/dsh-llm";
import { defineTool, type ToolResult, type ToolRunContext } from "@deepseek-ai/dsh-tools";
import { trySaveScreenshot } from "./image";
import { actionForLabel, type ObservationService } from "./observation";
import type { KeyedExecutor } from "./queue";
import { type BskRunner, bskInstallMessage, isCommandNotFound, parseBskJson } from "./runner";
import type { SessionRegistry } from "./sessions";
@@ -33,6 +34,8 @@ export interface ToolDeps {
registry: SessionRegistry;
config: PluginConfig;
observation: ObservationService;
/** Per-session FIFO: the daemon rejects a second command while one is unfinished. */
queue: KeyedExecutor;
}
/** Device presets supported by `bsk emulate --device`. */
@@ -74,11 +77,16 @@ async function runBsk(
try {
let result;
try {
result = await deps.runner.run(args, {
signal: exec.signal,
timeoutMs: deps.config.defaultTimeoutMs,
...(observeSession !== undefined ? { tag: observeSession } : {}),
});
const runOnce = () =>
deps.runner.run(args, {
signal: exec.signal,
timeoutMs: deps.config.defaultTimeoutMs,
...(observeSession !== undefined ? { tag: observeSession } : {}),
});
result =
observeSession !== undefined
? await deps.queue.run(observeSession, runOnce, exec.signal)
: await runOnce();
} catch (error) {
if (isCommandNotFound(error)) {
throw new Error(bskInstallMessage(deps.config.bskPath));