mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-05 04:42:55 +00:00
* fix(codex): route workspace memory through tools * fix(codex): preserve extra memory bootstrap files * fix(codex): support memory_get-only context routing * fix(codex): only tool-route canonical workspace memory * fix(codex): keep memory fallback for sandbox workspaces
236 lines
7.3 KiB
TypeScript
236 lines
7.3 KiB
TypeScript
import fs from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import type {
|
|
AgentMessage,
|
|
EmbeddedRunAttemptParams,
|
|
} from "openclaw/plugin-sdk/agent-harness-runtime";
|
|
import { describe, expect, it } from "vitest";
|
|
import {
|
|
buildCodexWorkspaceBootstrapContext,
|
|
buildCodexSystemPromptReport,
|
|
readContextEngineThreadBootstrapProjection,
|
|
remapCodexContextFilePath,
|
|
resolveContextEngineBootstrapProjectionDecision,
|
|
shouldProjectMirroredHistoryForCodexStart,
|
|
} from "./attempt-context.js";
|
|
import type { CodexDynamicToolSpec } from "./protocol.js";
|
|
import type { CodexAppServerContextEngineBinding } from "./session-binding.js";
|
|
|
|
describe("Codex app-server attempt context", () => {
|
|
it("returns a run context report without deferred Codex dynamic tool schemas", () => {
|
|
const tools = [
|
|
{
|
|
name: "message",
|
|
description: "Send a message.",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
text: { type: "string" },
|
|
},
|
|
},
|
|
},
|
|
{
|
|
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("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("projects mirrored history for transient native-disabled Codex threads", () => {
|
|
expect(
|
|
shouldProjectMirroredHistoryForCodexStart({
|
|
startupBinding: {
|
|
threadId: "thread-existing",
|
|
dynamicToolsFingerprint: "same-tools",
|
|
} as never,
|
|
dynamicToolsFingerprint: "same-tools",
|
|
historyMessages: [
|
|
{
|
|
role: "user",
|
|
content: "earlier request",
|
|
timestamp: Date.now(),
|
|
} as AgentMessage,
|
|
],
|
|
forceProject: true,
|
|
}),
|
|
).toBe(true);
|
|
|
|
expect(
|
|
shouldProjectMirroredHistoryForCodexStart({
|
|
startupBinding: {
|
|
threadId: "thread-existing",
|
|
dynamicToolsFingerprint: "same-tools",
|
|
} as never,
|
|
dynamicToolsFingerprint: "same-tools",
|
|
historyMessages: [
|
|
{
|
|
role: "assistant",
|
|
content: "earlier response",
|
|
timestamp: Date.now(),
|
|
} as unknown as AgentMessage,
|
|
],
|
|
forceProject: true,
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
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",
|
|
});
|
|
});
|
|
});
|