improve: doctor migration checks no longer load every bundled plugin runtime (#120678)

* perf(plugins): declare doctor contract surfaces

* perf(doctor): slim migration import closures

* perf(plugins): narrow doctor declaration record surface and wire owner-test lane

Registry records carry only the doctorContract declaration instead of the whole
parsed manifest, and check:changed now selects the src/plugins-owned declaration
honesty and closure-guard tests for extension module/manifest changes so
cross-lane drift cannot pass PR classification.

* fix(doctor): keep control-plane dist imports require-safe

Keep doctor and channel control-plane chunks off exec-class dependencies, and enforce native require(esm) loading during postbuild.

* chore(plugin-sdk): regenerate API baseline

* chore(plugin-sdk): sync export ordering

* fix(plugins): satisfy doctor contract CI boundaries

* perf(doctor): make qqbot doctor closure dependency-light

qqbot was the last plugin above 5s in doctor state-migration enumeration
(~8s under tsx/jiti). The cost was not the state-key builder (already a
leaf): its doctor closure value-imported the runtime-doctor SDK barrel,
whose plugin-state-store/state-db re-exports pull kysely (~330 modules),
plus security-runtime for one fileExists (~200 modules), all resolved
per-module by jiti during enumeration.

Split the migration-define helpers and light re-exports into a new
private-local plugin-sdk/runtime-doctor-migrations subpath; runtime-doctor
re-exports it so its public surface is byte-identical (API baseline hash
unchanged). qqbot's doctor-contract and state-migrations now import only
the light subpath, swapping fileExists for the equivalent async
legacyStateFileExists already in the closure.

qqbot enumeration: ~8.0s/531 modules -> ~0.25s/18 modules.

* chore(plugin-sdk): drop private-local subpath from API baseline

runtime-doctor-migrations is private-local-only; the baseline tracks public
modules, and the earlier line was generated before the classification.

* fix(plugins): register runtime-doctor-migrations boundary paths

The private-local subpath list feeds the extension package boundary map;
the shared paths config and xai's derived overrides must carry the same
entry or the boundary contract test fails.
This commit is contained in:
Peter Steinberger
2026-08-08 13:29:18 -07:00
committed by GitHub
parent bb23b6b5ca
commit da4a656cdb
117 changed files with 2082 additions and 951 deletions
+3 -1
View File
@@ -7,7 +7,9 @@ import type { OpenClawConfig } from "../../config/types.openclaw.js";
import type { PluginHealthErrorSummary } from "../../gateway/health/types.js";
import { resolveGatewayProbeAuthSafeWithSecretInputs } from "../../gateway/probe-auth.js";
import { probeGateway } from "../../gateway/probe.js";
import { inspectPortUsage, LOOPBACK_PORT_PROBE_HOSTS, type PortUsage } from "../../infra/ports.js";
import { inspectPortUsage } from "../../infra/ports-inspect.js";
import { LOOPBACK_PORT_PROBE_HOSTS } from "../../infra/ports-probe.js";
import type { PortUsage } from "../../infra/ports-types.js";
import type { GatewayPortHealthSnapshot } from "./restart-health.types.js";
import { allListenersOwnedByRuntimePid } from "./restart-port-ownership.js";
@@ -1,9 +1,11 @@
import { vi } from "vitest";
import type { GatewayService } from "../../daemon/service.js";
import type { GatewayLockIdentity } from "../../infra/gateway-lock.js";
import type { PortUsage } from "../../infra/ports.js";
import type { PortUsage } from "../../infra/ports-types.js";
type PortListenerKind = ReturnType<typeof import("../../infra/ports.js").classifyPortListener>;
type PortListenerKind = ReturnType<
typeof import("../../infra/ports-format.js").classifyPortListener
>;
export const inspectPortUsage =
vi.fn<(port: number, options?: { probeHosts?: readonly string[] }) => Promise<PortUsage>>();
@@ -26,11 +28,17 @@ export const resolveGatewayServiceProbeHosts = vi.fn<
(_params?: unknown) => Promise<readonly string[]>
>(async () => ["127.0.0.1"]);
vi.mock("../../infra/ports.js", () => ({
vi.mock("../../infra/ports-format.js", () => ({
classifyPortListener: (listener: unknown, port: number) => classifyPortListener(listener, port),
formatPortDiagnostics: vi.fn(() => []),
}));
vi.mock("../../infra/ports-inspect.js", () => ({
inspectPortUsage: (port: number, options?: { probeHosts?: readonly string[] }) =>
inspectPortUsage(port, options),
}));
vi.mock("../../infra/ports-probe.js", () => ({
LOOPBACK_PORT_PROBE_HOSTS: ["127.0.0.1"],
}));
+3 -1
View File
@@ -4,7 +4,9 @@ import { resolveGatewayServiceProbeHosts } from "../../daemon/gateway-service-pr
import type { GatewayServiceRuntime } from "../../daemon/service-runtime.js";
import type { GatewayService } from "../../daemon/service.js";
import type { PluginHealthErrorSummary } from "../../gateway/health/types.js";
import { classifyPortListener, inspectPortUsage, type PortUsage } from "../../infra/ports.js";
import { classifyPortListener } from "../../infra/ports-format.js";
import { inspectPortUsage } from "../../infra/ports-inspect.js";
import type { PortUsage } from "../../infra/ports-types.js";
import {
hasActiveStartupMigrationLease,
STARTUP_MIGRATION_LEASE_TTL_MS,
+6 -3
View File
@@ -5,7 +5,7 @@ import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { StaleOpenClawUpdateLaunchdJob } from "../../daemon/launchd.js";
import { createMockGatewayService } from "../../daemon/service.test-helpers.js";
import type { PortListener, PortUsageStatus } from "../../infra/ports.js";
import type { PortListener, PortUsageStatus } from "../../infra/ports-types.js";
import type { GatewayRestartHandoff } from "../../infra/restart-handoff.js";
import { defaultRuntime } from "../../runtime.js";
import { captureEnv, deleteTestEnvValue, setTestEnvValue } from "../../test-utils/env.js";
@@ -15,7 +15,7 @@ import { gatherDaemonStatus } from "./status.gather.js";
import { printDaemonStatus } from "./status.print.js";
type PortConnections = Awaited<
ReturnType<typeof import("../../infra/ports.js").inspectPortConnections>
ReturnType<typeof import("../../infra/ports-inspect.js").inspectPortConnections>
>;
const callGatewayStatusProbe = vi.fn<
@@ -270,7 +270,7 @@ vi.mock("../../gateway/probe-auth.js", async (importOriginal) => {
};
});
vi.mock("../../infra/ports.js", () => ({
vi.mock("../../infra/ports-inspect.js", () => ({
inspectPortConnections: (port: number) => inspectPortConnections(port),
inspectPortUsage: (port: number, options?: PortUsageInspectionOptions) =>
inspectPortUsage(port, options),
@@ -278,6 +278,9 @@ vi.mock("../../infra/ports.js", () => ({
ports: readonly number[],
options?: { probeHostsByPort?: ReadonlyMap<number, readonly string[]> },
) => inspectPortUsages(ports, options),
}));
vi.mock("../../infra/ports-format.js", () => ({
formatPortDiagnostics: () => [],
}));
+3 -5
View File
@@ -39,15 +39,13 @@ import {
resolveBestEffortGatewayBindHostForDisplay,
} from "../../infra/network-discovery-display.js";
import { parseStrictPositiveInteger } from "../../infra/parse-finite-number.js";
import { formatPortDiagnostics } from "../../infra/ports-format.js";
import {
formatPortDiagnostics,
inspectPortConnections,
inspectPortUsage,
inspectPortUsages,
type PortConnection,
type PortListener,
type PortUsageStatus,
} from "../../infra/ports.js";
} from "../../infra/ports-inspect.js";
import type { PortConnection, PortListener, PortUsageStatus } from "../../infra/ports-types.js";
import {
readGatewayRestartHandoffSync,
type GatewayRestartHandoff,