mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 09:30:43 +00:00
fix: guard stale plugin cleanup
This commit is contained in:
@@ -37,6 +37,8 @@ Docs: https://docs.openclaw.ai
|
||||
- Plugins/update: clean stale bundled load paths for already-externalized pinned npm and ClawHub plugin installs, so release-channel sync does not leave removed bundled paths ahead of the installed external package. Thanks @vincentkoc.
|
||||
- Telegram: accept plugin-owned numeric forum-topic targets in the agent message tool and keep reply-dispatch provider chunks behind a real stable runtime alias during in-place package updates. Fixes #77137. Thanks @richardmqq.
|
||||
- Google Meet: preserve `realtime.introMessage: ""` so realtime Chrome joins can stay silent instead of restoring the default spoken intro. Thanks @vincentkoc.
|
||||
- Plugins/SDK: add bounded `before_agent_finalize` retry instructions so workflow plugins can request one more model pass. Thanks @100yenadmin.
|
||||
- Discord/status: add degraded Discord transport and gateway event-loop starvation signals to `openclaw channels status`, `openclaw status --deep`, and fetch-timeout logs so intermittent socket resets do not look like a healthy running channel. (#76327) Thanks @joshavant.
|
||||
- Providers/OpenRouter: add opt-in response caching params that send OpenRouter's `X-OpenRouter-Cache`, `X-OpenRouter-Cache-TTL`, and cache-clear headers only on verified OpenRouter routes. Thanks @vincentkoc.
|
||||
- Providers/OpenRouter: expand app-attribution categories so OpenClaw advertises coding, programming, writing, chat, and personal-agent usage on verified OpenRouter routes. Thanks @vincentkoc.
|
||||
- Plugins/runtime state: add `registerIfAbsent` for atomic keyed-store dedupe claims that return whether a plugin successfully claimed a key without overwriting an existing live value. Thanks @amknight.
|
||||
@@ -465,6 +467,9 @@ Docs: https://docs.openclaw.ai
|
||||
- Gateway/CLI: make `openclaw gateway start` repair stale managed service definitions that point at old OpenClaw versions, missing binaries, or temporary installer paths before starting.
|
||||
- Heartbeat/scheduler: make heartbeat phase scheduling active-hours-aware so the scheduler seeks forward to the first in-window phase slot instead of arming timers for quiet-hours slots and relying solely on the runtime guard. Non-UTC `activeHours.timezone` values (e.g. `Asia/Shanghai`) now correctly influence when the next heartbeat timer fires, avoiding wasted quiet-hours ticks and long dormant gaps after gateway restarts. Fixes #75487. Thanks @amknight.
|
||||
- Providers/Arcee AI: mark Trinity Large Thinking as tool-incompatible so main-session runs use the same text-only request shape that made subagent runs recover, avoiding the remaining main-session response-shape mismatch after the #62848 transport failover fix. Fixes #62851 and #62847; carries forward #62848. Thanks @Adam-Researchh.
|
||||
- Plugins/SDK: harden run-scoped plugin context cleanup so finalized workflow runs do not leak per-run state. Thanks @100yenadmin.
|
||||
- Plugins/SDK: keep stale async registry cleanup from clearing restored plugin run context and scheduler state after a plugin registry is reactivated. (#75600) Thanks @100yenadmin.
|
||||
- Plugins/SDK: preserve restored plugin scheduler state when earlier delayed replacement cleanup finishes after reactivation. Thanks @100yenadmin.
|
||||
- Status: show the `openai-codex` OAuth profile for `openai/gpt-*` sessions running through the native Codex runtime instead of reporting auth as unknown. (#76197) Thanks @mbelinky.
|
||||
- Gateway: avoid repeated plugin tool descriptor config hashing so large runtime configs do not block reply startup and trigger reconnect/timeouts. (#75944) Thanks @joshavant.
|
||||
- Plugins/externalization: keep diagnostics ClawHub packages and persisted bundled-plugin relocation on npm-first install metadata for launch, and omit Discord from the core package now that its external package is published. Thanks @vincentkoc.
|
||||
|
||||
@@ -118,6 +118,76 @@ describe("plugin run context lifecycle", () => {
|
||||
).toEqual({ restored: true });
|
||||
});
|
||||
|
||||
it("keeps restored active registry state after stale async cleanup finishes", async () => {
|
||||
let releaseCleanup: (() => void) | undefined;
|
||||
let markCleanupStarted: (() => void) | undefined;
|
||||
let capturedApi: OpenClawPluginApi | undefined;
|
||||
const cleanupStarted = new Promise<void>((resolve) => {
|
||||
markCleanupStarted = resolve;
|
||||
});
|
||||
const cleanupRelease = new Promise<void>((resolve) => {
|
||||
releaseCleanup = resolve;
|
||||
});
|
||||
const schedulerCleanup = vi.fn();
|
||||
const { config, registry } = createPluginRegistryFixture();
|
||||
registerTestPlugin({
|
||||
registry,
|
||||
config,
|
||||
record: createPluginRecord({
|
||||
id: "delayed-restored-registry-plugin",
|
||||
name: "Delayed Restored Registry Plugin",
|
||||
}),
|
||||
register(api) {
|
||||
capturedApi = api;
|
||||
api.registerRuntimeLifecycle({
|
||||
id: "delayed-cleanup",
|
||||
async cleanup() {
|
||||
markCleanupStarted?.();
|
||||
await cleanupRelease;
|
||||
},
|
||||
});
|
||||
api.registerSessionSchedulerJob({
|
||||
id: "live-job",
|
||||
sessionKey: "agent:main:main",
|
||||
kind: "session-turn",
|
||||
cleanup: schedulerCleanup,
|
||||
});
|
||||
},
|
||||
});
|
||||
setActivePluginRegistry(registry.registry);
|
||||
setActivePluginRegistry(createEmptyPluginRegistry());
|
||||
await cleanupStarted;
|
||||
setActivePluginRegistry(registry.registry);
|
||||
|
||||
expect(
|
||||
capturedApi?.setRunContext({
|
||||
runId: "restored-after-cleanup-started",
|
||||
namespace: "state",
|
||||
value: { restored: true },
|
||||
}),
|
||||
).toBe(true);
|
||||
|
||||
releaseCleanup?.();
|
||||
await waitForPluginEventHandlers();
|
||||
await waitForPluginEventHandlers();
|
||||
|
||||
expect(
|
||||
getPluginRunContext({
|
||||
pluginId: "delayed-restored-registry-plugin",
|
||||
get: { runId: "restored-after-cleanup-started", namespace: "state" },
|
||||
}),
|
||||
).toEqual({ restored: true });
|
||||
expect(schedulerCleanup).not.toHaveBeenCalled();
|
||||
expect(listPluginSessionSchedulerJobs("delayed-restored-registry-plugin")).toEqual([
|
||||
{
|
||||
id: "live-job",
|
||||
pluginId: "delayed-restored-registry-plugin",
|
||||
sessionKey: "agent:main:main",
|
||||
kind: "session-turn",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not let delayed non-terminal subscriptions resurrect closed run context", async () => {
|
||||
let releaseToolHandler: (() => void) | undefined;
|
||||
let delayedToolHandlerSawContext: unknown;
|
||||
|
||||
@@ -114,10 +114,15 @@ export async function runPluginHostCleanup(params: {
|
||||
sessionKey?: string;
|
||||
runId?: string;
|
||||
preserveSchedulerJobIds?: ReadonlySet<string>;
|
||||
shouldCleanup?: () => boolean;
|
||||
}): Promise<PluginHostCleanupResult> {
|
||||
const failures: PluginHostCleanupFailure[] = [];
|
||||
const shouldCleanup = params.shouldCleanup ?? (() => true);
|
||||
if (!shouldCleanup()) {
|
||||
return { cleanupCount: 0, failures };
|
||||
}
|
||||
let persistentCleanupCount = 0;
|
||||
if (params.reason !== "restart") {
|
||||
if (params.reason !== "restart" && shouldCleanup()) {
|
||||
try {
|
||||
persistentCleanupCount = await clearPluginOwnedSessionStores({
|
||||
cfg: params.cfg ?? getRuntimeConfig(),
|
||||
@@ -136,6 +141,9 @@ export async function runPluginHostCleanup(params: {
|
||||
let cleanupCount = persistentCleanupCount;
|
||||
if (registry) {
|
||||
for (const registration of registry.sessionExtensions ?? []) {
|
||||
if (!shouldCleanup()) {
|
||||
return { cleanupCount, failures };
|
||||
}
|
||||
if (!shouldCleanPlugin(registration.pluginId, params.pluginId)) {
|
||||
continue;
|
||||
}
|
||||
@@ -161,6 +169,9 @@ export async function runPluginHostCleanup(params: {
|
||||
}
|
||||
}
|
||||
for (const registration of registry.runtimeLifecycles ?? []) {
|
||||
if (!shouldCleanup()) {
|
||||
return { cleanupCount, failures };
|
||||
}
|
||||
if (!shouldCleanPlugin(registration.pluginId, params.pluginId)) {
|
||||
continue;
|
||||
}
|
||||
@@ -192,12 +203,13 @@ export async function runPluginHostCleanup(params: {
|
||||
sessionKey: params.sessionKey,
|
||||
records: registry.sessionSchedulerJobs,
|
||||
preserveJobIds: params.preserveSchedulerJobIds,
|
||||
shouldCleanup,
|
||||
});
|
||||
for (const failure of schedulerFailures) {
|
||||
failures.push(failure);
|
||||
}
|
||||
}
|
||||
if (params.reason !== "restart") {
|
||||
if (params.reason !== "restart" && shouldCleanup()) {
|
||||
const registrySchedulerJobKeys = new Set(
|
||||
(registry?.sessionSchedulerJobs ?? [])
|
||||
.filter((record) => !params.pluginId || record.pluginId === params.pluginId)
|
||||
@@ -214,12 +226,17 @@ export async function runPluginHostCleanup(params: {
|
||||
sessionKey: params.sessionKey,
|
||||
preserveJobIds: params.preserveSchedulerJobIds,
|
||||
excludeJobKeys: registrySchedulerJobKeys,
|
||||
shouldCleanup,
|
||||
});
|
||||
for (const failure of runtimeSchedulerFailures) {
|
||||
failures.push(failure);
|
||||
}
|
||||
}
|
||||
if ((params.pluginId || params.runId) && (params.reason !== "restart" || params.runId)) {
|
||||
if (
|
||||
shouldCleanup() &&
|
||||
(params.pluginId || params.runId) &&
|
||||
(params.reason !== "restart" || params.runId)
|
||||
) {
|
||||
clearPluginRunContext({ pluginId: params.pluginId, runId: params.runId });
|
||||
}
|
||||
return { cleanupCount, failures };
|
||||
@@ -266,9 +283,11 @@ export async function cleanupReplacedPluginHostRegistry(params: {
|
||||
cfg: OpenClawConfig;
|
||||
previousRegistry?: PluginRegistry | null;
|
||||
nextRegistry?: PluginRegistry | null;
|
||||
shouldCleanup?: () => boolean;
|
||||
}): Promise<PluginHostCleanupResult> {
|
||||
const previousRegistry = params.previousRegistry;
|
||||
if (!previousRegistry || previousRegistry === params.nextRegistry) {
|
||||
const shouldCleanup = params.shouldCleanup ?? (() => true);
|
||||
if (!previousRegistry || previousRegistry === params.nextRegistry || !shouldCleanup()) {
|
||||
return { cleanupCount: 0, failures: [] };
|
||||
}
|
||||
const nextPluginIds = params.nextRegistry
|
||||
@@ -281,6 +300,9 @@ export async function cleanupReplacedPluginHostRegistry(params: {
|
||||
const failures: PluginHostCleanupFailure[] = [];
|
||||
let cleanupCount = 0;
|
||||
for (const pluginId of previousPluginIds) {
|
||||
if (!shouldCleanup()) {
|
||||
break;
|
||||
}
|
||||
const restarted = nextPluginIds.has(pluginId);
|
||||
const result = await runPluginHostCleanup({
|
||||
cfg: params.cfg,
|
||||
@@ -290,6 +312,7 @@ export async function cleanupReplacedPluginHostRegistry(params: {
|
||||
preserveSchedulerJobIds: restarted
|
||||
? collectSchedulerJobIds(params.nextRegistry, pluginId)
|
||||
: undefined,
|
||||
shouldCleanup,
|
||||
});
|
||||
cleanupCount += result.cleanupCount;
|
||||
failures.push(...result.failures);
|
||||
|
||||
@@ -449,11 +449,19 @@ export async function cleanupPluginSessionSchedulerJobs(params: {
|
||||
}[];
|
||||
preserveJobIds?: ReadonlySet<string>;
|
||||
excludeJobKeys?: ReadonlySet<string>;
|
||||
shouldCleanup?: () => boolean;
|
||||
}): Promise<Array<{ pluginId: string; hookId: string; error: unknown }>> {
|
||||
const state = getPluginHostRuntimeState();
|
||||
const failures: Array<{ pluginId: string; hookId: string; error: unknown }> = [];
|
||||
const shouldCleanup = params.shouldCleanup ?? (() => true);
|
||||
if (!shouldCleanup()) {
|
||||
return failures;
|
||||
}
|
||||
if (params.records) {
|
||||
for (const record of params.records) {
|
||||
if (!shouldCleanup()) {
|
||||
return failures;
|
||||
}
|
||||
if (params.pluginId && record.pluginId !== params.pluginId) {
|
||||
continue;
|
||||
}
|
||||
@@ -512,6 +520,9 @@ export async function cleanupPluginSessionSchedulerJobs(params: {
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (!shouldCleanup()) {
|
||||
continue;
|
||||
}
|
||||
deletePluginSessionSchedulerJob({
|
||||
pluginId: record.pluginId,
|
||||
jobId,
|
||||
@@ -523,11 +534,17 @@ export async function cleanupPluginSessionSchedulerJobs(params: {
|
||||
}
|
||||
const pluginIds = params.pluginId ? [params.pluginId] : [...state.schedulerJobsByPlugin.keys()];
|
||||
for (const pluginId of pluginIds) {
|
||||
if (!shouldCleanup()) {
|
||||
return failures;
|
||||
}
|
||||
const jobs = state.schedulerJobsByPlugin.get(pluginId);
|
||||
if (!jobs) {
|
||||
continue;
|
||||
}
|
||||
for (const [jobId, record] of jobs.entries()) {
|
||||
if (!shouldCleanup()) {
|
||||
return failures;
|
||||
}
|
||||
if (params.sessionKey && record.job.sessionKey !== params.sessionKey) {
|
||||
continue;
|
||||
}
|
||||
@@ -554,6 +571,9 @@ export async function cleanupPluginSessionSchedulerJobs(params: {
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (!shouldCleanup()) {
|
||||
continue;
|
||||
}
|
||||
jobs.delete(jobId);
|
||||
}
|
||||
if (jobs.size === 0) {
|
||||
|
||||
@@ -65,16 +65,24 @@ function registryHasPluginHostCleanupWork(registry: PluginRegistry | null): bool
|
||||
|
||||
async function cleanupPreviousPluginHostRegistry(params: {
|
||||
previousRegistry: PluginRegistry;
|
||||
nextRegistry: PluginRegistry;
|
||||
}): Promise<void> {
|
||||
const [{ getRuntimeConfig }, { cleanupReplacedPluginHostRegistry }] = await Promise.all([
|
||||
import("../config/config.js"),
|
||||
import("./host-hook-cleanup.js"),
|
||||
]);
|
||||
const nextRegistry = asPluginRegistry(state.activeRegistry);
|
||||
if (!nextRegistry || nextRegistry === params.previousRegistry) {
|
||||
return;
|
||||
}
|
||||
const cleanupActiveVersion = state.activeVersion;
|
||||
// Async cleanup must not clear state after another registry becomes live.
|
||||
const shouldCleanup = () =>
|
||||
state.activeVersion === cleanupActiveVersion && state.activeRegistry === nextRegistry;
|
||||
await cleanupReplacedPluginHostRegistry({
|
||||
cfg: getRuntimeConfig(),
|
||||
previousRegistry: params.previousRegistry,
|
||||
nextRegistry: params.nextRegistry,
|
||||
nextRegistry,
|
||||
shouldCleanup,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -151,7 +159,6 @@ export function setActivePluginRegistry(
|
||||
}
|
||||
void cleanupPreviousPluginHostRegistry({
|
||||
previousRegistry,
|
||||
nextRegistry: registry,
|
||||
}).catch((error) => {
|
||||
log.warn(`plugin host registry cleanup failed: ${String(error)}`);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user