Files
openclaw/src/plugins/runtime-workspace-state.ts
lsr911 583b7195b4 fix: pin plugin workspace dir for sessions.list to avoid O(rows) memo busting (#93719)
* fix: pin plugin workspace dir for sessions.list to avoid O(rows) memo busting

sessions.list was O(rows) slow under concurrent agent/cron load because
each row read a process-global active plugin-registry workspace dir
that was mutated by other turns between rows. The per-row memo key
changed every time, so loadPluginMetadataSnapshot scanned fresh
(~100ms per row).

Fix:
1. Add AsyncLocalStorage-based workspace dir pinning to
   runtime-workspace-state.ts — withPinnedActivePluginRegistryWorkspaceDir()
   snapshots the current workspace dir for the duration of a callback.
2. Wrap listSessionsFromStoreAsync body in the pin so all per-row
   metadata lookups use a stable memo key.

Fixes #90814

* test(plugins): cover request-scoped workspace pins

* fix(plugins): pin canonical runtime workspace reads

* fix(plugins): preserve workspace pins across reloads

---------

Co-authored-by: lsr911 <lsr911@github.com>
Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
2026-06-17 03:33:46 +08:00

48 lines
1.9 KiB
TypeScript

// Shares plugin runtime workspace state across module reloads.
import { AsyncLocalStorage } from "node:async_hooks";
import { resolveGlobalSingleton } from "../shared/global-singleton.js";
const PLUGIN_REGISTRY_STATE = Symbol.for("openclaw.pluginRegistryState");
const PINNED_PLUGIN_REGISTRY_WORKSPACE_KEY = Symbol.for(
"openclaw.pinnedPluginRegistryWorkspaceDir",
);
type GlobalRegistryWorkspaceState = typeof globalThis & {
[PLUGIN_REGISTRY_STATE]?: {
workspaceDir?: string | null;
};
};
const pinnedWorkspaceDirStorage = resolveGlobalSingleton<
AsyncLocalStorage<{ workspaceDir: string | undefined }>
>(PINNED_PLUGIN_REGISTRY_WORKSPACE_KEY, () => new AsyncLocalStorage());
/** Reads the active plugin registry workspace directory from global runtime state,
* respecting any pinned workspace from the current async context. */
export function getActivePluginRegistryWorkspaceDirFromState(): string | undefined {
const pinned = pinnedWorkspaceDirStorage.getStore();
if (pinned) {
return pinned.workspaceDir;
}
return (
(globalThis as GlobalRegistryWorkspaceState)[PLUGIN_REGISTRY_STATE]?.workspaceDir ?? undefined
);
}
/**
* Pin the active plugin-registry workspace dir for the duration of `fn`.
* While pinned, calls to `getActivePluginRegistryWorkspaceDirFromState()` return
* the snapshot taken at pin time, ignoring concurrent mutations from other
* agent turns or crons. This prevents per-row memo-busting in operations that
* iterate over many rows (e.g. sessions.list).
*/
export function withPinnedActivePluginRegistryWorkspaceDir<T>(fn: () => T): T {
if (pinnedWorkspaceDirStorage.getStore()) {
// Already pinned in an outer scope — reuse it.
return fn();
}
const workspaceDir =
(globalThis as GlobalRegistryWorkspaceState)[PLUGIN_REGISTRY_STATE]?.workspaceDir ?? undefined;
return pinnedWorkspaceDirStorage.run({ workspaceDir }, fn);
}