mirror of
https://github.com/farcasclaudiu/openclaw.git
synced 2026-06-29 11:02:12 +03:00
Merge remote-tracking branch 'origin/main'
This commit is contained in:
@@ -132,6 +132,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- Telegram: scope skill commands to the resolved agent for default accounts so `setMyCommands` no longer triggers `BOT_COMMANDS_TOO_MUCH` when multiple agents are configured. (#15599)
|
- Telegram: scope skill commands to the resolved agent for default accounts so `setMyCommands` no longer triggers `BOT_COMMANDS_TOO_MUCH` when multiple agents are configured. (#15599)
|
||||||
- Discord: avoid misrouting numeric guild allowlist entries to `/channels/<guildId>` by prefixing guild-only inputs with `guild:` during resolution. (#12326) Thanks @headswim.
|
- Discord: avoid misrouting numeric guild allowlist entries to `/channels/<guildId>` by prefixing guild-only inputs with `guild:` during resolution. (#12326) Thanks @headswim.
|
||||||
- Memory/QMD: default `memory.qmd.searchMode` to `search` for faster CPU-only recall and always scope `search`/`vsearch` requests to managed collections (auto-falling back to `query` when required). (#16047) Thanks @togotago.
|
- Memory/QMD: default `memory.qmd.searchMode` to `search` for faster CPU-only recall and always scope `search`/`vsearch` requests to managed collections (auto-falling back to `query` when required). (#16047) Thanks @togotago.
|
||||||
|
- Memory/LanceDB: add configurable `captureMaxChars` for auto-capture while keeping the legacy 500-char default. (#16641) Thanks @ciberponk.
|
||||||
- MS Teams: preserve parsed mention entities/text when appending OneDrive fallback file links, and accept broader real-world Teams mention ID formats (`29:...`, `8:orgid:...`) while still rejecting placeholder patterns. (#15436) Thanks @hyojin.
|
- MS Teams: preserve parsed mention entities/text when appending OneDrive fallback file links, and accept broader real-world Teams mention ID formats (`29:...`, `8:orgid:...`) while still rejecting placeholder patterns. (#15436) Thanks @hyojin.
|
||||||
- Media: classify `text/*` MIME types as documents in media-kind routing so text attachments are no longer treated as unknown. (#12237) Thanks @arosstale.
|
- Media: classify `text/*` MIME types as documents in media-kind routing so text attachments are no longer treated as unknown. (#12237) Thanks @arosstale.
|
||||||
- Inbound/Web UI: preserve literal `\n` sequences when normalizing inbound text so Windows paths like `C:\\Work\\nxxx\\README.md` are not corrupted. (#11547) Thanks @mcaxtr.
|
- Inbound/Web UI: preserve literal `\n` sequences when normalizing inbound text so Windows paths like `C:\\Work\\nxxx\\README.md` are not corrupted. (#11547) Thanks @mcaxtr.
|
||||||
|
|||||||
@@ -11,12 +11,14 @@ export type MemoryConfig = {
|
|||||||
dbPath?: string;
|
dbPath?: string;
|
||||||
autoCapture?: boolean;
|
autoCapture?: boolean;
|
||||||
autoRecall?: boolean;
|
autoRecall?: boolean;
|
||||||
|
captureMaxChars?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MEMORY_CATEGORIES = ["preference", "fact", "decision", "entity", "other"] as const;
|
export const MEMORY_CATEGORIES = ["preference", "fact", "decision", "entity", "other"] as const;
|
||||||
export type MemoryCategory = (typeof MEMORY_CATEGORIES)[number];
|
export type MemoryCategory = (typeof MEMORY_CATEGORIES)[number];
|
||||||
|
|
||||||
const DEFAULT_MODEL = "text-embedding-3-small";
|
const DEFAULT_MODEL = "text-embedding-3-small";
|
||||||
|
export const DEFAULT_CAPTURE_MAX_CHARS = 500;
|
||||||
const LEGACY_STATE_DIRS: string[] = [];
|
const LEGACY_STATE_DIRS: string[] = [];
|
||||||
|
|
||||||
function resolveDefaultDbPath(): string {
|
function resolveDefaultDbPath(): string {
|
||||||
@@ -89,7 +91,11 @@ export const memoryConfigSchema = {
|
|||||||
throw new Error("memory config required");
|
throw new Error("memory config required");
|
||||||
}
|
}
|
||||||
const cfg = value as Record<string, unknown>;
|
const cfg = value as Record<string, unknown>;
|
||||||
assertAllowedKeys(cfg, ["embedding", "dbPath", "autoCapture", "autoRecall"], "memory config");
|
assertAllowedKeys(
|
||||||
|
cfg,
|
||||||
|
["embedding", "dbPath", "autoCapture", "autoRecall", "captureMaxChars"],
|
||||||
|
"memory config",
|
||||||
|
);
|
||||||
|
|
||||||
const embedding = cfg.embedding as Record<string, unknown> | undefined;
|
const embedding = cfg.embedding as Record<string, unknown> | undefined;
|
||||||
if (!embedding || typeof embedding.apiKey !== "string") {
|
if (!embedding || typeof embedding.apiKey !== "string") {
|
||||||
@@ -99,6 +105,15 @@ export const memoryConfigSchema = {
|
|||||||
|
|
||||||
const model = resolveEmbeddingModel(embedding);
|
const model = resolveEmbeddingModel(embedding);
|
||||||
|
|
||||||
|
const captureMaxChars =
|
||||||
|
typeof cfg.captureMaxChars === "number" ? Math.floor(cfg.captureMaxChars) : undefined;
|
||||||
|
if (
|
||||||
|
typeof captureMaxChars === "number" &&
|
||||||
|
(captureMaxChars < 100 || captureMaxChars > 10_000)
|
||||||
|
) {
|
||||||
|
throw new Error("captureMaxChars must be between 100 and 10000");
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
embedding: {
|
embedding: {
|
||||||
provider: "openai",
|
provider: "openai",
|
||||||
@@ -108,6 +123,7 @@ export const memoryConfigSchema = {
|
|||||||
dbPath: typeof cfg.dbPath === "string" ? cfg.dbPath : DEFAULT_DB_PATH,
|
dbPath: typeof cfg.dbPath === "string" ? cfg.dbPath : DEFAULT_DB_PATH,
|
||||||
autoCapture: cfg.autoCapture !== false,
|
autoCapture: cfg.autoCapture !== false,
|
||||||
autoRecall: cfg.autoRecall !== false,
|
autoRecall: cfg.autoRecall !== false,
|
||||||
|
captureMaxChars: captureMaxChars ?? DEFAULT_CAPTURE_MAX_CHARS,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
uiHints: {
|
uiHints: {
|
||||||
@@ -135,5 +151,11 @@ export const memoryConfigSchema = {
|
|||||||
label: "Auto-Recall",
|
label: "Auto-Recall",
|
||||||
help: "Automatically inject relevant memories into context",
|
help: "Automatically inject relevant memories into context",
|
||||||
},
|
},
|
||||||
|
captureMaxChars: {
|
||||||
|
label: "Capture Max Chars",
|
||||||
|
help: "Maximum message length eligible for auto-capture",
|
||||||
|
advanced: true,
|
||||||
|
placeholder: String(DEFAULT_CAPTURE_MAX_CHARS),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ describe("memory plugin e2e", () => {
|
|||||||
expect(config).toBeDefined();
|
expect(config).toBeDefined();
|
||||||
expect(config?.embedding?.apiKey).toBe(OPENAI_API_KEY);
|
expect(config?.embedding?.apiKey).toBe(OPENAI_API_KEY);
|
||||||
expect(config?.dbPath).toBe(dbPath);
|
expect(config?.dbPath).toBe(dbPath);
|
||||||
|
expect(config?.captureMaxChars).toBe(500);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("config schema resolves env vars", async () => {
|
test("config schema resolves env vars", async () => {
|
||||||
@@ -92,6 +93,33 @@ describe("memory plugin e2e", () => {
|
|||||||
}).toThrow("embedding.apiKey is required");
|
}).toThrow("embedding.apiKey is required");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("config schema validates captureMaxChars range", async () => {
|
||||||
|
const { default: memoryPlugin } = await import("./index.js");
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
memoryPlugin.configSchema?.parse?.({
|
||||||
|
embedding: { apiKey: OPENAI_API_KEY },
|
||||||
|
dbPath,
|
||||||
|
captureMaxChars: 99,
|
||||||
|
});
|
||||||
|
}).toThrow("captureMaxChars must be between 100 and 10000");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("config schema accepts captureMaxChars override", async () => {
|
||||||
|
const { default: memoryPlugin } = await import("./index.js");
|
||||||
|
|
||||||
|
const config = memoryPlugin.configSchema?.parse?.({
|
||||||
|
embedding: {
|
||||||
|
apiKey: OPENAI_API_KEY,
|
||||||
|
model: "text-embedding-3-small",
|
||||||
|
},
|
||||||
|
dbPath,
|
||||||
|
captureMaxChars: 1800,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(config?.captureMaxChars).toBe(1800);
|
||||||
|
});
|
||||||
|
|
||||||
test("shouldCapture applies real capture rules", async () => {
|
test("shouldCapture applies real capture rules", async () => {
|
||||||
const { shouldCapture } = await import("./index.js");
|
const { shouldCapture } = await import("./index.js");
|
||||||
|
|
||||||
@@ -104,6 +132,14 @@ describe("memory plugin e2e", () => {
|
|||||||
expect(shouldCapture("<relevant-memories>injected</relevant-memories>")).toBe(false);
|
expect(shouldCapture("<relevant-memories>injected</relevant-memories>")).toBe(false);
|
||||||
expect(shouldCapture("<system>status</system>")).toBe(false);
|
expect(shouldCapture("<system>status</system>")).toBe(false);
|
||||||
expect(shouldCapture("Here is a short **summary**\n- bullet")).toBe(false);
|
expect(shouldCapture("Here is a short **summary**\n- bullet")).toBe(false);
|
||||||
|
const defaultAllowed = `I always prefer this style. ${"x".repeat(400)}`;
|
||||||
|
const defaultTooLong = `I always prefer this style. ${"x".repeat(600)}`;
|
||||||
|
expect(shouldCapture(defaultAllowed)).toBe(true);
|
||||||
|
expect(shouldCapture(defaultTooLong)).toBe(false);
|
||||||
|
const customAllowed = `I always prefer this style. ${"x".repeat(1200)}`;
|
||||||
|
const customTooLong = `I always prefer this style. ${"x".repeat(1600)}`;
|
||||||
|
expect(shouldCapture(customAllowed, { maxChars: 1500 })).toBe(true);
|
||||||
|
expect(shouldCapture(customTooLong, { maxChars: 1500 })).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("detectCategory classifies using production logic", async () => {
|
test("detectCategory classifies using production logic", async () => {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { Type } from "@sinclair/typebox";
|
|||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
import OpenAI from "openai";
|
import OpenAI from "openai";
|
||||||
import {
|
import {
|
||||||
|
DEFAULT_CAPTURE_MAX_CHARS,
|
||||||
MEMORY_CATEGORIES,
|
MEMORY_CATEGORIES,
|
||||||
type MemoryCategory,
|
type MemoryCategory,
|
||||||
memoryConfigSchema,
|
memoryConfigSchema,
|
||||||
@@ -194,8 +195,9 @@ const MEMORY_TRIGGERS = [
|
|||||||
/always|never|important/i,
|
/always|never|important/i,
|
||||||
];
|
];
|
||||||
|
|
||||||
export function shouldCapture(text: string): boolean {
|
export function shouldCapture(text: string, options?: { maxChars?: number }): boolean {
|
||||||
if (text.length < 10 || text.length > 500) {
|
const maxChars = options?.maxChars ?? DEFAULT_CAPTURE_MAX_CHARS;
|
||||||
|
if (text.length < 10 || text.length > maxChars) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// Skip injected context from memory recall
|
// Skip injected context from memory recall
|
||||||
@@ -570,7 +572,9 @@ const memoryPlugin = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Filter for capturable content
|
// Filter for capturable content
|
||||||
const toCapture = texts.filter((text) => text && shouldCapture(text));
|
const toCapture = texts.filter(
|
||||||
|
(text) => text && shouldCapture(text, { maxChars: cfg.captureMaxChars }),
|
||||||
|
);
|
||||||
if (toCapture.length === 0) {
|
if (toCapture.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,12 @@
|
|||||||
"autoRecall": {
|
"autoRecall": {
|
||||||
"label": "Auto-Recall",
|
"label": "Auto-Recall",
|
||||||
"help": "Automatically inject relevant memories into context"
|
"help": "Automatically inject relevant memories into context"
|
||||||
|
},
|
||||||
|
"captureMaxChars": {
|
||||||
|
"label": "Capture Max Chars",
|
||||||
|
"help": "Maximum message length eligible for auto-capture",
|
||||||
|
"advanced": true,
|
||||||
|
"placeholder": "500"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"configSchema": {
|
"configSchema": {
|
||||||
@@ -53,6 +59,11 @@
|
|||||||
},
|
},
|
||||||
"autoRecall": {
|
"autoRecall": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"captureMaxChars": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 100,
|
||||||
|
"maximum": 10000
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["embedding"]
|
"required": ["embedding"]
|
||||||
|
|||||||
Reference in New Issue
Block a user