Files
openclaw/src/plugins/command-execution-lock.ts
T
Peter Steinberger d6f70a96cb fix(plugins): native commands execute the selected plugin (#121544)
* fix(plugins): preserve selected command identity

* test(telegram): use scoped command registries

* test(telegram): isolate command runtime fixtures

* test(telegram): warm native command runtime

* refactor(plugins): keep command metadata private

* fix(plugins): accept synchronous command handlers

* fix(plugins): scope command drain bypass to live execution

* test(telegram): use scoped command registry fixtures

* test(telegram): isolate native menu runtime fixtures

* test(telegram): isolate login session store

* test(telegram): surface login flow failures

* test(telegram): preload native login module

* test(telegram): scope native command registries

* fix(plugins): complete command dispatch contracts

* fix(plugins): break command dispatch import cycles

* fix(plugins): stabilize command dispatch contracts

* fix(channels): keep plugin dispatch options internal

* fix(plugins): keep command dispatch carrier opaque

* test(channels): align delivery adapter fixtures

* test(delivery): align custody ownership coverage

* test(delivery): align latest queue reconciliation

* test(channels): drop obsolete delivery wrappers

* fix(plugins): rebind channel reload starts

* fix(plugins): scope command catalog reloads

* fix(ci): align current runtime contracts

* chore(plugin-sdk): refresh API baseline
2026-08-10 19:30:47 -07:00

91 lines
2.7 KiB
TypeScript

/** Per-registry command execution admission and retirement drain. */
import { AsyncLocalStorage } from "node:async_hooks";
import { isPluginRegistryRetired } from "./registry-lifecycle.js";
import type { PluginRegistry } from "./registry-types.js";
type PluginCommandExecutionState = {
count: number;
waiters: Array<() => void>;
};
type PluginCommandExecutionToken = {
registry: PluginRegistry;
active: boolean;
};
const executionStates = new WeakMap<PluginRegistry, PluginCommandExecutionState>();
const executionContext = new AsyncLocalStorage<ReadonlySet<PluginCommandExecutionToken>>();
function getExecutionState(registry: PluginRegistry): PluginCommandExecutionState {
const existing = executionStates.get(registry);
if (existing) {
return existing;
}
const created = { count: 0, waiters: [] };
executionStates.set(registry, created);
return created;
}
export function getPluginCommandExecutionCount(registry: PluginRegistry): number {
return executionStates.get(registry)?.count ?? 0;
}
function beginPluginCommandExecution(registry: PluginRegistry): boolean {
if (isPluginRegistryRetired(registry)) {
return false;
}
getExecutionState(registry).count += 1;
return true;
}
function endPluginCommandExecution(registry: PluginRegistry): void {
const state = getExecutionState(registry);
if (state.count <= 0) {
throw new Error("Plugin command execution lock is unbalanced.");
}
state.count -= 1;
if (state.count !== 0) {
return;
}
const waiters = state.waiters.splice(0);
for (const resolve of waiters) {
resolve();
}
}
export function isPluginCommandExecutionActiveHere(registry: PluginRegistry): boolean {
return [...(executionContext.getStore() ?? [])].some(
(token) => token.registry === registry && token.active,
);
}
export async function withPluginCommandExecution<T>(
registry: PluginRegistry,
run: () => T | Promise<T>,
): Promise<{ admitted: true; value: T } | { admitted: false }> {
if (!beginPluginCommandExecution(registry)) {
return { admitted: false };
}
const token: PluginCommandExecutionToken = { registry, active: true };
const active = new Set(
[...(executionContext.getStore() ?? [])].filter((inherited) => inherited.registry !== registry),
);
active.add(token);
try {
return { admitted: true, value: await executionContext.run(active, run) };
} finally {
token.active = false;
endPluginCommandExecution(registry);
}
}
export async function waitForPluginCommandExecutions(registry: PluginRegistry): Promise<void> {
const state = getExecutionState(registry);
if (state.count === 0) {
return;
}
await new Promise<void>((resolve) => {
state.waiters.push(resolve);
});
}