diff --git a/scripts/bench-gateway-restart.ts b/scripts/bench-gateway-restart.ts index 937ad3da8a0..317114483f3 100644 --- a/scripts/bench-gateway-restart.ts +++ b/scripts/bench-gateway-restart.ts @@ -8,6 +8,7 @@ import path from "node:path"; import { performance } from "node:perf_hooks"; import { pathToFileURL } from "node:url"; import { parseStrictIntegerOption } from "./lib/dev-tooling-safety.ts"; +import { delay, stopChild, type StopChildResult } from "./lib/gateway-bench-child.ts"; type GatewayBenchCase = { config: Record; @@ -68,15 +69,6 @@ type GatewayRestartFailureCode = | "child_nonzero_exit" | "cleanup_failed"; -type ChildExit = { - exitCode: number | null; - signal: string | null; -}; - -type StopChildResult = ChildExit & { - exitedBeforeTeardown: boolean; -}; - type RestartIteration = { cpuCoreRatio: number | null; cpuMs: number | null; @@ -173,8 +165,6 @@ const DEFAULT_TIMEOUT_MS = 30_000; const DEFAULT_POST_READY_DELAY_MS = 250; const DEFAULT_ENTRY = "dist/entry.js"; const RESTART_INTENT_FILENAME = "gateway-restart-intent.json"; -const TEARDOWN_GRACE_MS = 2_000; -const TEARDOWN_KILL_GRACE_MS = 1_000; const BASE_CONFIG = { browser: { enabled: false }, @@ -769,10 +759,6 @@ function requestStatus(port: number, pathname: string): Promise { }); } -function delay(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - function writePluginFixtures( root: string, count: number, @@ -881,83 +867,6 @@ function writeRestartIntent(env: NodeJS.ProcessEnv, targetPid: number, reason: s } } -async function stopChild( - child: ChildProcessWithoutNullStreams, - options: { killGraceMs?: number; teardownGraceMs?: number } = {}, -): Promise { - const currentExit = (): ChildExit | null => - child.exitCode != null || child.signalCode != null - ? { exitCode: child.exitCode, signal: child.signalCode } - : null; - - const existingExit = currentExit(); - if (existingExit != null) { - return { ...existingExit, exitedBeforeTeardown: true }; - } - - let observedExit: ChildExit | null = null; - const exited = new Promise((resolve) => { - child.once("exit", (exitCode, signal) => { - observedExit = { exitCode, signal }; - resolve(observedExit); - }); - }); - const waitForExit = async (ms: number): Promise => - await Promise.race([exited, delay(ms).then(() => null)]); - - await new Promise((resolve) => setImmediate(resolve)); - const queuedExit = observedExit ?? currentExit(); - if (queuedExit != null) { - return { ...queuedExit, exitedBeforeTeardown: true }; - } - - const teardownGraceMs = options.teardownGraceMs ?? TEARDOWN_GRACE_MS; - const killGraceMs = options.killGraceMs ?? TEARDOWN_KILL_GRACE_MS; - const sentTeardownSignal = killProcessTree(child, "SIGTERM"); - const gracefulExit = await waitForExit(teardownGraceMs); - if (gracefulExit != null) { - return { ...gracefulExit, exitedBeforeTeardown: !sentTeardownSignal }; - } - - const postGraceExit = currentExit() ?? observedExit; - if (postGraceExit != null) { - return { ...postGraceExit, exitedBeforeTeardown: !sentTeardownSignal }; - } - if (!sentTeardownSignal) { - releaseUnsettledChild(child); - return { exitCode: null, exitedBeforeTeardown: true, signal: null }; - } - - killProcessTree(child, "SIGKILL"); - const killedExit = await waitForExit(killGraceMs); - const finalExit = killedExit ?? currentExit() ?? observedExit; - if (finalExit != null) { - return { ...finalExit, exitedBeforeTeardown: false }; - } - - releaseUnsettledChild(child); - return { exitCode: null, exitedBeforeTeardown: false, signal: "SIGKILL" }; -} - -function releaseUnsettledChild(child: ChildProcessWithoutNullStreams): void { - child.stdin.destroy(); - child.stdout.destroy(); - child.stderr.destroy(); - child.unref(); -} - -function killProcessTree(child: ChildProcessWithoutNullStreams, signal: NodeJS.Signals): boolean { - if (process.platform !== "win32" && child.pid !== undefined) { - try { - process.kill(-child.pid, signal); - return true; - } catch { - // Fall back to the direct child below. - } - } - return child.kill(signal); -} - function readProcessRssMb(pid: number | undefined): number | null { if (!pid || process.platform === "win32") { return null; diff --git a/scripts/bench-gateway-startup.ts b/scripts/bench-gateway-startup.ts index 46c832cceb1..acf6f3fa315 100644 --- a/scripts/bench-gateway-startup.ts +++ b/scripts/bench-gateway-startup.ts @@ -1,4 +1,4 @@ -import { spawn, spawnSync, type ChildProcessWithoutNullStreams } from "node:child_process"; +import { spawn, spawnSync } from "node:child_process"; import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { request } from "node:http"; import { createServer } from "node:net"; @@ -7,6 +7,7 @@ import path from "node:path"; import { performance } from "node:perf_hooks"; import { pathToFileURL } from "node:url"; import { parseStrictIntegerOption } from "./lib/dev-tooling-safety.ts"; +import { delay, stopChild } from "./lib/gateway-bench-child.ts"; type GatewayBenchCase = { config: Record; @@ -80,15 +81,6 @@ type BenchmarkFailure = { sampleIndex: number; }; -type ChildExit = { - exitCode: number | null; - signal: string | null; -}; - -type StopChildResult = ChildExit & { - exitedBeforeTeardown: boolean; -}; - type PluginFixtureResult = { pluginIds: string[]; pluginsDir: string; @@ -109,8 +101,6 @@ const DEFAULT_RUNS = 5; const DEFAULT_WARMUP = 1; const DEFAULT_TIMEOUT_MS = 30_000; const DEFAULT_ENTRY = "dist/entry.js"; -const TEARDOWN_GRACE_MS = 2_000; -const TEARDOWN_KILL_GRACE_MS = 1_000; const BASE_CONFIG = { browser: { enabled: false }, @@ -624,10 +614,6 @@ function requestStatus(port: number, pathname: string): Promise { }); } -function delay(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - function writePluginFixtures( root: string, count: number, @@ -710,83 +696,6 @@ function sanitizedEnv( return env; } -async function stopChild( - child: ChildProcessWithoutNullStreams, - options: { killGraceMs?: number; teardownGraceMs?: number } = {}, -): Promise { - const currentExit = (): ChildExit | null => - child.exitCode != null || child.signalCode != null - ? { exitCode: child.exitCode, signal: child.signalCode } - : null; - - const existingExit = currentExit(); - if (existingExit != null) { - return { ...existingExit, exitedBeforeTeardown: true }; - } - - let observedExit: ChildExit | null = null; - const exited = new Promise((resolve) => { - child.once("exit", (exitCode, signal) => { - observedExit = { exitCode, signal }; - resolve(observedExit); - }); - }); - const waitForExit = async (ms: number): Promise => - await Promise.race([exited, delay(ms).then(() => null)]); - - await new Promise((resolve) => setImmediate(resolve)); - const queuedExit = observedExit ?? currentExit(); - if (queuedExit != null) { - return { ...queuedExit, exitedBeforeTeardown: true }; - } - - const teardownGraceMs = options.teardownGraceMs ?? TEARDOWN_GRACE_MS; - const killGraceMs = options.killGraceMs ?? TEARDOWN_KILL_GRACE_MS; - const sentTeardownSignal = killProcessTree(child, "SIGTERM"); - const gracefulExit = await waitForExit(teardownGraceMs); - if (gracefulExit != null) { - return { ...gracefulExit, exitedBeforeTeardown: !sentTeardownSignal }; - } - - const postGraceExit = currentExit() ?? observedExit; - if (postGraceExit != null) { - return { ...postGraceExit, exitedBeforeTeardown: !sentTeardownSignal }; - } - if (!sentTeardownSignal) { - releaseUnsettledChild(child); - return { exitCode: null, exitedBeforeTeardown: true, signal: null }; - } - - killProcessTree(child, "SIGKILL"); - const killedExit = await waitForExit(killGraceMs); - const finalExit = killedExit ?? currentExit() ?? observedExit; - if (finalExit != null) { - return { ...finalExit, exitedBeforeTeardown: false }; - } - - releaseUnsettledChild(child); - return { exitCode: null, exitedBeforeTeardown: false, signal: "SIGKILL" }; -} - -function releaseUnsettledChild(child: ChildProcessWithoutNullStreams): void { - child.stdin.destroy(); - child.stdout.destroy(); - child.stderr.destroy(); - child.unref(); -} - -function killProcessTree(child: ChildProcessWithoutNullStreams, signal: NodeJS.Signals): boolean { - if (process.platform !== "win32" && child.pid !== undefined) { - try { - process.kill(-child.pid, signal); - return true; - } catch { - // Fall back to the direct child below. - } - } - return child.kill(signal); -} - function collectStartupTrace(line: string, startupTrace: Record): void { const phaseMatch = /startup trace: ([^ ]+) ([0-9.]+)ms total=([0-9.]+)ms(?: (.*))?/u.exec(line); if (phaseMatch) { diff --git a/scripts/lib/gateway-bench-child.ts b/scripts/lib/gateway-bench-child.ts new file mode 100644 index 00000000000..b5ac7387dc5 --- /dev/null +++ b/scripts/lib/gateway-bench-child.ts @@ -0,0 +1,94 @@ +import type { ChildProcessWithoutNullStreams } from "node:child_process"; + +const TEARDOWN_GRACE_MS = 2_000; +const TEARDOWN_KILL_GRACE_MS = 1_000; + +export type ChildExit = { + exitCode: number | null; + signal: string | null; +}; + +export type StopChildResult = ChildExit & { + exitedBeforeTeardown: boolean; +}; + +export function delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +export async function stopChild( + child: ChildProcessWithoutNullStreams, + options: { killGraceMs?: number; teardownGraceMs?: number } = {}, +): Promise { + const currentExit = (): ChildExit | null => + child.exitCode != null || child.signalCode != null + ? { exitCode: child.exitCode, signal: child.signalCode } + : null; + + const existingExit = currentExit(); + if (existingExit != null) { + return { ...existingExit, exitedBeforeTeardown: true }; + } + + let observedExit: ChildExit | null = null; + const exited = new Promise((resolve) => { + child.once("exit", (exitCode, signal) => { + observedExit = { exitCode, signal }; + resolve(observedExit); + }); + }); + const waitForExit = async (ms: number): Promise => + await Promise.race([exited, delay(ms).then(() => null)]); + + await new Promise((resolve) => setImmediate(resolve)); + const queuedExit = observedExit ?? currentExit(); + if (queuedExit != null) { + return { ...queuedExit, exitedBeforeTeardown: true }; + } + + const teardownGraceMs = options.teardownGraceMs ?? TEARDOWN_GRACE_MS; + const killGraceMs = options.killGraceMs ?? TEARDOWN_KILL_GRACE_MS; + const sentTeardownSignal = killProcessTree(child, "SIGTERM"); + const gracefulExit = await waitForExit(teardownGraceMs); + if (gracefulExit != null) { + return { ...gracefulExit, exitedBeforeTeardown: !sentTeardownSignal }; + } + + const postGraceExit = currentExit() ?? observedExit; + if (postGraceExit != null) { + return { ...postGraceExit, exitedBeforeTeardown: !sentTeardownSignal }; + } + if (!sentTeardownSignal) { + releaseUnsettledChild(child); + return { exitCode: null, exitedBeforeTeardown: true, signal: null }; + } + + killProcessTree(child, "SIGKILL"); + const killedExit = await waitForExit(killGraceMs); + const finalExit = killedExit ?? currentExit() ?? observedExit; + if (finalExit != null) { + return { ...finalExit, exitedBeforeTeardown: false }; + } + + releaseUnsettledChild(child); + return { exitCode: null, exitedBeforeTeardown: false, signal: "SIGKILL" }; +} + +function releaseUnsettledChild(child: ChildProcessWithoutNullStreams): void { + child.stdin.destroy(); + child.stdout.destroy(); + child.stderr.destroy(); + child.unref(); +} + +function killProcessTree(child: ChildProcessWithoutNullStreams, signal: NodeJS.Signals): boolean { + if (process.platform !== "win32" && child.pid !== undefined) { + try { + process.kill(-child.pid, signal); + return true; + } catch { + // Fall back to the direct child below. + } + } + return child.kill(signal); +} diff --git a/src/plugin-sdk/approval-reaction-runtime.ts b/src/plugin-sdk/approval-reaction-runtime.ts index 749d02c5f50..92fb1bb898d 100644 --- a/src/plugin-sdk/approval-reaction-runtime.ts +++ b/src/plugin-sdk/approval-reaction-runtime.ts @@ -4,14 +4,11 @@ */ import { sanitizeForPromptLiteral } from "../agents/sanitize-for-prompt.js"; import { formatApprovalDisplayPath } from "../infra/approval-display-paths.js"; -import { matchesApprovalRequestFilters } from "../infra/approval-request-filters.js"; import { buildPendingApprovalView } from "../infra/approval-view-model.js"; import type { ApprovalRequest, PendingApprovalView } from "../infra/approval-view-model.types.js"; import { buildExecApprovalPendingReplyPayload, formatExecApprovalExpiresIn, - getExecApprovalReplyMetadata, - type ExecApprovalReplyMetadata, type ExecApprovalPendingReplyParams, type ExecApprovalReplyDecision, } from "../infra/exec-approval-reply.js"; @@ -20,8 +17,7 @@ import { buildApprovalPendingReplyPayload, buildPluginApprovalPendingReplyPayload, } from "./approval-renderers.js"; -import type { ChannelOutboundPayloadHint } from "./channel-contract.js"; -import type { OpenClawConfig } from "./config-runtime.js"; +export { shouldSuppressLocalNativeExecApprovalPrompt } from "./approval-native-helpers.js"; import type { ReplyPayload } from "./reply-payload.js"; type ApprovalKind = "exec" | "plugin"; @@ -30,12 +26,6 @@ type KeyedStore = { lookup(key: string): Promise; delete(key: string): Promise; }; -type LocalNativeExecApprovalConfig = { - enabled?: boolean | "auto"; - mode?: string | null; - agentFilter?: string[]; - sessionFilter?: string[]; -}; type PersistedApprovalReactionTarget = { version: 1; @@ -107,82 +97,6 @@ function normalizeDecisionList( return APPROVAL_REACTION_ORDER.filter((decision) => allowed.has(decision)); } -export function shouldSuppressLocalNativeExecApprovalPrompt(params: { - cfg: OpenClawConfig; - accountId?: string | null; - payload: ReplyPayload; - hint?: ChannelOutboundPayloadHint; - isTransportEnabled?: (params: { cfg: OpenClawConfig; accountId?: string | null }) => boolean; - isNativeDeliveryEnabled?: (params: { cfg: OpenClawConfig; accountId?: string | null }) => boolean; - resolveApprovalConfig?: (params: { - cfg: OpenClawConfig; - accountId?: string | null; - metadata: ExecApprovalReplyMetadata; - }) => LocalNativeExecApprovalConfig | undefined; - requireApprovalConfigEnabled?: boolean; - enforceForwardingMode?: boolean; - isSessionRouteEligible?: (params: { - cfg: OpenClawConfig; - accountId?: string | null; - metadata: ExecApprovalReplyMetadata; - }) => boolean; - hasExactTargetProof?: boolean; - fallbackAgentIdFromSessionKey?: boolean; -}): boolean { - if (params.hint?.kind !== "approval-pending" || params.hint.approvalKind !== "exec") { - return false; - } - if (params.hint.nativeRouteActive !== true) { - return false; - } - const metadata = getExecApprovalReplyMetadata(params.payload); - if (!metadata || metadata.approvalKind !== "exec") { - return false; - } - const isDeliveryEnabled = params.isNativeDeliveryEnabled ?? params.isTransportEnabled; - if (!isDeliveryEnabled?.({ cfg: params.cfg, accountId: params.accountId })) { - return false; - } - const config = - params.resolveApprovalConfig?.({ - cfg: params.cfg, - accountId: params.accountId, - metadata, - }) ?? params.cfg.approvals?.exec; - const requireConfigEnabled = - params.requireApprovalConfigEnabled ?? params.resolveApprovalConfig === undefined; - if (requireConfigEnabled && !config?.enabled) { - return false; - } - const enforceForwardingMode = - params.enforceForwardingMode ?? params.resolveApprovalConfig === undefined; - if (enforceForwardingMode) { - const mode = config?.mode ?? "session"; - if (mode !== "session" && mode !== "both" && !params.hasExactTargetProof) { - return false; - } - } - if ( - params.isSessionRouteEligible && - !params.isSessionRouteEligible({ - cfg: params.cfg, - accountId: params.accountId, - metadata, - }) - ) { - return false; - } - return matchesApprovalRequestFilters({ - request: { - agentId: metadata.agentId, - sessionKey: metadata.sessionKey, - }, - agentFilter: config?.agentFilter, - sessionFilter: config?.sessionFilter, - fallbackAgentIdFromSessionKey: params.fallbackAgentIdFromSessionKey ?? true, - }); -} - export function listApprovalReactionBindings(params: { allowedDecisions: readonly ExecApprovalReplyDecision[]; }): ApprovalReactionDecisionBinding[] {