Fix Cortex review comments and docs lint
This commit is contained in:
parent
45660538fa
commit
d8db426fbd
@ -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`).
|
||||
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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", {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user