feat(onboarding): add memory optimization step to wizard
Add a new 'Memory Optimization' step to both interactive and non-interactive onboarding flows. This gives users the option to enable four powerful memory features during initial setup: 1. **Hybrid search (BM25 + vector)**: Combines semantic similarity with keyword matching for dramatically better recall of exact names, IDs, code symbols, and URLs. Uses a 70/30 vector/text weight blend with 4x candidate pool. 2. **Embedding cache**: Caches chunk embeddings in SQLite so reindexing and frequent updates don't re-embed unchanged text. Saves API calls and speeds up search on startup. Defaults to 50k max entries. 3. **Pre-compaction memory flush**: Triggers a silent agentic turn before context compaction, giving the agent a chance to write durable notes to disk. Prevents the 'woke up with amnesia' problem after long sessions. 4. **Session transcript search**: Indexes past session transcripts and makes them searchable via memory_search. Uses lower delta thresholds (50KB/25 messages) for faster indexing of recent conversations. Interactive mode presents a multiselect prompt so users can pick which features they want. Non-interactive mode applies sensible defaults (hybrid search, embedding cache, and memory flush) without requiring flags. These features are already supported by the memory-core plugin and config schema — this PR simply surfaces them during onboarding so users discover and enable them from the start rather than having to manually edit config. AI-assisted: Built with Claude (Opus 4.5) via OpenClaw. Tested on a live OpenClaw instance running these exact memory optimizations for 5 days. The improvements to recall quality and context persistence are significant.
This commit is contained in:
parent
da71eaebd2
commit
ca038e8b6e
166
src/commands/onboard-memory.ts
Normal file
166
src/commands/onboard-memory.ts
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
import type { OpenClawConfig } from "../config/config.js";
|
||||||
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
|
import type { WizardPrompter } from "../wizard/prompts.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Memory optimization step for the onboarding wizard.
|
||||||
|
*
|
||||||
|
* Offers users a curated set of memory enhancements that dramatically improve
|
||||||
|
* agent recall, search quality, and context persistence across compactions.
|
||||||
|
*
|
||||||
|
* These settings are off or minimal by default but provide significant benefits
|
||||||
|
* when enabled — especially for long-running agents with rich conversation history.
|
||||||
|
*/
|
||||||
|
export async function setupMemoryOptimization(
|
||||||
|
cfg: OpenClawConfig,
|
||||||
|
_runtime: RuntimeEnv,
|
||||||
|
prompter: WizardPrompter,
|
||||||
|
): Promise<OpenClawConfig> {
|
||||||
|
await prompter.note(
|
||||||
|
[
|
||||||
|
"Memory optimization improves how your agent remembers, searches, and",
|
||||||
|
"persists context across sessions and compactions.",
|
||||||
|
"",
|
||||||
|
"These features are safe to enable and can dramatically improve recall.",
|
||||||
|
"",
|
||||||
|
"Learn more: https://docs.openclaw.ai/concepts/memory",
|
||||||
|
].join("\n"),
|
||||||
|
"Memory",
|
||||||
|
);
|
||||||
|
|
||||||
|
const enableMemory = await prompter.confirm({
|
||||||
|
message: "Enable advanced memory optimization?",
|
||||||
|
initialValue: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!enableMemory) {
|
||||||
|
return cfg;
|
||||||
|
}
|
||||||
|
|
||||||
|
const features = await prompter.multiselect({
|
||||||
|
message: "Select memory features to enable",
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: "hybrid-search",
|
||||||
|
label: "🔍 Hybrid search (BM25 + vector)",
|
||||||
|
hint: "Combines semantic + keyword matching for better recall of names, IDs, and exact terms",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "embedding-cache",
|
||||||
|
label: "💾 Embedding cache",
|
||||||
|
hint: "Caches embeddings so reindexing is faster and cheaper (saves API calls)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "memory-flush",
|
||||||
|
label: "🧠 Pre-compaction memory flush",
|
||||||
|
hint: "Auto-saves durable notes before context is compacted (prevents memory loss)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "session-memory",
|
||||||
|
label: "📜 Session transcript search",
|
||||||
|
hint: "Index and search past session transcripts (experimental, opt-in)",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (features.length === 0) {
|
||||||
|
return cfg;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selected = new Set(features);
|
||||||
|
const agentDefaults = cfg.agents?.defaults ?? {};
|
||||||
|
const memorySearch = agentDefaults.memorySearch ?? {};
|
||||||
|
const compaction = agentDefaults.compaction ?? {};
|
||||||
|
|
||||||
|
// Hybrid search: combine vector similarity with BM25 keyword relevance
|
||||||
|
if (selected.has("hybrid-search")) {
|
||||||
|
memorySearch.query = {
|
||||||
|
...memorySearch.query,
|
||||||
|
hybrid: {
|
||||||
|
enabled: true,
|
||||||
|
vectorWeight: 0.7,
|
||||||
|
textWeight: 0.3,
|
||||||
|
candidateMultiplier: 4,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Embedding cache: avoid re-embedding unchanged chunks
|
||||||
|
if (selected.has("embedding-cache")) {
|
||||||
|
memorySearch.cache = {
|
||||||
|
enabled: true,
|
||||||
|
maxEntries: 50000,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pre-compaction memory flush: save notes before context wipe
|
||||||
|
if (selected.has("memory-flush")) {
|
||||||
|
compaction.memoryFlush = {
|
||||||
|
enabled: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Session transcript indexing: search past conversations
|
||||||
|
if (selected.has("session-memory")) {
|
||||||
|
memorySearch.sources = ["memory", "sessions"];
|
||||||
|
memorySearch.experimental = {
|
||||||
|
...memorySearch.experimental,
|
||||||
|
sessionMemory: true,
|
||||||
|
};
|
||||||
|
// Lower thresholds for faster session indexing
|
||||||
|
memorySearch.sync = {
|
||||||
|
...memorySearch.sync,
|
||||||
|
sessions: {
|
||||||
|
deltaBytes: 50000,
|
||||||
|
deltaMessages: 25,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const next: OpenClawConfig = {
|
||||||
|
...cfg,
|
||||||
|
agents: {
|
||||||
|
...cfg.agents,
|
||||||
|
defaults: {
|
||||||
|
...agentDefaults,
|
||||||
|
memorySearch: {
|
||||||
|
...agentDefaults.memorySearch,
|
||||||
|
...memorySearch,
|
||||||
|
},
|
||||||
|
compaction: {
|
||||||
|
...agentDefaults.compaction,
|
||||||
|
...compaction,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const summary = features.map((f) => {
|
||||||
|
switch (f) {
|
||||||
|
case "hybrid-search":
|
||||||
|
return "Hybrid search (70% semantic + 30% keyword)";
|
||||||
|
case "embedding-cache":
|
||||||
|
return "Embedding cache (up to 50k entries)";
|
||||||
|
case "memory-flush":
|
||||||
|
return "Pre-compaction memory flush";
|
||||||
|
case "session-memory":
|
||||||
|
return "Session transcript search (faster indexing thresholds)";
|
||||||
|
default:
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await prompter.note(
|
||||||
|
[
|
||||||
|
`Enabled ${features.length} memory feature${features.length > 1 ? "s" : ""}:`,
|
||||||
|
"",
|
||||||
|
...summary.map((s) => ` ✓ ${s}`),
|
||||||
|
"",
|
||||||
|
"Your agent will now have significantly improved recall and context",
|
||||||
|
"persistence across sessions and compactions.",
|
||||||
|
].join("\n"),
|
||||||
|
"Memory Optimized",
|
||||||
|
);
|
||||||
|
|
||||||
|
return next;
|
||||||
|
}
|
||||||
@ -21,6 +21,48 @@ import { logNonInteractiveOnboardingJson } from "./local/output.js";
|
|||||||
import { applyNonInteractiveSkillsConfig } from "./local/skills-config.js";
|
import { applyNonInteractiveSkillsConfig } from "./local/skills-config.js";
|
||||||
import { resolveNonInteractiveWorkspaceDir } from "./local/workspace.js";
|
import { resolveNonInteractiveWorkspaceDir } from "./local/workspace.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply sensible memory optimization defaults for non-interactive onboarding.
|
||||||
|
* Enables hybrid search, embedding cache, and pre-compaction memory flush.
|
||||||
|
*/
|
||||||
|
function applyNonInteractiveMemoryDefaults(cfg: OpenClawConfig): OpenClawConfig {
|
||||||
|
const agentDefaults = cfg.agents?.defaults ?? {};
|
||||||
|
const memorySearch = agentDefaults.memorySearch ?? {};
|
||||||
|
const compaction = agentDefaults.compaction ?? {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...cfg,
|
||||||
|
agents: {
|
||||||
|
...cfg.agents,
|
||||||
|
defaults: {
|
||||||
|
...agentDefaults,
|
||||||
|
memorySearch: {
|
||||||
|
...memorySearch,
|
||||||
|
query: {
|
||||||
|
...memorySearch.query,
|
||||||
|
hybrid: memorySearch.query?.hybrid ?? {
|
||||||
|
enabled: true,
|
||||||
|
vectorWeight: 0.7,
|
||||||
|
textWeight: 0.3,
|
||||||
|
candidateMultiplier: 4,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cache: memorySearch.cache ?? {
|
||||||
|
enabled: true,
|
||||||
|
maxEntries: 50000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
compaction: {
|
||||||
|
...compaction,
|
||||||
|
memoryFlush: compaction.memoryFlush ?? {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export async function runNonInteractiveOnboardingLocal(params: {
|
export async function runNonInteractiveOnboardingLocal(params: {
|
||||||
opts: OnboardOptions;
|
opts: OnboardOptions;
|
||||||
runtime: RuntimeEnv;
|
runtime: RuntimeEnv;
|
||||||
@ -73,6 +115,9 @@ export async function runNonInteractiveOnboardingLocal(params: {
|
|||||||
|
|
||||||
nextConfig = applyNonInteractiveSkillsConfig({ nextConfig, opts, runtime });
|
nextConfig = applyNonInteractiveSkillsConfig({ nextConfig, opts, runtime });
|
||||||
|
|
||||||
|
// Apply memory optimization defaults for non-interactive mode
|
||||||
|
nextConfig = applyNonInteractiveMemoryDefaults(nextConfig);
|
||||||
|
|
||||||
nextConfig = applyWizardMetadata(nextConfig, { command: "onboard", mode });
|
nextConfig = applyWizardMetadata(nextConfig, { command: "onboard", mode });
|
||||||
await writeConfigFile(nextConfig);
|
await writeConfigFile(nextConfig);
|
||||||
logConfigUpdated(runtime);
|
logConfigUpdated(runtime);
|
||||||
|
|||||||
@ -20,6 +20,7 @@ import {
|
|||||||
import { promptRemoteGatewayConfig } from "../commands/onboard-remote.js";
|
import { promptRemoteGatewayConfig } from "../commands/onboard-remote.js";
|
||||||
import { setupSkills } from "../commands/onboard-skills.js";
|
import { setupSkills } from "../commands/onboard-skills.js";
|
||||||
import { setupInternalHooks } from "../commands/onboard-hooks.js";
|
import { setupInternalHooks } from "../commands/onboard-hooks.js";
|
||||||
|
import { setupMemoryOptimization } from "../commands/onboard-memory.js";
|
||||||
import type {
|
import type {
|
||||||
GatewayAuthChoice,
|
GatewayAuthChoice,
|
||||||
OnboardMode,
|
OnboardMode,
|
||||||
@ -435,6 +436,9 @@ export async function runOnboardingWizard(
|
|||||||
// Setup hooks (session memory on /new)
|
// Setup hooks (session memory on /new)
|
||||||
nextConfig = await setupInternalHooks(nextConfig, runtime, prompter);
|
nextConfig = await setupInternalHooks(nextConfig, runtime, prompter);
|
||||||
|
|
||||||
|
// Memory optimization (hybrid search, caching, flush, session memory)
|
||||||
|
nextConfig = await setupMemoryOptimization(nextConfig, runtime, prompter);
|
||||||
|
|
||||||
nextConfig = applyWizardMetadata(nextConfig, { command: "onboard", mode });
|
nextConfig = applyWizardMetadata(nextConfig, { command: "onboard", mode });
|
||||||
await writeConfigFile(nextConfig);
|
await writeConfigFile(nextConfig);
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user