mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-29 01:31:18 +00:00
perf: skip cache-busting for bundled hooks, use mtime for workspace hooks (openclaw#16960) thanks @mudrii
Verified: - pnpm build - pnpm check - pnpm test:macmini Co-authored-by: mudrii <220262+mudrii@users.noreply.github.com> Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
@@ -76,6 +76,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- Cron/Schedule: for `every` jobs, prefer `lastRunAtMs + everyMs` when still in the future after restarts, then fall back to anchor scheduling for catch-up windows, so NEXT timing matches the last successful cadence. (#22895) Thanks @SidQin-cyber.
|
- Cron/Schedule: for `every` jobs, prefer `lastRunAtMs + everyMs` when still in the future after restarts, then fall back to anchor scheduling for catch-up windows, so NEXT timing matches the last successful cadence. (#22895) Thanks @SidQin-cyber.
|
||||||
- Agents/Compaction: restore embedded compaction safeguard/context-pruning extension loading in production by wiring bundled extension factories into the resource loader instead of runtime file-path resolution. (#22349) Thanks @Glucksberg.
|
- Agents/Compaction: restore embedded compaction safeguard/context-pruning extension loading in production by wiring bundled extension factories into the resource loader instead of runtime file-path resolution. (#22349) Thanks @Glucksberg.
|
||||||
- Feishu/Media: for inbound video messages that include both `file_key` (video) and `image_key` (thumbnail), prefer `file_key` when downloading media so video attachments are saved instead of silently failing on thumbnail keys. (#23633)
|
- Feishu/Media: for inbound video messages that include both `file_key` (video) and `image_key` (thumbnail), prefer `file_key` when downloading media so video attachments are saved instead of silently failing on thumbnail keys. (#23633)
|
||||||
|
- Hooks/Loader: avoid redundant hook-module recompilation on gateway restart by skipping cache-busting for bundled hooks and using stable file metadata keys (`mtime+size`) for mutable workspace/managed/plugin hook imports. (#16953) Thanks @mudrii.
|
||||||
- Hooks/Cron: suppress duplicate main-session events for delivered hook turns and mark `SILENT_REPLY_TOKEN` (`NO_REPLY`) early exits as delivered to prevent hook context pollution. (#20678) Thanks @JonathanWorks.
|
- Hooks/Cron: suppress duplicate main-session events for delivered hook turns and mark `SILENT_REPLY_TOKEN` (`NO_REPLY`) early exits as delivered to prevent hook context pollution. (#20678) Thanks @JonathanWorks.
|
||||||
- Providers/OpenRouter: inject `cache_control` on system prompts for OpenRouter Anthropic models to improve prompt-cache reuse. (#17473) Thanks @rrenamed.
|
- Providers/OpenRouter: inject `cache_control` on system prompts for OpenRouter Anthropic models to improve prompt-cache reuse. (#17473) Thanks @rrenamed.
|
||||||
- Installer/Smoke tests: remove legacy `OPENCLAW_USE_GUM` overrides from docker install-smoke runs so tests exercise installer auto TTY detection behavior directly.
|
- Installer/Smoke tests: remove legacy `OPENCLAW_USE_GUM` overrides from docker install-smoke runs so tests exercise installer auto TTY detection behavior directly.
|
||||||
|
|||||||
62
src/hooks/import-url.test.ts
Normal file
62
src/hooks/import-url.test.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||||
|
import { buildImportUrl } from "./import-url.js";
|
||||||
|
|
||||||
|
describe("buildImportUrl", () => {
|
||||||
|
let tmpDir: string;
|
||||||
|
let tmpFile: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "import-url-test-"));
|
||||||
|
tmpFile = path.join(tmpDir, "handler.js");
|
||||||
|
fs.writeFileSync(tmpFile, "export default () => {};");
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns bare URL for bundled hooks (no query string)", () => {
|
||||||
|
const url = buildImportUrl(tmpFile, "openclaw-bundled");
|
||||||
|
expect(url).not.toContain("?t=");
|
||||||
|
expect(url).toMatch(/^file:\/\//);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("appends mtime-based cache buster for workspace hooks", () => {
|
||||||
|
const url = buildImportUrl(tmpFile, "openclaw-workspace");
|
||||||
|
expect(url).toMatch(/\?t=[\d.]+&s=\d+/);
|
||||||
|
|
||||||
|
const { mtimeMs, size } = fs.statSync(tmpFile);
|
||||||
|
expect(url).toContain(`?t=${mtimeMs}`);
|
||||||
|
expect(url).toContain(`&s=${size}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("appends mtime-based cache buster for managed hooks", () => {
|
||||||
|
const url = buildImportUrl(tmpFile, "openclaw-managed");
|
||||||
|
expect(url).toMatch(/\?t=[\d.]+&s=\d+/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("appends mtime-based cache buster for plugin hooks", () => {
|
||||||
|
const url = buildImportUrl(tmpFile, "openclaw-plugin");
|
||||||
|
expect(url).toMatch(/\?t=[\d.]+&s=\d+/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns same URL for bundled hooks across calls (cacheable)", () => {
|
||||||
|
const url1 = buildImportUrl(tmpFile, "openclaw-bundled");
|
||||||
|
const url2 = buildImportUrl(tmpFile, "openclaw-bundled");
|
||||||
|
expect(url1).toBe(url2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns same URL for workspace hooks when file is unchanged", () => {
|
||||||
|
const url1 = buildImportUrl(tmpFile, "openclaw-workspace");
|
||||||
|
const url2 = buildImportUrl(tmpFile, "openclaw-workspace");
|
||||||
|
expect(url1).toBe(url2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to Date.now() when file does not exist", () => {
|
||||||
|
const url = buildImportUrl("/nonexistent/handler.js", "openclaw-workspace");
|
||||||
|
expect(url).toMatch(/\?t=\d+/);
|
||||||
|
});
|
||||||
|
});
|
||||||
38
src/hooks/import-url.ts
Normal file
38
src/hooks/import-url.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* Build an import URL for a hook handler module.
|
||||||
|
*
|
||||||
|
* Bundled hooks (shipped in dist/) are immutable between installs, so they
|
||||||
|
* can be imported without a cache-busting suffix — letting V8 reuse its
|
||||||
|
* module cache across gateway restarts.
|
||||||
|
*
|
||||||
|
* Workspace, managed, and plugin hooks may be edited by the user between
|
||||||
|
* restarts. For those we append `?t=<mtime>&s=<size>` so the module key
|
||||||
|
* reflects on-disk changes while staying stable for unchanged files.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fs from "node:fs";
|
||||||
|
import { pathToFileURL } from "node:url";
|
||||||
|
import type { HookSource } from "./types.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sources whose handler files never change between `npm install` runs.
|
||||||
|
* Imports from these sources skip cache busting entirely.
|
||||||
|
*/
|
||||||
|
const IMMUTABLE_SOURCES: ReadonlySet<HookSource> = new Set(["openclaw-bundled"]);
|
||||||
|
|
||||||
|
export function buildImportUrl(handlerPath: string, source: HookSource): string {
|
||||||
|
const base = pathToFileURL(handlerPath).href;
|
||||||
|
|
||||||
|
if (IMMUTABLE_SOURCES.has(source)) {
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use file metadata so the cache key only changes when the file changes
|
||||||
|
try {
|
||||||
|
const { mtimeMs, size } = fs.statSync(handlerPath);
|
||||||
|
return `${base}?t=${mtimeMs}&s=${size}`;
|
||||||
|
} catch {
|
||||||
|
// If stat fails (unlikely), fall back to Date.now() to guarantee freshness
|
||||||
|
return `${base}?t=${Date.now()}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,9 +11,10 @@ import { createSubsystemLogger } from "../logging/subsystem.js";
|
|||||||
import { isPathInsideWithRealpath } from "../security/scan-paths.js";
|
import { isPathInsideWithRealpath } from "../security/scan-paths.js";
|
||||||
import { resolveHookConfig } from "./config.js";
|
import { resolveHookConfig } from "./config.js";
|
||||||
import { shouldIncludeHook } from "./config.js";
|
import { shouldIncludeHook } from "./config.js";
|
||||||
|
import { buildImportUrl } from "./import-url.js";
|
||||||
import type { InternalHookHandler } from "./internal-hooks.js";
|
import type { InternalHookHandler } from "./internal-hooks.js";
|
||||||
import { registerInternalHook } from "./internal-hooks.js";
|
import { registerInternalHook } from "./internal-hooks.js";
|
||||||
import { importFileModule, resolveFunctionModuleExport } from "./module-loader.js";
|
import { resolveFunctionModuleExport } from "./module-loader.js";
|
||||||
import { loadWorkspaceHookEntries } from "./workspace.js";
|
import { loadWorkspaceHookEntries } from "./workspace.js";
|
||||||
|
|
||||||
const log = createSubsystemLogger("hooks:loader");
|
const log = createSubsystemLogger("hooks:loader");
|
||||||
@@ -82,12 +83,12 @@ export async function loadInternalHooks(
|
|||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
// Import handler module — only cache-bust mutable (workspace/managed) hooks
|
||||||
|
const importUrl = buildImportUrl(entry.hook.handlerPath, entry.hook.source);
|
||||||
|
const mod = (await import(importUrl)) as Record<string, unknown>;
|
||||||
|
|
||||||
// Get handler function (default or named export)
|
// Get handler function (default or named export)
|
||||||
const exportName = entry.metadata?.export ?? "default";
|
const exportName = entry.metadata?.export ?? "default";
|
||||||
const mod = await importFileModule({
|
|
||||||
modulePath: entry.hook.handlerPath,
|
|
||||||
cacheBust: true,
|
|
||||||
});
|
|
||||||
const handler = resolveFunctionModuleExport<InternalHookHandler>({
|
const handler = resolveFunctionModuleExport<InternalHookHandler>({
|
||||||
mod,
|
mod,
|
||||||
exportName,
|
exportName,
|
||||||
@@ -159,12 +160,12 @@ export async function loadInternalHooks(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Legacy handlers are always workspace-relative, so use mtime-based cache busting
|
||||||
|
const importUrl = buildImportUrl(modulePath, "openclaw-workspace");
|
||||||
|
const mod = (await import(importUrl)) as Record<string, unknown>;
|
||||||
|
|
||||||
// Get the handler function
|
// Get the handler function
|
||||||
const exportName = handlerConfig.export ?? "default";
|
const exportName = handlerConfig.export ?? "default";
|
||||||
const mod = await importFileModule({
|
|
||||||
modulePath,
|
|
||||||
cacheBust: true,
|
|
||||||
});
|
|
||||||
const handler = resolveFunctionModuleExport<InternalHookHandler>({
|
const handler = resolveFunctionModuleExport<InternalHookHandler>({
|
||||||
mod,
|
mod,
|
||||||
exportName,
|
exportName,
|
||||||
|
|||||||
Reference in New Issue
Block a user