mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
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:
committed by
GitHub
parent
b7add0b560
commit
c2e7c819f5
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 }>();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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,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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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"
|
||||
],
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
],
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>();
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user