fix: repair memory sync and config regressions

This commit is contained in:
Junebugg1214 2026-03-13 13:58:01 -04:00
parent ce1db6e425
commit 3eeae2cb05
5 changed files with 83 additions and 18 deletions

View File

@ -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>,

View File

@ -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":

View File

@ -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",
});

View File

@ -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) {

View File

@ -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");