refactor: remove plugin file-lock sdk

This commit is contained in:
Peter Steinberger
2026-05-10 02:24:06 +01:00
parent 2243c9a32e
commit 2e66a73ebd
10 changed files with 4 additions and 229 deletions

View File

@@ -449,7 +449,6 @@ releases.
| Bounded async task concurrency | `openclaw/plugin-sdk/concurrency-runtime` |
| Numeric coercion | `openclaw/plugin-sdk/number-runtime` |
| Process-local async lock | `openclaw/plugin-sdk/async-lock-runtime` |
| File locks | `openclaw/plugin-sdk/file-lock` |
Bundled plugins are scanner-guarded against `infra-runtime`, so repo code
cannot regress to the broad barrel.

View File

@@ -258,7 +258,6 @@ focused channel/runtime subpaths, `config-contracts`, `string-coerce-runtime`,
| `plugin-sdk/model-session-runtime` | Model/session override helpers such as `applyModelOverrideToSessionEntry` and `resolveAgentMaxConcurrent` |
| `plugin-sdk/talk-config-runtime` | Talk provider config resolution helpers |
| `plugin-sdk/json-store` | Small JSON state read/write helpers |
| `plugin-sdk/file-lock` | Re-entrant file-lock helpers |
| `plugin-sdk/persistent-dedupe` | SQLite-backed dedupe cache helpers |
| `plugin-sdk/acp-runtime` | ACP runtime/session and reply-dispatch helpers |
| `plugin-sdk/acp-runtime-backend` | Lightweight ACP backend registration and reply-dispatch helpers for startup-loaded plugins |

View File

