Fix Cortex review comments and docs lint

This commit is contained in:
Junebugg1214 2026-03-20 11:41:38 -04:00
parent 45660538fa
commit d8db426fbd
11 changed files with 141 additions and 32 deletions

View File

@ -284,10 +284,12 @@ Azure Bastion Standard SKU runs approximately **\$140/month** and the VM (Standa
To reduce costs:
- **Deallocate the VM** when not in use (stops compute billing; disk charges remain). The OpenClaw Gateway will not be reachable while the VM is deallocated — restart it when you need it live again:
```bash
az vm deallocate -g "${RG}" -n "${VM_NAME}"
az vm start -g "${RG}" -n "${VM_NAME}" # restart later
```
- **Delete Bastion when not needed** and recreate it when you need SSH access. Bastion is the largest cost component and takes only a few minutes to provision.
- **Use the Basic Bastion SKU** (~\$38/month) if you only need Portal-based SSH and don't require CLI tunneling (`az network bastion ssh`).

View File

@ -1,5 +1,7 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import { setActivePluginRegistry } from "../plugins/runtime.js";
import { createChannelTestPluginBase, createTestRegistry } from "../test-utils/channel-plugins.js";
const {
previewCortexContext,
@ -42,6 +44,7 @@ import {
} from "./cortex.js";
beforeEach(() => {
setActivePluginRegistry(createTestRegistry([]));
getCortexStatus.mockResolvedValue({
available: true,
workspaceDir: "/tmp/openclaw-workspace",
@ -52,6 +55,7 @@ beforeEach(() => {
afterEach(() => {
vi.clearAllMocks();
setActivePluginRegistry(createTestRegistry([]));
resetAgentCortexConflictNoticeStateForTests();
});
@ -588,6 +592,54 @@ describe("ingestAgentCortexMemoryCandidate", () => {
expect(syncCortexCodingContext).not.toHaveBeenCalled();
});
it("does not auto-sync generic technical chatter from registered channel plugins", async () => {
setActivePluginRegistry(
createTestRegistry([
{
pluginId: "matrix",
source: "test",
plugin: createChannelTestPluginBase({
id: "matrix",
label: "Matrix",
docsPath: "/channels/matrix",
}),
},
]),
);
ingestCortexMemoryFromText.mockResolvedValueOnce({
workspaceDir: "/tmp/openclaw-workspace",
graphPath: "/tmp/openclaw-workspace/.cortex/context.json",
stored: true,
});
const cfg: OpenClawConfig = {
agents: {
defaults: {
cortex: {
enabled: true,
},
},
list: [{ id: "main" }],
},
};
const result = await ingestAgentCortexMemoryCandidate({
cfg,
agentId: "main",
workspaceDir: "/tmp/openclaw-workspace",
commandBody: "I am debugging a Python API bug right now.",
sessionId: "session-1",
channelId: "!room:example.org",
provider: "matrix",
});
expect(result).toMatchObject({
captured: true,
syncedCodingContext: false,
});
expect(syncCortexCodingContext).not.toHaveBeenCalled();
});
it("skips low-signal text", async () => {
const cfg: OpenClawConfig = {
agents: {

View File

@ -10,6 +10,7 @@ import {
type CortexPolicy,
type CortexStatus,
} from "../memory/cortex.js";
import { resolveGatewayMessageChannel } from "../utils/message-channel.js";
import { resolveAgentConfig } from "./agent-scope.js";
import {
appendCortexCaptureHistory,
@ -136,16 +137,7 @@ const CORTEX_CODING_PROVIDER_PLATFORM_MAP: Record<string, string[]> = {
cursor: ["cursor"],
"gemini-cli": ["gemini-cli"],
};
const CORTEX_MESSAGING_PROVIDERS = new Set([
"discord",
"imessage",
"signal",
"slack",
"telegram",
"voice",
"webchat",
"whatsapp",
]);
const CORTEX_NON_GATEWAY_MESSAGING_PROVIDERS = new Set(["voice"]);
const cortexCodingSyncCooldowns = new Map<string, number>();
function normalizeMode(mode?: AgentCortexConfig["mode"]): CortexPolicy {
@ -162,6 +154,17 @@ function normalizeMaxChars(value?: number): number {
return Math.min(MAX_CORTEX_MAX_CHARS, Math.max(1, Math.floor(value)));
}
function isCortexMessagingProvider(provider?: string): boolean {
const normalized = provider?.trim().toLowerCase();
if (!normalized) {
return false;
}
return (
CORTEX_NON_GATEWAY_MESSAGING_PROVIDERS.has(normalized) ||
resolveGatewayMessageChannel(normalized) !== undefined
);
}
export function resolveAgentCortexConfig(
cfg: OpenClawConfig,
agentId: string,
@ -391,7 +394,7 @@ function resolveAutoSyncCortexCodingContext(params: {
const hasStrongCodingIntent = STRONG_CODING_SYNC_PATTERNS.some((pattern) =>
pattern.test(params.commandBody),
);
if (provider && CORTEX_MESSAGING_PROVIDERS.has(provider) && !hasStrongCodingIntent) {
if (provider && isCortexMessagingProvider(provider) && !hasStrongCodingIntent) {
return null;
}

View File

@ -4,7 +4,9 @@ import type { OpenClawConfig, MemorySearchConfig } from "../config/config.js";
import { resolveStateDir } from "../config/paths.js";
import type { SecretInput } from "../config/types.secrets.js";
import {
isMemoryMultimodalEnabled,
normalizeMemoryMultimodalSettings,
supportsMemoryMultimodalEmbeddings,
type MemoryMultimodalSettings,
} from "../memory/multimodal.js";
import { clampInt, clampNumber, resolveUserPath } from "../utils.js";
@ -386,5 +388,22 @@ export function resolveMemorySearchConfig(
if (!resolved.enabled) {
return null;
}
const multimodalActive = isMemoryMultimodalEnabled(resolved.multimodal);
if (
multimodalActive &&
!supportsMemoryMultimodalEmbeddings({
provider: resolved.provider,
model: resolved.model,
})
) {
throw new Error(
'agents.*.memorySearch.multimodal requires memorySearch.provider = "gemini" and model = "gemini-embedding-2-preview".',
);
}
if (multimodalActive && resolved.fallback !== "none") {
throw new Error(
'agents.*.memorySearch.multimodal does not support memorySearch.fallback. Set fallback to "none".',
);
}
return resolved;
}

View File

@ -8,6 +8,7 @@ const getMemorySearchManager = vi.hoisted(() => vi.fn());
const getCortexStatus = vi.hoisted(() => vi.fn());
const previewCortexContext = vi.hoisted(() => vi.fn());
const ensureCortexGraphInitialized = vi.hoisted(() => vi.fn());
const resolveAgentCortexConfig = vi.hoisted(() => vi.fn());
const getCortexModeOverride = vi.hoisted(() => vi.fn());
const setCortexModeOverride = vi.hoisted(() => vi.fn());
const clearCortexModeOverride = vi.hoisted(() => vi.fn());
@ -33,6 +34,10 @@ vi.mock("../memory/cortex.js", () => ({
previewCortexContext,
}));
vi.mock("../agents/cortex.js", () => ({
resolveAgentCortexConfig,
}));
vi.mock("../memory/cortex-mode-overrides.js", () => ({
getCortexModeOverride,
setCortexModeOverride,
@ -68,6 +73,7 @@ beforeAll(async () => {
beforeEach(() => {
getMemorySearchManager.mockReset();
loadConfig.mockReset().mockReturnValue({});
resolveAgentCortexConfig.mockReset().mockReturnValue(null);
resolveDefaultAgentId.mockReset().mockReturnValue("main");
resolveCommandSecretRefsViaGateway.mockReset().mockImplementation(async ({ config }) => ({
resolvedConfig: config,
@ -751,6 +757,30 @@ describe("memory cli", () => {
);
});
it("initializes the configured Cortex graph path when --graph is omitted", async () => {
resolveAgentCortexConfig.mockReturnValue({
enabled: true,
graphPath: ".cortex/agent-main.json",
mode: "technical",
maxChars: 1500,
});
ensureCortexGraphInitialized.mockResolvedValueOnce({
graphPath: "/tmp/openclaw-workspace/.cortex/agent-main.json",
created: true,
});
const log = spyRuntimeLogs();
await runMemoryCli(["cortex", "init"]);
expect(ensureCortexGraphInitialized).toHaveBeenCalledWith({
workspaceDir: "/tmp/openclaw-workspace",
graphPath: ".cortex/agent-main.json",
});
expect(log).toHaveBeenCalledWith(
"Initialized Cortex graph: /tmp/openclaw-workspace/.cortex/agent-main.json",
);
});
it("disables Cortex prompt bridge for a specific agent", async () => {
mockWritableConfigSnapshot({
agents: {

View File

@ -4,6 +4,7 @@ import os from "node:os";
import path from "node:path";
import type { Command } from "commander";
import { resolveDefaultAgentId, resolveAgentWorkspaceDir } from "../agents/agent-scope.js";
import { resolveAgentCortexConfig } from "../agents/cortex.js";
import { loadConfig, readConfigFileSnapshot, writeConfigFile } from "../config/config.js";
import { resolveStateDir } from "../config/paths.js";
import { resolveSessionTranscriptsDirForAgent } from "../config/sessions/paths.js";
@ -405,9 +406,10 @@ async function runCortexInit(opts: CortexCommandOptions): Promise<void> {
const agentId = resolveAgent(cfg, opts.agent);
const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId);
try {
const graphPath = opts.graph?.trim() || resolveAgentCortexConfig(cfg, agentId)?.graphPath;
const result = await ensureCortexGraphInitialized({
workspaceDir,
graphPath: opts.graph,
graphPath,
});
if (opts.json) {
defaultRuntime.log(JSON.stringify({ agentId, workspaceDir, ...result }, null, 2));

View File

@ -12,7 +12,7 @@ const resolveGatewayAuthMock = vi.hoisted(() => vi.fn());
const getUpdateAvailableMock = vi.hoisted(() => vi.fn());
const resolveAgentCortexModeStatusMock = vi.hoisted(() => vi.fn());
const resolveCortexChannelTargetMock = vi.hoisted(() => vi.fn());
const getCachedLatestCortexCaptureHistoryEntryMock = vi.hoisted(() => vi.fn());
const getLatestCortexCaptureHistoryEntryMock = vi.hoisted(() => vi.fn());
vi.mock("../../config/config.js", () => ({
STATE_DIR: "/tmp/openclaw-state",
@ -57,7 +57,7 @@ vi.mock("../../agents/cortex.js", () => ({
}));
vi.mock("../../agents/cortex-history.js", () => ({
getCachedLatestCortexCaptureHistoryEntry: getCachedLatestCortexCaptureHistoryEntryMock,
getLatestCortexCaptureHistoryEntry: getLatestCortexCaptureHistoryEntryMock,
}));
import { buildGatewaySnapshot } from "./health-state.js";
@ -99,7 +99,7 @@ describe("buildGatewaySnapshot", () => {
graphPath: ".cortex/context.json",
});
resolveCortexChannelTargetMock.mockReturnValue("telegram:user-123");
getCachedLatestCortexCaptureHistoryEntryMock.mockReturnValue({
getLatestCortexCaptureHistoryEntryMock.mockResolvedValue({
agentId: "main",
sessionId: "session-1",
channelId: "telegram:user-123",
@ -129,7 +129,7 @@ describe("buildGatewaySnapshot", () => {
nativeChannelId: "telegram:user-123",
to: "telegram:user-123",
});
expect(getCachedLatestCortexCaptureHistoryEntryMock).toHaveBeenCalledWith({
expect(getLatestCortexCaptureHistoryEntryMock).toHaveBeenCalledWith({
agentId: "main",
sessionId: "session-1",
channelId: "telegram:user-123",

View File

@ -1,5 +1,5 @@
import { resolveDefaultAgentId } from "../../agents/agent-scope.js";
import { getCachedLatestCortexCaptureHistoryEntry } from "../../agents/cortex-history.js";
import { getLatestCortexCaptureHistoryEntry } from "../../agents/cortex-history.js";
import { resolveAgentCortexModeStatus, resolveCortexChannelTarget } from "../../agents/cortex.js";
import { getHealthSnapshot, type HealthSummary } from "../../commands/health.js";
import { STATE_DIR, createConfigIO, loadConfig } from "../../config/config.js";
@ -42,11 +42,11 @@ export async function buildGatewaySnapshot(): Promise<Snapshot> {
channelId,
});
const latestCortexCapture = cortex
? getCachedLatestCortexCaptureHistoryEntry({
? await getLatestCortexCaptureHistoryEntry({
agentId: defaultAgentId,
sessionId: mainSessionEntry?.sessionId,
channelId,
})
}).catch(() => null)
: null;
const scope = cfg.session?.scope ?? "per-sender";
const presence = listSystemPresence();

View File

@ -22,7 +22,7 @@ describe("cortex mode overrides", () => {
return path.join(dir, "cortex-mode-overrides.json");
}
it("prefers session overrides over channel overrides", async () => {
it("prefers channel overrides over session overrides", async () => {
const pathname = await createStorePath();
await setCortexModeOverride({
pathname,
@ -46,8 +46,8 @@ describe("cortex mode overrides", () => {
channelId: "slack",
});
expect(resolved?.mode).toBe("minimal");
expect(resolved?.scope).toBe("session");
expect(resolved?.mode).toBe("professional");
expect(resolved?.scope).toBe("channel");
});
it("can clear a stored override", async () => {

View File

@ -59,13 +59,6 @@ export async function getCortexModeOverride(params: {
pathname?: string;
}): Promise<CortexModeOverride | null> {
const store = await readStore(params.pathname);
const sessionId = params.sessionId?.trim();
if (sessionId) {
const session = store.session[buildKey(params.agentId, sessionId)];
if (session) {
return session;
}
}
const channelId = params.channelId?.trim();
if (channelId) {
const channel = store.channel[buildKey(params.agentId, channelId)];
@ -73,6 +66,13 @@ export async function getCortexModeOverride(params: {
return channel;
}
}
const sessionId = params.sessionId?.trim();
if (sessionId) {
const session = store.session[buildKey(params.agentId, sessionId)];
if (session) {
return session;
}
}
return null;
}

View File

@ -687,10 +687,11 @@ export abstract class MemoryManagerSyncOps {
this.settings.multimodal,
);
const fileEntries = (
await Promise.all(
files.map(async (file) =>
buildFileEntry(file, this.workspaceDir, this.settings.multimodal),
await runWithConcurrency(
files.map(
(file) => async () => buildFileEntry(file, this.workspaceDir, this.settings.multimodal),
),
this.getIndexConcurrency(),
)
).filter((entry): entry is MemoryFileEntry => entry !== null);
log.debug("memory sync: indexing memory files", {