mirror of
https://github.com/Tencent/BrowserSkill.git
synced 2026-09-10 13:01:23 +08:00
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:
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Per-key FIFO executor. The bsk daemon serializes commands per session (a
|
||||
* second command while one is unfinished is rejected), so every plugin
|
||||
* command — model-facing tool calls AND observation captures — funnels
|
||||
* through one queue per session. A queued task rejects early if its abort
|
||||
* signal fires before it starts; once running, cancellation is owned by the
|
||||
* task's own signal handling (the runner kills the child).
|
||||
*/
|
||||
|
||||
export class KeyedExecutor {
|
||||
private readonly tails = new Map<string, Promise<void>>();
|
||||
|
||||
/** Run `fn` after every previously queued task for `key` settled. */
|
||||
run<T>(key: string, fn: () => Promise<T>, signal?: AbortSignal): Promise<T> {
|
||||
const previous = this.tails.get(key) ?? Promise.resolve();
|
||||
const task = new Promise<T>((resolve, reject) => {
|
||||
let settled = false;
|
||||
const onAbort = () => {
|
||||
if (!settled) {
|
||||
settled = true;
|
||||
reject(abortError());
|
||||
}
|
||||
};
|
||||
signal?.addEventListener("abort", onAbort, { once: true });
|
||||
void previous.then(() => {
|
||||
if (settled) return;
|
||||
if (signal?.aborted) {
|
||||
settled = true;
|
||||
reject(abortError());
|
||||
return;
|
||||
}
|
||||
// Running now: cancellation flows through the task's own signal.
|
||||
signal?.removeEventListener("abort", onAbort);
|
||||
fn().then(
|
||||
(value) => {
|
||||
settled = true;
|
||||
resolve(value);
|
||||
},
|
||||
(error: unknown) => {
|
||||
settled = true;
|
||||
reject(error);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
// The stored tail never rejects, so one failure never strands the queue.
|
||||
this.tails.set(
|
||||
key,
|
||||
task.then(
|
||||
() => {},
|
||||
() => {},
|
||||
),
|
||||
);
|
||||
return task;
|
||||
}
|
||||
}
|
||||
|
||||
function abortError(): Error {
|
||||
const error = new Error("tool call aborted");
|
||||
error.name = "AbortError";
|
||||
return error;
|
||||
}
|
||||
Reference in New Issue
Block a user