From 2e66a73ebdab1dfc54c5bf5ba9bc2ebf2f28ddf2 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 10 May 2026 02:24:06 +0100 Subject: [PATCH] refactor: remove plugin file-lock sdk --- docs/plugins/sdk-migration.md | 1 - docs/plugins/sdk-subpaths.md | 1 - docs/refactor/database-first.md | 4 +- package.json | 4 - scripts/lib/plugin-sdk-entrypoints.json | 1 - src/infra/file-lock.ts | 12 --- src/plugin-sdk/file-lock.test.ts | 78 ---------------- src/plugin-sdk/file-lock.ts | 116 ------------------------ src/plugin-sdk/infra-runtime.ts | 1 - test/setup-openclaw-runtime.ts | 15 +-- 10 files changed, 4 insertions(+), 229 deletions(-) delete mode 100644 src/infra/file-lock.ts delete mode 100644 src/plugin-sdk/file-lock.test.ts delete mode 100644 src/plugin-sdk/file-lock.ts diff --git a/docs/plugins/sdk-migration.md b/docs/plugins/sdk-migration.md index 0698c0bc558..bda2a8c54cb 100644 --- a/docs/plugins/sdk-migration.md +++ b/docs/plugins/sdk-migration.md @@ -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. diff --git a/docs/plugins/sdk-subpaths.md b/docs/plugins/sdk-subpaths.md index df9f830c503..a04128a9ec0 100644 --- a/docs/plugins/sdk-subpaths.md +++ b/docs/plugins/sdk-subpaths.md @@ -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 | diff --git a/docs/refactor/database-first.md b/docs/refactor/database-first.md index e20f7cd2e15..ae2c6d53a16 100644 --- a/docs/refactor/database-first.md +++ b/docs/refactor/database-first.md @@ -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//sessions/` as required runtime diff --git a/package.json b/package.json index c042c7569f2..2a79c5902bf 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/scripts/lib/plugin-sdk-entrypoints.json b/scripts/lib/plugin-sdk-entrypoints.json index c09deffd70b..2999160c549 100644 --- a/scripts/lib/plugin-sdk-entrypoints.json +++ b/scripts/lib/plugin-sdk-entrypoints.json @@ -195,7 +195,6 @@ "channel-route", "channel-targets", "context-visibility-runtime", - "file-lock", "fetch-runtime", "runtime-fetch", "response-limit-runtime", diff --git a/src/infra/file-lock.ts b/src/infra/file-lock.ts deleted file mode 100644 index 90f14f390f4..00000000000 --- a/src/infra/file-lock.ts +++ /dev/null @@ -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"; diff --git a/src/plugin-sdk/file-lock.test.ts b/src/plugin-sdk/file-lock.test.ts deleted file mode 100644 index 20b7dab593e..00000000000 --- a/src/plugin-sdk/file-lock.test.ts +++ /dev/null @@ -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>); - - await expect( - acquireFileLock(filePath, { - retries: { - retries: 0, - factor: 1, - minTimeout: 1, - maxTimeout: 1, - }, - stale: 100, - }), - ).rejects.toThrow(writeError); - - expect(close).toHaveBeenCalledTimes(1); - }); -}); diff --git a/src/plugin-sdk/file-lock.ts b/src/plugin-sdk/file-lock.ts deleted file mode 100644 index 11130538957..00000000000 --- a/src/plugin-sdk/file-lock.ts +++ /dev/null @@ -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; -}; - -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 | 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 | null; - staleMs: number; - nowMs: number; -}): Promise { - 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 { - 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 { - 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( - filePath: string, - options: FileLockOptions, - fn: () => Promise, -): Promise { - const lock = await acquireFileLock(filePath, options); - try { - return await fn(); - } finally { - await lock.release(); - } -} diff --git a/src/plugin-sdk/infra-runtime.ts b/src/plugin-sdk/infra-runtime.ts index a2d11a73391..373f993d4a7 100644 --- a/src/plugin-sdk/infra-runtime.ts +++ b/src/plugin-sdk/infra-runtime.ts @@ -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"; diff --git a/test/setup-openclaw-runtime.ts b/test/setup-openclaw-runtime.ts index ba8c043554a..7a48103da81 100644 --- a/test/setup-openclaw-runtime.ts +++ b/test/setup-openclaw-runtime.ts @@ -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 { vi.importActual( "../src/state/openclaw-agent-db.js", ), - vi.importActual("../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(); });