diff --git a/extensions/discord/src/monitor/native-command.think-autocomplete.test.ts b/extensions/discord/src/monitor/native-command.think-autocomplete.test.ts index 316a45ddc9d..db250ae2ea1 100644 --- a/extensions/discord/src/monitor/native-command.think-autocomplete.test.ts +++ b/extensions/discord/src/monitor/native-command.think-autocomplete.test.ts @@ -11,30 +11,48 @@ import { clearSessionStoreCacheForTest } from "openclaw/plugin-sdk/config-runtim import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { createNoopThreadBindingManager } from "./thread-bindings.js"; +type EnsureConfiguredBindingRouteReady = + typeof import("openclaw/plugin-sdk/conversation-runtime").ensureConfiguredBindingRouteReady; +type ResolveConfiguredBindingRoute = + typeof import("openclaw/plugin-sdk/conversation-runtime").resolveConfiguredBindingRoute; + const ensureConfiguredBindingRouteReadyMock = vi.hoisted(() => - vi.fn<() => Promise<{ ok: boolean; error?: string }>>(async () => ({ ok: true })), + vi.fn(async () => ({ ok: true })), ); const resolveConfiguredBindingRouteMock = vi.hoisted(() => - vi.fn< - () => { - bindingResolution: { - record: { - conversation: { - channel: string; - accountId: string; - conversationId: string; - }; - }; - }; - boundSessionKey: string; - route: { - agentId: string; - sessionKey: string; - }; - } | null - >(() => null), + vi.fn(({ route }) => ({ + bindingResolution: null, + route, + })), ); +type ConfiguredBindingRoute = ReturnType; +type ConfiguredBindingResolution = NonNullable; + +function createConfiguredRouteResult( + params: Parameters[0], +): ConfiguredBindingRoute { + return { + bindingResolution: { + record: { + conversation: { + channel: "discord", + accountId: "default", + conversationId: "C1", + }, + }, + } as ConfiguredBindingResolution, + boundSessionKey: SESSION_KEY, + route: { + ...params.route, + agentId: "main", + sessionKey: SESSION_KEY, + matchedBy: "binding.channel", + lastRoutePolicy: "session", + }, + }; +} + vi.mock("openclaw/plugin-sdk/conversation-runtime", async () => { const { createConfiguredBindingConversationRuntimeModuleMock } = await import("../test-support/configured-binding-runtime.js"); @@ -69,7 +87,10 @@ describe("discord native /think autocomplete", () => { ensureConfiguredBindingRouteReadyMock.mockReset(); ensureConfiguredBindingRouteReadyMock.mockResolvedValue({ ok: true }); resolveConfiguredBindingRouteMock.mockReset(); - resolveConfiguredBindingRouteMock.mockReturnValue(null); + resolveConfiguredBindingRouteMock.mockImplementation(({ route }) => ({ + bindingResolution: null, + route, + })); fs.mkdirSync(path.dirname(STORE_PATH), { recursive: true }); fs.writeFileSync( STORE_PATH, @@ -154,22 +175,7 @@ describe("discord native /think autocomplete", () => { it("falls back when a configured binding is unavailable", async () => { const cfg = createConfig(); - resolveConfiguredBindingRouteMock.mockReturnValue({ - bindingResolution: { - record: { - conversation: { - channel: "discord", - accountId: "default", - conversationId: "C1", - }, - }, - }, - boundSessionKey: SESSION_KEY, - route: { - agentId: "main", - sessionKey: SESSION_KEY, - }, - }); + resolveConfiguredBindingRouteMock.mockImplementation(createConfiguredRouteResult); ensureConfiguredBindingRouteReadyMock.mockResolvedValue({ ok: false, error: "acpx exited", diff --git a/scripts/lib/local-heavy-check-runtime.mjs b/scripts/lib/local-heavy-check-runtime.mjs index 6baf8b6319d..8517939e93d 100644 --- a/scripts/lib/local-heavy-check-runtime.mjs +++ b/scripts/lib/local-heavy-check-runtime.mjs @@ -7,6 +7,7 @@ const DEFAULT_LOCAL_GO_GC = "30"; const DEFAULT_LOCAL_GO_MEMORY_LIMIT = "3GiB"; const DEFAULT_LOCK_TIMEOUT_MS = 10 * 60 * 1000; const DEFAULT_LOCK_POLL_MS = 500; +const DEFAULT_LOCK_PROGRESS_MS = 15 * 1000; const DEFAULT_STALE_LOCK_MS = 30 * 1000; const SLEEP_BUFFER = new Int32Array(new SharedArrayBuffer(4)); @@ -73,12 +74,17 @@ export function acquireLocalHeavyCheckLockSync(params) { DEFAULT_LOCK_TIMEOUT_MS, ); const pollMs = readPositiveInt(env.OPENCLAW_HEAVY_CHECK_LOCK_POLL_MS, DEFAULT_LOCK_POLL_MS); + const progressMs = readPositiveInt( + env.OPENCLAW_HEAVY_CHECK_LOCK_PROGRESS_MS, + DEFAULT_LOCK_PROGRESS_MS, + ); const staleLockMs = readPositiveInt( env.OPENCLAW_HEAVY_CHECK_STALE_LOCK_MS, DEFAULT_STALE_LOCK_MS, ); const startedAt = Date.now(); let waitingLogged = false; + let lastProgressAt = 0; fs.mkdirSync(locksDir, { recursive: true }); @@ -120,11 +126,20 @@ export function acquireLocalHeavyCheckLockSync(params) { if (!waitingLogged) { const ownerLabel = describeOwner(owner); console.error( - `[${params.toolName}] waiting for the local heavy-check lock${ + `[${params.toolName}] queued behind the local heavy-check lock${ ownerLabel ? ` held by ${ownerLabel}` : "" }...`, ); waitingLogged = true; + lastProgressAt = Date.now(); + } else if (Date.now() - lastProgressAt >= progressMs) { + const ownerLabel = describeOwner(owner); + console.error( + `[${params.toolName}] still waiting ${formatElapsedMs(elapsedMs)} for the local heavy-check lock${ + ownerLabel ? ` held by ${ownerLabel}` : "" + }...`, + ); + lastProgressAt = Date.now(); } sleepSync(pollMs); @@ -213,6 +228,19 @@ function describeOwner(owner) { return `${tool}, ${pid}, cwd ${cwd}`; } +function formatElapsedMs(elapsedMs) { + if (elapsedMs < 1000) { + return `${elapsedMs}ms`; + } + const seconds = elapsedMs / 1000; + if (seconds < 60) { + return `${seconds.toFixed(seconds >= 10 ? 0 : 1)}s`; + } + const minutes = Math.floor(seconds / 60); + const remainderSeconds = Math.round(seconds % 60); + return `${minutes}m ${remainderSeconds}s`; +} + function sleepSync(ms) { Atomics.wait(SLEEP_BUFFER, 0, 0, ms); }