fix(memory): keep wiki search inside protected recall visibility (#118265)

* fix(memory): unify wiki session authorization

* build: expose memory core boundary declarations
This commit is contained in:
Peter Steinberger
2026-08-02 18:22:17 -07:00
committed by GitHub
parent c965909a30
commit cbd4b8dec8
16 changed files with 204 additions and 211 deletions

View File

@@ -15,3 +15,4 @@ export { filterRecallEntriesWithinLookback } from "./src/dreaming-phases.js";
export { previewRemHarness } from "./src/rem-harness.js";
export type { PreviewRemHarnessOptions, PreviewRemHarnessResult } from "./src/rem-harness.js";
export { configureMemoryCoreDreamingState } from "./src/dreaming-state.js";
export { filterMemorySearchHitsBySessionVisibility } from "./src/session-search-visibility.js";

View File

@@ -4,6 +4,9 @@
"private": true,
"description": "OpenClaw core memory search plugin",
"type": "module",
"exports": {
"./api.js": "./api.ts"
},
"dependencies": {
"chokidar": "5.0.0",
"json5": "2.2.3",

View File

@@ -253,6 +253,12 @@ describe("filterMemorySearchHitsBySessionVisibility for QMD", () => {
sessionFile: "/tmp/sessions/current.jsonl",
chatType: "direct",
},
"agent:main:visible-export": {
sessionId: "visible-export",
updatedAt: 1,
sessionFile: "/tmp/sessions/visible-export.jsonl",
chatType: "direct",
},
"agent:main:explicit:laptop": {
sessionId: "actual-session-id",
updatedAt: 1,
@@ -267,10 +273,12 @@ describe("filterMemorySearchHitsBySessionVisibility for QMD", () => {
chatType: "group",
},
};
const searchPath = "qmd/sessions-main/shared-transcript-export.md";
// The filename resolves to an allowed decoy session; only the attached QMD
// identity reveals that the actual transcript also has a shared alias.
const searchPath = "qmd/sessions-main/visible-export.md";
const indexPath = await createQmdArtifactIndex({
agentId: "main",
artifactPath: "shared-transcript-export.md",
artifactPath: "visible-export.md",
collection: "sessions-main",
searchPath,
sessionId: "actual-session-id",
@@ -285,17 +293,17 @@ describe("filterMemorySearchHitsBySessionVisibility for QMD", () => {
endLine: 2,
},
{
artifactPath: "shared-transcript-export.md",
artifactPath: "visible-export.md",
collection: "sessions-main",
indexPath,
searchPath,
},
);
const cfg = asOpenClawConfig({ tools: { sessions: { visibility: "self" } } });
const cfg = asOpenClawConfig({ tools: { sessions: { visibility: "all" } } });
const filtered = await filterMemorySearchHitsBySessionVisibility({
cfg,
requesterSessionKey: "agent:main:telegram:direct:owner",
requesterSessionKey: "agent:main:telegram:direct:owner:active-memory:abcdef123456",
sandboxed: false,
hits: [hit],
conversationRecall: {

View File

@@ -164,6 +164,8 @@ export async function filterMemorySearchHitsBySessionVisibility(params: {
sandboxed: boolean;
hits: MemorySearchResult[];
conversationRecall?: ConversationRecallContext;
/** Trusted control-plane calls may authorize only hits already scoped to this agent. */
trustedAgentScope?: boolean;
}): Promise<MemorySearchResult[]> {
const visibility = resolveEffectiveSessionToolsVisibility({
cfg: params.cfg,
@@ -192,6 +194,9 @@ export async function filterMemorySearchHitsBySessionVisibility(params: {
);
const conversationRecall = params.conversationRecall;
const trustedAgentScope = Boolean(
params.trustedAgentScope && scopedAgentId && !params.requesterSessionKey && !conversationRecall,
);
const anchorSessionKey = conversationRecall?.anchorSessionKey.trim();
const recallAgentId = anchorSessionKey
? resolveSessionAgentId({ sessionKey: anchorSessionKey, config: params.cfg })
@@ -235,7 +240,7 @@ export async function filterMemorySearchHitsBySessionVisibility(params: {
scopedAgentId && isGlobalSessionKeyForSharedScope(params.cfg, key)
? `agent:${scopedAgentId}:global`
: key;
return guard?.check(visibilityKey).allowed === true;
return trustedAgentScope || guard?.check(visibilityKey).allowed === true;
}
const candidateEntry = combinedSessionStore[key];
// Canonical and legacy alias keys can identify one transcript. Exclude the
@@ -290,7 +295,7 @@ export async function filterMemorySearchHitsBySessionVisibility(params: {
}
continue;
}
if (!params.requesterSessionKey || (!guard && !conversationRecall)) {
if (!trustedAgentScope && (!params.requesterSessionKey || (!guard && !conversationRecall))) {
continue;
}
const artifactIdentity = readQmdSessionArtifactIdentity(hit);

View File

@@ -154,6 +154,34 @@ describe("memory-wiki plugin", () => {
}
});
it("forwards protected recall authorization to wiki search", () => {
const { api, registerTool } = createPluginApi();
plugin.register(api);
const registration = registerTool.mock.calls.find((call) => call[1]?.name === "wiki_search");
const factory = registration?.[0];
const conversationRecall = {
anchorSessionKey: "agent:main:telegram:direct:owner",
scope: "same-agent-private",
corpus: "sessions",
} as const;
expect(
factory?.({
agentId: "main",
sessionKey: "agent:main:telegram:direct:owner:active-memory:abcdef123456",
sandboxed: false,
conversationRecall,
}),
).toMatchObject({
testMemoryContext: {
agentId: "main",
agentSessionKey: "agent:main:telegram:direct:owner:active-memory:abcdef123456",
sandboxed: false,
conversationRecall,
},
});
});
it("activates an initialized legacy vault before an external compile", async () => {
const rootDir = await createTempDir("memory-wiki-index-legacy-vault-");
await fs.mkdir(path.join(rootDir, ".openclaw-wiki"), { recursive: true });

View File

@@ -202,6 +202,7 @@ export default definePluginEntry({
agentId: resolved.config.agentId ?? ctx.agentId,
agentSessionKey: ctx.sessionKey,
sandboxed: ctx.sandboxed,
conversationRecall: ctx.conversationRecall,
});
},
{ name: "wiki_search" },

View File

@@ -12,6 +12,7 @@
"zod": "4.4.3"
},
"devDependencies": {
"@openclaw/memory-core": "workspace:*",
"@openclaw/plugin-sdk": "workspace:*",
"openclaw": "workspace:*"
},

View File

@@ -2,6 +2,8 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { filterMemorySearchHitsBySessionVisibility } from "@openclaw/memory-core/api.js";
import type { MemorySearchResult } from "openclaw/plugin-sdk/memory-core-host-runtime-files";
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../api.js";
import { compileMemoryWikiVault } from "./compile.js";
@@ -29,6 +31,8 @@ vi.mock("openclaw/plugin-sdk/memory-host-search", () => ({
getActiveMemorySearchManager: getActiveMemorySearchManagerMock,
}));
vi.mock("@openclaw/memory-core/api.js", { spy: true });
vi.mock("openclaw/plugin-sdk/memory-host-core", () => ({
resolveDefaultAgentId: resolveDefaultAgentIdMock,
resolveSessionAgentId: resolveSessionAgentIdMock,
@@ -75,6 +79,7 @@ beforeEach(() => {
loadCombinedSessionStoreForGatewayMock.mockReturnValue({ storePath: "(test)", store: {} });
resolveDefaultAgentIdMock.mockClear();
resolveSessionAgentIdMock.mockClear();
vi.mocked(filterMemorySearchHitsBySessionVisibility).mockClear();
});
beforeAll(async () => {
@@ -932,6 +937,60 @@ describe("searchMemoryWiki", () => {
]);
});
it("delegates protected QMD recall with the raw hit identity intact", async () => {
const { config } = await createQueryVault({
initialize: true,
config: { search: { backend: "shared", corpus: "memory" } },
});
const appConfig = createSessionVisibilityAppConfig();
const conversationRecall = {
anchorSessionKey: "agent:main:telegram:direct:owner",
scope: "same-agent-private",
corpus: "sessions",
} as const;
const requesterSessionKey = "agent:main:telegram:direct:owner:active-memory:abcdef123456";
const qmdIdentity = Symbol("qmd-identity");
const qmdHit: MemorySearchResult = {
path: "qmd/sessions-main/visible-export.md",
startLine: 1,
endLine: 2,
score: 30,
snippet: "protected transcript",
source: "sessions",
};
Object.defineProperty(qmdHit, qmdIdentity, {
value: { agentId: "peer", sessionId: "peer-session" },
});
const manager = createMemoryManager({ searchResults: [qmdHit] });
getActiveMemorySearchManagerMock.mockResolvedValue({ manager });
vi.mocked(filterMemorySearchHitsBySessionVisibility).mockResolvedValueOnce([]);
const results = await searchMemoryWiki({
config,
appConfig,
agentId: "main",
agentSessionKey: requesterSessionKey,
sandboxed: false,
conversationRecall,
query: "protected",
});
expect(results).toStrictEqual([]);
expect(filterMemorySearchHitsBySessionVisibility).toHaveBeenCalledWith({
cfg: appConfig,
agentId: "main",
requesterSessionKey,
sandboxed: false,
hits: [qmdHit],
conversationRecall,
trustedAgentScope: false,
});
expect(Reflect.get(qmdHit, qmdIdentity)).toEqual({
agentId: "peer",
sessionId: "peer-session",
});
});
it("keeps QMD archived session search hits inside visibility policy", async () => {
const { config } = await createQueryVault({
initialize: true,

View File

@@ -1,19 +1,11 @@
// Memory Wiki plugin module implements query behavior.
import fs from "node:fs/promises";
import path from "node:path";
import { filterMemorySearchHitsBySessionVisibility } from "@openclaw/memory-core/api.js";
import type { MemorySearchResult } from "openclaw/plugin-sdk/memory-core-host-runtime-files";
import { resolveDefaultAgentId, resolveSessionAgentId } from "openclaw/plugin-sdk/memory-host-core";
import { getActiveMemorySearchManager } from "openclaw/plugin-sdk/memory-host-search";
import {
extractTranscriptIdentityFromSessionsMemoryHit,
loadCombinedSessionStoreForGateway,
resolveTranscriptStemToSessionKeys,
} from "openclaw/plugin-sdk/session-transcript-hit";
import {
createAgentToAgentPolicy,
createSessionVisibilityGuard,
resolveEffectiveSessionToolsVisibility,
} from "openclaw/plugin-sdk/session-visibility";
import type { OpenClawPluginToolContext } from "openclaw/plugin-sdk/plugin-entry";
import {
normalizeLowercaseStringOrEmpty,
uniqueStrings,
@@ -171,6 +163,8 @@ type QuerySearchOverrides = {
searchCorpus?: WikiSearchCorpus;
};
type ConversationRecallContext = NonNullable<OpenClawPluginToolContext["conversationRecall"]>;
function sortWikiSearchResults(results: WikiSearchResult[]): WikiSearchResult[] {
return results.toSorted((left, right) => {
if (left.score !== right.score) {
@@ -1194,184 +1188,6 @@ function toMemoryWikiSearchResult(
};
}
async function filterMemoryWikiSearchHitsBySessionVisibility(params: {
cfg: OpenClawConfig;
agentId: string | undefined;
requesterSessionKey: string | undefined;
sandboxed: boolean;
hits: MemorySearchResult[];
}): Promise<MemorySearchResult[]> {
if (!params.hits.some((hit) => hit.source === "sessions")) {
return params.hits;
}
const canReadSessionPath = await createSessionMemoryPathVisibilityChecker({
cfg: params.cfg,
agentId: params.agentId,
requesterSessionKey: params.requesterSessionKey,
sandboxed: params.sandboxed,
});
return filterMemoryWikiSearchHitsWithSessionVisibility({
canReadSessionPath,
hits: params.hits,
});
}
type SessionMemoryPathVisibilityChecker = (relPath: string) => boolean;
function filterSessionKeysByScopedAgent(params: {
cfg: OpenClawConfig;
keys: string[];
scopedAgentId: string | undefined;
}): string[] {
const scopedAgentId = normalizeLowercaseStringOrEmpty(params.scopedAgentId);
if (!scopedAgentId) {
return params.keys;
}
return params.keys.filter((key) => {
if (params.cfg.session?.scope === "global" && key.trim().toLowerCase() === "global") {
return true;
}
const ownerAgentId = resolveSessionAgentId({
sessionKey: key,
config: params.cfg,
});
return normalizeLowercaseStringOrEmpty(ownerAgentId) === scopedAgentId;
});
}
async function createSessionMemoryPathVisibilityChecker(params: {
cfg: OpenClawConfig;
agentId: string | undefined;
requesterSessionKey: string | undefined;
sandboxed: boolean;
}): Promise<SessionMemoryPathVisibilityChecker> {
const visibility = resolveEffectiveSessionToolsVisibility({
cfg: params.cfg,
sandboxed: params.sandboxed,
});
const a2aPolicy = createAgentToAgentPolicy(params.cfg);
const requesterAgentId = params.requesterSessionKey
? resolveSessionAgentId({
sessionKey: params.requesterSessionKey,
config: params.cfg,
})
: undefined;
const scopedAgentId = params.agentId?.trim() || requesterAgentId;
const guard = params.requesterSessionKey
? await createSessionVisibilityGuard({
action: "history",
requesterSessionKey: params.requesterSessionKey,
visibility,
a2aPolicy,
})
: null;
const { store: combinedSessionStore } = loadCombinedSessionStoreForGateway(
params.cfg,
scopedAgentId ? { agentId: scopedAgentId } : {},
);
return (relPath) => {
const identity = extractTranscriptIdentityFromSessionsMemoryHit(relPath);
if (!identity) {
return false;
}
const isQmdSessionPath = relPath.replace(/\\/g, "/").startsWith("qmd/");
const normalizedScopedAgentId = normalizeLowercaseStringOrEmpty(scopedAgentId);
const normalizedOwnerAgentId = normalizeLowercaseStringOrEmpty(identity.ownerAgentId);
if (
normalizedScopedAgentId &&
normalizedOwnerAgentId &&
normalizedOwnerAgentId !== normalizedScopedAgentId
) {
return false;
}
const sameAgentLiveOwnerId =
!identity.archived &&
normalizedScopedAgentId &&
normalizedOwnerAgentId === normalizedScopedAgentId
? normalizedOwnerAgentId
: undefined;
const archivedOwnerMatchesScope = Boolean(
identity.archived &&
((identity.ownerAgentId &&
(!normalizedScopedAgentId || normalizedOwnerAgentId === normalizedScopedAgentId)) ||
(isQmdSessionPath && scopedAgentId)),
);
const archivedOwnerAgentId = archivedOwnerMatchesScope
? (identity.ownerAgentId ?? scopedAgentId)
: undefined;
const liveKeys = identity.liveStem
? resolveTranscriptStemToSessionKeys({
store: combinedSessionStore,
stem: identity.liveStem,
allowQmdSlugFallback: false,
})
: [];
const resolvedKeys =
liveKeys.length > 0
? liveKeys
: resolveTranscriptStemToSessionKeys({
store: combinedSessionStore,
stem: identity.stem,
allowQmdSlugFallback: isQmdSessionPath && !identity.archived,
...(archivedOwnerAgentId ? { archivedOwnerAgentId } : {}),
});
const keys = filterSessionKeysByScopedAgent({
cfg: params.cfg,
scopedAgentId,
keys: resolvedKeys,
});
if (keys.length === 0) {
const agentWideVisibility = visibility === "agent" || visibility === "all";
return Boolean(sameAgentLiveOwnerId && agentWideVisibility);
}
if (!guard) {
return Boolean(scopedAgentId);
}
return keys.some((key) => guard.check(key).allowed);
};
}
function filterMemoryWikiSearchHitsWithSessionVisibility(params: {
canReadSessionPath: SessionMemoryPathVisibilityChecker;
hits: MemorySearchResult[];
}): MemorySearchResult[] {
const next: MemorySearchResult[] = [];
for (const hit of params.hits) {
if (hit.source !== "sessions") {
next.push(hit);
continue;
}
if (params.canReadSessionPath(hit.path)) {
next.push(hit);
}
}
return next;
}
function canReadSessionMemoryPath(params: {
canReadSessionPath: SessionMemoryPathVisibilityChecker;
relPath: string;
}): boolean {
// Reuses the search filter with a synthetic hit; update this if the filter needs more than path/source.
const filtered = filterMemoryWikiSearchHitsWithSessionVisibility({
canReadSessionPath: params.canReadSessionPath,
hits: [
{
path: params.relPath,
startLine: 1,
endLine: 1,
score: 0,
snippet: "",
source: "sessions",
},
],
});
return filtered.length > 0;
}
async function searchWikiCorpus(params: {
config: ResolvedMemoryWikiConfig;
query: string;
@@ -1449,6 +1265,7 @@ export async function searchMemoryWiki(params: {
agentId?: string;
agentSessionKey?: string;
sandboxed?: boolean;
conversationRecall?: ConversationRecallContext;
query: string;
maxResults?: number;
searchBackend?: WikiSearchBackend;
@@ -1496,12 +1313,14 @@ export async function searchMemoryWiki(params: {
shouldEnforceSessionVisibility(params) &&
rawMemoryResults.some((hit) => hit.source === "sessions")
) {
rawMemoryResults = await filterMemoryWikiSearchHitsBySessionVisibility({
rawMemoryResults = await filterMemorySearchHitsBySessionVisibility({
cfg: params.appConfig,
agentId: params.agentId,
requesterSessionKey: params.agentSessionKey,
sandboxed: params.sandboxed === true,
hits: rawMemoryResults,
conversationRecall: params.conversationRecall,
trustedAgentScope: !params.agentSessionKey && Boolean(params.agentId?.trim()),
});
}
const memoryResults = rawMemoryResults.map((result) => toMemoryWikiSearchResult(result, mode));
@@ -1591,27 +1410,35 @@ export async function getMemoryWikiPage(params: {
}
const lookupCandidates = buildLookupCandidates(params.lookup);
const canReadSessionPath =
const visibleSessionPaths =
params.appConfig &&
shouldEnforceSessionVisibility(params) &&
lookupCandidates.some((relPath) => isSessionMemoryPath(relPath))
? await createSessionMemoryPathVisibilityChecker({
cfg: params.appConfig,
agentId: params.agentId,
requesterSessionKey: params.agentSessionKey,
sandboxed: params.sandboxed === true,
})
? new Set(
(
await filterMemorySearchHitsBySessionVisibility({
cfg: params.appConfig,
agentId: params.agentId,
requesterSessionKey: params.agentSessionKey,
sandboxed: params.sandboxed === true,
trustedAgentScope: !params.agentSessionKey && Boolean(params.agentId?.trim()),
hits: lookupCandidates
.filter((relPath) => isSessionMemoryPath(relPath))
.map((relPath) => ({
path: relPath,
startLine: 1,
endLine: 1,
score: 0,
snippet: "",
source: "sessions" as const,
})),
})
).map((hit) => hit.path),
)
: null;
for (const relPath of lookupCandidates) {
if (
canReadSessionPath &&
isSessionMemoryPath(relPath) &&
!canReadSessionMemoryPath({
canReadSessionPath,
relPath,
})
) {
if (visibleSessionPaths && isSessionMemoryPath(relPath) && !visibleSessionPaths.has(relPath)) {
continue;
}

View File

@@ -1,6 +1,7 @@
// Memory Wiki plugin module implements tool behavior.
import path from "node:path";
import { optionalFiniteNumberSchema } from "openclaw/plugin-sdk/channel-actions";
import type { OpenClawPluginToolContext } from "openclaw/plugin-sdk/plugin-entry";
import { Type } from "typebox";
import type { AnyAgentTool, OpenClawConfig } from "../api.js";
import { applyMemoryWikiMutation, normalizeMemoryWikiMutationInput } from "./apply.js";
@@ -112,6 +113,7 @@ type WikiToolMemoryContext = {
agentId?: string;
agentSessionKey?: string;
sandboxed?: boolean;
conversationRecall?: OpenClawPluginToolContext["conversationRecall"];
};
export function createWikiStatusTool(
@@ -165,6 +167,7 @@ export function createWikiSearchTool(
agentId: memoryContext.agentId,
agentSessionKey: memoryContext.agentSessionKey,
sandboxed: memoryContext.sandboxed,
conversationRecall: memoryContext.conversationRecall,
query: params.query,
maxResults: params.maxResults,
...(params.backend ? { searchBackend: params.backend } : {}),

View File

@@ -554,6 +554,9 @@
"@openclaw/qa-channel/api.js": [
"../dist/plugin-sdk/extensions/qa-channel/api.d.ts"
],
"@openclaw/memory-core/api.js": [
"../dist/plugin-sdk/extensions/memory-core/api.d.ts"
],
"@openclaw/matrix/test-api.js": [
"../dist/plugin-sdk/extensions/matrix/test-api.d.ts"
],

View File

@@ -548,6 +548,9 @@
"@openclaw/qa-channel/api.js": [
"../../dist/plugin-sdk/extensions/qa-channel/api.d.ts"
],
"@openclaw/memory-core/api.js": [
"../../dist/plugin-sdk/extensions/memory-core/api.d.ts"
],
"@openclaw/ai": [
"../../dist/plugin-sdk/packages/ai/src/index.d.ts"
],

3
pnpm-lock.yaml generated
View File

@@ -1282,6 +1282,9 @@ importers:
specifier: 4.4.3
version: 4.4.3
devDependencies:
'@openclaw/memory-core':
specifier: workspace:*
version: link:../memory-core
'@openclaw/plugin-sdk':
specifier: workspace:*
version: link:../../packages/plugin-sdk

View File

@@ -70,6 +70,7 @@ export const EXTENSION_PACKAGE_BOUNDARY_BASE_PATHS = {
"openclaw/plugin-sdk/secret-ref-runtime": ["../dist/plugin-sdk/secret-ref-runtime.d.ts"],
"openclaw/plugin-sdk/ssrf-runtime": ["../dist/plugin-sdk/ssrf-runtime.d.ts"],
"@openclaw/qa-channel/api.js": ["../dist/plugin-sdk/extensions/qa-channel/api.d.ts"],
"@openclaw/memory-core/api.js": ["../dist/plugin-sdk/extensions/memory-core/api.d.ts"],
"@openclaw/matrix/test-api.js": ["../dist/plugin-sdk/extensions/matrix/test-api.d.ts"],
"@openclaw/discord/api.js": ["../dist/plugin-sdk/extensions/discord/api.d.ts"],
"@openclaw/slack/api.js": ["../dist/plugin-sdk/extensions/slack/api.d.ts"],

View File

@@ -274,6 +274,13 @@ const QA_CHANNEL_DTS_INPUTS = [
];
const QA_CHANNEL_DTS_STAMP = "dist/plugin-sdk/extensions/qa-channel/.boundary-dts.stamp";
const QA_CHANNEL_DTS_REQUIRED_OUTPUTS = ["dist/plugin-sdk/extensions/qa-channel/api.d.ts"];
const MEMORY_CORE_DTS_INPUTS = [
"extensions/memory-core/api.ts",
"extensions/memory-core/src",
"extensions/memory-core/tsconfig.json",
];
const MEMORY_CORE_DTS_STAMP = "dist/plugin-sdk/extensions/memory-core/.boundary-dts.stamp";
const MEMORY_CORE_DTS_REQUIRED_OUTPUTS = ["dist/plugin-sdk/extensions/memory-core/api.d.ts"];
const MATRIX_DTS_INPUTS = [
"extensions/matrix/test-api.ts",
"extensions/matrix/src",
@@ -768,6 +775,12 @@ async function main(argv = process.argv.slice(2)) {
outputPaths: [QA_CHANNEL_DTS_STAMP, ...QA_CHANNEL_DTS_REQUIRED_OUTPUTS],
includeFile: isRelevantTypeInput,
}) && !hasMissingOutput(QA_CHANNEL_DTS_REQUIRED_OUTPUTS);
const memoryCoreDtsFresh =
isArtifactSetFresh({
inputPaths: MEMORY_CORE_DTS_INPUTS,
outputPaths: [MEMORY_CORE_DTS_STAMP, ...MEMORY_CORE_DTS_REQUIRED_OUTPUTS],
includeFile: isRelevantTypeInput,
}) && !hasMissingOutput(MEMORY_CORE_DTS_REQUIRED_OUTPUTS);
const matrixDtsFresh =
isArtifactSetFresh({
inputPaths: MATRIX_DTS_INPUTS,
@@ -862,6 +875,36 @@ async function main(argv = process.argv.slice(2)) {
} else {
process.stdout.write("[qa-channel boundary dts] fresh; skipping\n");
}
if (!memoryCoreDtsFresh) {
removeStaleIncrementalState({
tsBuildInfoPath: "dist/plugin-sdk/extensions/memory-core/.tsbuildinfo",
});
dependentSteps.push({
label: "memory-core boundary dts",
args: [
runTsgoScript,
"-p",
"extensions/memory-core/tsconfig.json",
"--declaration",
"true",
"--emitDeclarationOnly",
"true",
"--noEmit",
"false",
"--outDir",
"dist/plugin-sdk/extensions/memory-core",
"--rootDir",
"extensions/memory-core",
"--tsBuildInfoFile",
"dist/plugin-sdk/extensions/memory-core/.tsbuildinfo",
],
env: { OPENCLAW_TSGO_HEAVY_CHECK_LOCK_HELD: "1" },
timeoutMs: 300_000,
stampPath: MEMORY_CORE_DTS_STAMP,
});
} else {
process.stdout.write("[memory-core boundary dts] fresh; skipping\n");
}
if (!matrixDtsFresh) {
removeStaleIncrementalState({
tsBuildInfoPath: "dist/plugin-sdk/extensions/matrix/.tsbuildinfo",

View File

@@ -203,6 +203,10 @@ export const sharedVitestConfig = {
find: "@openclaw/matrix/test-api.js",
replacement: path.join(repoRoot, "extensions", "matrix", "test-api.ts"),
},
{
find: "@openclaw/memory-core/api.js",
replacement: path.join(repoRoot, "extensions", "memory-core", "api.ts"),
},
{
find: "@openclaw/slack/api.js",
replacement: path.join(repoRoot, "extensions", "slack", "api.ts"),