mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-18 19:01:40 +00:00
* feat(memory-wiki): isolate per-agent vaults Refs #63829. Co-authored-by: SunnyShu <shu.zongyu@xydigit.com> * fix(memory-wiki): scope agent status metadata Refs #103088 and #103196. Co-authored-by: SunnyShu <shu.zongyu@xydigit.com> --------- Co-authored-by: SunnyShu <shu.zongyu@xydigit.com>
283 lines
9.1 KiB
TypeScript
283 lines
9.1 KiB
TypeScript
// Codex tests cover attempt context plugin behavior.
|
|
import fs from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import {
|
|
embeddedAgentLog,
|
|
type EmbeddedRunAttemptParams,
|
|
} from "openclaw/plugin-sdk/agent-harness-runtime";
|
|
import {
|
|
clearMemoryPluginState,
|
|
registerMemoryCapability,
|
|
} from "openclaw/plugin-sdk/memory-host-core";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import {
|
|
buildCodexWorkspaceBootstrapContext,
|
|
buildCodexSystemPromptReport,
|
|
readContextEngineThreadBootstrapProjection,
|
|
readMirroredSessionHistoryMessages,
|
|
remapCodexContextFilePath,
|
|
resolveContextEngineBootstrapProjectionDecision,
|
|
} from "./attempt-context.js";
|
|
import type { CodexDynamicToolSpec } from "./protocol.js";
|
|
import type { CodexAppServerContextEngineBinding } from "./session-binding.js";
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
clearMemoryPluginState();
|
|
});
|
|
|
|
describe("Codex app-server attempt context", () => {
|
|
it("treats missing mirrored session history as empty without hook warning", async () => {
|
|
const warn = vi.spyOn(embeddedAgentLog, "warn").mockImplementation(() => undefined);
|
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "codex-attempt-context-history-"));
|
|
const sessionFile = path.join(dir, "session.jsonl");
|
|
try {
|
|
await expect(
|
|
readMirroredSessionHistoryMessages({
|
|
sessionFile,
|
|
sessionId: "codex-session",
|
|
sessionKey: "codex-session",
|
|
}),
|
|
).resolves.toEqual([]);
|
|
expect(warn).not.toHaveBeenCalled();
|
|
} finally {
|
|
await fs.rm(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("returns a run context report without deferred Codex dynamic tool schemas", () => {
|
|
const tools = [
|
|
{
|
|
type: "function",
|
|
name: "message",
|
|
description: "Send a message.",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
text: { type: "string" },
|
|
},
|
|
},
|
|
},
|
|
{
|
|
type: "namespace",
|
|
name: "openclaw",
|
|
description: "",
|
|
tools: [
|
|
{
|
|
type: "function",
|
|
name: "web_search",
|
|
description: "Search the web.",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
query: { type: "string" },
|
|
},
|
|
},
|
|
deferLoading: true,
|
|
},
|
|
],
|
|
},
|
|
] as CodexDynamicToolSpec[];
|
|
|
|
const report = buildCodexSystemPromptReport({
|
|
attempt: {
|
|
sessionId: "session-1",
|
|
provider: "codex",
|
|
modelId: "gpt-5.4-codex",
|
|
} as EmbeddedRunAttemptParams,
|
|
sessionKey: "agent:main:session-1",
|
|
workspaceDir: path.join("tmp", "workspace"),
|
|
developerInstructions: "test developer instructions",
|
|
workspaceBootstrapContext: {
|
|
bootstrapFiles: [],
|
|
contextFiles: [],
|
|
promptContextFiles: [],
|
|
developerInstructionFiles: [],
|
|
heartbeatReferenceFiles: [],
|
|
},
|
|
skillsPrompt: "",
|
|
tools,
|
|
});
|
|
|
|
expect(report.source).toBe("run");
|
|
expect(report.provider).toBe("codex");
|
|
expect(report.model).toBe("gpt-5.4-codex");
|
|
expect(report.systemPrompt.chars).toBeGreaterThan(0);
|
|
expect(report.systemPrompt.hash).toMatch(/^[a-f0-9]{64}$/u);
|
|
expect(report.skills.hash).toMatch(/^[a-f0-9]{64}$/u);
|
|
|
|
const message = report.tools.entries.find((tool) => tool.name === "message");
|
|
const webSearch = report.tools.entries.find((tool) => tool.name === "web_search");
|
|
expect(message?.schemaChars).toBeGreaterThan(0);
|
|
expect(message?.summaryHash).toMatch(/^[a-f0-9]{64}$/u);
|
|
expect(message?.schemaHash).toMatch(/^[a-f0-9]{64}$/u);
|
|
expect(webSearch?.schemaChars).toBe(0);
|
|
expect(webSearch?.summaryHash).toMatch(/^[a-f0-9]{64}$/u);
|
|
expect(webSearch?.schemaHash).toMatch(/^[a-f0-9]{64}$/u);
|
|
expect(report.tools.schemaChars).toBe(message?.schemaChars);
|
|
});
|
|
|
|
it("keeps MEMORY.md injected when sandbox effective workspace differs", async () => {
|
|
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "codex-memory-workspace-"));
|
|
const sandboxWorkspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "codex-memory-sandbox-"));
|
|
const memorySummary = "Sandboxed turns need bounded memory fallback.";
|
|
await fs.writeFile(path.join(workspaceDir, "MEMORY.md"), memorySummary);
|
|
|
|
const context = await buildCodexWorkspaceBootstrapContext({
|
|
params: {
|
|
sessionId: "session-1",
|
|
sessionKey: "agent:main:session-1",
|
|
config: {
|
|
agents: {
|
|
defaults: {
|
|
workspace: workspaceDir,
|
|
},
|
|
},
|
|
},
|
|
} as EmbeddedRunAttemptParams,
|
|
resolvedWorkspace: workspaceDir,
|
|
effectiveWorkspace: sandboxWorkspaceDir,
|
|
sessionKey: "agent:main:session-1",
|
|
sessionAgentId: "main",
|
|
memoryToolNames: ["memory_search", "memory_get"],
|
|
});
|
|
|
|
expect(context.memoryReferenceFiles).toEqual([]);
|
|
expect(context.promptContext).toContain(memorySummary);
|
|
expect(context.memoryToolRouted).toBe(false);
|
|
});
|
|
|
|
it("passes agent context to Codex memory collaboration guidance", async () => {
|
|
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "codex-agent-memory-"));
|
|
let observedContext:
|
|
| { agentId?: string; agentSessionKey?: string; sandboxed?: boolean }
|
|
| undefined;
|
|
registerMemoryCapability("memory-core", {
|
|
promptBuilder: (context) => {
|
|
observedContext = context;
|
|
return [
|
|
"## Agent Memory",
|
|
`agent=${context.agentId} session=${context.agentSessionKey}`,
|
|
"",
|
|
];
|
|
},
|
|
});
|
|
|
|
try {
|
|
const context = await buildCodexWorkspaceBootstrapContext({
|
|
params: {
|
|
sessionId: "session-1",
|
|
sessionKey: "agent:marketing-agent:session-1",
|
|
config: {
|
|
agents: {
|
|
defaults: { workspace: workspaceDir },
|
|
list: [{ id: "marketing-agent", default: true, workspace: workspaceDir }],
|
|
},
|
|
},
|
|
} as EmbeddedRunAttemptParams,
|
|
resolvedWorkspace: workspaceDir,
|
|
effectiveWorkspace: workspaceDir,
|
|
sessionKey: "agent:marketing-agent:session-1",
|
|
sessionAgentId: "marketing-agent",
|
|
memoryToolNames: ["memory_search", "memory_get"],
|
|
});
|
|
|
|
expect(context.memoryToolRouted).toBe(true);
|
|
expect(observedContext).toMatchObject({
|
|
agentId: "marketing-agent",
|
|
agentSessionKey: "agent:marketing-agent:session-1",
|
|
});
|
|
expect(context.memoryCollaborationInstructions).toContain(
|
|
"agent=marketing-agent session=agent:marketing-agent:session-1",
|
|
);
|
|
} finally {
|
|
await fs.rm(workspaceDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("remaps Codex bootstrap files under dot-prefixed workspace directories", () => {
|
|
expect(
|
|
remapCodexContextFilePath({
|
|
file: {
|
|
path: "/real/workspace/..context/SOUL.md",
|
|
content: "Soul voice goes here.",
|
|
},
|
|
sourceWorkspaceDir: "/real/workspace",
|
|
targetWorkspaceDir: "/sandbox/workspace",
|
|
}),
|
|
).toEqual({
|
|
path: "/sandbox/workspace/..context/SOUL.md",
|
|
content: "Soul voice goes here.",
|
|
});
|
|
expect(
|
|
remapCodexContextFilePath({
|
|
file: {
|
|
path: "/outside/SOUL.md",
|
|
content: "outside",
|
|
},
|
|
sourceWorkspaceDir: "/real/workspace",
|
|
targetWorkspaceDir: "/sandbox/workspace",
|
|
}),
|
|
).toEqual({
|
|
path: "/outside/SOUL.md",
|
|
content: "outside",
|
|
});
|
|
});
|
|
|
|
it("reads and compares thread-bootstrap context-engine projections", () => {
|
|
const projection = readContextEngineThreadBootstrapProjection({
|
|
mode: "thread_bootstrap",
|
|
epoch: " epoch-1 ",
|
|
fingerprint: " fingerprint-1 ",
|
|
});
|
|
expect(projection).toEqual({
|
|
mode: "thread_bootstrap",
|
|
epoch: "epoch-1",
|
|
fingerprint: "fingerprint-1",
|
|
});
|
|
|
|
const expectedBinding = {
|
|
schemaVersion: 1,
|
|
engineId: "lossless",
|
|
policyFingerprint: "policy-v1",
|
|
projection: {
|
|
schemaVersion: 1,
|
|
mode: "thread_bootstrap",
|
|
epoch: "epoch-1",
|
|
fingerprint: "fingerprint-1",
|
|
},
|
|
} satisfies CodexAppServerContextEngineBinding;
|
|
expect(
|
|
resolveContextEngineBootstrapProjectionDecision({
|
|
startupBinding: {
|
|
threadId: "thread-existing",
|
|
dynamicToolsFingerprint: "same-tools",
|
|
contextEngine: expectedBinding,
|
|
} as never,
|
|
expectedBinding,
|
|
projection: projection!,
|
|
dynamicToolsFingerprint: "same-tools",
|
|
}),
|
|
).toEqual({
|
|
project: false,
|
|
reason: "matching-thread-bootstrap-binding",
|
|
});
|
|
expect(
|
|
resolveContextEngineBootstrapProjectionDecision({
|
|
startupBinding: {
|
|
threadId: "thread-existing",
|
|
dynamicToolsFingerprint: "old-tools",
|
|
contextEngine: expectedBinding,
|
|
} as never,
|
|
expectedBinding,
|
|
projection: projection!,
|
|
dynamicToolsFingerprint: "new-tools",
|
|
}),
|
|
).toEqual({
|
|
project: true,
|
|
reason: "dynamic-tools-mismatch",
|
|
});
|
|
});
|
|
});
|