
























@@ -6,17 +6,22 @@ import type { OpenClawConfig } from "../api.js";
66import { compileMemoryWikiVault } from "./compile.js";
77import type { MemoryWikiPluginConfig } from "./config.js";
88import { renderWikiMarkdown } from "./markdown.js";
9-import { getMemoryWikiPage, searchMemoryWiki } from "./query.js";
9+import { getMemoryWikiPage, isSessionMemoryPath, searchMemoryWiki } from "./query.js";
1010import { createMemoryWikiTestHarness } from "./test-helpers.js";
111112-const { getActiveMemorySearchManagerMock, resolveDefaultAgentIdMock, resolveSessionAgentIdMock } =
13-vi.hoisted(() => ({
14-getActiveMemorySearchManagerMock: vi.fn(),
15-resolveDefaultAgentIdMock: vi.fn(() => "main"),
16-resolveSessionAgentIdMock: vi.fn(({ sessionKey }: { sessionKey?: string }) =>
17-sessionKey === "agent:secondary:thread" ? "secondary" : "main",
18-),
19-}));
12+const {
13+ getActiveMemorySearchManagerMock,
14+ loadCombinedSessionStoreForGatewayMock,
15+ resolveDefaultAgentIdMock,
16+ resolveSessionAgentIdMock,
17+} = vi.hoisted(() => ({
18+getActiveMemorySearchManagerMock: vi.fn(),
19+loadCombinedSessionStoreForGatewayMock: vi.fn(),
20+resolveDefaultAgentIdMock: vi.fn(() => "main"),
21+resolveSessionAgentIdMock: vi.fn(({ sessionKey }: { sessionKey?: string }) =>
22+sessionKey === "agent:secondary:thread" ? "secondary" : "main",
23+),
24+}));
20252126vi.mock("openclaw/plugin-sdk/memory-host-search", () => ({
2227getActiveMemorySearchManager: getActiveMemorySearchManagerMock,
@@ -27,13 +32,24 @@ vi.mock("openclaw/plugin-sdk/memory-host-core", () => ({
2732resolveSessionAgentId: resolveSessionAgentIdMock,
2833}));
293435+vi.mock("openclaw/plugin-sdk/session-transcript-hit", async (importOriginal) => {
36+const actual =
37+await importOriginal<typeof import("openclaw/plugin-sdk/session-transcript-hit")>();
38+return {
39+ ...actual,
40+loadCombinedSessionStoreForGateway: loadCombinedSessionStoreForGatewayMock,
41+};
42+});
43+3044const { createVault } = createMemoryWikiTestHarness();
3145let suiteRoot = "";
3246let caseIndex = 0;
33473448beforeEach(() => {
3549getActiveMemorySearchManagerMock.mockReset();
3650getActiveMemorySearchManagerMock.mockResolvedValue({ manager: null, error: "unavailable" });
51+loadCombinedSessionStoreForGatewayMock.mockReset();
52+loadCombinedSessionStoreForGatewayMock.mockReturnValue({ storePath: "(test)", store: {} });
3753resolveDefaultAgentIdMock.mockClear();
3854resolveSessionAgentIdMock.mockClear();
3955});
@@ -68,6 +84,36 @@ function createAppConfig(): OpenClawConfig {
6884} as OpenClawConfig;
6985}
708687+function createSessionVisibilityAppConfig(): OpenClawConfig {
88+return {
89+agents: {
90+defaults: { sandbox: { sessionToolsVisibility: "all" } },
91+list: [{ id: "main", default: true }],
92+},
93+tools: {
94+sessions: { visibility: "self" },
95+},
96+} as OpenClawConfig;
97+}
98+99+function mockSessionTranscriptStore() {
100+loadCombinedSessionStoreForGatewayMock.mockReturnValue({
101+storePath: "(test)",
102+store: {
103+"agent:main:child-session": {
104+sessionId: "child-session",
105+updatedAt: 1,
106+sessionFile: "/tmp/openclaw/child-session.jsonl",
107+},
108+"agent:main:sibling-session": {
109+sessionId: "sibling-session",
110+updatedAt: 2,
111+sessionFile: "/tmp/openclaw/sibling-session.jsonl",
112+},
113+},
114+});
115+}
116+71117function createMemoryManager(overrides?: {
72118searchResults?: Array<{
73119path: string;
@@ -95,6 +141,29 @@ function createMemoryManager(overrides?: {
95141};
96142}
97143144+describe("isSessionMemoryPath", () => {
145+it("classifies all current session storage layouts", () => {
146+for (const relPath of [
147+"sessions/child-session.jsonl",
148+"qmd/sessions/child-session.md",
149+"qmd/sessions-main/child-session.md",
150+"qmd\\sessions-main\\child-session.md",
151+"qmd/sessions",
152+]) {
153+expect(isSessionMemoryPath(relPath)).toBe(true);
154+}
155+156+for (const relPath of [
157+"sessionsx/child-session.jsonl",
158+"qmd/sessionsxxx",
159+"wiki/sessions/foo.md",
160+"wiki\\sessions\\foo.md",
161+]) {
162+expect(isSessionMemoryPath(relPath)).toBe(false);
163+}
164+});
165+});
166+98167describe("searchMemoryWiki", () => {
99168it("finds wiki pages by title and body", async () => {
100169const { rootDir, config } = await createQueryVault({
@@ -634,6 +703,132 @@ describe("searchMemoryWiki", () => {
634703expect(manager.search).toHaveBeenCalledWith("alpha", { maxResults: 5 });
635704});
636705706+it("filters session memory hits outside the caller visibility policy", async () => {
707+const { config } = await createQueryVault({
708+initialize: true,
709+config: {
710+search: { backend: "shared", corpus: "memory" },
711+},
712+});
713+mockSessionTranscriptStore();
714+const manager = createMemoryManager({
715+searchResults: [
716+{
717+path: "sessions/child-session.jsonl",
718+startLine: 1,
719+endLine: 2,
720+score: 30,
721+snippet: "caller transcript",
722+source: "sessions",
723+},
724+{
725+path: "qmd/sessions-main/sibling-session.md",
726+startLine: 3,
727+endLine: 4,
728+score: 20,
729+snippet: "sibling transcript",
730+source: "sessions",
731+},
732+{
733+path: "MEMORY.md",
734+startLine: 5,
735+endLine: 6,
736+score: 10,
737+snippet: "durable memory",
738+source: "memory",
739+},
740+],
741+});
742+getActiveMemorySearchManagerMock.mockResolvedValue({ manager });
743+744+const results = await searchMemoryWiki({
745+ config,
746+appConfig: createSessionVisibilityAppConfig(),
747+agentSessionKey: "agent:main:child-session",
748+sandboxed: true,
749+query: "transcript",
750+maxResults: 10,
751+});
752+753+expect(results.map((result) => result.path)).toEqual([
754+"sessions/child-session.jsonl",
755+"MEMORY.md",
756+]);
757+expect(results.some((result) => result.path.includes("sibling-session"))).toBe(false);
758+});
759+760+it("filters session memory hits for session-bound non-sandboxed callers", async () => {
761+const { config } = await createQueryVault({
762+initialize: true,
763+config: {
764+search: { backend: "shared", corpus: "memory" },
765+},
766+});
767+mockSessionTranscriptStore();
768+const manager = createMemoryManager({
769+searchResults: [
770+{
771+path: "sessions/child-session.jsonl",
772+startLine: 1,
773+endLine: 2,
774+score: 30,
775+snippet: "caller transcript",
776+source: "sessions",
777+},
778+{
779+path: "qmd/sessions-main/sibling-session.md",
780+startLine: 3,
781+endLine: 4,
782+score: 20,
783+snippet: "sibling transcript",
784+source: "sessions",
785+},
786+{
787+path: "MEMORY.md",
788+startLine: 5,
789+endLine: 6,
790+score: 10,
791+snippet: "durable memory",
792+source: "memory",
793+},
794+],
795+});
796+getActiveMemorySearchManagerMock.mockResolvedValue({ manager });
797+798+const results = await searchMemoryWiki({
799+ config,
800+appConfig: createSessionVisibilityAppConfig(),
801+agentSessionKey: "agent:main:child-session",
802+sandboxed: false,
803+query: "transcript",
804+maxResults: 10,
805+});
806+807+expect(results.map((result) => result.path)).toEqual([
808+"sessions/child-session.jsonl",
809+"MEMORY.md",
810+]);
811+expect(results.some((result) => result.path.includes("sibling-session"))).toBe(false);
812+});
813+814+it("requires appConfig for session-bound shared memory searches", async () => {
815+const { config } = await createQueryVault({
816+initialize: true,
817+config: {
818+search: { backend: "shared", corpus: "memory" },
819+},
820+});
821+822+await expect(
823+searchMemoryWiki({
824+ config,
825+agentSessionKey: "agent:main:child-session",
826+sandboxed: true,
827+query: "transcript",
828+}),
829+).rejects.toThrow(/wiki_search requires appConfig/);
830+});
831+637832it("uses the active session agent for shared memory search", async () => {
638833const { config } = await createQueryVault({
639834initialize: true,
@@ -902,6 +1097,89 @@ describe("getMemoryWikiPage", () => {
9021097});
9031098});
90410991100+it("skips session memory reads outside the caller visibility policy", async () => {
1101+const { config } = await createQueryVault({
1102+initialize: true,
1103+config: {
1104+search: { backend: "shared", corpus: "memory" },
1105+},
1106+});
1107+mockSessionTranscriptStore();
1108+const manager = createMemoryManager({
1109+readResult: {
1110+path: "qmd/sessions-main/sibling-session.md",
1111+text: "sibling transcript content",
1112+},
1113+});
1114+getActiveMemorySearchManagerMock.mockResolvedValue({ manager });
1115+1116+const result = await getMemoryWikiPage({
1117+ config,
1118+appConfig: createSessionVisibilityAppConfig(),
1119+agentSessionKey: "agent:main:child-session",
1120+sandboxed: true,
1121+lookup: "qmd/sessions-main/sibling-session.md",
1122+});
1123+1124+expect(result).toBeNull();
1125+expect(manager.readFile).not.toHaveBeenCalled();
1126+});
1127+1128+it("permits session memory reads inside the caller visibility policy", async () => {
1129+const { config } = await createQueryVault({
1130+initialize: true,
1131+config: {
1132+search: { backend: "shared", corpus: "memory" },
1133+},
1134+});
1135+mockSessionTranscriptStore();
1136+const manager = createMemoryManager({
1137+readResult: {
1138+path: "qmd/sessions-main/child-session.md",
1139+text: "own transcript content",
1140+},
1141+});
1142+getActiveMemorySearchManagerMock.mockResolvedValue({ manager });
1143+1144+const result = await getMemoryWikiPage({
1145+ config,
1146+appConfig: createSessionVisibilityAppConfig(),
1147+agentSessionKey: "agent:main:child-session",
1148+sandboxed: true,
1149+lookup: "qmd/sessions-main/child-session.md",
1150+});
1151+1152+expect(result).toMatchObject({
1153+corpus: "memory",
1154+path: "qmd/sessions-main/child-session.md",
1155+content: "own transcript content",
1156+});
1157+expect(manager.readFile).toHaveBeenCalledTimes(1);
1158+expect(manager.readFile).toHaveBeenCalledWith({
1159+relPath: "qmd/sessions-main/child-session.md",
1160+from: 1,
1161+lines: 200,
1162+});
1163+});
1164+1165+it("requires appConfig for session-bound shared memory reads", async () => {
1166+const { config } = await createQueryVault({
1167+initialize: true,
1168+config: {
1169+search: { backend: "shared", corpus: "memory" },
1170+},
1171+});
1172+1173+await expect(
1174+getMemoryWikiPage({
1175+ config,
1176+agentSessionKey: "agent:main:child-session",
1177+sandboxed: true,
1178+lookup: "sessions/child-session.jsonl",
1179+}),
1180+).rejects.toThrow(/wiki_get requires appConfig/);
1181+});
1182+9051183it("uses the active session agent for shared memory reads", async () => {
9061184const { config } = await createQueryVault({
9071185initialize: true,
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。