From f35a4203d45e26b87959a7fff7e9b23b178a4b5a Mon Sep 17 00:00:00 2001 From: lubolin0925 <163701194+lubolin0925@users.noreply.github.com> Date: Sat, 7 Mar 2026 10:46:46 +0800 Subject: [PATCH 01/12] Add /learn and memory --- src/auto-reply/commands-registry.data.ts | 16 +++ src/auto-reply/reply/commands-compact.ts | 33 +++++ src/auto-reply/reply/commands-core.ts | 41 ++++++ src/auto-reply/reply/commands-learn.ts | 166 +++++++++++++++++++++++ 4 files changed, 256 insertions(+) create mode 100644 src/auto-reply/reply/commands-learn.ts diff --git a/src/auto-reply/commands-registry.data.ts b/src/auto-reply/commands-registry.data.ts index 6a2bf205ffd..0330a736682 100644 --- a/src/auto-reply/commands-registry.data.ts +++ b/src/auto-reply/commands-registry.data.ts @@ -565,6 +565,22 @@ function buildChatCommands(): ChatCommandDefinition[] { }, ], }), + defineChatCommand({ + key: "learn", + nativeName: "learn", + description: "Trigger agent to learn and remember insights from the session.", + textAlias: "/learn", + acceptsArgs: true, + category: "session", + args: [ + { + name: "focus", + description: "What to focus on learning (optional)", + type: "string", + captureRemaining: true, + }, + ], + }), defineChatCommand({ key: "think", nativeName: "think", diff --git a/src/auto-reply/reply/commands-compact.ts b/src/auto-reply/reply/commands-compact.ts index 1533bb24393..ad2756a3bc9 100644 --- a/src/auto-reply/reply/commands-compact.ts +++ b/src/auto-reply/reply/commands-compact.ts @@ -16,6 +16,7 @@ import { formatContextUsageShort, formatTokenCount } from "../status.js"; import type { CommandHandler } from "./commands-types.js"; import { stripMentions, stripStructuralPrefixes } from "./mentions.js"; import { incrementCompactionCount } from "./session-updates.js"; +import { runLearnForSession } from "./commands-learn.js"; function extractCompactInstructions(params: { rawBody?: string; @@ -68,6 +69,38 @@ export const handleCompactCommand: CommandHandler = async (params) => { abortEmbeddedPiRun(sessionId); await waitForEmbeddedPiRunEnd(sessionId, 15_000); } + + const learnResult = await runLearnForSession({ + sessionId, + sessionKey: params.sessionKey, + messageChannel: params.command.channel, + groupId: params.sessionEntry.groupId, + groupChannel: params.sessionEntry.groupChannel, + groupSpace: params.sessionEntry.space, + spawnedBy: params.sessionEntry.spawnedBy, + sessionFile: resolveSessionFilePath( + sessionId, + params.sessionEntry, + resolveSessionFilePathOptions({ + agentId: params.agentId, + storePath: params.storePath, + }), + ), + workspaceDir: params.workspaceDir, + agentDir: params.agentDir, + config: params.cfg, + skillsSnapshot: params.sessionEntry.skillsSnapshot, + provider: params.provider, + model: params.model, + thinkLevel: params.resolvedThinkLevel ?? (await params.resolveDefaultThinkingLevel()), + customFocus: "What insights and lessons should be remembered before context compaction?", + senderIsOwner: params.command.senderIsOwner, + ownerNumbers: params.command.ownerList.length > 0 ? params.command.ownerList : undefined, + }); + if (learnResult.ok) { + logVerbose(`Pre-compaction learning completed for session ${params.sessionKey}`); + } + const customInstructions = extractCompactInstructions({ rawBody: params.ctx.CommandBody ?? params.ctx.RawBody ?? params.ctx.Body, ctx: params.ctx, diff --git a/src/auto-reply/reply/commands-core.ts b/src/auto-reply/reply/commands-core.ts index d57d679fdb6..a42bd7018b0 100644 --- a/src/auto-reply/reply/commands-core.ts +++ b/src/auto-reply/reply/commands-core.ts @@ -13,6 +13,7 @@ import { handleApproveCommand } from "./commands-approve.js"; import { handleBashCommand } from "./commands-bash.js"; import { handleCompactCommand } from "./commands-compact.js"; import { handleConfigCommand, handleDebugCommand } from "./commands-config.js"; +import { handleLearnCommand } from "./commands-learn.js"; import { handleCommandsListCommand, handleContextCommand, @@ -40,6 +41,11 @@ import type { HandleCommandsParams, } from "./commands-types.js"; import { routeReply } from "./route-reply.js"; +import { runLearnForSession } from "./commands-learn.js"; +import { + resolveSessionFilePath, + resolveSessionFilePathOptions, +} from "../../config/sessions.js"; let HANDLERS: CommandHandler[] | null = null; @@ -194,6 +200,7 @@ export async function handleCommands(params: HandleCommandsParams): Promise 0 ? params.command.ownerList : undefined, + }); + if (learnResult.ok) { + logVerbose(`Pre-reset learning completed for session ${params.sessionKey}`); + } + } + // Trigger internal hook for reset/new commands if (resetRequested && params.command.isAuthorizedSender) { const commandAction: ResetCommandAction = resetMatch?.[1] === "reset" ? "reset" : "new"; diff --git a/src/auto-reply/reply/commands-learn.ts b/src/auto-reply/reply/commands-learn.ts new file mode 100644 index 00000000000..4d200b91abc --- /dev/null +++ b/src/auto-reply/reply/commands-learn.ts @@ -0,0 +1,166 @@ +import { compactEmbeddedPiSession } from "../../agents/pi-embedded.js"; +import type { OpenClawConfig } from "../../config/config.js"; +import { + resolveSessionFilePath, + resolveSessionFilePathOptions, +} from "../../config/sessions.js"; +import { logVerbose } from "../../globals.js"; + +const LEARN_SYSTEM_PROMPT = [ + "Learning turn.", + "Analyze the session history and remember important insights.", +].join(" "); + +const LEARN_DEFAULT_PROMPT = [ + "Learning turn.", + "What important insights, lessons, or information should be remembered from this session?", +].join(" "); + +function extractLearnFocus(params: { + rawBody?: string; + ctx: import("../templating.js").MsgContext; + cfg: OpenClawConfig; + agentId?: string; + isGroup: boolean; +}): string | undefined { + const trimmed = params.rawBody?.trim() ?? ""; + if (!trimmed) { + return undefined; + } + const lowered = trimmed.toLowerCase(); + const prefix = lowered.startsWith("/learn") ? "/learn" : null; + if (!prefix) { + return undefined; + } + let rest = trimmed.slice(prefix.length).trimStart(); + if (rest.startsWith(":")) { + rest = rest.slice(1).trimStart(); + } + return rest.length ? rest : undefined; +} + +export async function runLearnForSession(params: { + sessionId: string; + sessionKey: string; + messageChannel: string; + groupId?: string; + groupChannel?: string; + groupSpace?: string; + spawnedBy?: string; + sessionFile: string; + workspaceDir: string; + agentDir?: string; + config: OpenClawConfig; + skillsSnapshot?: Record; + provider: string; + model: string; + thinkLevel?: string; + customFocus?: string; + senderIsOwner: boolean; + ownerNumbers?: string[]; +}): Promise<{ ok: boolean; message?: string }> { + const customInstructions = params.customFocus + ? `Focus area: ${params.customFocus}. ${LEARN_DEFAULT_PROMPT}` + : LEARN_DEFAULT_PROMPT; + + const result = await compactEmbeddedPiSession({ + sessionId: params.sessionId, + sessionKey: params.sessionKey, + messageChannel: params.messageChannel, + groupId: params.groupId, + groupChannel: params.groupChannel, + groupSpace: params.groupSpace, + spawnedBy: params.spawnedBy, + sessionFile: params.sessionFile, + workspaceDir: params.workspaceDir, + agentDir: params.agentDir, + config: params.config, + skillsSnapshot: params.skillsSnapshot, + provider: params.provider, + model: params.model, + thinkLevel: params.thinkLevel ?? "medium", + bashElevated: { + enabled: false, + allowed: false, + defaultLevel: "off", + }, + customInstructions, + extraSystemPrompt: LEARN_SYSTEM_PROMPT, + trigger: "manual", + senderIsOwner: params.senderIsOwner, + ownerNumbers: params.ownerNumbers, + }); + + if (result.ok) { + return { ok: true, message: "Learning completed. Insights saved to memory." }; + } + return { ok: false, message: result.reason ?? "Learning failed" }; +} + +export const handleLearnCommand = async ( + params: import("./commands-types.js").HandleCommandsParams, + allowTextCommands: boolean, +): Promise => { + const learnRequested = + params.command.commandBodyNormalized === "/learn" || + params.command.commandBodyNormalized.startsWith("/learn "); + if (!learnRequested) { + return null; + } + + if (!params.command.isAuthorizedSender) { + logVerbose( + `Ignoring /learn from unauthorized sender: ${params.command.senderId || ""}`, + ); + return { shouldContinue: false }; + } + + if (!params.sessionEntry?.sessionId) { + return { + shouldContinue: false, + reply: { text: "Learning unavailable (missing session id)." }, + }; + } + + const sessionId = params.sessionEntry.sessionId; + const customFocus = extractLearnFocus({ + rawBody: params.ctx.CommandBody ?? params.ctx.RawBody ?? params.ctx.Body, + ctx: params.ctx, + cfg: params.cfg, + agentId: params.agentId, + isGroup: params.isGroup, + }); + + const result = await runLearnForSession({ + sessionId, + sessionKey: params.sessionKey, + messageChannel: params.command.channel, + groupId: params.sessionEntry.groupId, + groupChannel: params.sessionEntry.groupChannel, + groupSpace: params.sessionEntry.space, + spawnedBy: params.sessionEntry.spawnedBy, + sessionFile: resolveSessionFilePath( + sessionId, + params.sessionEntry, + resolveSessionFilePathOptions({ + agentId: params.agentId, + storePath: params.storePath, + }), + ), + workspaceDir: params.workspaceDir, + agentDir: params.agentDir, + config: params.cfg, + skillsSnapshot: params.sessionEntry.skillsSnapshot, + provider: params.provider, + model: params.model, + thinkLevel: params.resolvedThinkLevel ?? (await params.resolveDefaultThinkingLevel()), + customFocus, + senderIsOwner: params.command.senderIsOwner, + ownerNumbers: params.command.ownerList.length > 0 ? params.command.ownerList : undefined, + }); + + return { + shouldContinue: false, + reply: { text: result.ok ? `📚 ${result.message}` : `⚠️ ${result.message}` }, + }; +}; From 172d2f3d70ea48fcd4bbedcd6e2a5cbe6dad9563 Mon Sep 17 00:00:00 2001 From: lubolin0925 <163701194+lubolin0925@users.noreply.github.com> Date: Sat, 7 Mar 2026 11:15:09 +0800 Subject: [PATCH 02/12] fix the problems --- src/auto-reply/reply/commands-compact.ts | 8 ++- src/auto-reply/reply/commands-core.ts | 79 +++++++++++---------- src/auto-reply/reply/commands-learn.ts | 88 +++++++++++------------- 3 files changed, 91 insertions(+), 84 deletions(-) diff --git a/src/auto-reply/reply/commands-compact.ts b/src/auto-reply/reply/commands-compact.ts index ad2756a3bc9..6866a994afa 100644 --- a/src/auto-reply/reply/commands-compact.ts +++ b/src/auto-reply/reply/commands-compact.ts @@ -70,6 +70,8 @@ export const handleCompactCommand: CommandHandler = async (params) => { await waitForEmbeddedPiRunEnd(sessionId, 15_000); } + const thinkLevel = params.resolvedThinkLevel ?? (await params.resolveDefaultThinkingLevel()); + const learnResult = await runLearnForSession({ sessionId, sessionKey: params.sessionKey, @@ -92,13 +94,15 @@ export const handleCompactCommand: CommandHandler = async (params) => { skillsSnapshot: params.sessionEntry.skillsSnapshot, provider: params.provider, model: params.model, - thinkLevel: params.resolvedThinkLevel ?? (await params.resolveDefaultThinkingLevel()), + thinkLevel, customFocus: "What insights and lessons should be remembered before context compaction?", senderIsOwner: params.command.senderIsOwner, ownerNumbers: params.command.ownerList.length > 0 ? params.command.ownerList : undefined, }); if (learnResult.ok) { logVerbose(`Pre-compaction learning completed for session ${params.sessionKey}`); + } else { + logVerbose(`Pre-compaction learning failed for session ${params.sessionKey}: ${learnResult.message ?? "unknown error"}`); } const customInstructions = extractCompactInstructions({ @@ -130,7 +134,7 @@ export const handleCompactCommand: CommandHandler = async (params) => { skillsSnapshot: params.sessionEntry.skillsSnapshot, provider: params.provider, model: params.model, - thinkLevel: params.resolvedThinkLevel ?? (await params.resolveDefaultThinkingLevel()), + thinkLevel, bashElevated: { enabled: false, allowed: false, diff --git a/src/auto-reply/reply/commands-core.ts b/src/auto-reply/reply/commands-core.ts index a42bd7018b0..a16e7be2be6 100644 --- a/src/auto-reply/reply/commands-core.ts +++ b/src/auto-reply/reply/commands-core.ts @@ -13,7 +13,7 @@ import { handleApproveCommand } from "./commands-approve.js"; import { handleBashCommand } from "./commands-bash.js"; import { handleCompactCommand } from "./commands-compact.js"; import { handleConfigCommand, handleDebugCommand } from "./commands-config.js"; -import { handleLearnCommand } from "./commands-learn.js"; +import { handleLearnCommand, runLearnForSession } from "./commands-learn.js"; import { handleCommandsListCommand, handleContextCommand, @@ -41,7 +41,6 @@ import type { HandleCommandsParams, } from "./commands-types.js"; import { routeReply } from "./route-reply.js"; -import { runLearnForSession } from "./commands-learn.js"; import { resolveSessionFilePath, resolveSessionFilePathOptions, @@ -213,40 +212,6 @@ export async function handleCommands(params: HandleCommandsParams): Promise 0 ? params.command.ownerList : undefined, - }); - if (learnResult.ok) { - logVerbose(`Pre-reset learning completed for session ${params.sessionKey}`); - } - } - // Trigger internal hook for reset/new commands if (resetRequested && params.command.isAuthorizedSender) { const commandAction: ResetCommandAction = resetMatch?.[1] === "reset" ? "reset" : "new"; @@ -259,6 +224,48 @@ export async function handleCommands(params: HandleCommandsParams): Promise 0 ? params.command.ownerList : undefined, + }); + if (learnResult.ok) { + logVerbose(`Pre-reset learning completed for session ${targetSessionKey}`); + } else { + logVerbose(`Pre-reset learning failed for session ${targetSessionKey}: ${learnResult.message ?? "unknown error"}`); + } + } if (boundAcpKey) { const resetResult = await resetAcpSessionInPlace({ cfg: params.cfg, diff --git a/src/auto-reply/reply/commands-learn.ts b/src/auto-reply/reply/commands-learn.ts index 4d200b91abc..6ae3e9b0cd8 100644 --- a/src/auto-reply/reply/commands-learn.ts +++ b/src/auto-reply/reply/commands-learn.ts @@ -1,4 +1,4 @@ -import { compactEmbeddedPiSession } from "../../agents/pi-embedded.js"; +import { runEmbeddedPiAgent } from "../../agents/pi-embedded.js"; import type { OpenClawConfig } from "../../config/config.js"; import { resolveSessionFilePath, @@ -16,14 +16,8 @@ const LEARN_DEFAULT_PROMPT = [ "What important insights, lessons, or information should be remembered from this session?", ].join(" "); -function extractLearnFocus(params: { - rawBody?: string; - ctx: import("../templating.js").MsgContext; - cfg: OpenClawConfig; - agentId?: string; - isGroup: boolean; -}): string | undefined { - const trimmed = params.rawBody?.trim() ?? ""; +function extractLearnFocus(rawBody?: string): string | undefined { + const trimmed = rawBody?.trim() ?? ""; if (!trimmed) { return undefined; } @@ -59,42 +53,50 @@ export async function runLearnForSession(params: { senderIsOwner: boolean; ownerNumbers?: string[]; }): Promise<{ ok: boolean; message?: string }> { - const customInstructions = params.customFocus + const prompt = params.customFocus ? `Focus area: ${params.customFocus}. ${LEARN_DEFAULT_PROMPT}` : LEARN_DEFAULT_PROMPT; - const result = await compactEmbeddedPiSession({ - sessionId: params.sessionId, - sessionKey: params.sessionKey, - messageChannel: params.messageChannel, - groupId: params.groupId, - groupChannel: params.groupChannel, - groupSpace: params.groupSpace, - spawnedBy: params.spawnedBy, - sessionFile: params.sessionFile, - workspaceDir: params.workspaceDir, - agentDir: params.agentDir, - config: params.config, - skillsSnapshot: params.skillsSnapshot, - provider: params.provider, - model: params.model, - thinkLevel: params.thinkLevel ?? "medium", - bashElevated: { - enabled: false, - allowed: false, - defaultLevel: "off", - }, - customInstructions, - extraSystemPrompt: LEARN_SYSTEM_PROMPT, - trigger: "manual", - senderIsOwner: params.senderIsOwner, - ownerNumbers: params.ownerNumbers, - }); + const sessionFilePath = resolveSessionFilePath( + params.sessionId, + { sessionId: params.sessionId, sessionFile: params.sessionFile }, + resolveSessionFilePathOptions({ agentId: undefined, storePath: undefined }), + ); + + try { + await runEmbeddedPiAgent({ + sessionId: params.sessionId, + sessionKey: params.sessionKey, + sessionFile: sessionFilePath, + messageChannel: params.messageChannel, + groupId: params.groupId, + groupChannel: params.groupChannel, + groupSpace: params.groupSpace, + spawnedBy: params.spawnedBy, + workspaceDir: params.workspaceDir, + agentDir: params.agentDir, + config: params.config, + skillsSnapshot: params.skillsSnapshot, + provider: params.provider, + model: params.model, + thinkLevel: params.thinkLevel ?? "medium", + bashElevated: { + enabled: false, + allowed: false, + defaultLevel: "off", + }, + prompt, + extraSystemPrompt: LEARN_SYSTEM_PROMPT, + trigger: "memory", + senderIsOwner: params.senderIsOwner, + ownerNumbers: params.ownerNumbers, + }); - if (result.ok) { return { ok: true, message: "Learning completed. Insights saved to memory." }; + } catch (err) { + logVerbose(`Learning failed for session ${params.sessionKey}: ${String(err)}`); + return { ok: false, message: String(err) }; } - return { ok: false, message: result.reason ?? "Learning failed" }; } export const handleLearnCommand = async ( @@ -123,13 +125,7 @@ export const handleLearnCommand = async ( } const sessionId = params.sessionEntry.sessionId; - const customFocus = extractLearnFocus({ - rawBody: params.ctx.CommandBody ?? params.ctx.RawBody ?? params.ctx.Body, - ctx: params.ctx, - cfg: params.cfg, - agentId: params.agentId, - isGroup: params.isGroup, - }); + const customFocus = extractLearnFocus(params.ctx.CommandBody ?? params.ctx.RawBody ?? params.ctx.Body); const result = await runLearnForSession({ sessionId, From d000dcad941e355765ba44b96d0cff8b311ef75e Mon Sep 17 00:00:00 2001 From: lubolin0925 <163701194+lubolin0925@users.noreply.github.com> Date: Sat, 7 Mar 2026 11:21:36 +0800 Subject: [PATCH 03/12] fix: formatting issues in learn command files --- src/auto-reply/reply/commands-compact.ts | 6 ++++-- src/auto-reply/reply/commands-core.ts | 14 +++++++------- src/auto-reply/reply/commands-learn.ts | 9 ++++----- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/auto-reply/reply/commands-compact.ts b/src/auto-reply/reply/commands-compact.ts index 6866a994afa..648e6b889e7 100644 --- a/src/auto-reply/reply/commands-compact.ts +++ b/src/auto-reply/reply/commands-compact.ts @@ -13,10 +13,10 @@ import { import { logVerbose } from "../../globals.js"; import { enqueueSystemEvent } from "../../infra/system-events.js"; import { formatContextUsageShort, formatTokenCount } from "../status.js"; +import { runLearnForSession } from "./commands-learn.js"; import type { CommandHandler } from "./commands-types.js"; import { stripMentions, stripStructuralPrefixes } from "./mentions.js"; import { incrementCompactionCount } from "./session-updates.js"; -import { runLearnForSession } from "./commands-learn.js"; function extractCompactInstructions(params: { rawBody?: string; @@ -102,7 +102,9 @@ export const handleCompactCommand: CommandHandler = async (params) => { if (learnResult.ok) { logVerbose(`Pre-compaction learning completed for session ${params.sessionKey}`); } else { - logVerbose(`Pre-compaction learning failed for session ${params.sessionKey}: ${learnResult.message ?? "unknown error"}`); + logVerbose( + `Pre-compaction learning failed for session ${params.sessionKey}: ${learnResult.message ?? "unknown error"}`, + ); } const customInstructions = extractCompactInstructions({ diff --git a/src/auto-reply/reply/commands-core.ts b/src/auto-reply/reply/commands-core.ts index a16e7be2be6..fb2ea132e48 100644 --- a/src/auto-reply/reply/commands-core.ts +++ b/src/auto-reply/reply/commands-core.ts @@ -1,5 +1,6 @@ import fs from "node:fs/promises"; import { resetAcpSessionInPlace } from "../../acp/persistent-bindings.js"; +import { resolveSessionFilePath, resolveSessionFilePathOptions } from "../../config/sessions.js"; import { logVerbose } from "../../globals.js"; import { createInternalHookEvent, triggerInternalHook } from "../../hooks/internal-hooks.js"; import { getGlobalHookRunner } from "../../plugins/hook-runner-global.js"; @@ -13,7 +14,6 @@ import { handleApproveCommand } from "./commands-approve.js"; import { handleBashCommand } from "./commands-bash.js"; import { handleCompactCommand } from "./commands-compact.js"; import { handleConfigCommand, handleDebugCommand } from "./commands-config.js"; -import { handleLearnCommand, runLearnForSession } from "./commands-learn.js"; import { handleCommandsListCommand, handleContextCommand, @@ -22,6 +22,7 @@ import { handleStatusCommand, handleWhoamiCommand, } from "./commands-info.js"; +import { handleLearnCommand, runLearnForSession } from "./commands-learn.js"; import { handleModelsCommand } from "./commands-models.js"; import { handlePluginCommand } from "./commands-plugin.js"; import { @@ -41,10 +42,6 @@ import type { HandleCommandsParams, } from "./commands-types.js"; import { routeReply } from "./route-reply.js"; -import { - resolveSessionFilePath, - resolveSessionFilePathOptions, -} from "../../config/sessions.js"; let HANDLERS: CommandHandler[] | null = null; @@ -256,14 +253,17 @@ export async function handleCommands(params: HandleCommandsParams): Promise 0 ? params.command.ownerList : undefined, }); if (learnResult.ok) { logVerbose(`Pre-reset learning completed for session ${targetSessionKey}`); } else { - logVerbose(`Pre-reset learning failed for session ${targetSessionKey}: ${learnResult.message ?? "unknown error"}`); + logVerbose( + `Pre-reset learning failed for session ${targetSessionKey}: ${learnResult.message ?? "unknown error"}`, + ); } } if (boundAcpKey) { diff --git a/src/auto-reply/reply/commands-learn.ts b/src/auto-reply/reply/commands-learn.ts index 6ae3e9b0cd8..3d87c333ff5 100644 --- a/src/auto-reply/reply/commands-learn.ts +++ b/src/auto-reply/reply/commands-learn.ts @@ -1,9 +1,6 @@ import { runEmbeddedPiAgent } from "../../agents/pi-embedded.js"; import type { OpenClawConfig } from "../../config/config.js"; -import { - resolveSessionFilePath, - resolveSessionFilePathOptions, -} from "../../config/sessions.js"; +import { resolveSessionFilePath, resolveSessionFilePathOptions } from "../../config/sessions.js"; import { logVerbose } from "../../globals.js"; const LEARN_SYSTEM_PROMPT = [ @@ -125,7 +122,9 @@ export const handleLearnCommand = async ( } const sessionId = params.sessionEntry.sessionId; - const customFocus = extractLearnFocus(params.ctx.CommandBody ?? params.ctx.RawBody ?? params.ctx.Body); + const customFocus = extractLearnFocus( + params.ctx.CommandBody ?? params.ctx.RawBody ?? params.ctx.Body, + ); const result = await runLearnForSession({ sessionId, From 672728a692c98389e759d46086ce1c3d3cd89916 Mon Sep 17 00:00:00 2001 From: lubolin0925 <163701194+lubolin0925@users.noreply.github.com> Date: Sat, 7 Mar 2026 11:24:40 +0800 Subject: [PATCH 04/12] fix: use previousSessionEntry for pre-reset learning and preserve session file path --- src/auto-reply/reply/commands-compact.ts | 9 +-------- src/auto-reply/reply/commands-core.ts | 21 ++++++++++----------- src/auto-reply/reply/commands-learn.ts | 9 +-------- 3 files changed, 12 insertions(+), 27 deletions(-) diff --git a/src/auto-reply/reply/commands-compact.ts b/src/auto-reply/reply/commands-compact.ts index 648e6b889e7..2ef77fb77e2 100644 --- a/src/auto-reply/reply/commands-compact.ts +++ b/src/auto-reply/reply/commands-compact.ts @@ -80,14 +80,7 @@ export const handleCompactCommand: CommandHandler = async (params) => { groupChannel: params.sessionEntry.groupChannel, groupSpace: params.sessionEntry.space, spawnedBy: params.sessionEntry.spawnedBy, - sessionFile: resolveSessionFilePath( - sessionId, - params.sessionEntry, - resolveSessionFilePathOptions({ - agentId: params.agentId, - storePath: params.storePath, - }), - ), + sessionFile: params.sessionEntry.sessionFile, workspaceDir: params.workspaceDir, agentDir: params.agentDir, config: params.cfg, diff --git a/src/auto-reply/reply/commands-core.ts b/src/auto-reply/reply/commands-core.ts index fb2ea132e48..f76604ed5e4 100644 --- a/src/auto-reply/reply/commands-core.ts +++ b/src/auto-reply/reply/commands-core.ts @@ -223,10 +223,16 @@ export async function handleCommands(params: HandleCommandsParams): Promise Date: Sat, 7 Mar 2026 13:32:35 +0800 Subject: [PATCH 05/12] fix: read archived .reset.* session file for pre-reset learning --- src/auto-reply/reply/commands-learn.ts | 32 +++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/src/auto-reply/reply/commands-learn.ts b/src/auto-reply/reply/commands-learn.ts index 6279d959b83..da9ea82317b 100644 --- a/src/auto-reply/reply/commands-learn.ts +++ b/src/auto-reply/reply/commands-learn.ts @@ -1,3 +1,5 @@ +import fs from "node:fs/promises"; +import path from "node:path"; import { runEmbeddedPiAgent } from "../../agents/pi-embedded.js"; import type { OpenClawConfig } from "../../config/config.js"; import { logVerbose } from "../../globals.js"; @@ -12,6 +14,31 @@ const LEARN_DEFAULT_PROMPT = [ "What important insights, lessons, or information should be remembered from this session?", ].join(" "); +async function resolveSessionFileWithResetFallback(sessionFile: string): Promise { + // For pre-reset learning, the session file has been archived to .reset.* + // so we need to look for the archived file directly + try { + const dir = path.dirname(sessionFile); + const base = path.basename(sessionFile); + const resetPrefix = `${base}.reset.`; + const files = await fs.readdir(dir); + const resetCandidates = files + .filter((name) => name.startsWith(resetPrefix)) + .sort() + .reverse(); + + if (resetCandidates.length > 0) { + const archivedPath = path.join(dir, resetCandidates[0]); + logVerbose(`Learning: using archived session file ${archivedPath}`); + return archivedPath; + } + } catch { + // Fallback to original path + } + + return sessionFile; +} + function extractLearnFocus(rawBody?: string): string | undefined { const trimmed = rawBody?.trim() ?? ""; if (!trimmed) { @@ -53,11 +80,14 @@ export async function runLearnForSession(params: { ? `Focus area: ${params.customFocus}. ${LEARN_DEFAULT_PROMPT}` : LEARN_DEFAULT_PROMPT; + // Resolve session file, falling back to archived .reset.* file if needed + const resolvedSessionFile = await resolveSessionFileWithResetFallback(params.sessionFile); + try { await runEmbeddedPiAgent({ sessionId: params.sessionId, sessionKey: params.sessionKey, - sessionFile: params.sessionFile, + sessionFile: resolvedSessionFile, messageChannel: params.messageChannel, groupId: params.groupId, groupChannel: params.groupChannel, From 35159c7a5428df200a3a9e69173695061edfaea7 Mon Sep 17 00:00:00 2001 From: lubolin0925 <163701194+lubolin0925@users.noreply.github.com> Date: Sat, 7 Mar 2026 13:49:25 +0800 Subject: [PATCH 06/12] fix: add timeout for learning runs --- src/auto-reply/reply/commands-learn.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/auto-reply/reply/commands-learn.ts b/src/auto-reply/reply/commands-learn.ts index da9ea82317b..628796be56b 100644 --- a/src/auto-reply/reply/commands-learn.ts +++ b/src/auto-reply/reply/commands-learn.ts @@ -108,6 +108,7 @@ export async function runLearnForSession(params: { prompt, extraSystemPrompt: LEARN_SYSTEM_PROMPT, trigger: "memory", + timeoutMs: 5 * 60 * 1000, // 5 minutes senderIsOwner: params.senderIsOwner, ownerNumbers: params.ownerNumbers, }); From 6351caaf7e17427d56646c30cecdc34365f7035f Mon Sep 17 00:00:00 2001 From: lubolin0925 <163701194+lubolin0925@users.noreply.github.com> Date: Sat, 7 Mar 2026 15:06:03 +0800 Subject: [PATCH 07/12] fix: add type imports and sessionFile checks for learn command --- src/auto-reply/reply/commands-compact.ts | 47 +++++++++++++----------- src/auto-reply/reply/commands-core.ts | 2 +- src/auto-reply/reply/commands-learn.ts | 23 +++++++----- 3 files changed, 39 insertions(+), 33 deletions(-) diff --git a/src/auto-reply/reply/commands-compact.ts b/src/auto-reply/reply/commands-compact.ts index 2ef77fb77e2..fece4e2a63c 100644 --- a/src/auto-reply/reply/commands-compact.ts +++ b/src/auto-reply/reply/commands-compact.ts @@ -72,29 +72,32 @@ export const handleCompactCommand: CommandHandler = async (params) => { const thinkLevel = params.resolvedThinkLevel ?? (await params.resolveDefaultThinkingLevel()); - const learnResult = await runLearnForSession({ - sessionId, - sessionKey: params.sessionKey, - messageChannel: params.command.channel, - groupId: params.sessionEntry.groupId, - groupChannel: params.sessionEntry.groupChannel, - groupSpace: params.sessionEntry.space, - spawnedBy: params.sessionEntry.spawnedBy, - sessionFile: params.sessionEntry.sessionFile, - workspaceDir: params.workspaceDir, - agentDir: params.agentDir, - config: params.cfg, - skillsSnapshot: params.sessionEntry.skillsSnapshot, - provider: params.provider, - model: params.model, - thinkLevel, - customFocus: "What insights and lessons should be remembered before context compaction?", - senderIsOwner: params.command.senderIsOwner, - ownerNumbers: params.command.ownerList.length > 0 ? params.command.ownerList : undefined, - }); - if (learnResult.ok) { + const learnResult = params.sessionEntry.sessionFile + ? await runLearnForSession({ + sessionId, + sessionKey: params.sessionKey, + messageChannel: params.command.channel, + groupId: params.sessionEntry.groupId, + groupChannel: params.sessionEntry.groupChannel, + groupSpace: params.sessionEntry.space, + spawnedBy: params.sessionEntry.spawnedBy, + sessionFile: params.sessionEntry.sessionFile, + workspaceDir: params.workspaceDir, + agentDir: params.agentDir, + config: params.cfg, + skillsSnapshot: params.sessionEntry.skillsSnapshot, + provider: params.provider, + model: params.model, + thinkLevel, + customFocus: "What insights and lessons should be remembered before context compaction?", + senderIsOwner: params.command.senderIsOwner, + ownerNumbers: params.command.ownerList.length > 0 ? params.command.ownerList : undefined, + }) + : null; + + if (learnResult?.ok) { logVerbose(`Pre-compaction learning completed for session ${params.sessionKey}`); - } else { + } else if (learnResult) { logVerbose( `Pre-compaction learning failed for session ${params.sessionKey}: ${learnResult.message ?? "unknown error"}`, ); diff --git a/src/auto-reply/reply/commands-core.ts b/src/auto-reply/reply/commands-core.ts index f76604ed5e4..abbeacf3fab 100644 --- a/src/auto-reply/reply/commands-core.ts +++ b/src/auto-reply/reply/commands-core.ts @@ -235,7 +235,7 @@ export async function handleCommands(params: HandleCommandsParams): Promise; + skillsSnapshot?: SkillSnapshot; provider: string; model: string; - thinkLevel?: string; + thinkLevel?: ThinkLevel; customFocus?: string; senderIsOwner: boolean; ownerNumbers?: string[]; @@ -85,6 +87,7 @@ export async function runLearnForSession(params: { try { await runEmbeddedPiAgent({ + runId: crypto.randomUUID(), sessionId: params.sessionId, sessionKey: params.sessionKey, sessionFile: resolvedSessionFile, @@ -150,6 +153,13 @@ export const handleLearnCommand = async ( params.ctx.CommandBody ?? params.ctx.RawBody ?? params.ctx.Body, ); + if (!params.sessionEntry.sessionFile) { + return { + shouldContinue: false, + reply: { text: "Learning unavailable (missing session file)." }, + }; + } + const result = await runLearnForSession({ sessionId, sessionKey: params.sessionKey, @@ -158,14 +168,7 @@ export const handleLearnCommand = async ( groupChannel: params.sessionEntry.groupChannel, groupSpace: params.sessionEntry.space, spawnedBy: params.sessionEntry.spawnedBy, - sessionFile: resolveSessionFilePath( - sessionId, - params.sessionEntry, - resolveSessionFilePathOptions({ - agentId: params.agentId, - storePath: params.storePath, - }), - ), + sessionFile: params.sessionEntry.sessionFile, workspaceDir: params.workspaceDir, agentDir: params.agentDir, config: params.cfg, From f469c7a439a7cf431d3df906bf7057b18744fdd2 Mon Sep 17 00:00:00 2001 From: lubolin0925 <163701194+lubolin0925@users.noreply.github.com> Date: Sun, 8 Mar 2026 13:34:00 +0800 Subject: [PATCH 08/12] fix: run pre-reset learning in background, update learn prompt --- src/auto-reply/reply/commands-core.ts | 21 ++++++++++++--------- src/auto-reply/reply/commands-learn.ts | 6 ++++-- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/auto-reply/reply/commands-core.ts b/src/auto-reply/reply/commands-core.ts index abbeacf3fab..6f173a11145 100644 --- a/src/auto-reply/reply/commands-core.ts +++ b/src/auto-reply/reply/commands-core.ts @@ -235,8 +235,10 @@ export async function handleCommands(params: HandleCommandsParams): Promise 0 ? params.command.ownerList : undefined, + }).then((learnResult) => { + if (learnResult.ok) { + logVerbose(`Background pre-reset learning completed for session ${targetSessionKey}`); + } else { + logVerbose( + `Background pre-reset learning failed for session ${targetSessionKey}: ${learnResult.message ?? "unknown error"}`, + ); + } }); - if (learnResult.ok) { - logVerbose(`Pre-reset learning completed for session ${targetSessionKey}`); - } else { - logVerbose( - `Pre-reset learning failed for session ${targetSessionKey}: ${learnResult.message ?? "unknown error"}`, - ); - } } if (boundAcpKey) { const resetResult = await resetAcpSessionInPlace({ diff --git a/src/auto-reply/reply/commands-learn.ts b/src/auto-reply/reply/commands-learn.ts index 3c4ce50b632..7c10ca2e0c9 100644 --- a/src/auto-reply/reply/commands-learn.ts +++ b/src/auto-reply/reply/commands-learn.ts @@ -8,12 +8,14 @@ import type { ThinkLevel } from "./directives.js"; const LEARN_SYSTEM_PROMPT = [ "Learning turn.", - "Analyze the session history and remember important insights.", + "Analyze the session history and remember important insights in the specific way the user requests.", ].join(" "); const LEARN_DEFAULT_PROMPT = [ "Learning turn.", - "What important insights, lessons, or information should be remembered from this session?", + "Analyze the session history and remember important insights in the specific way the user requests.", + "Focus on: problems identified, solutions discovered, methods that worked, patterns noticed, and any valuable context.", + "IMPORTANT: Remember ONLY what is truly useful and worth retaining - filter out noise.", ].join(" "); async function resolveSessionFileWithResetFallback(sessionFile: string): Promise { From b1aff9fe8ac67e31bc330493f3fe2a1c7c6aa471 Mon Sep 17 00:00:00 2001 From: lubolin0925 <163701194+lubolin0925@users.noreply.github.com> Date: Sun, 8 Mar 2026 13:46:59 +0800 Subject: [PATCH 09/12] fix: use dedicated lane for background learning to avoid blocking --- src/auto-reply/reply/commands-core.ts | 3 ++- src/auto-reply/reply/commands-learn.ts | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/auto-reply/reply/commands-core.ts b/src/auto-reply/reply/commands-core.ts index 6f173a11145..2c0bd6d5f36 100644 --- a/src/auto-reply/reply/commands-core.ts +++ b/src/auto-reply/reply/commands-core.ts @@ -235,7 +235,7 @@ export async function handleCommands(params: HandleCommandsParams): Promise 0 ? params.command.ownerList : undefined, + lane: "learn", }).then((learnResult) => { if (learnResult.ok) { logVerbose(`Background pre-reset learning completed for session ${targetSessionKey}`); diff --git a/src/auto-reply/reply/commands-learn.ts b/src/auto-reply/reply/commands-learn.ts index 7c10ca2e0c9..6e6242d9c15 100644 --- a/src/auto-reply/reply/commands-learn.ts +++ b/src/auto-reply/reply/commands-learn.ts @@ -79,6 +79,7 @@ export async function runLearnForSession(params: { customFocus?: string; senderIsOwner: boolean; ownerNumbers?: string[]; + lane?: string; }): Promise<{ ok: boolean; message?: string }> { const prompt = params.customFocus ? `Focus area: ${params.customFocus}. ${LEARN_DEFAULT_PROMPT}` @@ -114,6 +115,7 @@ export async function runLearnForSession(params: { extraSystemPrompt: LEARN_SYSTEM_PROMPT, trigger: "memory", timeoutMs: 5 * 60 * 1000, // 5 minutes + lane: params.lane, senderIsOwner: params.senderIsOwner, ownerNumbers: params.ownerNumbers, }); From 641fc7343407a75909a2fa034d8c161e56de056f Mon Sep 17 00:00:00 2001 From: lubolin0925 <163701194+lubolin0925@users.noreply.github.com> Date: Sun, 8 Mar 2026 14:22:11 +0800 Subject: [PATCH 10/12] fix: use empty sessionKey to isolate learn lane, parse focus from normalized body --- src/auto-reply/reply/commands-core.ts | 2 +- src/auto-reply/reply/commands-learn.ts | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/auto-reply/reply/commands-core.ts b/src/auto-reply/reply/commands-core.ts index 2c0bd6d5f36..6bba1ea3965 100644 --- a/src/auto-reply/reply/commands-core.ts +++ b/src/auto-reply/reply/commands-core.ts @@ -240,7 +240,7 @@ export async function handleCommands(params: HandleCommandsParams): Promise Date: Sun, 8 Mar 2026 14:23:36 +0800 Subject: [PATCH 11/12] fix: revert sessionKey change, keep dedicated learn lane --- src/auto-reply/reply/commands-core.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/auto-reply/reply/commands-core.ts b/src/auto-reply/reply/commands-core.ts index 6bba1ea3965..2c0bd6d5f36 100644 --- a/src/auto-reply/reply/commands-core.ts +++ b/src/auto-reply/reply/commands-core.ts @@ -240,7 +240,7 @@ export async function handleCommands(params: HandleCommandsParams): Promise Date: Thu, 19 Mar 2026 21:40:08 +0800 Subject: [PATCH 12/12] fix: add memoryFlushWritePath for learn command --- src/auto-reply/reply/commands-learn.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/auto-reply/reply/commands-learn.ts b/src/auto-reply/reply/commands-learn.ts index a4a3d8b18a0..506cc4f608a 100644 --- a/src/auto-reply/reply/commands-learn.ts +++ b/src/auto-reply/reply/commands-learn.ts @@ -88,7 +88,16 @@ export async function runLearnForSession(params: { // Resolve session file, falling back to archived .reset.* file if needed const resolvedSessionFile = await resolveSessionFileWithResetFallback(params.sessionFile); + // Build memory write path for learning output + const memoryDir = path.join(params.workspaceDir, "memory"); + const now = new Date(); + const dateStr = now.toISOString().split("T")[0]; + const memoryFlushWritePath = path.join(memoryDir, `${dateStr}.md`); + try { + // Ensure memory directory exists + await fs.mkdir(memoryDir, { recursive: true }); + await runEmbeddedPiAgent({ runId: crypto.randomUUID(), sessionId: params.sessionId, @@ -100,6 +109,7 @@ export async function runLearnForSession(params: { groupSpace: params.groupSpace, spawnedBy: params.spawnedBy, workspaceDir: params.workspaceDir, + memoryFlushWritePath, agentDir: params.agentDir, config: params.config, skillsSnapshot: params.skillsSnapshot,