@@ -377,8 +377,8 @@ sessionId}` and session key context.
SQLite transcript stats instead of filesystem stat calls.
- Runtime session locks and the standalone legacy `.jsonl.lock` doctor
lane have been removed.
- The Microsoft Teams runtime barrel no longer re-exports the old plugin SDK
file-lock helper; its durable state paths are SQLite-backed.
- The Microsoft Teams runtime barrel and public plugin SDK no longer re-export
the old file-lock helper; durable plugin state paths are SQLite-backed.
- Session age/count pruning and explicit session cleanup have been removed.
Doctor owns legacy import; stale sessions are reset or deleted explicitly.
- Doctor no longer treats `agents/<agent>/sessions/` as required runtime

View File

@@ -875,10 +875,6 @@
"types": "./dist/plugin-sdk/context-visibility-runtime.d.ts",
"default": "./dist/plugin-sdk/context-visibility-runtime.js"
},
"./plugin-sdk/file-lock": {
"types": "./dist/plugin-sdk/file-lock.d.ts",
"default": "./dist/plugin-sdk/file-lock.js"
},
"./plugin-sdk/fetch-runtime": {
"types": "./dist/plugin-sdk/fetch-runtime.d.ts",
"default": "./dist/plugin-sdk/fetch-runtime.js"

View File

@@ -195,7 +195,6 @@
"channel-route",
"channel-targets",
"context-visibility-runtime",
"file-lock",
"fetch-runtime",
"runtime-fetch",
"response-limit-runtime",

View File

@@ -1,12 +0,0 @@
export type {
FileLockHandle,
FileLockOptions,
FileLockTimeoutError,
} from "../plugin-sdk/file-lock.js";
export {
acquireFileLock,
drainFileLockStateForTest,
FILE_LOCK_TIMEOUT_ERROR_CODE,
resetFileLockStateForTest,
withFileLock,
} from "../plugin-sdk/file-lock.js";

View File

@@ -1,78 +0,0 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
acquireFileLock,
drainFileLockStateForTest,
FILE_LOCK_TIMEOUT_ERROR_CODE,
resetFileLockStateForTest,
} from "./file-lock.js";
describe("acquireFileLock", () => {
let tempDir = "";
beforeEach(async () => {
resetFileLockStateForTest();
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-file-lock-"));
});
afterEach(async () => {
await drainFileLockStateForTest();
if (tempDir) {
await fs.rm(tempDir, { recursive: true, force: true });
}
});
it("respects the configured retry budget even when stale windows are much larger", async () => {
const filePath = path.join(tempDir, "oauth-refresh");
const lockPath = `${filePath}.lock`;
const options = {
retries: {
retries: 1,
factor: 1,
minTimeout: 20,
maxTimeout: 20,
},
stale: 100,
} as const;
await fs.writeFile(
lockPath,
JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() }, null, 2),
"utf8",
);
await expect(acquireFileLock(filePath, options)).rejects.toSatisfy((error) => {
expect(error).toMatchObject({
code: FILE_LOCK_TIMEOUT_ERROR_CODE,
});
expect((error as { lockPath?: string }).lockPath).toMatch(/oauth-refresh\.lock$/);
return true;
});
}, 5_000);
it("closes an opened lock handle when writing the owner payload fails", async () => {
const filePath = path.join(tempDir, "write-fails");
const writeError = new Error("owner write failed");
const close = vi.fn().mockResolvedValue(undefined);
vi.spyOn(fs, "open").mockResolvedValue({
close,
writeFile: vi.fn().mockRejectedValue(writeError),
} as unknown as Awaited<ReturnType<typeof fs.open>>);
await expect(
acquireFileLock(filePath, {
retries: {
retries: 0,
factor: 1,
minTimeout: 1,
maxTimeout: 1,
},
stale: 100,
}),
).rejects.toThrow(writeError);
expect(close).toHaveBeenCalledTimes(1);
});
});

View File

@@ -1,116 +0,0 @@
import "../infra/fs-safe-defaults.js";
import {
acquireFileLock as acquireFsSafeFileLock,
drainFileLockManagerForTest,
resetFileLockManagerForTest,
} from "@openclaw/fs-safe/file-lock";
import { isPidAlive } from "../shared/pid-alive.js";
export type FileLockOptions = {
retries: {
retries: number;
factor: number;
minTimeout: number;
maxTimeout: number;
randomize?: boolean;
};
stale: number;
};
type LockFilePayload = {
pid?: number;
createdAt?: string;
};
export type FileLockHandle = {
lockPath: string;
release: () => Promise<void>;
};
export const FILE_LOCK_TIMEOUT_ERROR_CODE = "file_lock_timeout";
export type FileLockTimeoutError = Error & {
code: typeof FILE_LOCK_TIMEOUT_ERROR_CODE;
lockPath: string;
};
const FILE_LOCK_MANAGER_KEY = "openclaw.plugin-sdk.file-lock";
function readLockPayload(value: Record<string, unknown> | null): LockFilePayload | null {
if (!value) {
return null;
}
return {
pid: typeof value.pid === "number" ? value.pid : undefined,
createdAt: typeof value.createdAt === "string" ? value.createdAt : undefined,
};
}
async function shouldReclaimPluginLock(params: {
lockPath: string;
payload: Record<string, unknown> | null;
staleMs: number;
nowMs: number;
}): Promise<boolean> {
const payload = readLockPayload(params.payload);
if (payload?.pid && !isPidAlive(payload.pid)) {
return true;
}
if (payload?.createdAt) {
const createdAt = Date.parse(payload.createdAt);
return !Number.isFinite(createdAt) || params.nowMs - createdAt > params.staleMs;
}
return true;
}
function normalizeTimeoutError(err: unknown): never {
if ((err as { code?: unknown }).code === FILE_LOCK_TIMEOUT_ERROR_CODE) {
throw Object.assign(new Error((err as Error).message), {
code: FILE_LOCK_TIMEOUT_ERROR_CODE,
lockPath: (err as { lockPath?: string }).lockPath ?? "",
}) as FileLockTimeoutError;
}
throw err;
}
export function resetFileLockStateForTest(): void {
resetFileLockManagerForTest(FILE_LOCK_MANAGER_KEY, FILE_LOCK_MANAGER_KEY);
}
export async function drainFileLockStateForTest(): Promise<void> {
await drainFileLockManagerForTest(FILE_LOCK_MANAGER_KEY, FILE_LOCK_MANAGER_KEY);
}
/** Acquire a re-entrant process-local file lock backed by a `.lock` sidecar file. */
export async function acquireFileLock(
filePath: string,
options: FileLockOptions,
): Promise<FileLockHandle> {
try {
const lock = await acquireFsSafeFileLock(filePath, {
managerKey: FILE_LOCK_MANAGER_KEY,
staleMs: options.stale,
retry: options.retries,
allowReentrant: true,
payload: () => ({ pid: process.pid, createdAt: new Date().toISOString() }),
shouldReclaim: shouldReclaimPluginLock,
});
return { lockPath: lock.lockPath, release: lock.release };
} catch (err) {
return normalizeTimeoutError(err);
}
}
/** Run an async callback while holding a file lock, always releasing the lock afterward. */
export async function withFileLock<T>(
filePath: string,
options: FileLockOptions,
fn: () => Promise<T>,
): Promise<T> {
const lock = await acquireFileLock(filePath, options);
try {
return await fn();
} finally {
await lock.release();
}
}

View File

@@ -29,7 +29,6 @@ export * from "../infra/approval-native-runtime.ts";
export * from "../infra/approval-display-paths.ts";
export * from "../infra/plugin-approvals.ts";
export * from "../infra/fetch.js";
export * from "../infra/file-lock.js";
export * from "../infra/format-time/format-duration.ts";
export * from "../infra/fs-safe.ts";
export * from "../infra/heartbeat-events.ts";

View File

@@ -24,9 +24,7 @@ type WorkerPluginRuntimeHelpers = {
};
type WorkerCleanupHelpers = {
closeOpenClawAgentDatabasesForTest: typeof import("../src/state/openclaw-agent-db.js").closeOpenClawAgentDatabasesForTest;
drainFileLockStateForTest: typeof import("../src/infra/file-lock.js").drainFileLockStateForTest;
resetContextWindowCacheForTest: typeof import("../src/agents/context-runtime-state.js").resetContextWindowCacheForTest;
resetFileLockStateForTest: typeof import("../src/infra/file-lock.js").resetFileLockStateForTest;
resetModelCatalogReadyCacheForTest: typeof import("../src/agents/models-config-state.js").resetModelCatalogReadyCacheForTest;
};
@@ -74,12 +72,9 @@ function loadWorkerCleanupHelpers(): Promise<WorkerCleanupHelpers> {
vi.importActual<typeof import("../src/state/openclaw-agent-db.js")>(
"../src/state/openclaw-agent-db.js",
),
vi.importActual<typeof import("../src/infra/file-lock.js")>("../src/infra/file-lock.js"),
]).then(([contextRuntimeState, modelsConfigState, agentDb, fileLock]) => ({
]).then(([contextRuntimeState, modelsConfigState, agentDb]) => ({
closeOpenClawAgentDatabasesForTest: agentDb.closeOpenClawAgentDatabasesForTest,
drainFileLockStateForTest: fileLock.drainFileLockStateForTest,
resetContextWindowCacheForTest: contextRuntimeState.resetContextWindowCacheForTest,
resetFileLockStateForTest: fileLock.resetFileLockStateForTest,
resetModelCatalogReadyCacheForTest: modelsConfigState.resetModelCatalogReadyCacheForTest,
}));
return globalState[WORKER_CLEANUP_HELPERS];
@@ -371,13 +366,9 @@ beforeAll(async () => {
afterEach(async () => {
const {
closeOpenClawAgentDatabasesForTest,
drainFileLockStateForTest,
resetContextWindowCacheForTest,
resetFileLockStateForTest,
resetModelCatalogReadyCacheForTest,
} = await loadWorkerCleanupHelpers();
await drainFileLockStateForTest();
resetFileLockStateForTest();
closeOpenClawAgentDatabasesForTest();
resetContextWindowCacheForTest();
resetModelCatalogReadyCacheForTest();
@@ -385,8 +376,6 @@ afterEach(async () => {
});
afterAll(async () => {
const { closeOpenClawAgentDatabasesForTest, drainFileLockStateForTest } =
await loadWorkerCleanupHelpers();
await drainFileLockStateForTest();
const { closeOpenClawAgentDatabasesForTest } = await loadWorkerCleanupHelpers();
closeOpenClawAgentDatabasesForTest();
});