feat: harden cortex bridge workflows
This commit is contained in:
parent
b9fc0b94ca
commit
9d9dd2d77b
@ -304,6 +304,10 @@
|
||||
"test:all": "pnpm lint && pnpm build && pnpm test && pnpm test:e2e && pnpm test:live && pnpm test:docker:all",
|
||||
"test:auth:compat": "vitest run --config vitest.gateway.config.ts src/gateway/server.auth.compat-baseline.test.ts src/gateway/client.test.ts src/gateway/reconnect-gating.test.ts src/gateway/protocol/connect-error-details.test.ts",
|
||||
"test:channels": "vitest run --config vitest.channels.config.ts",
|
||||
"test:ci:cortex": "pnpm exec vitest run src/cli/memory-cli.test.ts src/auto-reply/reply/commands.test.ts src/agents/cortex.test.ts",
|
||||
"test:ci:daemon-windows": "pnpm exec vitest run src/daemon/schtasks.stop.test.ts src/daemon/schtasks.startup-fallback.test.ts",
|
||||
"test:ci:path-windows": "pnpm exec vitest run src/infra/update-global.test.ts src/infra/home-dir.test.ts src/infra/executable-path.test.ts src/infra/exec-approvals-store.test.ts src/infra/pairing-files.test.ts src/infra/stable-node-path.test.ts src/infra/hardlink-guards.test.ts src/infra/exec-allowlist-pattern.test.ts src/security/temp-path-guard.test.ts src/infra/run-node.test.ts",
|
||||
"test:ci:ui-parse": "pnpm exec vitest run ui/src/ui/views/agents-utils.test.ts",
|
||||
"test:coverage": "vitest run --config vitest.unit.config.ts --coverage",
|
||||
"test:docker:all": "pnpm test:docker:live-models && pnpm test:docker:live-gateway && pnpm test:docker:onboard && pnpm test:docker:gateway-network && pnpm test:docker:qr && pnpm test:docker:doctor-switch && pnpm test:docker:plugins && pnpm test:docker:cleanup",
|
||||
"test:docker:cleanup": "bash scripts/test-cleanup-docker.sh",
|
||||
|
||||
@ -37,6 +37,7 @@ import {
|
||||
resolveAgentCortexConfig,
|
||||
resolveAgentCortexModeStatus,
|
||||
resolveAgentCortexPromptContext,
|
||||
resolveAgentTurnCortexContext,
|
||||
resolveCortexChannelTarget,
|
||||
} from "./cortex.js";
|
||||
|
||||
@ -258,6 +259,44 @@ describe("resolveAgentCortexPromptContext", () => {
|
||||
|
||||
expect(result.error).toContain("Cortex graph not found");
|
||||
});
|
||||
|
||||
it("reuses resolved turn status when provided", async () => {
|
||||
getCortexModeOverride.mockResolvedValueOnce(null);
|
||||
previewCortexContext.mockResolvedValueOnce({
|
||||
workspaceDir: "/tmp/openclaw-workspace",
|
||||
graphPath: "/tmp/openclaw-workspace/.cortex/context.json",
|
||||
policy: "technical",
|
||||
maxChars: 1500,
|
||||
context: "## Cortex Context\n- Shipping",
|
||||
});
|
||||
|
||||
const cfg: OpenClawConfig = {
|
||||
agents: {
|
||||
defaults: {
|
||||
cortex: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
list: [{ id: "main" }],
|
||||
},
|
||||
};
|
||||
|
||||
const resolved = await resolveAgentTurnCortexContext({
|
||||
cfg,
|
||||
agentId: "main",
|
||||
workspaceDir: "/tmp/openclaw-workspace",
|
||||
});
|
||||
const result = await resolveAgentCortexPromptContext({
|
||||
cfg,
|
||||
agentId: "main",
|
||||
workspaceDir: "/tmp/openclaw-workspace",
|
||||
promptMode: "full",
|
||||
resolved,
|
||||
});
|
||||
|
||||
expect(result.context).toContain("Shipping");
|
||||
expect(getCortexStatus).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveAgentCortexConflictNotice", () => {
|
||||
@ -309,6 +348,46 @@ describe("resolveAgentCortexConflictNotice", () => {
|
||||
expect(second).toBeNull();
|
||||
});
|
||||
|
||||
it("reuses resolved turn status when checking conflicts", async () => {
|
||||
listCortexMemoryConflicts.mockResolvedValueOnce([
|
||||
{
|
||||
id: "conf_1",
|
||||
type: "temporal_flip",
|
||||
severity: 0.91,
|
||||
summary: "Hiring status changed from active to paused",
|
||||
},
|
||||
]);
|
||||
|
||||
const cfg: OpenClawConfig = {
|
||||
agents: {
|
||||
defaults: {
|
||||
cortex: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
list: [{ id: "main" }],
|
||||
},
|
||||
};
|
||||
|
||||
const resolved = await resolveAgentTurnCortexContext({
|
||||
cfg,
|
||||
agentId: "main",
|
||||
workspaceDir: "/tmp/openclaw-workspace",
|
||||
sessionId: "session-1",
|
||||
channelId: "channel-1",
|
||||
});
|
||||
await resolveAgentCortexConflictNotice({
|
||||
cfg,
|
||||
agentId: "main",
|
||||
workspaceDir: "/tmp/openclaw-workspace",
|
||||
sessionId: "session-1",
|
||||
channelId: "channel-1",
|
||||
resolved,
|
||||
});
|
||||
|
||||
expect(getCortexStatus).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("applies cooldown even when no Cortex conflicts are found", async () => {
|
||||
listCortexMemoryConflicts.mockResolvedValueOnce([]);
|
||||
|
||||
|
||||
@ -8,6 +8,7 @@ import {
|
||||
previewCortexContext,
|
||||
syncCortexCodingContext,
|
||||
type CortexPolicy,
|
||||
type CortexStatus,
|
||||
} from "../memory/cortex.js";
|
||||
import { resolveAgentConfig } from "./agent-scope.js";
|
||||
import {
|
||||
@ -35,6 +36,11 @@ export type ResolvedAgentCortexModeStatus = {
|
||||
maxChars: number;
|
||||
};
|
||||
|
||||
export type ResolvedAgentTurnCortexContext = {
|
||||
config: ResolvedAgentCortexModeStatus;
|
||||
status: CortexStatus;
|
||||
};
|
||||
|
||||
export type AgentCortexConflictNotice = {
|
||||
text: string;
|
||||
conflictId: string;
|
||||
@ -245,30 +251,30 @@ export async function resolveAgentCortexPromptContext(params: {
|
||||
promptMode: "full" | "minimal";
|
||||
sessionId?: string;
|
||||
channelId?: string;
|
||||
resolved?: ResolvedAgentTurnCortexContext | null;
|
||||
}): Promise<AgentCortexPromptContextResult> {
|
||||
if (!params.cfg || params.promptMode !== "full") {
|
||||
return {};
|
||||
}
|
||||
const cortex = await resolveAgentCortexModeStatus({
|
||||
cfg: params.cfg,
|
||||
agentId: params.agentId,
|
||||
sessionId: params.sessionId,
|
||||
channelId: params.channelId,
|
||||
});
|
||||
if (!cortex) {
|
||||
const resolved =
|
||||
params.resolved ??
|
||||
(await resolveAgentTurnCortexContext({
|
||||
cfg: params.cfg,
|
||||
agentId: params.agentId,
|
||||
workspaceDir: params.workspaceDir,
|
||||
sessionId: params.sessionId,
|
||||
channelId: params.channelId,
|
||||
}));
|
||||
if (!resolved) {
|
||||
return {};
|
||||
}
|
||||
try {
|
||||
const status = await getCortexStatus({
|
||||
workspaceDir: params.workspaceDir,
|
||||
graphPath: cortex.graphPath,
|
||||
});
|
||||
const preview = await previewCortexContext({
|
||||
workspaceDir: params.workspaceDir,
|
||||
graphPath: cortex.graphPath,
|
||||
policy: cortex.mode,
|
||||
maxChars: cortex.maxChars,
|
||||
status,
|
||||
graphPath: resolved.config.graphPath,
|
||||
policy: resolved.config.mode,
|
||||
maxChars: resolved.config.maxChars,
|
||||
status: resolved.status,
|
||||
});
|
||||
return preview.context ? { context: preview.context } : {};
|
||||
} catch (error) {
|
||||
@ -278,6 +284,32 @@ export async function resolveAgentCortexPromptContext(params: {
|
||||
}
|
||||
}
|
||||
|
||||
export async function resolveAgentTurnCortexContext(params: {
|
||||
cfg?: OpenClawConfig;
|
||||
agentId: string;
|
||||
workspaceDir: string;
|
||||
sessionId?: string;
|
||||
channelId?: string;
|
||||
}): Promise<ResolvedAgentTurnCortexContext | null> {
|
||||
if (!params.cfg) {
|
||||
return null;
|
||||
}
|
||||
const config = await resolveAgentCortexModeStatus({
|
||||
cfg: params.cfg,
|
||||
agentId: params.agentId,
|
||||
sessionId: params.sessionId,
|
||||
channelId: params.channelId,
|
||||
});
|
||||
if (!config) {
|
||||
return null;
|
||||
}
|
||||
const status = await getCortexStatus({
|
||||
workspaceDir: params.workspaceDir,
|
||||
graphPath: config.graphPath,
|
||||
});
|
||||
return { config, status };
|
||||
}
|
||||
|
||||
export function resetAgentCortexConflictNoticeStateForTests(): void {
|
||||
cortexConflictNoticeCooldowns.clear();
|
||||
cortexMemoryCaptureStatuses.clear();
|
||||
@ -378,12 +410,21 @@ export async function resolveAgentCortexConflictNotice(params: {
|
||||
minSeverity?: number;
|
||||
now?: number;
|
||||
cooldownMs?: number;
|
||||
resolved?: ResolvedAgentTurnCortexContext | null;
|
||||
}): Promise<AgentCortexConflictNotice | null> {
|
||||
if (!params.cfg) {
|
||||
return null;
|
||||
}
|
||||
const cortex = resolveAgentCortexConfig(params.cfg, params.agentId);
|
||||
if (!cortex) {
|
||||
const resolved =
|
||||
params.resolved ??
|
||||
(await resolveAgentTurnCortexContext({
|
||||
cfg: params.cfg,
|
||||
agentId: params.agentId,
|
||||
workspaceDir: params.workspaceDir,
|
||||
sessionId: params.sessionId,
|
||||
channelId: params.channelId,
|
||||
}));
|
||||
if (!resolved) {
|
||||
return null;
|
||||
}
|
||||
const targetKey = buildAgentCortexConversationKey({
|
||||
@ -398,15 +439,11 @@ export async function resolveAgentCortexConflictNotice(params: {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const status = await getCortexStatus({
|
||||
workspaceDir: params.workspaceDir,
|
||||
graphPath: cortex.graphPath,
|
||||
});
|
||||
const conflicts = await listCortexMemoryConflicts({
|
||||
workspaceDir: params.workspaceDir,
|
||||
graphPath: cortex.graphPath,
|
||||
graphPath: resolved.config.graphPath,
|
||||
minSeverity: params.minSeverity ?? DEFAULT_CORTEX_CONFLICT_SEVERITY,
|
||||
status,
|
||||
status: resolved.status,
|
||||
});
|
||||
const topConflict = conflicts
|
||||
.filter((entry) => entry.id && entry.summary)
|
||||
@ -438,6 +475,7 @@ export async function ingestAgentCortexMemoryCandidate(params: {
|
||||
sessionId?: string;
|
||||
channelId?: string;
|
||||
provider?: string;
|
||||
resolved?: ResolvedAgentTurnCortexContext | null;
|
||||
}): Promise<AgentCortexMemoryCaptureResult> {
|
||||
const conversationKey = buildAgentCortexConversationKey({
|
||||
agentId: params.agentId,
|
||||
@ -449,8 +487,16 @@ export async function ingestAgentCortexMemoryCandidate(params: {
|
||||
cortexMemoryCaptureStatuses.set(conversationKey, { ...result, updatedAt: Date.now() });
|
||||
return result;
|
||||
}
|
||||
const cortex = resolveAgentCortexConfig(params.cfg, params.agentId);
|
||||
if (!cortex) {
|
||||
const resolved =
|
||||
params.resolved ??
|
||||
(await resolveAgentTurnCortexContext({
|
||||
cfg: params.cfg,
|
||||
agentId: params.agentId,
|
||||
workspaceDir: params.workspaceDir,
|
||||
sessionId: params.sessionId,
|
||||
channelId: params.channelId,
|
||||
}));
|
||||
if (!resolved) {
|
||||
const result = { captured: false, score: 0, reason: "cortex disabled" };
|
||||
cortexMemoryCaptureStatuses.set(conversationKey, { ...result, updatedAt: Date.now() });
|
||||
return result;
|
||||
@ -461,13 +507,9 @@ export async function ingestAgentCortexMemoryCandidate(params: {
|
||||
return decision;
|
||||
}
|
||||
try {
|
||||
const status = await getCortexStatus({
|
||||
workspaceDir: params.workspaceDir,
|
||||
graphPath: cortex.graphPath,
|
||||
});
|
||||
await ingestCortexMemoryFromText({
|
||||
workspaceDir: params.workspaceDir,
|
||||
graphPath: cortex.graphPath,
|
||||
graphPath: resolved.config.graphPath,
|
||||
event: {
|
||||
actor: "user",
|
||||
text: params.commandBody,
|
||||
@ -476,7 +518,7 @@ export async function ingestAgentCortexMemoryCandidate(params: {
|
||||
channelId: params.channelId,
|
||||
provider: params.provider,
|
||||
},
|
||||
status,
|
||||
status: resolved.status,
|
||||
});
|
||||
let syncedCodingContext = false;
|
||||
let syncPlatforms: string[] | undefined;
|
||||
@ -491,10 +533,10 @@ export async function ingestAgentCortexMemoryCandidate(params: {
|
||||
try {
|
||||
const syncResult = await syncCortexCodingContext({
|
||||
workspaceDir: params.workspaceDir,
|
||||
graphPath: cortex.graphPath,
|
||||
graphPath: resolved.config.graphPath,
|
||||
policy: syncPolicy.policy,
|
||||
platforms: syncPolicy.platforms,
|
||||
status,
|
||||
status: resolved.status,
|
||||
});
|
||||
syncedCodingContext = true;
|
||||
syncPlatforms = syncResult.platforms;
|
||||
|
||||
@ -3,7 +3,7 @@ import { lookupContextTokens } from "../../agents/context.js";
|
||||
import {
|
||||
ingestAgentCortexMemoryCandidate,
|
||||
resolveAgentCortexConflictNotice,
|
||||
resolveAgentCortexModeStatus,
|
||||
resolveAgentTurnCortexContext,
|
||||
resolveCortexChannelTarget,
|
||||
} from "../../agents/cortex.js";
|
||||
import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js";
|
||||
@ -705,15 +705,15 @@ export async function runReplyAgent(params: {
|
||||
to: sessionCtx.To,
|
||||
from: sessionCtx.From,
|
||||
});
|
||||
const cortexModeStatus =
|
||||
verboseEnabled && cfg
|
||||
? await resolveAgentCortexModeStatus({
|
||||
cfg,
|
||||
agentId: cortexAgentId,
|
||||
sessionId: followupRun.run.sessionId,
|
||||
channelId: cortexChannelId,
|
||||
})
|
||||
: null;
|
||||
const resolvedTurnCortex = cfg
|
||||
? await resolveAgentTurnCortexContext({
|
||||
cfg,
|
||||
agentId: cortexAgentId,
|
||||
workspaceDir: followupRun.run.workspaceDir,
|
||||
sessionId: followupRun.run.sessionId,
|
||||
channelId: cortexChannelId,
|
||||
})
|
||||
: null;
|
||||
const cortexMemoryCapture = cfg
|
||||
? await ingestAgentCortexMemoryCandidate({
|
||||
cfg,
|
||||
@ -723,6 +723,7 @@ export async function runReplyAgent(params: {
|
||||
sessionId: followupRun.run.sessionId,
|
||||
channelId: cortexChannelId,
|
||||
provider: followupRun.run.messageProvider,
|
||||
resolved: resolvedTurnCortex,
|
||||
})
|
||||
: null;
|
||||
const cortexConflictNotice = cfg
|
||||
@ -732,17 +733,18 @@ export async function runReplyAgent(params: {
|
||||
workspaceDir: followupRun.run.workspaceDir,
|
||||
sessionId: followupRun.run.sessionId,
|
||||
channelId: cortexChannelId,
|
||||
resolved: resolvedTurnCortex,
|
||||
})
|
||||
: null;
|
||||
if (verboseEnabled && cortexModeStatus) {
|
||||
if (verboseEnabled && resolvedTurnCortex) {
|
||||
const sourceLabel =
|
||||
cortexModeStatus.source === "session-override"
|
||||
resolvedTurnCortex.config.source === "session-override"
|
||||
? "session override"
|
||||
: cortexModeStatus.source === "channel-override"
|
||||
: resolvedTurnCortex.config.source === "channel-override"
|
||||
? "channel override"
|
||||
: "agent config";
|
||||
verboseNotices.push({
|
||||
text: `🧠 Cortex: ${cortexModeStatus.mode} (${sourceLabel})`,
|
||||
text: `🧠 Cortex: ${resolvedTurnCortex.config.mode} (${sourceLabel})`,
|
||||
});
|
||||
}
|
||||
if (verboseEnabled && cortexMemoryCapture?.captured) {
|
||||
|
||||
@ -53,7 +53,7 @@ function parseResolveAction(value?: string): CortexMemoryResolveAction | null {
|
||||
}
|
||||
|
||||
function resolveActiveSessionId(params: HandleCommandsParams): string | undefined {
|
||||
return params.sessionEntry?.sessionId;
|
||||
return params.sessionEntry?.sessionId ?? params.ctx.SessionId;
|
||||
}
|
||||
|
||||
function resolveActiveChannelId(params: HandleCommandsParams): string {
|
||||
@ -195,13 +195,21 @@ async function buildCortexWhyReply(params: HandleCommandsParams): Promise<ReplyP
|
||||
text: "Cortex prompt bridge is disabled for this agent. Enable it in config or with `openclaw memory cortex enable`.",
|
||||
};
|
||||
}
|
||||
const preview = await previewCortexContext({
|
||||
workspaceDir: params.workspaceDir,
|
||||
graphPath: state.cortex.graphPath,
|
||||
policy: state.mode,
|
||||
maxChars: state.cortex.maxChars,
|
||||
});
|
||||
const previewBody = preview.context || "No Cortex context is currently being injected.";
|
||||
let previewBody = "No Cortex context is currently being injected.";
|
||||
let previewGraphPath = state.cortex.graphPath ?? ".cortex/context.json";
|
||||
let previewError: string | null = null;
|
||||
try {
|
||||
const preview = await previewCortexContext({
|
||||
workspaceDir: params.workspaceDir,
|
||||
graphPath: state.cortex.graphPath,
|
||||
policy: state.mode,
|
||||
maxChars: state.cortex.maxChars,
|
||||
});
|
||||
previewGraphPath = preview.graphPath;
|
||||
previewBody = preview.context || previewBody;
|
||||
} catch (error) {
|
||||
previewError = error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
const captureStatus = await getAgentCortexMemoryCaptureStatusWithHistory({
|
||||
agentId: state.agentId,
|
||||
sessionId: state.sessionId,
|
||||
@ -213,7 +221,7 @@ async function buildCortexWhyReply(params: HandleCommandsParams): Promise<ReplyP
|
||||
"",
|
||||
`Mode: ${state.mode}`,
|
||||
`Source: ${state.source}`,
|
||||
`Graph: ${preview.graphPath}`,
|
||||
`Graph: ${previewGraphPath}`,
|
||||
state.sessionId ? `Session: ${state.sessionId}` : null,
|
||||
state.channelId ? `Channel: ${state.channelId}` : null,
|
||||
captureStatus
|
||||
@ -223,6 +231,7 @@ async function buildCortexWhyReply(params: HandleCommandsParams): Promise<ReplyP
|
||||
captureStatus?.syncedCodingContext
|
||||
? `Coding sync: updated (${(captureStatus.syncPlatforms ?? []).join(", ")})`
|
||||
: null,
|
||||
previewError ? `Preview error: ${previewError}` : null,
|
||||
"",
|
||||
"Injected Cortex context:",
|
||||
previewBody,
|
||||
|
||||
@ -785,6 +785,45 @@ describe("/cortex command", () => {
|
||||
expect(result.reply?.text).toContain("Injected Cortex context:");
|
||||
});
|
||||
|
||||
it("keeps /cortex why useful when preview fails", async () => {
|
||||
const cfg = {
|
||||
commands: { text: true },
|
||||
channels: { whatsapp: { allowFrom: ["*"] } },
|
||||
agents: {
|
||||
defaults: {
|
||||
cortex: {
|
||||
enabled: true,
|
||||
mode: "technical",
|
||||
maxChars: 1500,
|
||||
graphPath: ".cortex/context.json",
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
resolveAgentCortexModeStatusMock.mockResolvedValueOnce({
|
||||
enabled: true,
|
||||
mode: "professional",
|
||||
source: "agent-config",
|
||||
maxChars: 1500,
|
||||
graphPath: path.join(testWorkspaceDir, ".cortex", "context.json"),
|
||||
});
|
||||
previewCortexContextMock.mockRejectedValueOnce(new Error("Cortex graph not found"));
|
||||
|
||||
const params = buildParams("/cortex why", cfg, {
|
||||
SessionId: "session-1",
|
||||
NativeChannelId: "C123",
|
||||
});
|
||||
|
||||
const result = await handleCommands(params);
|
||||
|
||||
expect(result.shouldContinue).toBe(false);
|
||||
expect(result.reply?.text).toContain("Why I answered this way");
|
||||
expect(result.reply?.text).toContain("Mode: professional");
|
||||
expect(result.reply?.text).toContain("Preview error: Cortex graph not found");
|
||||
expect(result.reply?.text).toContain("Injected Cortex context:");
|
||||
expect(result.reply?.text).toContain("No Cortex context is currently being injected.");
|
||||
});
|
||||
|
||||
it("shows continuity details for the active conversation", async () => {
|
||||
const cfg = {
|
||||
commands: { text: true },
|
||||
|
||||
@ -7,6 +7,7 @@ import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
const getMemorySearchManager = vi.fn();
|
||||
const getCortexStatus = vi.fn();
|
||||
const previewCortexContext = vi.fn();
|
||||
const ensureCortexGraphInitialized = vi.fn();
|
||||
const getCortexModeOverride = vi.fn();
|
||||
const setCortexModeOverride = vi.fn();
|
||||
const clearCortexModeOverride = vi.fn();
|
||||
@ -25,6 +26,7 @@ vi.mock("../memory/index.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../memory/cortex.js", () => ({
|
||||
ensureCortexGraphInitialized,
|
||||
getCortexStatus,
|
||||
previewCortexContext,
|
||||
}));
|
||||
@ -66,6 +68,7 @@ afterEach(() => {
|
||||
getMemorySearchManager.mockClear();
|
||||
getCortexStatus.mockClear();
|
||||
previewCortexContext.mockClear();
|
||||
ensureCortexGraphInitialized.mockClear();
|
||||
getCortexModeOverride.mockClear();
|
||||
setCortexModeOverride.mockClear();
|
||||
clearCortexModeOverride.mockClear();
|
||||
@ -686,6 +689,10 @@ describe("memory cli", () => {
|
||||
|
||||
it("enables Cortex prompt bridge in agent defaults", async () => {
|
||||
mockWritableConfigSnapshot({});
|
||||
ensureCortexGraphInitialized.mockResolvedValueOnce({
|
||||
graphPath: "/tmp/openclaw-workspace/.cortex/context.json",
|
||||
created: true,
|
||||
});
|
||||
|
||||
const log = spyRuntimeLogs();
|
||||
await runMemoryCli(["cortex", "enable", "--mode", "professional", "--max-chars", "2200"]);
|
||||
@ -704,6 +711,32 @@ describe("memory cli", () => {
|
||||
expect(log).toHaveBeenCalledWith(
|
||||
"Enabled Cortex prompt bridge for agent defaults (professional, 2200 chars).",
|
||||
);
|
||||
expect(log).toHaveBeenCalledWith(
|
||||
"Initialized Cortex graph: /tmp/openclaw-workspace/.cortex/context.json",
|
||||
);
|
||||
expect(ensureCortexGraphInitialized).toHaveBeenCalledWith({
|
||||
workspaceDir: "/tmp/openclaw-workspace",
|
||||
graphPath: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("initializes a Cortex graph without changing config", async () => {
|
||||
ensureCortexGraphInitialized.mockResolvedValueOnce({
|
||||
graphPath: "/tmp/openclaw-workspace/.cortex/context.json",
|
||||
created: false,
|
||||
});
|
||||
|
||||
const log = spyRuntimeLogs();
|
||||
await runMemoryCli(["cortex", "init"]);
|
||||
|
||||
expect(writeConfigFile).not.toHaveBeenCalled();
|
||||
expect(ensureCortexGraphInitialized).toHaveBeenCalledWith({
|
||||
workspaceDir: "/tmp/openclaw-workspace",
|
||||
graphPath: undefined,
|
||||
});
|
||||
expect(log).toHaveBeenCalledWith(
|
||||
"Cortex graph already present: /tmp/openclaw-workspace/.cortex/context.json",
|
||||
);
|
||||
});
|
||||
|
||||
it("disables Cortex prompt bridge for a specific agent", async () => {
|
||||
|
||||
@ -14,7 +14,12 @@ import {
|
||||
setCortexModeOverride,
|
||||
type CortexModeScope,
|
||||
} from "../memory/cortex-mode-overrides.js";
|
||||
import { getCortexStatus, previewCortexContext, type CortexPolicy } from "../memory/cortex.js";
|
||||
import {
|
||||
ensureCortexGraphInitialized,
|
||||
getCortexStatus,
|
||||
previewCortexContext,
|
||||
type CortexPolicy,
|
||||
} from "../memory/cortex.js";
|
||||
import { getMemorySearchManager, type MemorySearchManagerResult } from "../memory/index.js";
|
||||
import { listMemoryFiles, normalizeExtraMemoryPaths } from "../memory/internal.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
@ -395,6 +400,30 @@ async function runCortexPreview(
|
||||
}
|
||||
}
|
||||
|
||||
async function runCortexInit(opts: CortexCommandOptions): Promise<void> {
|
||||
const cfg = loadConfig();
|
||||
const agentId = resolveAgent(cfg, opts.agent);
|
||||
const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId);
|
||||
try {
|
||||
const result = await ensureCortexGraphInitialized({
|
||||
workspaceDir,
|
||||
graphPath: opts.graph,
|
||||
});
|
||||
if (opts.json) {
|
||||
defaultRuntime.log(JSON.stringify({ agentId, workspaceDir, ...result }, null, 2));
|
||||
return;
|
||||
}
|
||||
defaultRuntime.log(
|
||||
result.created
|
||||
? `Initialized Cortex graph: ${shortenHomePath(result.graphPath)}`
|
||||
: `Cortex graph already present: ${shortenHomePath(result.graphPath)}`,
|
||||
);
|
||||
} catch (err) {
|
||||
defaultRuntime.error(formatErrorMessage(err));
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadWritableMemoryConfig(): Promise<Record<string, unknown> | null> {
|
||||
const snapshot = await readConfigFileSnapshot();
|
||||
if (!snapshot.valid) {
|
||||
@ -471,6 +500,9 @@ function updateAgentCortexConfig(params: {
|
||||
|
||||
async function runCortexEnable(opts: CortexEnableCommandOptions): Promise<void> {
|
||||
try {
|
||||
const cfg = loadConfig();
|
||||
const agentId = resolveAgent(cfg, opts.agent);
|
||||
const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId);
|
||||
const next = await loadWritableMemoryConfig();
|
||||
if (!next) {
|
||||
return;
|
||||
@ -487,11 +519,20 @@ async function runCortexEnable(opts: CortexEnableCommandOptions): Promise<void>
|
||||
}),
|
||||
});
|
||||
await writeConfigFile(next);
|
||||
const initResult = await ensureCortexGraphInitialized({
|
||||
workspaceDir,
|
||||
graphPath: opts.graph,
|
||||
});
|
||||
|
||||
const scope = opts.agent?.trim() ? `agent ${opts.agent.trim()}` : "agent defaults";
|
||||
defaultRuntime.log(
|
||||
`Enabled Cortex prompt bridge for ${scope} (${parseCortexMode(opts.mode)}, ${normalizeCortexMaxChars(opts.maxChars)} chars).`,
|
||||
);
|
||||
defaultRuntime.log(
|
||||
initResult.created
|
||||
? `Initialized Cortex graph: ${shortenHomePath(initResult.graphPath)}`
|
||||
: `Cortex graph ready: ${shortenHomePath(initResult.graphPath)}`,
|
||||
);
|
||||
} catch (err) {
|
||||
defaultRuntime.error(formatErrorMessage(err));
|
||||
process.exitCode = 1;
|
||||
@ -1176,6 +1217,16 @@ export function registerMemoryCli(program: Command) {
|
||||
},
|
||||
);
|
||||
|
||||
cortex
|
||||
.command("init")
|
||||
.description("Create the default Cortex graph if it does not exist")
|
||||
.option("--agent <id>", "Agent id (default: default agent)")
|
||||
.option("--graph <path>", "Override Cortex graph path")
|
||||
.option("--json", "Print JSON")
|
||||
.action(async (opts: CortexCommandOptions) => {
|
||||
await runCortexInit(opts);
|
||||
});
|
||||
|
||||
cortex
|
||||
.command("enable")
|
||||
.description("Enable Cortex prompt context injection in config")
|
||||
|
||||
@ -70,6 +70,14 @@ const DEFAULT_GRAPH_RELATIVE_PATH = path.join(".cortex", "context.json");
|
||||
const DEFAULT_POLICY: CortexPolicy = "technical";
|
||||
const DEFAULT_MAX_CHARS = 1_500;
|
||||
export const DEFAULT_CORTEX_CODING_PLATFORMS = ["claude-code", "cursor", "copilot"] as const;
|
||||
const EMPTY_CORTEX_GRAPH = {
|
||||
schema_version: "5.0",
|
||||
graph: {
|
||||
nodes: [],
|
||||
edges: [],
|
||||
},
|
||||
meta: {},
|
||||
} as const;
|
||||
|
||||
type CortexStatusParams = {
|
||||
workspaceDir: string;
|
||||
@ -96,6 +104,19 @@ export function resolveCortexGraphPath(workspaceDir: string, graphPath?: string)
|
||||
return path.normalize(path.resolve(workspaceDir, trimmed));
|
||||
}
|
||||
|
||||
export async function ensureCortexGraphInitialized(params: {
|
||||
workspaceDir: string;
|
||||
graphPath?: string;
|
||||
}): Promise<{ graphPath: string; created: boolean }> {
|
||||
const graphPath = resolveCortexGraphPath(params.workspaceDir, params.graphPath);
|
||||
if (await pathExists(graphPath)) {
|
||||
return { graphPath, created: false };
|
||||
}
|
||||
await fs.mkdir(path.dirname(graphPath), { recursive: true });
|
||||
await fs.writeFile(graphPath, `${JSON.stringify(EMPTY_CORTEX_GRAPH, null, 2)}\n`, "utf8");
|
||||
return { graphPath, created: true };
|
||||
}
|
||||
|
||||
async function pathExists(pathname: string): Promise<boolean> {
|
||||
try {
|
||||
await fs.access(pathname);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user