perf(doctor): slim remaining heavy doctor contract closures (#120811)

* perf(doctor): slim remaining heavy doctor contract closures

Follow-up to #120698: several doctor closures still cold-loaded multi-second
kysely-bearing graphs through other broad barrels (session-store-runtime,
realtime-voice, channel-outbound, logging-core, memory-host-core/-events,
sqlite-runtime, persistent-dedupe, and plugin-local barrels).

- lazy-import heavy helpers inside async migration bodies (codex, msteams,
  zalouser, workboard, matrix inbound-dedupe, memory-core migrations)
- bypass plugin-local barrels to defining modules (reef protocol,
  memory-core short-term-promotion)
- move to lighter existing subpaths (slack -> channel-streaming, matrix
  logger -> security-runtime, memory-wiki -> agent-scope-runtime, which now
  also exports resolveSessionAgentId)
- add narrow openclaw/plugin-sdk/realtime-voice-activation for discord's
  sync wake-name doctor rules
- split src/infra/kysely-sync-cache-state.ts so sqlite-transaction clears
  Kysely caches without value-loading kysely; split the memory-host-sdk
  kysely bridge off the schema/transaction bridge
- guard: forbid the heavy barrels in doctor closures with per-kind scoping

Cold enumeration per plugin: discord 52.6s->0.3s, msteams 30.9s->0.5s,
codex 29.6s->2.6s, zalouser 28.8s->2.3s, matrix 27.2s->3.2s,
slack 17.5s->1.5s, reef 9.9s->0.7s, memory-core 6.4s->3.6s,
workboard 3.4s->0.25s; all kysely-free except llm-task (named follow-up).

* fix(plugins): route slack streaming compat through a focused streaming-config subpath

The channel-streaming compat barrel is deprecated for extension production
code (deprecated-api-usage guard + SDK package contract). Add the narrow
non-deprecated openclaw/plugin-sdk/channel-streaming-config subpath for the
pure streaming config readers, and drop the now consumer-less
short-term-promotion barrel re-exports knip flagged.

* test(plugins): register memory-host-sdk kysely bridge in package boundary inventory

* fix(plugins): classify realtime-voice-activation as private-local

ClawSweeper P2: the subpath exports only a default target, which is the
private-local shape; register it in plugin-sdk-private-local-only-subpaths,
the package-boundary d.ts alias maps, and correct the public surface budgets
(realtime-voice-activation no longer counts as public).

* fix(release): exclude realtime-voice-activation declarations from the published package

Private-local subpaths ship without d.ts; register the files negation the
release pack-path check requires.
This commit is contained in:
Peter Steinberger
2026-08-08 20:28:58 -07:00
committed by GitHub
parent b7add0b560
commit c2e7c819f5
39 changed files with 289 additions and 121 deletions
@@ -7,7 +7,7 @@ e5e67ddf3cab38fcbf9220bc3160715897e2709d9a9ff6ff36f1ecc9453c2367 module/agent-c
a43840ff36833daef61050fd801e825ec3ba806348b4fbe6c613f20a8807a5e7 module/agent-harness-runtime
5168648cd946abad8a92822889f13ceacc87ed502314a66190d0b1eb8ebe76ea module/agent-media-payload
e82bf122ca0787ec0bcdd12b669cfe24f07da16be1f4090e84c7d645b07ac79f module/agent-runtime
770bcb35221a019c70eace0200233a3a7985b24a6df48625f3e67b478d071ed1 module/agent-scope-runtime
6d12eed0833b9f63b5cea9a763c470487c85c5a722a3a35ec1f5ce07dc8715a9 module/agent-scope-runtime
56b6d5fb6af3d95af1200065aca2e7d4f59e5fa59740505fe6ff433077ef6646 module/allow-from
55cea5390d68839ca7768b4a0cc570b17b65fa0fa3bc4d76130ef0f16cb79ede module/allowlist-config-edit
7ddd81bd5f55de9adf64bf4d92d012f24b37b6da0a72805a3a220d8feff24ca3 module/approval-auth-runtime
@@ -48,6 +48,7 @@ f3cee48527f5cddde81912a588e72947d40443322b716871a8d9e5dc0c37c542 module/channel
fb123c1b557ed2527e335f13c3d6de41ab0c3305151001cf2b1075d1da8034c5 module/channel-setup
aeac9bb8127faf636dde79772a577d9cb003660ef3d1df03ddf606101020c402 module/channel-status
f008d1d9ea73cd17e9833143bd117436599438523041fd157b6b6cb17500ac06 module/channel-streaming
d1ff8728d99acf8130b94db7ba75ad7050a549c96365f56b8d4f601a533ca947 module/channel-streaming-config
14f0103adb14627b662fbe9fdd5ed08596ed702a250853e8beb8bb8dd1361588 module/cli-argv
c89ec1b194b76f67a6f4dd108dccf460da6065646cba31374c8aa748f23a39e4 module/collection-runtime
391e6f0c77da2e17a058fc5aea87fd193830b2a14e12fae77c7f63b0226f0bb5 module/command-auth
+2
View File
@@ -124,6 +124,7 @@ deprecated for new code; see the per-row notes below.
| `plugin-sdk/channel-inbound-debounce` | Narrow inbound debounce helpers |
| `plugin-sdk/channel-mention-gating` | Private-local after July 2026; Narrow mention-policy, mention marker, and mention text helpers without the broader inbound runtime surface |
| `plugin-sdk/channel-streaming` | Deprecated compatibility facade. Use `plugin-sdk/channel-outbound`. |
| `plugin-sdk/channel-streaming-config` | Dependency-light channel streaming config readers (`getChannelStreamingConfigObject`, `resolveChannelStreamingNativeTransport`) for doctor contract closures and other control-plane paths that must not load the reply pipeline |
| `plugin-sdk/channel-send-result` | Reply result types |
| `plugin-sdk/channel-actions` | Channel message-action helpers, plus deprecated native schema helpers kept for plugin compatibility |
| `plugin-sdk/channel-route` | Private-local after July 2026; Shared route normalization, parser-driven target resolution, thread-id stringification, dedupe/compact route keys, parsed-target types, and route/target comparison helpers |
@@ -340,6 +341,7 @@ Use `isLoopbackHost(host)` when a plugin must accept only the local machine. It
| `plugin-sdk/realtime-transcription` | Private-local after July 2026; Realtime transcription provider types, registry helpers, and shared WebSocket session helper |
| `plugin-sdk/realtime-bootstrap-context` | Private-local after July 2026; Realtime profile bootstrap helper for bounded `IDENTITY.md`, `USER.md`, and `SOUL.md` context injection |
| `plugin-sdk/realtime-voice-audio-queue` | Private-local JavaScript-only host runtime for bundled or separately published official plugins; narrow bounded audio queue seam for lazy realtime voice provider facades without importing the broader realtime voice runtime; not for third-party plugins |
| `plugin-sdk/realtime-voice-activation` | Private-local; dependency-light realtime-voice activation-name helpers (normalize, match, word-count, sort) for doctor contract closures and other control-plane paths that must not load the realtime voice runtime |
| `plugin-sdk/realtime-voice` | Private-local after July 2026; Realtime voice provider types, registry helpers, shared audio-energy/speech-onset gates, and realtime voice behavior helpers, including the transport-independent session harness and output activity tracking. For official runtime consumers, sender-auth contract revision 1 forwards ingress-authenticated `senderId` and `senderIsOwner` unchanged; ingress owns authentication, and consumers requiring the handoff must fail closed on other revisions. |
| `plugin-sdk/meeting-runtime` | Browser-meeting session runtime, realtime audio engines/transports, `MeetingPlatformAdapter`, browser/node control, agent-consult, voice-call delegation, setup checks, and SoX command helpers |
| `plugin-sdk/image-generation` | Private-local after July 2026; Image generation provider types plus image asset/data URL helpers and the OpenAI-compatible image provider builder |
@@ -18,7 +18,6 @@ import {
isPathInside,
pathExists,
} from "openclaw/plugin-sdk/security-runtime";
import { patchSessionEntry, resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime";
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
CODEX_APP_SERVER_BINDING_MAX_ENTRIES,
@@ -97,6 +96,9 @@ type MigratedBindingRow =
};
async function collectSessionSurfaces(params: MigrationEnvironment): Promise<SessionSurface[]> {
// Doctor enumeration cold-loads this closure; session-store-runtime pulls the
// session-accessor/kysely graph, so it stays behind lazy imports in async bodies.
const { resolveStorePath } = await import("openclaw/plugin-sdk/session-store-runtime");
const surfaces = new Map<string, SessionSurface>();
const stateRoot = await canonicalPathFromExistingAncestor(params.stateDir);
const add = async (root: string, storePath: string, agentId: string, scan: boolean) => {
@@ -305,6 +307,7 @@ async function collectBindingOwners(
surfaces: SessionSurface[],
params: MigrationEnvironment,
): Promise<BindingOwnerCollection> {
const { resolveStorePath } = await import("openclaw/plugin-sdk/session-store-runtime");
const sourcePaths = new Set(
await Promise.all(
sources.map((source) => canonicalPathFromExistingAncestor(source.transcriptPath)),
@@ -679,6 +682,7 @@ async function recordSessionOwner(
owner: LegacyBindingOwner,
env: NodeJS.ProcessEnv,
): Promise<string | undefined> {
const { patchSessionEntry } = await import("openclaw/plugin-sdk/session-store-runtime");
const currentIndex = await readLegacySessionIndex(owner.storePath);
if ("failure" in currentIndex) {
return "its legacy session owner could not be revalidated";
+3 -1
View File
@@ -4,10 +4,12 @@ import type {
ChannelDoctorLegacyConfigRule,
} from "openclaw/plugin-sdk/channel-contract";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
// The narrow activation subpath avoids realtime-voice's agent-consult/session
// graph, which doctor enumeration must not cold-load.
import {
isSupportedRealtimeVoiceActivationName,
normalizeRealtimeVoiceActivationNamePrefix,
} from "openclaw/plugin-sdk/realtime-voice";
} from "openclaw/plugin-sdk/realtime-voice-activation";
import {
asObjectRecord,
defineChannelAliasMigration,
@@ -15,23 +15,12 @@ import type { Dirent } from "node:fs";
import fs from "node:fs/promises";
import path from "node:path";
import type { DatabaseSync } from "node:sqlite";
import {
createPersistentDedupeImportEntry,
type PersistentDedupeEntry,
} from "openclaw/plugin-sdk/persistent-dedupe";
// Doctor enumeration cold-loads this closure; persistent-dedupe pulls the
// plugin-state-store/kysely graph, so the value import stays lazy below.
import type { PersistentDedupeEntry } from "openclaw/plugin-sdk/persistent-dedupe";
import type { PluginDoctorStateMigrationContext } from "openclaw/plugin-sdk/runtime-doctor-migrations";
import {
openNodeSqliteDatabase,
runSqliteImmediateTransactionSync,
} from "openclaw/plugin-sdk/sqlite-runtime";
import { isRecord } from "../../record-shared.js";
import { normalizeMatrixStorageMetadata } from "../client/storage.js";
import {
buildMatrixInboundDedupeEventKey,
MATRIX_INBOUND_DEDUPE_STATE_MAX_ENTRIES,
MATRIX_INBOUND_DEDUPE_TTL_MS,
resolveMatrixInboundDedupeStateNamespace,
} from "./inbound-dedupe.js";
const LEGACY_SQLITE_NAMESPACE = "inbound-dedupe";
const LEGACY_MARKERS_NAMESPACE = "inbound-dedupe-migrations";
@@ -249,6 +238,9 @@ function parseLegacySqliteRow(row: {
export async function readLegacyInboundDedupeSqliteSource(
storageRootDir: string,
): Promise<{ markers: LegacyInboundDedupeMarker[]; legacyRowCount: number }> {
// sqlite-runtime re-exports the agent-db/kysely graph; keep it lazy so doctor
// enumeration does not cold-load it with this closure.
const { openNodeSqliteDatabase } = await import("openclaw/plugin-sdk/sqlite-runtime");
const databasePath = path.join(storageRootDir, STATE_DATABASE_RELATIVE_PATH);
const db = openNodeSqliteDatabase(databasePath, { readOnly: true });
try {
@@ -282,6 +274,8 @@ export async function readLegacyInboundDedupeSqliteSource(
/** Deletes only the two retired Matrix namespaces after a successful import. */
export async function retireLegacyInboundDedupeSqliteRows(storageRootDir: string): Promise<void> {
const { openNodeSqliteDatabase, runSqliteImmediateTransactionSync } =
await import("openclaw/plugin-sdk/sqlite-runtime");
const databasePath = path.join(storageRootDir, STATE_DATABASE_RELATIVE_PATH);
const db = openNodeSqliteDatabase(databasePath);
try {
@@ -406,6 +400,16 @@ export async function importNewestInboundDedupeMarkers(params: {
now?: number;
stateMaxEntries?: number;
}): Promise<{ imported: number; total: number }> {
const { createPersistentDedupeImportEntry } =
await import("openclaw/plugin-sdk/persistent-dedupe");
// inbound-dedupe.js value-imports persistent-dedupe's replay guard, so the
// runtime module also stays out of this closure's static import graph.
const {
buildMatrixInboundDedupeEventKey,
MATRIX_INBOUND_DEDUPE_STATE_MAX_ENTRIES,
MATRIX_INBOUND_DEDUPE_TTL_MS,
resolveMatrixInboundDedupeStateNamespace,
} = await import("./inbound-dedupe.js");
const now = params.now ?? Date.now();
const stateMaxEntries = params.stateMaxEntries ?? MATRIX_INBOUND_DEDUPE_STATE_MAX_ENTRIES;
const newestByKey = new Map<string, LegacyInboundDedupeMarker & { key: string }>();
+3 -1
View File
@@ -1,7 +1,9 @@
// Matrix plugin module implements logger behavior.
import { format } from "node:util";
import { redactSensitiveText } from "openclaw/plugin-sdk/logging-core";
import type { RuntimeLogger } from "openclaw/plugin-sdk/plugin-runtime";
// security-runtime exports the same redaction helper without logging-core's
// diagnostic/config graph, which doctor enumeration must not cold-load.
import { redactSensitiveText } from "openclaw/plugin-sdk/security-runtime";
import { getMatrixRuntime } from "../../runtime.js";
type Logger = {
@@ -21,12 +21,14 @@ import {
writeMemoryCoreWorkspaceEntries,
writeMemoryCoreWorkspaceEntry,
} from "../dreaming-state.js";
// Import from the defining modules, not the short-term-promotion barrel: the
// barrel pulls memory-host-events/kysely, which doctor enumeration cold-loads.
import { normalizeShortTermPhaseSignalStore } from "../short-term-promotion-store.js";
import {
SHORT_TERM_PHASE_SIGNAL_RELATIVE_PATH,
SHORT_TERM_STORE_RELATIVE_PATH,
normalizeShortTermPhaseSignalStore,
normalizeShortTermRecallStore,
} from "../short-term-promotion.js";
} from "../short-term-promotion-types.js";
import { normalizeShortTermRecallStore } from "../short-term-promotion-utils.js";
import { resolveConfiguredWorkspaces } from "./doctor-workspaces.js";
import { dreamingStateComparison } from "./dreaming-state-comparison.js";
@@ -2,7 +2,8 @@ import crypto from "node:crypto";
import fs from "node:fs/promises";
import path from "node:path";
import { root } from "openclaw/plugin-sdk/memory-core-host-engine-fs";
import { resolveMemoryHostEventLogPath } from "openclaw/plugin-sdk/memory-host-events";
// Doctor enumeration cold-loads this closure; memory-host-events pulls the
// event-store/kysely graph, so the path resolver loads lazily in async bodies.
import { resolveConfiguredWorkspaces } from "./doctor-workspaces.js";
export type LegacyMemoryHostEventSource =
@@ -45,6 +46,7 @@ export async function collectLegacyMemoryHostEventSources(
config: unknown,
env: NodeJS.ProcessEnv,
): Promise<LegacyMemoryHostEventSource[]> {
const { resolveMemoryHostEventLogPath } = await import("openclaw/plugin-sdk/memory-host-events");
const sources: LegacyMemoryHostEventSource[] = [];
const seenWorkspaces = new Set<string>();
for (const workspaceDir of resolveConfiguredWorkspaces(config, env)) {
@@ -154,6 +156,7 @@ export async function collectLegacyMemoryHostEventSources(
export async function resolveMemoryHostEventArchivePath(
source: ReadyLegacyMemoryHostEventSource,
): Promise<{ archiveRelativePath: string; claimRelativePath: string; generationKey: string }> {
const { resolveMemoryHostEventLogPath } = await import("openclaw/plugin-sdk/memory-host-events");
const activeRelativePath = path.relative(
source.workspaceDir,
resolveMemoryHostEventLogPath(source.workspaceDir),
@@ -1,9 +1,8 @@
import crypto from "node:crypto";
import path from "node:path";
import {
normalizeMemoryHostEventRecordForStorage,
resolveMemoryHostEventLogPath,
} from "openclaw/plugin-sdk/memory-host-events";
// Doctor enumeration cold-loads this closure; memory-host-events pulls the
// event-store/kysely graph, so values load lazily inside the async migration.
import type { normalizeMemoryHostEventRecordForStorage } from "openclaw/plugin-sdk/memory-host-events";
import type {
PluginDoctorStateMigration,
PluginDoctorStateMigrationContext,
@@ -157,6 +156,8 @@ async function migrateLegacyMemoryHostEventSource(params: {
changes: string[];
warnings: string[];
}): Promise<"completed" | "blocked"> {
const { normalizeMemoryHostEventRecordForStorage, resolveMemoryHostEventLogPath } =
await import("openclaw/plugin-sdk/memory-host-events");
const activeRelativePath = path.relative(
params.source.workspaceDir,
resolveMemoryHostEventLogPath(params.source.workspaceDir),
@@ -4,20 +4,15 @@ import fs from "node:fs/promises";
import path from "node:path";
import { reclaimDefinitelyStaleFileLock } from "openclaw/plugin-sdk/file-lock";
import { resolveUserPath } from "openclaw/plugin-sdk/memory-core-host-engine-fs";
import {
ensureMemoryIndexSchema,
loadSqliteVecExtension,
} from "openclaw/plugin-sdk/memory-core-host-engine-schema";
// Doctor enumeration cold-loads this closure; the host engine schema pulls the
// runtime-sqlite/kysely graph, so its helpers load lazily in the async migration.
import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
import {
legacyStateFileExists,
type PluginDoctorStateMigration,
} from "openclaw/plugin-sdk/runtime-doctor-migrations";
import {
ensureOpenClawAgentDatabaseSchema,
openNodeSqliteDatabase,
resolveOpenClawAgentSqlitePath,
} from "openclaw/plugin-sdk/sqlite-runtime";
// sqlite-runtime re-exports the agent-db/kysely graph; keep it lazy so doctor
// enumeration does not cold-load it with this closure.
import {
importLegacyMemorySidecarIndex,
LEGACY_MEMORY_SIDECAR_SUFFIXES,
@@ -144,6 +139,7 @@ async function collectLegacyMemorySidecarSources(params: {
env: NodeJS.ProcessEnv;
stateDir: string;
}): Promise<LegacyMemorySidecarSource[]> {
const { resolveOpenClawAgentSqlitePath } = await import("openclaw/plugin-sdk/sqlite-runtime");
const agentIds = new Set(resolveConfiguredAgentIds(params.config));
const legacyDir = path.join(params.stateDir, "memory");
const retrySidecars: Array<{ agentId: string; legacyPath: string }> = [];
@@ -373,6 +369,10 @@ async function migrateLegacyMemorySidecarSource(params: {
changes: string[];
warnings: string[];
}): Promise<{ archiveReady: boolean }> {
const { ensureMemoryIndexSchema, loadSqliteVecExtension } =
await import("openclaw/plugin-sdk/memory-core-host-engine-schema");
const { ensureOpenClawAgentDatabaseSchema, openNodeSqliteDatabase } =
await import("openclaw/plugin-sdk/sqlite-runtime");
// OpenClaw itself can leave a zero-byte placeholder at the legacy sidecar
// path while the live index is the per-agent SQLite database. An empty file
// holds no legacy rows, so remove it quietly instead of emitting a permanent
@@ -3,7 +3,8 @@ import path from "node:path";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import type { PluginDoctorStateMigration } from "openclaw/plugin-sdk/runtime-doctor-migrations";
import { openNodeSqliteDatabase } from "openclaw/plugin-sdk/sqlite-runtime";
// sqlite-runtime re-exports the agent-db/kysely graph; keep it lazy so doctor
// enumeration does not cold-load it with this closure.
const MEMORY_INDEX_META_KEY = "memory_index_meta_v1";
@@ -85,10 +86,11 @@ function listConfiguredAgentIds(config: OpenClawConfig): string[] {
return ids.size > 0 ? [...ids] : ["main"];
}
function readExistingVectorModel(databasePath: string): string | null {
async function readExistingVectorModel(databasePath: string): Promise<string | null> {
if (!fs.existsSync(databasePath)) {
return null;
}
const { openNodeSqliteDatabase } = await import("openclaw/plugin-sdk/sqlite-runtime");
let db: ReturnType<typeof openNodeSqliteDatabase> | undefined;
try {
db = openNodeSqliteDatabase(databasePath, { readOnly: true });
@@ -144,7 +146,7 @@ async function collectVectorProviderFindings(params: {
"agent",
"openclaw-agent.sqlite",
);
const model = readExistingVectorModel(agentDatabasePath);
const model = await readExistingVectorModel(agentDatabasePath);
if (!model) {
continue;
}
@@ -16,10 +16,10 @@ import {
readMemoryCoreWorkspaceEntries,
writeMemoryCoreWorkspaceEntry,
} from "../dreaming-state.js";
import {
normalizeShortTermPhaseSignalStore,
normalizeShortTermRecallStore,
} from "../short-term-promotion.js";
// Import from the defining modules, not the short-term-promotion barrel: the
// barrel pulls memory-host-events/kysely, which doctor enumeration cold-loads.
import { normalizeShortTermPhaseSignalStore } from "../short-term-promotion-store.js";
import { normalizeShortTermRecallStore } from "../short-term-promotion-utils.js";
type LegacyDreamingSource = {
workspaceDir: string;
@@ -240,8 +240,6 @@ export {
DEFAULT_PROMOTION_MIN_RECALL_COUNT,
DEFAULT_PROMOTION_MIN_SCORE,
DEFAULT_PROMOTION_MIN_UNIQUE_QUERIES,
SHORT_TERM_PHASE_SIGNAL_RELATIVE_PATH,
SHORT_TERM_STORE_RELATIVE_PATH,
type PromotionCandidate,
type RepairShortTermPromotionArtifactsResult,
type ShortTermAuditSummary,
@@ -249,8 +247,6 @@ export {
type ShortTermDreamingStatsEntry,
type ShortTermRecallEntry,
} from "./short-term-promotion-types.js";
export { normalizeShortTermPhaseSignalStore } from "./short-term-promotion-store.js";
export { normalizeShortTermRecallStore } from "./short-term-promotion-utils.js";
export {
filterFreshLightDreamingEntries,
loadShortTermPromotionDreamingStats,
+3 -5
View File
@@ -25,11 +25,9 @@ import {
writeMemoryCoreWorkspaceEntries,
writeMemoryCoreWorkspaceEntry,
} from "./dreaming-state.js";
import {
normalizeShortTermPhaseSignalStore,
normalizeShortTermRecallStore,
type ShortTermRecallEntry,
} from "./short-term-promotion.js";
import { normalizeShortTermPhaseSignalStore } from "./short-term-promotion-store.js";
import { normalizeShortTermRecallStore } from "./short-term-promotion-utils.js";
import type { ShortTermRecallEntry } from "./short-term-promotion.js";
const MEMORY_CORE_PLUGIN_ID = "memory-core";
+6 -1
View File
@@ -1,8 +1,13 @@
// Memory Wiki helper module supports config behavior.
import os from "node:os";
import path from "node:path";
// agent-scope-runtime exports the same resolvers without memory-host-core's
// event-store/kysely graph, which doctor enumeration must not cold-load.
import {
resolveDefaultAgentId,
resolveSessionAgentId,
} from "openclaw/plugin-sdk/agent-scope-runtime";
import { mapPluginConfigIssues } from "openclaw/plugin-sdk/extension-shared";
import { resolveDefaultAgentId, resolveSessionAgentId } from "openclaw/plugin-sdk/memory-host-core";
import { buildPluginConfigSchema, z, type OpenClawPluginConfigSchema } from "../api.js";
import type { OpenClawConfig } from "../api.js";
+11 -5
View File
@@ -13,7 +13,6 @@ import {
defineChannelAliasMigration,
type PluginDoctorStateMigration,
} from "openclaw/plugin-sdk/runtime-doctor-migrations";
import { resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime";
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import { normalizeStoredConversationId } from "./src/conversation-store-helpers.js";
import {
@@ -164,10 +163,13 @@ function listAgentIds(config: OpenClawConfig): string[] {
return [...ids];
}
function listCandidateStorePaths(params: {
async function listCandidateStorePaths(params: {
config: Parameters<PluginDoctorStateMigration["migrateLegacyState"]>[0]["config"];
env: NodeJS.ProcessEnv;
}): string[] {
}): Promise<string[]> {
// Doctor enumeration cold-loads this closure; session-store-runtime pulls the
// session-accessor/kysely graph, so it stays behind a lazy import here.
const { resolveStorePath } = await import("openclaw/plugin-sdk/session-store-runtime");
const paths = new Set<string>();
for (const agentId of listAgentIds(params.config)) {
paths.add(resolveStorePath(params.config.session?.store, { agentId, env: params.env }));
@@ -601,7 +603,9 @@ export const stateMigrations: PluginDoctorStateMigration[] = [
async detectLegacyState(params) {
const files = (
await Promise.all(
listCandidateStorePaths(params).map((storePath) => listLegacyLearningFiles(storePath)),
(
await listCandidateStorePaths(params)
).map((storePath) => listLegacyLearningFiles(storePath)),
)
).flat();
if (files.length === 0) {
@@ -618,7 +622,9 @@ export const stateMigrations: PluginDoctorStateMigration[] = [
const warnings: string[] = [];
const files = (
await Promise.all(
listCandidateStorePaths(params).map((storePath) => listLegacyLearningFiles(storePath)),
(
await listCandidateStorePaths(params)
).map((storePath) => listLegacyLearningFiles(storePath)),
)
).flat();
const store = params.context.openPluginStateKeyedStore<FeedbackLearningEntry>({
+3 -1
View File
@@ -3,12 +3,14 @@ import { setTimeout as sleep } from "node:timers/promises";
import { randomBytes } from "@noble/hashes/utils.js";
import type { PluginRuntime } from "openclaw/plugin-sdk/core";
import type { PluginStateSyncKeyedStore } from "openclaw/plugin-sdk/plugin-state-runtime";
// Import from the defining module, not the protocol barrel: index.js re-exports
// guard-adapters, whose provider-http graph doctor enumeration must not cold-load.
import {
createAuditEntry,
verifyChainSegment,
type AuditEntry,
type AuditStore,
} from "../protocol/index.js";
} from "../protocol/audit.js";
export const REEF_AUDIT_NAMESPACE = "audit";
export const REEF_AUDIT_HEAD_NAMESPACE = "audit-head";
+5 -7
View File
@@ -6,13 +6,11 @@ import {
type PluginDoctorStateMigration,
} from "openclaw/plugin-sdk/runtime-doctor-migrations";
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
verifyChain,
verifyChainSegment,
type AuditEntry,
type ReviewRequest,
type SignedReceipt,
} from "../protocol/index.js";
// Import from defining modules, not the protocol barrel: index.js re-exports
// guard-adapters, whose provider-http graph doctor enumeration must not cold-load.
import { verifyChain, verifyChainSegment, type AuditEntry } from "../protocol/audit.js";
import type { ReviewRequest } from "../protocol/pipeline.js";
import type { SignedReceipt } from "../protocol/receipts.js";
import {
legacyReefFileExists,
REEF_DURABLE_LEGACY_FILENAMES,
+8 -11
View File
@@ -3,24 +3,21 @@ import { gcm } from "@noble/ciphers/aes.js";
import { concatBytes, randomBytes } from "@noble/hashes/utils.js";
import type { PluginRuntime } from "openclaw/plugin-sdk/core";
import type { PluginStateSyncKeyedStore } from "openclaw/plugin-sdk/plugin-state-runtime";
// Import from defining modules, not the protocol barrel: index.js re-exports
// guard-adapters, whose provider-http graph doctor enumeration must not cold-load.
import { canonicalBytes } from "../protocol/canonical.js";
import { base64, base64url, decodeUtf8, fromBase64, fromBase64url } from "../protocol/encoding.js";
import {
base64,
base64url,
canonicalBytes,
decodeUtf8,
fromBase64,
fromBase64url,
generateIdentity,
REEF_ENVELOPE_MAX_AGE_SECONDS,
validateMessageBody,
type CompletedReplay,
type MessageBody,
type ReplayClaim,
type ReplayStore,
type ReviewApproval,
type ReviewRequest,
type SignedReceipt,
} from "../protocol/index.js";
} from "../protocol/envelope.js";
import { generateIdentity } from "../protocol/identity.js";
import type { ReviewApproval, ReviewRequest } from "../protocol/pipeline.js";
import type { SignedReceipt } from "../protocol/receipts.js";
import { openReefAuditStore } from "./audit-state.js";
import { loadReefIdentityBinding, type ReefIdentityBinding } from "./registration-state.js";
import type { ReefKeys } from "./types.js";
+3 -1
View File
@@ -1,8 +1,10 @@
// Slack plugin module implements streaming compat behavior.
// channel-streaming-config exports the same helpers without channel-outbound's
// reply-pipeline/channel-registry graph, which doctor enumeration cold-loads.
import {
getChannelStreamingConfigObject,
resolveChannelStreamingNativeTransport,
} from "openclaw/plugin-sdk/channel-outbound";
} from "openclaw/plugin-sdk/channel-streaming-config";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalString,
@@ -362,6 +362,9 @@
"openclaw/plugin-sdk/realtime-voice-audio-queue": [
"../packages/plugin-sdk/dist/src/plugin-sdk/realtime-voice-audio-queue.d.ts"
],
"openclaw/plugin-sdk/realtime-voice-activation": [
"../packages/plugin-sdk/dist/src/plugin-sdk/realtime-voice-activation.d.ts"
],
"openclaw/plugin-sdk/realtime-voice": [
"../packages/plugin-sdk/dist/src/plugin-sdk/realtime-voice.d.ts"
],
+4 -1
View File
@@ -10,7 +10,8 @@ import type {
PersistedWorkboardNotificationSubscription,
WorkboardKeyedStore,
} from "./src/persistence-types.js";
import { createWorkboardSqliteStores, resolveWorkboardSqlitePath } from "./src/sqlite-store.js";
// Doctor enumeration cold-loads this closure; sqlite-store pulls the
// plugin-state-runtime/kysely graph, so it stays behind lazy imports below.
const MAX_CARDS = 2000;
@@ -176,6 +177,7 @@ export const stateMigrations: PluginDoctorStateMigration[] = [
id: "workboard-28-kv-to-sqlite",
label: "Workboard .28 plugin-state KV",
async detectLegacyState(params) {
const { resolveWorkboardSqlitePath } = await import("./src/sqlite-store.js");
const env = migrationEnv(params);
const cards = await openLegacyStore<PersistedWorkboardCard>({
context: params.context,
@@ -212,6 +214,7 @@ export const stateMigrations: PluginDoctorStateMigration[] = [
};
},
async migrateLegacyState(params) {
const { createWorkboardSqliteStores } = await import("./src/sqlite-store.js");
const env = migrationEnv(params);
const cards = openLegacyStore<PersistedWorkboardCard>({
context: params.context,
+3
View File
@@ -359,6 +359,9 @@
"openclaw/plugin-sdk/realtime-voice-audio-queue": [
"../../packages/plugin-sdk/dist/src/plugin-sdk/realtime-voice-audio-queue.d.ts"
],
"openclaw/plugin-sdk/realtime-voice-activation": [
"../../packages/plugin-sdk/dist/src/plugin-sdk/realtime-voice-activation.d.ts"
],
"openclaw/plugin-sdk/realtime-voice": [
"../../packages/plugin-sdk/dist/src/plugin-sdk/realtime-voice.d.ts"
],
+11 -11
View File
@@ -8,13 +8,9 @@ import {
archiveLegacyStateSource,
type PluginDoctorStateMigration,
} from "openclaw/plugin-sdk/runtime-doctor-migrations";
import {
deleteSessionEntry,
deliveryContextFromSession,
listSessionEntries,
resolveStorePath,
upsertSessionEntry,
} from "openclaw/plugin-sdk/session-store-runtime";
// Doctor enumeration cold-loads this closure; session-store-runtime pulls the
// session-accessor/kysely graph, so values load lazily inside async bodies.
import type { listSessionEntries } from "openclaw/plugin-sdk/session-store-runtime";
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import { resolveZalouserDmSessionScope } from "./src/session-scope.js";
import {
@@ -82,11 +78,13 @@ async function collectLegacyZalouserCredentialSources(
.toSorted((left, right) => left.profile.localeCompare(right.profile));
}
function collectLegacyZalouserDmEntries(
async function collectLegacyZalouserDmEntries(
config: OpenClawConfig,
env: NodeJS.ProcessEnv,
options: { readOnly?: boolean } = {},
): LegacyZalouserDmEntry[] {
): Promise<LegacyZalouserDmEntry[]> {
const { deliveryContextFromSession, listSessionEntries, resolveStorePath } =
await import("openclaw/plugin-sdk/session-store-runtime");
const entries = new Map<string, LegacyZalouserDmEntry>();
const fallbackAccountId = config.channels?.zalouser?.defaultAccount?.trim() || "default";
const agentIds = new Set([
@@ -243,14 +241,16 @@ export const stateMigrations: PluginDoctorStateMigration[] = [
) {
return null;
}
const pending = collectLegacyZalouserDmEntries(config, env, { readOnly: true });
const pending = await collectLegacyZalouserDmEntries(config, env, { readOnly: true });
const count = pending.flatMap(({ legacyKeys }) => legacyKeys).length;
return count > 0
? { preview: [`- Zalo Personal direct-message session keys: ${count} legacy row(s)`] }
: null;
},
async migrateLegacyState({ config, env }) {
const pending = collectLegacyZalouserDmEntries(config, env);
const { deleteSessionEntry, upsertSessionEntry } =
await import("openclaw/plugin-sdk/session-store-runtime");
const pending = await collectLegacyZalouserDmEntries(config, env);
const warnings: string[] = [];
let migrated = 0;
for (const entry of pending) {
+8
View File
@@ -174,6 +174,7 @@
"!dist/plugin-sdk/realtime-bootstrap-context.d.ts",
"!dist/plugin-sdk/realtime-transcription.d.ts",
"!dist/plugin-sdk/realtime-voice-audio-queue.d.ts",
"!dist/plugin-sdk/realtime-voice-activation.d.ts",
"!dist/plugin-sdk/realtime-voice.d.ts",
"!dist/plugin-sdk/reply-payload-testing.js",
"!dist/plugin-sdk/reply-payload-testing.d.ts",
@@ -431,6 +432,10 @@
"types": "./dist/plugin-sdk/channel-streaming.d.ts",
"default": "./dist/plugin-sdk/channel-streaming.js"
},
"./plugin-sdk/channel-streaming-config": {
"types": "./dist/plugin-sdk/channel-streaming-config.d.ts",
"default": "./dist/plugin-sdk/channel-streaming-config.js"
},
"./plugin-sdk/setup-tools": {
"types": "./dist/plugin-sdk/setup-tools.d.ts",
"default": "./dist/plugin-sdk/setup-tools.js"
@@ -1141,6 +1146,9 @@
"./plugin-sdk/realtime-voice-audio-queue": {
"default": "./dist/plugin-sdk/realtime-voice-audio-queue.js"
},
"./plugin-sdk/realtime-voice-activation": {
"default": "./dist/plugin-sdk/realtime-voice-activation.js"
},
"./plugin-sdk/realtime-voice": {
"default": "./dist/plugin-sdk/realtime-voice.js"
},
@@ -1,6 +1,6 @@
import type { DatabaseSync } from "node:sqlite";
import { INVALID_PROJECT_ANNOTATION_KEY } from "./internal.js";
import { executeSqliteQuerySync, getNodeSqliteKysely } from "./openclaw-runtime-sqlite.js";
import { executeSqliteQuerySync, getNodeSqliteKysely } from "./openclaw-runtime-kysely.js";
type MemoryRecallMetadataDatabase = {
memory_index_chunks: {
@@ -0,0 +1,4 @@
// Narrow core bridge for Kysely-backed sync query helpers. Split from
// openclaw-runtime-sqlite so schema/transaction consumers stay off the kysely
// value graph, which doctor/setup control-plane paths must not cold-load.
export { executeSqliteQuerySync, getNodeSqliteKysely } from "../../../../src/infra/kysely-sync.js";
@@ -1,5 +1,6 @@
// Narrow core bridge for shared SQLite schema migration primitives.
// Kysely-backed query helpers live in openclaw-runtime-kysely.ts so this
// bridge stays off the kysely value graph for schema/transaction consumers.
export { migrateSqliteSchemaToStrict } from "../../../../src/infra/sqlite-strict.js";
export { runSqliteImmediateTransactionSync } from "../../../../src/infra/sqlite-transaction.js";
export { executeSqliteQuerySync, getNodeSqliteKysely } from "../../../../src/infra/kysely-sync.js";
+2
View File
@@ -13,6 +13,7 @@
"setup-runtime",
"channel-setup",
"channel-streaming",
"channel-streaming-config",
"setup-tools",
"archive",
"root-walk",
@@ -235,6 +236,7 @@
"realtime-transcription",
"realtime-bootstrap-context",
"realtime-voice-audio-queue",
"realtime-voice-activation",
"realtime-voice",
"meeting-runtime",
"transcripts",
@@ -128,6 +128,7 @@
"realtime-bootstrap-context",
"realtime-transcription",
"realtime-voice-audio-queue",
"realtime-voice-activation",
"realtime-voice",
"reply-payload-testing",
"reply-reference",
+7 -3
View File
@@ -167,7 +167,9 @@ export function readPluginSdkSurfaceBudgets(env = process.env) {
// +2: restore the documented session-catalog and tool-results plugin contracts.
// +1: focused inbound-event delivery correlation for channel plugins.
// +1: dependency-light agent scope helpers for doctor migration enumeration.
150,
// +1: dependency-light channel streaming config readers for doctor closures
// (realtime-voice-activation is private-local and not counted here).
151,
env,
),
publicExports: readPluginSdkSurfaceBudgetEnv(
@@ -228,7 +230,8 @@ export function readPluginSdkSurfaceBudgets(env = process.env) {
// +1: simple channel secret contract factory replacing repeated collectors.
// +4: focused agent scope functions for doctor migration enumeration.
// +1: shared transcript credential-safety prompt for plugin-owned agent harnesses.
4840,
// +3: channel streaming config reader re-exports and session-agent scope resolver.
4843,
env,
),
publicFunctionExports: readPluginSdkSurfaceBudgetEnv(
@@ -280,7 +283,8 @@ export function readPluginSdkSurfaceBudgets(env = process.env) {
// +1: shared channel interactive dispatcher with canonical binding authorization.
// +1: simple channel secret contract factory replacing repeated collectors.
// +4: focused agent scope functions for doctor migration enumeration.
2916,
// +3: channel streaming config reader functions and session-agent scope resolver.
2919,
env,
),
publicDeprecatedExports: readPluginSdkSurfaceBudgetEnv(
+20
View File
@@ -0,0 +1,20 @@
// Shared per-database Kysely cache state, split from kysely-sync so lifecycle
// owners (sqlite-transaction) can clear caches without value-loading kysely.
// Doctor/setup closures cold-load transaction consumers; keep this file
// dependency-free beyond node:sqlite types.
import type { DatabaseSync } from "node:sqlite";
export const kyselyByDatabase = new WeakMap<DatabaseSync, unknown>();
export const queryErrorHandlerByDatabase = new WeakMap<DatabaseSync, (error: unknown) => void>();
// Cached statements retain their database. Per-instance lifecycle wrappers clear
// both caches before the native database handle closes.
export const statementCacheSymbol = Symbol("openclaw.kyselySyncStatementCache");
/** Drop cached Kysely state for a DatabaseSync. */
export function clearNodeSqliteKyselyCacheForDatabase(db: DatabaseSync): void {
// Delete the database-owned cache before close so statements release their
// native database backreferences instead of recreating the WeakMap leak.
delete (db as DatabaseSync & { [statementCacheSymbol]?: unknown })[statementCacheSymbol];
kyselyByDatabase.delete(db);
queryErrorHandlerByDatabase.delete(db);
}
+8 -15
View File
@@ -2,16 +2,18 @@
import type { DatabaseSync, SQLInputValue, StatementSync } from "node:sqlite";
import type { Compilable, CompiledQuery, Kysely, QueryResult } from "kysely";
import { InsertQueryNode, Kysely as KyselyInstance, SqliteDialect } from "kysely";
import {
clearNodeSqliteKyselyCacheForDatabase,
kyselyByDatabase,
queryErrorHandlerByDatabase,
statementCacheSymbol,
} from "./kysely-sync-cache-state.js";
import { pruneMapToMaxSize } from "./map-size.js";
// Sync query helpers execute compiled Kysely SQL against node:sqlite without
// going through Kysely's async driver path.
const kyselyByDatabase = new WeakMap<DatabaseSync, Kysely<unknown>>();
const queryErrorHandlerByDatabase = new WeakMap<DatabaseSync, (error: unknown) => void>();
// Cached statements retain their database. Per-instance lifecycle wrappers clear
// both caches before the native database handle closes.
const statementCacheSymbol = Symbol("openclaw.kyselySyncStatementCache");
export { clearNodeSqliteKyselyCacheForDatabase } from "./kysely-sync-cache-state.js";
const statementInvalidationSymbol = Symbol("openclaw.kyselySyncStatementInvalidation");
const statementCacheEnabledSymbol = Symbol("openclaw.kyselySyncStatementCacheEnabled");
const authorizerActiveSymbol = Symbol("openclaw.kyselySyncAuthorizerActive");
@@ -45,7 +47,7 @@ const compileOnlySqliteDialect = new SqliteDialect({
});
export function getNodeSqliteKysely<Database>(db: DatabaseSync): Kysely<Database> {
const existing = kyselyByDatabase.get(db);
const existing = kyselyByDatabase.get(db) as Kysely<unknown> | undefined;
if (existing) {
return existing as Kysely<Database>;
}
@@ -313,12 +315,3 @@ export function executeSqliteQueryTakeFirstSync<Row>(
): Row | undefined {
return executeSqliteQuerySync<Row>(db, query).rows[0];
}
/** Drop cached Kysely state for a DatabaseSync. */
export function clearNodeSqliteKyselyCacheForDatabase(db: DatabaseSync): void {
// Delete the database-owned cache before close so statements release their
// native database backreferences instead of recreating the WeakMap leak.
delete (db as StatementCacheOwner)[statementCacheSymbol];
kyselyByDatabase.delete(db);
queryErrorHandlerByDatabase.delete(db);
}
+3 -1
View File
@@ -1,7 +1,9 @@
// Provides SQLite transaction helpers with nested savepoints.
import type { DatabaseSync } from "node:sqlite";
import { createSubsystemLogger, type SubsystemLogger } from "../logging/subsystem.js";
import { clearNodeSqliteKyselyCacheForDatabase } from "./kysely-sync.js";
// The cache-state module keeps this lifecycle edge off the kysely value graph
// so cold control-plane paths using transactions do not load kysely.
import { clearNodeSqliteKyselyCacheForDatabase } from "./kysely-sync-cache-state.js";
const transactionDepthByDatabase = new WeakMap<DatabaseSync, number>();
+1
View File
@@ -4,5 +4,6 @@ export {
listAgentIds,
resolveAgentDir,
resolveDefaultAgentId,
resolveSessionAgentId,
resolveSessionAgentIds,
} from "../agents/agent-scope.js";
@@ -0,0 +1,12 @@
/**
* Dependency-light channel streaming config readers.
*
* Doctor contract closures (e.g. Slack's streaming compat rules) need these
* pure config helpers; the broad `channel-outbound` barrel also value-loads
* the reply-pipeline/channel-registry graph, which doctor enumeration must
* not cold-load, and `channel-streaming` is a deprecated compat barrel.
*/
export {
getChannelStreamingConfigObject,
resolveChannelStreamingNativeTransport,
} from "../channels/streaming.js";
@@ -0,0 +1,20 @@
/**
* Dependency-light realtime-voice activation-name helpers.
*
* Doctor contract closures (e.g. Discord's wake-name migrations) need these
* pure helpers; the broad `realtime-voice` barrel also value-loads the agent
* consult runtime and session graphs, which enumeration must not cold-load.
*/
export {
REALTIME_VOICE_ACTIVATION_NAME_MAX_WORDS,
isSupportedRealtimeVoiceActivationName,
matchRealtimeVoiceActivationName,
normalizeRealtimeVoiceActivationName,
normalizeRealtimeVoiceActivationNamePrefix,
normalizeSupportedRealtimeVoiceActivationName,
realtimeVoiceActivationNameWordCount,
sortRealtimeVoiceActivationNames,
type RealtimeVoiceActivationNameEdge,
type RealtimeVoiceActivationNameMatchKind,
type RealtimeVoiceActivationNameTranscriptResult,
} from "../talk/activation-name.js";
@@ -59,6 +59,7 @@ const MEMORY_HOST_SDK_EXPORTS = [
const MEMORY_HOST_SDK_ALLOWED_CORE_BRIDGE_FILES = [
"packages/memory-host-sdk/src/host/config-utils.ts",
"packages/memory-host-sdk/src/host/openclaw-runtime-auth.ts",
"packages/memory-host-sdk/src/host/openclaw-runtime-kysely.ts",
"packages/memory-host-sdk/src/host/openclaw-runtime-network.ts",
"packages/memory-host-sdk/src/host/openclaw-runtime-sqlite.ts",
"packages/memory-host-sdk/src/host/openclaw-runtime.ts",
@@ -10,17 +10,74 @@ import { loadBundledPluginManifestRegistry } from "./manifest-registry.js";
const REPO_ROOT = path.resolve(import.meta.dirname, "../..");
const SOURCE_MODULE_EXTENSIONS = [".ts", ".mts", ".cts", ".js", ".mjs", ".cjs"] as const;
const FORBIDDEN_SPECIFIER = "openclaw/plugin-sdk/agent-runtime";
type ClosureKind = "doctor-contract" | "legacy-setup";
// Static value imports only; type-only and lazy dynamic imports of these stay allowed.
const FORBIDDEN_SPECIFIER_REASONS = new Map([
// Rules scoped to "doctor-contract" protect doctor enumeration cold-load cost;
// legacy-setup closures (telegram sent-message-cache, discord thread-bindings.state)
// still share sync runtime modules with these barrels and are a named follow-up.
const FORBIDDEN_SPECIFIER_RULES = new Map<string, { reason: string; kinds: Set<ClosureKind> }>([
[
FORBIDDEN_SPECIFIER,
"the deprecated broad barrel makes doctor enumeration cold-load the core agents graph; " +
"use openclaw/plugin-sdk/agent-scope-runtime or another focused subpath",
{
reason:
"the deprecated broad barrel makes doctor enumeration cold-load the core agents graph; " +
"use openclaw/plugin-sdk/agent-scope-runtime or another focused subpath",
kinds: new Set(["doctor-contract", "legacy-setup"]),
},
],
[
"openclaw/plugin-sdk/runtime-doctor",
"the heavy doctor barrel makes doctor enumeration cold-load the state-db/kysely graph; " +
"use openclaw/plugin-sdk/runtime-doctor-migrations, or defer heavy helpers behind a dynamic import",
{
reason:
"the heavy doctor barrel makes doctor enumeration cold-load the state-db/kysely graph; " +
"use openclaw/plugin-sdk/runtime-doctor-migrations, or defer heavy helpers behind a dynamic import",
kinds: new Set(["doctor-contract", "legacy-setup"]),
},
],
[
"openclaw/plugin-sdk/session-store-runtime",
{
reason:
"the session-store barrel makes doctor enumeration cold-load the session-accessor/kysely graph; " +
"defer it behind a dynamic import inside async migration bodies",
kinds: new Set(["doctor-contract"]),
},
],
[
"openclaw/plugin-sdk/logging-core",
{
reason:
"the logging barrel makes doctor enumeration cold-load the diagnostic/config graph; " +
"use openclaw/plugin-sdk/security-runtime for redaction helpers",
kinds: new Set(["doctor-contract", "legacy-setup"]),
},
],
[
"openclaw/plugin-sdk/realtime-voice",
{
reason:
"the realtime-voice barrel makes doctor enumeration cold-load the agent-consult/session graph; " +
"use openclaw/plugin-sdk/realtime-voice-activation for activation-name helpers",
kinds: new Set(["doctor-contract", "legacy-setup"]),
},
],
[
"openclaw/plugin-sdk/channel-outbound",
{
reason:
"the channel-outbound barrel makes doctor enumeration cold-load the reply-pipeline/channel-registry graph; " +
"use openclaw/plugin-sdk/channel-streaming-config for streaming config helpers",
kinds: new Set(["doctor-contract"]),
},
],
[
"openclaw/plugin-sdk/memory-host-core",
{
reason:
"the memory-host barrel makes doctor enumeration cold-load the event-store/kysely graph; " +
"use openclaw/plugin-sdk/agent-scope-runtime for agent scope resolvers",
kinds: new Set(["doctor-contract", "legacy-setup"]),
},
],
]);
const LEGACY_SETUP_PROPERTIES = new Set([
@@ -33,6 +90,7 @@ type ClosureEntry = {
pluginId: string;
pluginRoot: string;
entryPath: string;
kind: ClosureKind;
};
type ModuleReference = ReturnType<typeof collectModuleReferencesFromSource>[number];
@@ -148,7 +206,7 @@ function collectStaticValueReferences(filePath: string, source: string): ModuleR
return collectModuleReferencesFromSource(source, {
fileName: filePath,
acceptSpecifier: (specifier) =>
FORBIDDEN_SPECIFIER_REASONS.has(specifier) || specifier.startsWith("."),
FORBIDDEN_SPECIFIER_RULES.has(specifier) || specifier.startsWith("."),
}).filter((reference) =>
staticValueReferenceKeys.has(`${reference.kind}\0${reference.line}\0${reference.specifier}`),
);
@@ -164,7 +222,12 @@ function collectClosureEntries(): ClosureEntry[] {
const pluginRoot = path.resolve(record.rootDir);
const doctorContractPath = resolvePluginDoctorContractArtifactPath(pluginRoot);
if (doctorContractPath) {
entries.push({ pluginId: record.id, pluginRoot, entryPath: doctorContractPath });
entries.push({
pluginId: record.id,
pluginRoot,
entryPath: doctorContractPath,
kind: "doctor-contract",
});
}
if (record.channels.length === 0) {
@@ -185,7 +248,7 @@ function collectClosureEntries(): ClosureEntry[] {
for (const specifier of collectLegacySetupSpecifiers(setupEntryPath)) {
const entryPath = resolveRelativeSourceModule(setupEntryPath, specifier);
if (entryPath && isInsideRoot(pluginRoot, entryPath)) {
entries.push({ pluginId: record.id, pluginRoot, entryPath });
entries.push({ pluginId: record.id, pluginRoot, entryPath, kind: "legacy-setup" });
}
}
}
@@ -205,10 +268,10 @@ function collectForbiddenClosureImports(entry: ClosureEntry): string[] {
visited.add(filePath);
const source = fs.readFileSync(filePath, "utf8");
for (const reference of collectStaticValueReferences(filePath, source)) {
const reason = FORBIDDEN_SPECIFIER_REASONS.get(reference.specifier);
if (reason) {
const rule = FORBIDDEN_SPECIFIER_RULES.get(reference.specifier);
if (rule?.kinds.has(entry.kind)) {
violations.push(
`${entry.pluginId}: ${formatRepoPath(filePath)}:${reference.line} imports ${reference.specifier}; ${reason}`,
`${entry.pluginId}: ${formatRepoPath(filePath)}:${reference.line} imports ${reference.specifier}; ${rule.reason}`,
);
continue;
}