diff --git a/src/auto-reply/reply/commands.test.ts b/src/auto-reply/reply/commands.test.ts index 4847ba60832..33c07d8f566 100644 --- a/src/auto-reply/reply/commands.test.ts +++ b/src/auto-reply/reply/commands.test.ts @@ -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, -) { async function withTempConfigPath( initialConfig: Record, run: (configPath: string) => Promise, diff --git a/src/config/schema.help.ts b/src/config/schema.help.ts index a95918411d5..6e3e5ee21b4 100644 --- a/src/config/schema.help.ts +++ b/src/config/schema.help.ts @@ -801,6 +801,14 @@ export const FIELD_HELP: Record = { '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 = { "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": diff --git a/src/memory/embeddings.test.ts b/src/memory/embeddings.test.ts index 3949fb0b05f..e9e28ec557d 100644 --- a/src/memory/embeddings.test.ts +++ b/src/memory/embeddings.test.ts @@ -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", }); diff --git a/src/memory/manager-sync-ops.ts b/src/memory/manager-sync-ops.ts index e3b9a5473fa..6526412405c 100644 --- a/src/memory/manager-sync-ops.ts +++ b/src/memory/manager-sync-ops.ts @@ -761,19 +761,30 @@ export abstract class MemoryManagerSyncOps { private async syncSessionFiles(params: { needsFullReindex: boolean; progress?: MemorySyncProgressState; - }) { + targetSessionFiles?: string[]; + }): Promise> { // 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(); 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 { 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 { // 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) { diff --git a/ui/src/ui/views/agents-utils.test.ts b/ui/src/ui/views/agents-utils.test.ts index 37e8c5db576..b91c991336f 100644 --- a/ui/src/ui/views/agents-utils.test.ts +++ b/ui/src/ui/views/agents-utils.test.ts @@ -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");