diff --git a/extensions/browser/src/browser/cdp.helpers.ts b/extensions/browser/src/browser/cdp.helpers.ts index 5e02a3882fd..531931d63ff 100644 --- a/extensions/browser/src/browser/cdp.helpers.ts +++ b/extensions/browser/src/browser/cdp.helpers.ts @@ -5,6 +5,7 @@ * redaction/headers, and request/response correlation over WebSocket. */ import { parseBrowserHttpUrl, redactCdpUrl } from "openclaw/plugin-sdk/browser-config"; +import { sleep } from "openclaw/plugin-sdk/runtime-env"; import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime"; import WebSocket from "ws"; import { isLoopbackHost } from "../gateway/net.js"; @@ -423,12 +424,6 @@ type CdpSocketOptions = { handshakeMaxRetryDelayMs?: number; }; -function sleep(ms: number): Promise { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); -} - function normalizeRetryCount(value: number | undefined, fallback: number): number { if (typeof value !== "number" || !Number.isFinite(value)) { return fallback; diff --git a/extensions/browser/src/browser/routes/agent.act.ts b/extensions/browser/src/browser/routes/agent.act.ts index 3270c7e2a8e..023d14e4fca 100644 --- a/extensions/browser/src/browser/routes/agent.act.ts +++ b/extensions/browser/src/browser/routes/agent.act.ts @@ -4,6 +4,7 @@ * Dispatches normalized actions to either Playwright-backed OpenClaw browser * control or Chrome MCP existing-session operations with navigation guards. */ +import { sleep } from "openclaw/plugin-sdk/runtime-env"; import { formatErrorMessage } from "../../infra/errors.js"; import { clickChromeMcpElement, @@ -51,12 +52,6 @@ import { readRoutePositiveInteger, readRouteTimerTimeoutMs } from "./route-numer import type { BrowserRouteRegistrar } from "./types.js"; import { asyncBrowserRoute, jsonError, toStringOrEmpty } from "./utils.js"; -function sleep(ms: number): Promise { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); -} - const EXISTING_SESSION_INTERACTION_NAVIGATION_RECHECK_DELAYS_MS = [0, 250, 500] as const; async function readExistingSessionLocationHref(params: { diff --git a/extensions/codex/src/migration/apply.ts b/extensions/codex/src/migration/apply.ts index 19f9ff820a0..57f4cea098f 100644 --- a/extensions/codex/src/migration/apply.ts +++ b/extensions/codex/src/migration/apply.ts @@ -22,6 +22,7 @@ import type { MigrationPlan, MigrationProviderContext, } from "openclaw/plugin-sdk/plugin-entry"; +import { sleep } from "openclaw/plugin-sdk/runtime-env"; import { uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime"; import { defaultCodexAppInventoryCache } from "../app-server/app-inventory-cache.js"; import { @@ -385,12 +386,6 @@ function isCodexPluginLoadWarningItem(item: MigrationItem): boolean { ); } -async function sleep(ms: number): Promise { - await new Promise((resolve) => { - setTimeout(resolve, ms); - }); -} - async function buildTargetCodexPluginAppCacheKey(ctx: MigrationProviderContext): Promise { const targets = resolveCodexMigrationTargets(ctx); const appServer = resolveTargetCodexAppServer(ctx); diff --git a/extensions/discord/src/monitor/message-handler.process.ts b/extensions/discord/src/monitor/message-handler.process.ts index 55e8fda4648..27f346a0056 100644 --- a/extensions/discord/src/monitor/message-handler.process.ts +++ b/extensions/discord/src/monitor/message-handler.process.ts @@ -38,7 +38,7 @@ import { resolveSendableOutboundReplyParts, } from "openclaw/plugin-sdk/reply-payload"; import type { ReplyDispatchKind, ReplyPayload } from "openclaw/plugin-sdk/reply-runtime"; -import { danger, logVerbose, shouldLogVerbose } from "openclaw/plugin-sdk/runtime-env"; +import { danger, logVerbose, shouldLogVerbose, sleep } from "openclaw/plugin-sdk/runtime-env"; import { getSessionEntry, resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime"; import { readLatestAssistantTextByIdentity } from "openclaw/plugin-sdk/session-transcript-runtime"; import { resolveDiscordAccount, resolveDiscordMaxLinesPerMessage } from "../accounts.js"; @@ -71,12 +71,6 @@ import { DISCORD_ATTACHMENT_TOTAL_TIMEOUT_MS, } from "./timeouts.js"; -function sleep(ms: number): Promise { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); -} - const loadReplyRuntime = createLazyRuntimeModule(() => import("openclaw/plugin-sdk/reply-runtime")); function isProcessAborted(abortSignal?: AbortSignal): boolean { diff --git a/extensions/feishu/src/app-registration.ts b/extensions/feishu/src/app-registration.ts index cb4c5204aa2..60330282203 100644 --- a/extensions/feishu/src/app-registration.ts +++ b/extensions/feishu/src/app-registration.ts @@ -1,5 +1,6 @@ // Feishu plugin module implements app registration behavior. import { finiteSecondsToTimerSafeMilliseconds } from "openclaw/plugin-sdk/number-runtime"; +import { sleep } from "openclaw/plugin-sdk/runtime-env"; /** * Feishu app registration via OAuth device-code flow. * @@ -340,12 +341,6 @@ export async function getAppOwnerOpenId(params: { } } -function sleep(ms: number): Promise { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); -} - function sleepRegistrationPollInterval(intervalSeconds: number): Promise { const intervalMs = finiteSecondsToTimerSafeMilliseconds(intervalSeconds) ?? diff --git a/extensions/imessage/src/send.ts b/extensions/imessage/src/send.ts index 2db30cf39ef..59e5b53c0c1 100644 --- a/extensions/imessage/src/send.ts +++ b/extensions/imessage/src/send.ts @@ -15,6 +15,7 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/markdown-table-runtime"; import { kindFromMime, resolveOutboundAttachmentFromUrl } from "openclaw/plugin-sdk/media-runtime"; import { requireRuntimeConfig } from "openclaw/plugin-sdk/plugin-config-runtime"; +import { sleep as delay } from "openclaw/plugin-sdk/runtime-env"; import { convertMarkdownTables } from "openclaw/plugin-sdk/text-chunking"; import { stripInlineDirectiveTagsForDelivery } from "openclaw/plugin-sdk/text-chunking"; import { resolveIMessageAccount, type ResolvedIMessageAccount } from "./accounts.js"; @@ -419,12 +420,6 @@ async function resolveApprovalBindingMessageGuid(params: { ); } -function delay(ms: number): Promise { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); -} - async function resolveFallbackSentMessageGuid(params: { dbPath?: string; target: ParsedIMessageTarget; diff --git a/extensions/memory-core/src/memory/watch-settle.ts b/extensions/memory-core/src/memory/watch-settle.ts index 4403480690d..51d41bc0de5 100644 --- a/extensions/memory-core/src/memory/watch-settle.ts +++ b/extensions/memory-core/src/memory/watch-settle.ts @@ -1,6 +1,7 @@ // Memory Core plugin module implements watch settle behavior. import fsSync from "node:fs"; import path from "node:path"; +import { sleep as delay } from "openclaw/plugin-sdk/runtime-env"; export type MemoryWatchEventStats = { isDirectory?: () => boolean; @@ -46,12 +47,6 @@ function snapshotPath(filePath: string): WatchPathSnapshot | null { } } -async function delay(ms: number): Promise { - await new Promise((resolve) => { - setTimeout(resolve, ms); - }); -} - export function recordMemoryWatchEventPath( queue: MemoryWatchSettleQueue, watchPath?: string, diff --git a/extensions/memory-core/src/short-term-promotion.ts b/extensions/memory-core/src/short-term-promotion.ts index 1d1680b4675..1b89c034e99 100644 --- a/extensions/memory-core/src/short-term-promotion.ts +++ b/extensions/memory-core/src/short-term-promotion.ts @@ -9,6 +9,7 @@ import { isSameMemoryDreamingDay, } from "openclaw/plugin-sdk/memory-core-host-status"; import { appendMemoryHostEvent } from "openclaw/plugin-sdk/memory-host-events"; +import { sleep } from "openclaw/plugin-sdk/runtime-env"; import { normalizeLowercaseStringOrEmpty, normalizeStringEntries, @@ -847,12 +848,6 @@ function isProcessLikelyAlive(pid: number): boolean { } } -async function sleep(ms: number): Promise { - await new Promise((resolve) => { - setTimeout(resolve, ms); - }); -} - async function withInProcessShortTermLock(lockPath: string, task: () => Promise): Promise { const previous = inProcessShortTermLocks.get(lockPath) ?? Promise.resolve(); let releaseCurrent!: () => void; diff --git a/extensions/qqbot/src/engine/api/media-chunked.ts b/extensions/qqbot/src/engine/api/media-chunked.ts index 6db4d16b172..7f611b102ae 100644 --- a/extensions/qqbot/src/engine/api/media-chunked.ts +++ b/extensions/qqbot/src/engine/api/media-chunked.ts @@ -37,6 +37,7 @@ import * as crypto from "node:crypto"; import type { FileHandle } from "node:fs/promises"; import { readResponseTextLimited } from "openclaw/plugin-sdk/provider-http"; +import { sleep } from "openclaw/plugin-sdk/runtime-env"; import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime"; import type { MediaSource, OpenedLocalFile } from "../messaging/media-source.js"; import { openLocalFile } from "../messaging/media-source.js"; @@ -631,9 +632,3 @@ async function runWithConcurrency( await Promise.all(batch.map((task) => task())); } } - -function sleep(ms: number): Promise { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); -} diff --git a/extensions/qqbot/src/engine/api/retry.ts b/extensions/qqbot/src/engine/api/retry.ts index 9c20366110d..7ae12971157 100644 --- a/extensions/qqbot/src/engine/api/retry.ts +++ b/extensions/qqbot/src/engine/api/retry.ts @@ -10,6 +10,7 @@ * parameterized by `RetryPolicy` and optional `PersistentRetryPolicy`. */ +import { sleep } from "openclaw/plugin-sdk/runtime-env"; import type { EngineLogger } from "../types.js"; import { formatErrorMessage } from "../utils/format.js"; @@ -146,12 +147,6 @@ async function persistentRetryLoop( throw lastError ?? new Error(`Persistent retry timed out (${policy.timeoutMs / 1000}s)`); } -function sleep(ms: number): Promise { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); -} - // ============ Pre-built Retry Policies ============ /** Standard upload retry: exponential backoff, skip 400/401/timeout errors. */ diff --git a/extensions/signal/src/monitor/event-handler.ts b/extensions/signal/src/monitor/event-handler.ts index 42b4d681980..e7d27995f30 100644 --- a/extensions/signal/src/monitor/event-handler.ts +++ b/extensions/signal/src/monitor/event-handler.ts @@ -44,7 +44,12 @@ import { dispatchInboundMessage } from "openclaw/plugin-sdk/reply-runtime"; import { createReplyDispatcherWithTyping } from "openclaw/plugin-sdk/reply-runtime"; import { settleReplyDispatcher } from "openclaw/plugin-sdk/reply-runtime"; import { resolveAgentRoute, resolveInboundLastRouteSessionKey } from "openclaw/plugin-sdk/routing"; -import { danger, logVerbose, shouldLogVerbose } from "openclaw/plugin-sdk/runtime-env"; +import { + danger, + logVerbose, + shouldLogVerbose, + sleep as delay, +} from "openclaw/plugin-sdk/runtime-env"; import { resolvePinnedMainDmOwnerFromAllowlist } from "openclaw/plugin-sdk/security-runtime"; import { readSessionUpdatedAt, resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime"; import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime"; @@ -141,12 +146,6 @@ function hasSignalStatusReplyDeliveryFailure(result: SignalStatusDispatchResult) ); } -function delay(ms: number): Promise { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); -} - function resolveSignalStatusReactionEmojis( emojis: StatusReactionEmojis | undefined, ): StatusReactionEmojis | undefined { diff --git a/extensions/xai/xai-oauth.ts b/extensions/xai/xai-oauth.ts index 7ca162fbe43..4b580abc09c 100644 --- a/extensions/xai/xai-oauth.ts +++ b/extensions/xai/xai-oauth.ts @@ -5,7 +5,6 @@ import { resolveExpiresAtMsFromDurationSeconds, resolveExpiresAtMsFromEpochSeconds, } from "openclaw/plugin-sdk/number-runtime"; -import { readResponseWithLimit } from "openclaw/plugin-sdk/response-limit-runtime"; import type { ProviderAuthContext, ProviderAuthMethod } from "openclaw/plugin-sdk/plugin-entry"; import { buildOauthProviderAuthResult, @@ -13,6 +12,8 @@ import { type OAuthCredential, type ProviderAuthResult, } from "openclaw/plugin-sdk/provider-auth"; +import { readResponseWithLimit } from "openclaw/plugin-sdk/response-limit-runtime"; +import { sleep } from "openclaw/plugin-sdk/runtime-env"; import { applyXaiConfig, XAI_DEFAULT_MODEL_REF } from "./onboard.js"; import { xaiUserAgent } from "./src/xai-user-agent.js"; @@ -299,12 +300,6 @@ function describeXaiOAuthTokenFailure(params: { }; } -async function sleep(ms: number): Promise { - await new Promise((resolve) => { - setTimeout(resolve, ms); - }); -} - async function exchangeXaiOAuthToken( params: { tokenEndpoint: string; diff --git a/scripts/check-gateway-watch-regression.mjs b/scripts/check-gateway-watch-regression.mjs index cfee95eb122..90dac964078 100644 --- a/scripts/check-gateway-watch-regression.mjs +++ b/scripts/check-gateway-watch-regression.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node - // Measures gateway watch idle CPU and dist/runtime artifact churn. + import { spawn, spawnSync } from "node:child_process"; import fs from "node:fs"; import net from "node:net"; @@ -14,6 +14,7 @@ import { writeBuildStamp, writeRuntimePostBuildStamp, } from "./lib/local-build-metadata.mjs"; +import { sleep } from "./lib/sleep.mjs"; import { resolveBuildRequirement } from "./run-node.mjs"; const DEFAULTS = { @@ -334,12 +335,6 @@ function runCheckedCommand(command, args) { throw new Error(`${command} ${args.join(" ")} failed with status ${result.status ?? "unknown"}`); } -function sleep(ms) { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); -} - function parsePsCpuTimeMs(timeText) { const [maybeDays, clockText] = timeText.includes("-") ? timeText.split("-", 2) : ["0", timeText]; const days = Number(maybeDays); diff --git a/scripts/control-ui-i18n.ts b/scripts/control-ui-i18n.ts index 6d5a3259427..846873339a0 100644 --- a/scripts/control-ui-i18n.ts +++ b/scripts/control-ui-i18n.ts @@ -8,6 +8,7 @@ import { fileURLToPath, pathToFileURL } from "node:url"; import { completeSimple, type AssistantMessage, type Model } from "openclaw/plugin-sdk/llm"; import * as ts from "typescript"; import { formatErrorMessage } from "../src/infra/errors.ts"; +import { sleep } from "./lib/sleep.mjs"; import { resolveWindowsTaskkillPath } from "./lib/windows-taskkill.mjs"; const { formatGeneratedModule } = (await import( @@ -634,12 +635,6 @@ function buildBatchPrompt(items: readonly TranslationBatchItem[]): string { ].join("\n"); } -function sleep(ms: number) { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); -} - function formatDuration(ms: number): string { if (ms < 1_000) { return `${Math.round(ms)}ms`; diff --git a/scripts/dev/discord-acp-plain-language-smoke.ts b/scripts/dev/discord-acp-plain-language-smoke.ts index 85b2f8502f9..9044324b2ff 100644 --- a/scripts/dev/discord-acp-plain-language-smoke.ts +++ b/scripts/dev/discord-acp-plain-language-smoke.ts @@ -17,6 +17,7 @@ import { redactForDevToolLog, redactHomePath, } from "../lib/dev-tooling-safety.ts"; +import { sleep } from "../lib/sleep.mjs"; function writeStdoutLine(message: string): void { process.stdout.write(`${message}\n`); @@ -157,12 +158,6 @@ class CliArgumentError extends Error { override name = "CliArgumentError"; } -function sleep(ms: number): Promise { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); -} - function remainingTimeoutMs(deadlineMs: number, nowMs = Date.now()): number { const remaining = Math.floor(deadlineMs - nowMs); if (!Number.isFinite(deadlineMs) || remaining <= 0) { diff --git a/scripts/dev/tui-pty-test-watch.ts b/scripts/dev/tui-pty-test-watch.ts index e6f83424ef1..f189b43b1ce 100644 --- a/scripts/dev/tui-pty-test-watch.ts +++ b/scripts/dev/tui-pty-test-watch.ts @@ -4,6 +4,7 @@ import { mkdir, open, writeFile } from "node:fs/promises"; import { createRequire } from "node:module"; import path from "node:path"; import { pathToFileURL } from "node:url"; +import { sleep as delay } from "../lib/sleep.mjs"; import { resolveWindowsTaskkillPath } from "../lib/windows-taskkill.mjs"; type Options = { @@ -119,12 +120,6 @@ function parseOptions(args = process.argv.slice(2)): Options { }; } -function delay(ms: number): Promise { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); -} - function shouldUseAltScreen(options: Options) { return options.altScreen && process.stdout.isTTY; } diff --git a/scripts/e2e/lib/config-reload/assert-log.mjs b/scripts/e2e/lib/config-reload/assert-log.mjs index c702364b8f1..8c3b7d61d09 100644 --- a/scripts/e2e/lib/config-reload/assert-log.mjs +++ b/scripts/e2e/lib/config-reload/assert-log.mjs @@ -1,4 +1,5 @@ // Log assertions for config reload E2E scenarios. +import { sleep } from "../../../lib/sleep.mjs"; import { readPositiveIntEnv } from "../env-limits.mjs"; import { createConfigReloadLogScanner } from "./log-scanner.mjs"; @@ -6,10 +7,6 @@ const logPath = process.env.OPENCLAW_CONFIG_RELOAD_LOG_PATH ?? "/tmp/config-relo const deadlineMs = Date.now() + readPositiveIntEnv("OPENCLAW_CONFIG_RELOAD_LOG_TIMEOUT_MS", 30_000); const maxReadBytes = readPositiveIntEnv("OPENCLAW_CONFIG_RELOAD_LOG_MAX_READ_BYTES", 256 * 1024); -const sleep = (ms) => - new Promise((resolve) => { - setTimeout(resolve, ms); - }); const scanner = createConfigReloadLogScanner(logPath, { maxReadBytes, tailLineLimit: 160, diff --git a/scripts/e2e/lib/gateway-network/client.mjs b/scripts/e2e/lib/gateway-network/client.mjs index 66068ca757f..5b3eb9329cd 100644 --- a/scripts/e2e/lib/gateway-network/client.mjs +++ b/scripts/e2e/lib/gateway-network/client.mjs @@ -1,16 +1,11 @@ // WebSocket client helpers for gateway network E2E scenarios. import { pathToFileURL } from "node:url"; import { WebSocket } from "ws"; +import { sleep as delay } from "../../../lib/sleep.mjs"; import { waitForWebSocketOpen } from "../websocket-open.mjs"; import { readGatewayNetworkClientConnectTimeoutMs } from "./limits.mjs"; import { onceFrame } from "./ws-frames.mjs"; -function delay(ms) { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); -} - function remainingDeadlineMs(deadline) { return Math.max(1, deadline - Date.now()); } diff --git a/scripts/e2e/parallels/guest-transports.ts b/scripts/e2e/parallels/guest-transports.ts index eae03e17998..7b4c0372c81 100644 --- a/scripts/e2e/parallels/guest-transports.ts +++ b/scripts/e2e/parallels/guest-transports.ts @@ -1,5 +1,6 @@ // Guest Transports script supports OpenClaw repository automation. import { randomUUID } from "node:crypto"; +import { sleep } from "../../lib/sleep.mjs"; import { run } from "./host-command.ts"; import type { PhaseRunner } from "./phase-runner.ts"; import { encodePowerShell, psSingleQuote } from "./powershell.ts"; @@ -44,12 +45,6 @@ function timeoutBefore(deadline: number, fallbackMs: number): number { return Math.min(fallbackMs, Math.max(1_000, deadline - Date.now())); } -function sleep(ms: number): Promise { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); -} - function throwIfFailed(label: string, result: CommandResult, check: boolean | undefined): void { if (check === false || result.status === 0) { return; diff --git a/scripts/e2e/parallels/host-server.ts b/scripts/e2e/parallels/host-server.ts index 18ee2c3c01c..406c537308b 100644 --- a/scripts/e2e/parallels/host-server.ts +++ b/scripts/e2e/parallels/host-server.ts @@ -3,6 +3,7 @@ import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process"; import { createServer } from "node:http"; import { createConnection } from "node:net"; import path from "node:path"; +import { sleep as delay } from "../../lib/sleep.mjs"; import { die, run, say, sh, warn } from "./host-command.ts"; import type { HostServer } from "./types.ts"; @@ -195,12 +196,6 @@ async function canConnect(port: number): Promise { }); } -async function delay(ms: number): Promise { - await new Promise((resolve) => { - setTimeout(resolve, ms); - }); -} - export const testing = { appendBoundedOutput, stopHostServerChild, diff --git a/scripts/e2e/parallels/package-artifact.ts b/scripts/e2e/parallels/package-artifact.ts index 1eb1da4cff0..ab1b24a9f5e 100644 --- a/scripts/e2e/parallels/package-artifact.ts +++ b/scripts/e2e/parallels/package-artifact.ts @@ -3,6 +3,7 @@ import { randomUUID } from "node:crypto"; import { copyFile, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; +import { sleep as delay } from "../../lib/sleep.mjs"; import { readPositiveIntEnv } from "./env-limits.ts"; import { exists, readJson } from "./filesystem.ts"; import { die, repoRoot, run, say, sh } from "./host-command.ts"; @@ -294,12 +295,6 @@ function isErrorCode(error: unknown, code: string): boolean { return Boolean(error && typeof error === "object" && "code" in error && error.code === code); } -async function delay(ms: number): Promise { - await new Promise((resolve) => { - setTimeout(resolve, ms); - }); -} - export const testing = { acquirePackageLock, removeStalePackageLock, diff --git a/scripts/e2e/telegram-user-crabbox-proof.ts b/scripts/e2e/telegram-user-crabbox-proof.ts index f6513b91178..2b150ec5867 100644 --- a/scripts/e2e/telegram-user-crabbox-proof.ts +++ b/scripts/e2e/telegram-user-crabbox-proof.ts @@ -13,6 +13,7 @@ import os from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { clampTimerTimeoutMs } from "@openclaw/normalization-core/number-coercion"; +import { sleep } from "../lib/sleep.mjs"; import { resolveWindowsTaskkillPath } from "../lib/windows-taskkill.mjs"; import { createPnpmRunnerSpawnSpec } from "../pnpm-runner.mjs"; import { readPositiveIntEnv } from "./lib/env-limits.mjs"; @@ -995,12 +996,6 @@ function spawnDaemon(params: { return child.pid; } -function sleep(ms: number) { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); -} - function waitForChildExit(child: ChildProcess) { if (child.exitCode !== null || child.signalCode !== null) { return Promise.resolve(child.exitCode); diff --git a/scripts/lib/gateway-bench-child.ts b/scripts/lib/gateway-bench-child.ts index 2fe6c25fa98..5f594dd8f1e 100644 --- a/scripts/lib/gateway-bench-child.ts +++ b/scripts/lib/gateway-bench-child.ts @@ -1,7 +1,10 @@ // Gateway Bench Child script supports OpenClaw repository automation. import { spawnSync, type ChildProcessWithoutNullStreams } from "node:child_process"; +import { sleep as delay } from "./sleep.mjs"; import { resolveWindowsTaskkillPath } from "./windows-taskkill.mjs"; +export { delay }; + const TEARDOWN_GRACE_MS = 2_000; const TEARDOWN_KILL_GRACE_MS = 1_000; const EXIT_POLL_MS = 10; @@ -22,12 +25,6 @@ export type StopChildOptions = { teardownGraceMs?: number; }; -export function delay(ms: number): Promise { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); -} - export async function stopChild( child: ChildProcessWithoutNullStreams, options: StopChildOptions = {}, diff --git a/scripts/lib/sleep.d.mts b/scripts/lib/sleep.d.mts new file mode 100644 index 00000000000..d8654d791a0 --- /dev/null +++ b/scripts/lib/sleep.d.mts @@ -0,0 +1,2 @@ +/** Promise-based sleep that preserves the native global timer contract. */ +export declare function sleep(ms: number): Promise; diff --git a/scripts/lib/sleep.mjs b/scripts/lib/sleep.mjs new file mode 100644 index 00000000000..d43d0a00824 --- /dev/null +++ b/scripts/lib/sleep.mjs @@ -0,0 +1,6 @@ +/** Promise-based sleep that preserves the native global timer contract. */ +export function sleep(ms) { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} diff --git a/scripts/measure-rpc-rtt.mjs b/scripts/measure-rpc-rtt.mjs index 77734fbba2f..24b1a4a8feb 100644 --- a/scripts/measure-rpc-rtt.mjs +++ b/scripts/measure-rpc-rtt.mjs @@ -10,6 +10,7 @@ import path from "node:path"; import { performance } from "node:perf_hooks"; import { fileURLToPath, pathToFileURL } from "node:url"; import { readBoundedResponseText } from "./lib/bounded-response.mjs"; +import { sleep } from "./lib/sleep.mjs"; import { resolveWindowsTaskkillPath } from "./lib/windows-taskkill.mjs"; const DEFAULT_METHODS = ["health", "config.get"]; @@ -139,12 +140,6 @@ async function getFreePort() { }); } -async function sleep(ms) { - await new Promise((resolve) => { - setTimeout(resolve, ms); - }); -} - function formatErrorMessage(error) { if (error instanceof Error) { return error.message; diff --git a/scripts/run-node.mjs b/scripts/run-node.mjs index d7888ee5bb5..f97b9e0bf7a 100644 --- a/scripts/run-node.mjs +++ b/scripts/run-node.mjs @@ -21,6 +21,7 @@ import { writeBuildStamp as writeDistBuildStamp, writeRuntimePostBuildStamp as writeDistRuntimePostBuildStamp, } from "./lib/local-build-metadata.mjs"; +import { sleep } from "./lib/sleep.mjs"; import { discoverStaticExtensionAssets, listStaticExtensionAssetSources, @@ -686,11 +687,6 @@ const parsePositiveIntegerEnv = (env, name, fallback) => { return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback; }; -const sleep = (ms) => - new Promise((resolve) => { - setTimeout(resolve, ms); - }); - const resolveRunNodeOutputLogPath = (deps) => { const outputLog = deps.env[RUN_NODE_OUTPUT_LOG_ENV]?.trim(); if (!outputLog) { diff --git a/scripts/test-docker-all.mjs b/scripts/test-docker-all.mjs index e57ec6f524c..bb595d8518a 100644 --- a/scripts/test-docker-all.mjs +++ b/scripts/test-docker-all.mjs @@ -28,6 +28,7 @@ import { parseProfile, resolveDockerE2ePlan, } from "./lib/docker-e2e-plan.mjs"; +import { sleep } from "./lib/sleep.mjs"; const SCRIPT_ROOT_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const ROOT_DIR = path.resolve(process.env.OPENCLAW_DOCKER_E2E_REPO_ROOT || SCRIPT_ROOT_DIR); @@ -226,12 +227,6 @@ function orderLanes(poolLanes, timingStore) { .map(({ poolLane }) => poolLane); } -function sleep(ms) { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); -} - function utcStampForPath() { return new Date().toISOString().replaceAll("-", "").replaceAll(":", "").replace(/\..*$/, "Z"); } diff --git a/scripts/verify-plugin-npm-published-runtime.mjs b/scripts/verify-plugin-npm-published-runtime.mjs index e991fec055d..c0262098ecb 100644 --- a/scripts/verify-plugin-npm-published-runtime.mjs +++ b/scripts/verify-plugin-npm-published-runtime.mjs @@ -1,13 +1,14 @@ #!/usr/bin/env node - // Verifies published plugin npm packages include built runtime entries and // metadata expected by OpenClaw. + import { execFileSync } from "node:child_process"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { pathToFileURL } from "node:url"; import * as tar from "tar"; +import { sleep } from "./lib/sleep.mjs"; const DEFAULT_NPM_COMMAND_TIMEOUT_MS = 5 * 60 * 1000; const DEFAULT_NPM_COMMAND_MAX_BUFFER_BYTES = 16 * 1024 * 1024; @@ -271,12 +272,6 @@ function npmViewReadme(spec) { return runPluginNpmCommand(["view", spec, "readme", "--json", "--prefer-online"]); } -function sleep(ms) { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); -} - async function packPublishedPackage(spec, destinationDir) { const attempts = readPositiveIntEnv("OPENCLAW_PLUGIN_NPM_VERIFY_ATTEMPTS", 90); const delayMs = readPositiveIntEnv("OPENCLAW_PLUGIN_NPM_VERIFY_DELAY_MS", 10000); diff --git a/scripts/watch-node.mjs b/scripts/watch-node.mjs index dc3419ee9e2..f92086ec0fd 100644 --- a/scripts/watch-node.mjs +++ b/scripts/watch-node.mjs @@ -7,6 +7,7 @@ import fs from "node:fs"; import path from "node:path"; import process from "node:process"; import { pathToFileURL } from "node:url"; +import { sleep } from "./lib/sleep.mjs"; import { isRestartRelevantRunNodePath, runNodeWatchedPaths } from "./run-node-watch-paths.mjs"; const WATCH_NODE_RUNNER = "scripts/run-node.mjs"; @@ -95,11 +96,6 @@ const isProcessAlive = (pid, signalProcess) => { return true; }; -const sleep = (ms) => - new Promise((resolve) => { - setTimeout(resolve, ms); - }); - const createWatchLockKey = (cwd, args) => createHash("sha256").update(cwd).update("\0").update(args.join("\0")).digest("hex").slice(0, 12); diff --git a/src/cli/cron-cli/register.cron-simple.ts b/src/cli/cron-cli/register.cron-simple.ts index a812bcf2b8d..6d4ee8ac0b1 100644 --- a/src/cli/cron-cli/register.cron-simple.ts +++ b/src/cli/cron-cli/register.cron-simple.ts @@ -8,6 +8,7 @@ import type { Command } from "commander"; import type { CronDeliveryPreview, CronJob } from "../../cron/types.js"; import { parseStrictPositiveInteger } from "../../infra/parse-finite-number.js"; import { defaultRuntime } from "../../runtime.js"; +import { sleep } from "../../utils/sleep.js"; import type { GatewayRpcOpts } from "../gateway-rpc.js"; import { addGatewayClientOptions, callGatewayFromCli } from "../gateway-rpc.js"; import { parseDurationMs } from "../parse-duration.js"; @@ -36,12 +37,6 @@ type CronRunLogEntryResult = { status?: "ok" | "error" | "skipped"; }; -function sleep(ms: number): Promise { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); -} - function parseCronRunWaitDuration(raw: unknown, label: string): number { const input = typeof raw === "string" || typeof raw === "number" || typeof raw === "bigint" diff --git a/src/commands/doctor-whatsapp-responsiveness.ts b/src/commands/doctor-whatsapp-responsiveness.ts index d9330473614..1cce56a766e 100644 --- a/src/commands/doctor-whatsapp-responsiveness.ts +++ b/src/commands/doctor-whatsapp-responsiveness.ts @@ -5,6 +5,7 @@ import { note } from "../../packages/terminal-core/src/note.js"; import { formatCliCommand } from "../cli/command-format.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import type { HealthFinding } from "../flows/health-checks.js"; +import { sleep } from "../utils/sleep.js"; import type { StatusSummary } from "./status.types.js"; type LocalTuiProcess = { @@ -141,12 +142,6 @@ function isProcessAlive(controller: ProcessController, pid: number): boolean { } } -async function sleep(ms: number): Promise { - await new Promise((resolve) => { - setTimeout(resolve, ms); - }); -} - /** Terminates local TUI processes with SIGTERM, then SIGKILL for remaining pids. */ export async function terminateLocalTuiProcesses(params: { processes: LocalTuiProcess[]; diff --git a/src/config/io.clobber-snapshot.ts b/src/config/io.clobber-snapshot.ts index c10f2de2bdb..b799534d116 100644 --- a/src/config/io.clobber-snapshot.ts +++ b/src/config/io.clobber-snapshot.ts @@ -1,5 +1,6 @@ // Detects suspicious config clobbers and finds recovery snapshots. import path from "node:path"; +import { sleep } from "../utils/sleep.js"; /** Maximum retained clobbered-config snapshots per config file. */ const CONFIG_CLOBBER_SNAPSHOT_LIMIT = 32; @@ -52,12 +53,6 @@ function isFsErrorCode(error: unknown, code: string): boolean { ); } -function sleep(ms: number): Promise { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); -} - function resolveClobberPaths(configPath: string): { dir: string; prefix: string; diff --git a/src/gateway/agent-command.test-helpers.ts b/src/gateway/agent-command.test-helpers.ts index a90f2011ae2..08275d50871 100644 --- a/src/gateway/agent-command.test-helpers.ts +++ b/src/gateway/agent-command.test-helpers.ts @@ -1,6 +1,7 @@ // Gateway agent-command test helpers. // Waits for mocked agent command dispatches in async gateway tests. import { vi } from "vitest"; +import { sleep } from "../utils/sleep.js"; import { agentCommand } from "./test-helpers.runtime-state.js"; type AgentCommandCall = Record; @@ -9,11 +10,6 @@ function agentCommandCalls(): Array<[AgentCommandCall]> { return vi.mocked(agentCommand).mock.calls as unknown as Array<[AgentCommandCall]>; } -const sleep = (ms: number) => - new Promise((resolve) => { - setTimeout(resolve, ms); - }); - /** Waits until the mocked `agentCommand` receives a call for a specific run id. */ export async function waitForAgentCommandCall(runId: string): Promise { for (let elapsed = 0; elapsed <= 2_000; elapsed += 5) { diff --git a/src/gateway/gateway-cli-backend.live-helpers.ts b/src/gateway/gateway-cli-backend.live-helpers.ts index 432cdbdef9c..ad6999a7b47 100644 --- a/src/gateway/gateway-cli-backend.live-helpers.ts +++ b/src/gateway/gateway-cli-backend.live-helpers.ts @@ -23,6 +23,7 @@ import { import { isTruthyEnvValue } from "../infra/env.js"; import { getFreePortBlockWithPermissionFallback } from "../test-utils/ports.js"; import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js"; +import { sleep } from "../utils/sleep.js"; import { startGatewayClientWhenEventLoopReady } from "./client-start-readiness.js"; import { GatewayClient, type GatewayClientOptions } from "./client.js"; @@ -311,12 +312,6 @@ export async function createBootstrapWorkspace( return { expectedInjectedFiles, workspaceDir, workspaceRootDir }; } -function sleep(ms: number): Promise { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); -} - export function shouldRetryCliCronMcpProbeReply(text: string): boolean { const normalized = normalizeLowercaseStringOrEmpty(text); if (!normalized) { diff --git a/src/gateway/gateway-cli-backend.live-probe-helpers.ts b/src/gateway/gateway-cli-backend.live-probe-helpers.ts index d67e16b464a..b4891d0fda8 100644 --- a/src/gateway/gateway-cli-backend.live-probe-helpers.ts +++ b/src/gateway/gateway-cli-backend.live-probe-helpers.ts @@ -5,6 +5,7 @@ import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/st import { renderCatFacePngBase64 } from "../../test/helpers/live-image-probe.js"; import { isTruthyEnvValue } from "../infra/env.js"; import { parseStrictPositiveInteger } from "../infra/parse-finite-number.js"; +import { sleep } from "../utils/sleep.js"; import type { GatewayClient } from "./client.js"; import { shouldRetryCliCronMcpProbeReply, @@ -45,12 +46,6 @@ function logCliCronProbe(step: string, details?: Record): void console.error(`[gateway-cli-live:cron] ${step}${suffix}`); } -function sleep(ms: number): Promise { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); -} - async function pollCliCronJobVisible(params: { port: number; token: string; diff --git a/src/plugin-sdk/runtime-env.ts b/src/plugin-sdk/runtime-env.ts index a754f7209f2..7a80f70d750 100644 --- a/src/plugin-sdk/runtime-env.ts +++ b/src/plugin-sdk/runtime-env.ts @@ -16,7 +16,7 @@ export { success, warn, } from "../globals.js"; -export { sleep } from "../utils.js"; +export { sleep } from "../utils/sleep.js"; export { withTimeout } from "../utils/with-timeout.js"; export { isTruthyEnvValue } from "../infra/env.js"; export * from "../logging.js"; diff --git a/src/talk/fast-context-runtime.test.ts b/src/talk/fast-context-runtime.test.ts index d5e8a1cc32a..1b358f4f318 100644 --- a/src/talk/fast-context-runtime.test.ts +++ b/src/talk/fast-context-runtime.test.ts @@ -14,6 +14,7 @@ import { resolveRealtimeVoiceFastContextConsult } from "./fast-context-runtime.j describe("resolveRealtimeVoiceFastContextConsult", () => { afterEach(() => { + vi.useRealTimers(); vi.restoreAllMocks(); mocks.getActiveMemorySearchManager.mockReset(); }); @@ -45,4 +46,37 @@ describe("resolveRealtimeVoiceFastContextConsult", () => { expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), MAX_TIMER_TIMEOUT_MS); }); + + it("preserves the fast-context timeout error and clears the timer", async () => { + vi.useFakeTimers(); + const logger = { debug: vi.fn() }; + mocks.getActiveMemorySearchManager.mockResolvedValue({ + manager: { + search: vi.fn(() => new Promise(() => {})), + }, + }); + + const result = resolveRealtimeVoiceFastContextConsult({ + cfg: {}, + agentId: "main", + sessionKey: "voice:15550001234", + config: { + enabled: true, + timeoutMs: 25, + maxResults: 3, + sources: ["memory", "sessions"], + fallbackToConsult: true, + }, + args: { question: "What do you remember?" }, + logger, + }); + + await vi.advanceTimersByTimeAsync(25); + + await expect(result).resolves.toEqual({ handled: false }); + expect(logger.debug).toHaveBeenCalledWith( + "[talk] fast context lookup failed: fast context lookup timed out after 25ms", + ); + expect(vi.getTimerCount()).toBe(0); + }); }); diff --git a/src/talk/fast-context-runtime.ts b/src/talk/fast-context-runtime.ts index 908310073d9..32412a1ed8c 100644 --- a/src/talk/fast-context-runtime.ts +++ b/src/talk/fast-context-runtime.ts @@ -9,6 +9,7 @@ import { resolveTimerTimeoutMs } from "@openclaw/normalization-core/number-coerc import type { OpenClawConfig } from "../config/types.openclaw.js"; import { formatErrorMessage } from "../infra/errors.js"; import { getActiveMemorySearchManager } from "../plugins/memory-runtime.js"; +import { withTimeout } from "../utils/with-timeout.js"; import type { RealtimeVoiceAgentConsultResult } from "./agent-consult-runtime.js"; import { parseRealtimeVoiceAgentConsultArgs } from "./agent-consult-tool.js"; @@ -112,28 +113,6 @@ function buildMissText(query: string, labels: RealtimeVoiceFastContextLabels): s ].join("\n\n"); } -async function withTimeout(promise: Promise, timeoutMs: number): Promise { - const resolvedTimeoutMs = resolveTimerTimeoutMs(timeoutMs, 1); - let timer: ReturnType | undefined; - try { - return await Promise.race([ - promise, - new Promise((_resolve, reject) => { - // resolveTimerTimeoutMs caps huge configured deadlines before they - // reach Node's timer APIs. - timer = setTimeout( - () => reject(new RealtimeFastContextTimeoutError(resolvedTimeoutMs)), - resolvedTimeoutMs, - ); - }), - ]); - } finally { - if (timer) { - clearTimeout(timer); - } - } -} - async function lookupFastContext(params: { cfg: OpenClawConfig; agentId: string; @@ -178,6 +157,7 @@ export async function resolveRealtimeVoiceFastContextConsult(params: { const labels = resolveLabels(params.labels); const query = buildSearchQuery(params.args); try { + const timeoutMs = resolveTimerTimeoutMs(params.config.timeoutMs, 1); const lookup = await withTimeout( lookupFastContext({ cfg: params.cfg, @@ -186,7 +166,8 @@ export async function resolveRealtimeVoiceFastContextConsult(params: { config: params.config, query, }), - params.config.timeoutMs, + timeoutMs, + { createError: () => new RealtimeFastContextTimeoutError(timeoutMs) }, ); if (lookup.status === "unavailable") { params.logger.debug?.(`[talk] fast context unavailable: ${lookup.error}`); diff --git a/src/tui/gateway-chat.ts b/src/tui/gateway-chat.ts index ad22981c9ea..19c6a467467 100644 --- a/src/tui/gateway-chat.ts +++ b/src/tui/gateway-chat.ts @@ -28,6 +28,7 @@ import { startGatewayClientWhenEventLoopReady } from "../gateway/client-start-re import { GatewayClient, GatewayClientRequestError } from "../gateway/client.js"; import { isLoopbackHost } from "../gateway/net.js"; import { formatErrorMessage } from "../infra/errors.js"; +import { sleep } from "../utils/sleep.js"; import { VERSION } from "../version.js"; import { TUI_SETUP_AUTH_SOURCE_CONFIG, TUI_SETUP_AUTH_SOURCE_ENV } from "./setup-launch-env.js"; import type { @@ -95,12 +96,6 @@ function resolveStartupRetryDelayMs(err: GatewayClientRequestError): number { return Math.min(Math.max(retryAfterMs, 100), STARTUP_CHAT_HISTORY_MAX_RETRY_MS); } -function sleep(ms: number): Promise { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); -} - function nonEmptyString(value: unknown): string | undefined { return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined; } diff --git a/src/utils.ts b/src/utils.ts index dc92cb0c3aa..7ec5c58f269 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -9,8 +9,8 @@ import { resolveRequiredHomeDir, } from "./infra/home-dir.js"; import { isPlainObject } from "./infra/plain-object.js"; -import { resolveTimerTimeoutMs } from "./shared/number-coercion.js"; export { escapeRegExp } from "./shared/regexp.js"; +export { sleep } from "./utils/sleep.js"; /** Creates a directory tree if it does not already exist. */ export async function ensureDir(dir: string) { @@ -62,13 +62,6 @@ export function normalizeE164(number: string): string { return `+${digits}`; } -/** Promise-based sleep that clamps timer inputs through the shared timeout resolver. */ -export function sleep(ms: number) { - return new Promise((resolve) => { - setTimeout(resolve, resolveTimerTimeoutMs(ms, 0, 0)); - }); -} - // Surrogate-safe slicing helpers live in a node-free leaf module so browser/UI // bundles can import them without pulling in filesystem code. Re-exported here // to preserve the historical `utils.ts` import surface. diff --git a/src/utils/sleep.ts b/src/utils/sleep.ts new file mode 100644 index 00000000000..3280f2d744b --- /dev/null +++ b/src/utils/sleep.ts @@ -0,0 +1,8 @@ +import { resolveTimerTimeoutMs } from "../shared/number-coercion.js"; + +/** Promise-based sleep that clamps timer inputs through the shared timeout resolver. */ +export function sleep(ms: number): Promise { + return new Promise((resolve) => { + setTimeout(resolve, resolveTimerTimeoutMs(ms, 0, 0)); + }); +} diff --git a/test/scripts/sleep.test.ts b/test/scripts/sleep.test.ts new file mode 100644 index 00000000000..73e4f8a7744 --- /dev/null +++ b/test/scripts/sleep.test.ts @@ -0,0 +1,22 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { sleep } from "../../scripts/lib/sleep.mjs"; + +describe("scripts/lib/sleep.mjs", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it.each([0, -1, Number.NaN, Number.POSITIVE_INFINITY])( + "preserves the native global timer delay for %s", + async (delayMs) => { + const setTimeoutSpy = vi.spyOn(globalThis, "setTimeout").mockImplementation((callback) => { + queueMicrotask(() => callback()); + return 0 as unknown as ReturnType; + }); + + await expect(sleep(delayMs)).resolves.toBeUndefined(); + + expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), delayMs); + }, + ); +});