fix: repair memory sync and config regressions
This commit is contained in:
parent
ce1db6e425
commit
3eeae2cb05
@ -14,7 +14,7 @@ import * as internalHooks from "../../hooks/internal-hooks.js";
|
||||
import { clearPluginCommands, registerPluginCommand } from "../../plugins/commands.js";
|
||||
import { typedCases } from "../../test-utils/typed-cases.js";
|
||||
import { INTERNAL_MESSAGE_CHANNEL } from "../../utils/message-channel.js";
|
||||
import type { MsgContext, TemplateContext } from "../templating.js";
|
||||
import type { MsgContext } from "../templating.js";
|
||||
import { resetBashChatCommandForTests } from "./bash-command.js";
|
||||
import { handleCompactCommand } from "./commands-compact.js";
|
||||
import { buildCommandsPaginationKeyboard } from "./commands-info.js";
|
||||
@ -199,11 +199,6 @@ afterAll(async () => {
|
||||
await fs.rm(testWorkspaceDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function buildParams(
|
||||
commandBody: string,
|
||||
cfg: OpenClawConfig,
|
||||
ctxOverrides?: Partial<TemplateContext>,
|
||||
) {
|
||||
async function withTempConfigPath<T>(
|
||||
initialConfig: Record<string, unknown>,
|
||||
run: (configPath: string) => Promise<T>,
|
||||
|
||||
@ -801,6 +801,14 @@ export const FIELD_HELP: Record<string, string> = {
|
||||
'Chooses which sources are indexed: "memory" reads MEMORY.md + memory files, and "sessions" includes transcript history. Keep ["memory"] unless you need recall from prior chat transcripts.',
|
||||
"agents.defaults.memorySearch.extraPaths":
|
||||
"Adds extra directories or .md files to the memory index beyond default memory files. Use this when key reference docs live elsewhere in your repo; keep paths small and intentional to avoid noisy recall.",
|
||||
"agents.defaults.memorySearch.multimodal":
|
||||
"Optional multimodal indexing for image/audio files discovered through memorySearch.extraPaths. Enable this only when you intentionally want Gemini multimodal embeddings to include image or audio reference files alongside markdown memory.",
|
||||
"agents.defaults.memorySearch.multimodal.enabled":
|
||||
"Turns on multimodal extra-path indexing for supported image/audio file types. Keep this off unless the selected memory embedding provider and model support structured multimodal inputs.",
|
||||
"agents.defaults.memorySearch.multimodal.modalities":
|
||||
'Chooses which non-markdown media kinds are indexed from extra paths: "image", "audio", or both via "all". Limit this to the media you actually want searchable so indexing stays focused and cheap.',
|
||||
"agents.defaults.memorySearch.multimodal.maxFileBytes":
|
||||
"Maximum file size accepted for multimodal memory indexing before the file is skipped. Lower this when large media files would bloat embedding payloads or raise provider limits.",
|
||||
"agents.defaults.memorySearch.experimental.sessionMemory":
|
||||
"Indexes session transcripts into memory search so responses can reference prior chat turns. Keep this off unless transcript recall is needed, because indexing cost and storage usage both increase.",
|
||||
"agents.defaults.memorySearch.provider":
|
||||
@ -1409,6 +1417,18 @@ export const FIELD_HELP: Record<string, string> = {
|
||||
"Telegram bot token used to authenticate Bot API requests for this account/provider config. Use secret/env substitution and rotate tokens if exposure is suspected.",
|
||||
"channels.telegram.capabilities.inlineButtons":
|
||||
"Enable Telegram inline button components for supported command and interaction surfaces. Disable if your deployment needs plain-text-only compatibility behavior.",
|
||||
"channels.telegram.execApprovals":
|
||||
"Telegram-side execution approval routing for commands that require a human approver before running. Use this when Telegram is part of your operator approval flow and keep the filters narrow so approval prompts only reach the right reviewers.",
|
||||
"channels.telegram.execApprovals.enabled":
|
||||
"Enables Telegram delivery of execution-approval requests when command approvals are pending. Keep this disabled unless Telegram operators are expected to review and approve tool execution from chat.",
|
||||
"channels.telegram.execApprovals.approvers":
|
||||
"Allowlist of Telegram user or chat identities permitted to receive and act on execution-approval prompts. Keep this list explicit so approval authority does not drift to unintended accounts.",
|
||||
"channels.telegram.execApprovals.agentFilter":
|
||||
"Optional agent filter that limits which agents can emit Telegram execution-approval requests. Use this to keep sensitive approval workflows tied to only the agents that need operator review.",
|
||||
"channels.telegram.execApprovals.sessionFilter":
|
||||
"Optional session filter that narrows which conversations may trigger Telegram approval requests. Use this to keep noisy or untrusted sessions from generating approval prompts in operator chats.",
|
||||
"channels.telegram.execApprovals.target":
|
||||
"Telegram destination used for approval prompts, such as a specific operator DM or admin group/thread. Point this at a controlled channel where approvers already expect to handle execution requests.",
|
||||
"channels.slack.configWrites":
|
||||
"Allow Slack to write config in response to channel events/commands (default: true).",
|
||||
"channels.slack.botToken":
|
||||
|
||||
@ -236,7 +236,7 @@ describe("embedding provider remote overrides", () => {
|
||||
remote: {
|
||||
apiKey: "gemini-key",
|
||||
},
|
||||
model: "text-embedding-004",
|
||||
model: "gemini-embedding-2-preview",
|
||||
outputDimensionality: 768,
|
||||
fallback: "openai",
|
||||
});
|
||||
|
||||
@ -761,19 +761,30 @@ export abstract class MemoryManagerSyncOps {
|
||||
private async syncSessionFiles(params: {
|
||||
needsFullReindex: boolean;
|
||||
progress?: MemorySyncProgressState;
|
||||
}) {
|
||||
targetSessionFiles?: string[];
|
||||
}): Promise<Set<string>> {
|
||||
// FTS-only mode: skip embedding sync (no provider)
|
||||
if (!this.provider) {
|
||||
log.debug("Skipping session file sync in FTS-only mode (no embedding provider)");
|
||||
return;
|
||||
return new Set();
|
||||
}
|
||||
|
||||
const files = await listSessionFilesForAgent(this.agentId);
|
||||
const activePaths = new Set(files.map((file) => sessionPathForFile(file)));
|
||||
const indexAll = params.needsFullReindex || this.sessionsDirtyFiles.size === 0;
|
||||
const targetSessionFiles = new Set(
|
||||
(params.targetSessionFiles ?? [])
|
||||
.map((sessionFile) => sessionFile.trim())
|
||||
.filter((sessionFile) => sessionFile.length > 0),
|
||||
);
|
||||
const targetedSync = targetSessionFiles.size > 0;
|
||||
const indexAll =
|
||||
params.needsFullReindex || (!targetedSync && this.sessionsDirtyFiles.size === 0);
|
||||
const syncedPaths = new Set<string>();
|
||||
log.debug("memory sync: indexing session files", {
|
||||
files: files.length,
|
||||
indexAll,
|
||||
targetedSync,
|
||||
targetSessionFiles: targetSessionFiles.size,
|
||||
dirtyFiles: this.sessionsDirtyFiles.size,
|
||||
batch: this.batch.enabled,
|
||||
concurrency: this.getIndexConcurrency(),
|
||||
@ -788,7 +799,10 @@ export abstract class MemoryManagerSyncOps {
|
||||
}
|
||||
|
||||
const tasks = files.map((absPath) => async () => {
|
||||
if (!indexAll && !this.sessionsDirtyFiles.has(absPath)) {
|
||||
if (targetedSync && !targetSessionFiles.has(absPath)) {
|
||||
return;
|
||||
}
|
||||
if (!indexAll && !targetedSync && !this.sessionsDirtyFiles.has(absPath)) {
|
||||
if (params.progress) {
|
||||
params.progress.completed += 1;
|
||||
params.progress.report({
|
||||
@ -800,6 +814,7 @@ export abstract class MemoryManagerSyncOps {
|
||||
}
|
||||
const entry = await buildSessionEntry(absPath);
|
||||
if (!entry) {
|
||||
syncedPaths.add(absPath);
|
||||
if (params.progress) {
|
||||
params.progress.completed += 1;
|
||||
params.progress.report({
|
||||
@ -821,10 +836,12 @@ export abstract class MemoryManagerSyncOps {
|
||||
});
|
||||
}
|
||||
this.resetSessionDelta(absPath, entry.size);
|
||||
syncedPaths.add(absPath);
|
||||
return;
|
||||
}
|
||||
await this.indexFile(entry, { source: "sessions", content: entry.content });
|
||||
this.resetSessionDelta(absPath, entry.size);
|
||||
syncedPaths.add(absPath);
|
||||
if (params.progress) {
|
||||
params.progress.completed += 1;
|
||||
params.progress.report({
|
||||
@ -863,6 +880,7 @@ export abstract class MemoryManagerSyncOps {
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
return syncedPaths;
|
||||
}
|
||||
|
||||
private createSyncProgress(
|
||||
@ -948,9 +966,20 @@ export abstract class MemoryManagerSyncOps {
|
||||
}
|
||||
|
||||
if (shouldSyncSessions) {
|
||||
await this.syncSessionFiles({ needsFullReindex, progress: progress ?? undefined });
|
||||
this.sessionsDirty = false;
|
||||
this.sessionsDirtyFiles.clear();
|
||||
const syncedSessionFiles = await this.syncSessionFiles({
|
||||
needsFullReindex,
|
||||
progress: progress ?? undefined,
|
||||
targetSessionFiles: params?.sessionFiles,
|
||||
});
|
||||
if (needsFullReindex || !params?.sessionFiles?.length) {
|
||||
this.sessionsDirty = false;
|
||||
this.sessionsDirtyFiles.clear();
|
||||
} else {
|
||||
for (const syncedSessionFile of syncedSessionFiles) {
|
||||
this.sessionsDirtyFiles.delete(syncedSessionFile);
|
||||
}
|
||||
this.sessionsDirty = this.sessionsDirtyFiles.size > 0;
|
||||
}
|
||||
} else if (this.sessionsDirtyFiles.size > 0) {
|
||||
this.sessionsDirty = true;
|
||||
} else {
|
||||
@ -961,11 +990,19 @@ export abstract class MemoryManagerSyncOps {
|
||||
const activated =
|
||||
this.shouldFallbackOnError(reason) && (await this.activateFallbackProvider(reason));
|
||||
if (activated) {
|
||||
await this.runSafeReindex({
|
||||
const reindexParams = {
|
||||
reason: params?.reason ?? "fallback",
|
||||
force: true,
|
||||
progress: progress ?? undefined,
|
||||
});
|
||||
};
|
||||
if (
|
||||
process.env.OPENCLAW_TEST_FAST === "1" &&
|
||||
process.env.OPENCLAW_TEST_MEMORY_UNSAFE_REINDEX === "1"
|
||||
) {
|
||||
await this.runUnsafeReindex(reindexParams);
|
||||
} else {
|
||||
await this.runSafeReindex(reindexParams);
|
||||
}
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
@ -1057,6 +1094,7 @@ export abstract class MemoryManagerSyncOps {
|
||||
private async runSafeReindex(params: {
|
||||
reason?: string;
|
||||
force?: boolean;
|
||||
sessionFiles?: string[];
|
||||
progress?: MemorySyncProgressState;
|
||||
}): Promise<void> {
|
||||
const dbPath = resolveUserPath(this.settings.store.path);
|
||||
@ -1113,7 +1151,11 @@ export abstract class MemoryManagerSyncOps {
|
||||
}
|
||||
|
||||
if (shouldSyncSessions) {
|
||||
await this.syncSessionFiles({ needsFullReindex: true, progress: params.progress });
|
||||
await this.syncSessionFiles({
|
||||
needsFullReindex: true,
|
||||
progress: params.progress,
|
||||
targetSessionFiles: params.sessionFiles,
|
||||
});
|
||||
this.sessionsDirty = false;
|
||||
this.sessionsDirtyFiles.clear();
|
||||
} else if (this.sessionsDirtyFiles.size > 0) {
|
||||
@ -1166,6 +1208,7 @@ export abstract class MemoryManagerSyncOps {
|
||||
private async runUnsafeReindex(params: {
|
||||
reason?: string;
|
||||
force?: boolean;
|
||||
sessionFiles?: string[];
|
||||
progress?: MemorySyncProgressState;
|
||||
}): Promise<void> {
|
||||
// Perf: for test runs, skip atomic temp-db swapping. The index is isolated
|
||||
@ -1184,7 +1227,11 @@ export abstract class MemoryManagerSyncOps {
|
||||
}
|
||||
|
||||
if (shouldSyncSessions) {
|
||||
await this.syncSessionFiles({ needsFullReindex: true, progress: params.progress });
|
||||
await this.syncSessionFiles({
|
||||
needsFullReindex: true,
|
||||
progress: params.progress,
|
||||
targetSessionFiles: params.sessionFiles,
|
||||
});
|
||||
this.sessionsDirty = false;
|
||||
this.sessionsDirtyFiles.clear();
|
||||
} else if (this.sessionsDirtyFiles.size > 0) {
|
||||
|
||||
@ -181,6 +181,9 @@ describe("renderOverview", () => {
|
||||
expect(renderedValues).toContain("Sync coding");
|
||||
expect(renderedValues).toContain("technical · stored");
|
||||
expect(renderedValues).toContain("high-signal memory candidate");
|
||||
});
|
||||
});
|
||||
|
||||
describe("agentLogoUrl", () => {
|
||||
it("keeps base-mounted control UI logo paths absolute to the mount", () => {
|
||||
expect(agentLogoUrl("/ui")).toBe("/ui/favicon.svg");
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user