fix: address review - complete lifecycle filtering, agent-scoped slash commands, safe fallback

1. Expand before_agent_start lifecycle detection to check all EXCLUDED_LIFECYCLE_EVENT_TYPES
   and content patterns instead of only 3 hardcoded strings (covers user_re_engagement,
   session_start in addition to heartbeat, no_reply, lifecycle_event)

2-3. Fix /claude-mem-search and /claude-mem-timeline to use agent-specific project names
     (e.g., openclaw-<agentId>) instead of the base project name, via session→project
     tracking populated by before_agent_start

4. Use unique /tmp/claude-mem-<sessionId> subdirectory when HOME is unset to prevent
   cross-agent observation mixing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alex Newman
2026-04-18 14:32:24 -07:00
parent 00df944242
commit 2c7967ec81

View File

@@ -666,6 +666,10 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
const recentPromptInits = new Map<string, number>();
// Track heartbeat/system sessions to exempt them from summarization
const systemSessionIds = new Set<string>();
// Map session/channel keys to agent-specific project names for slash command scoping
const sessionProjectNames = new Map<string, string>();
// Last known agent-specific project name (fallback for slash commands)
let lastActiveProjectName = baseProjectName;
const completionDelayMs = (() => {
const val = Number((userConfig as Record<string, unknown>).completionDelayMs);
return Number.isFinite(val) ? Math.max(0, val) : 5000;
@@ -852,9 +856,16 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
const projectName = getProjectName(ctx);
const promptText = event.prompt || "agent run";
// Mark heartbeat/system sessions so they are exempt from summarization
// Mark heartbeat/system sessions so they are exempt from summarization.
// Check against the full EXCLUDED_LIFECYCLE_EVENT_TYPES set and content patterns
// to catch all routine lifecycle events (heartbeat, no_reply, lifecycle_event,
// user_re_engagement, session_start).
const promptLower = promptText.toLowerCase().trim();
if (promptLower === "heartbeat" || promptLower === "no_reply" || promptLower === "lifecycle_event") {
if (
EXCLUDED_LIFECYCLE_EVENT_TYPES.has(promptLower) ||
EXCLUDED_LIFECYCLE_EVENT_TYPES.has(promptText.trim()) ||
EXCLUDED_LIFECYCLE_CONTENT_PATTERNS.some((pattern) => pattern.test(promptText.trim()))
) {
systemSessionIds.add(contentSessionId);
}
@@ -877,6 +888,13 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
await workerPost(workerPort, "/api/sessions/init", initPayload, api.logger);
// Track agent-specific project name for slash command scoping.
// Map the session key so slash commands issued by this agent use the correct project.
lastActiveProjectName = projectName;
if (ctx.sessionKey) {
sessionProjectNames.set(ctx.sessionKey, projectName);
}
api.logger.info(`[claude-mem] Session initialized via before_agent_start: contentSessionId=${contentSessionId} project=${projectName}${systemSessionIds.has(contentSessionId) ? " (system_session)" : ""}`);
});
@@ -939,10 +957,15 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
// Resolve workspaceDir with fallback chain.
// Previously, observations were dropped when workspaceDir was unavailable.
// Now we fall back to the user's home directory to avoid losing observations.
// When HOME is also unset, use a unique /tmp subdirectory based on the
// contentSessionId to prevent cross-agent observation mixing.
let workspaceDir = ctx.workspaceDir;
if (!workspaceDir) {
const homeDir = typeof process !== "undefined" && process.env?.HOME ? process.env.HOME : "/tmp";
const homeDir =
typeof process !== "undefined" && process.env?.HOME
? process.env.HOME
: `/tmp/claude-mem-${contentSessionId}`;
api.logger.warn(`[claude-mem] workspaceDir unavailable for session=${canonicalKey} tool=${toolName}, falling back to ${homeDir}`);
workspaceDir = homeDir;
}
@@ -1165,8 +1188,10 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
const limit = hasTrailingLimit ? parseLimit(maybeLimit, 10) : 10;
const query = hasTrailingLimit ? pieces.slice(0, -1).join(" ") : raw;
// Scope search to the current agent's project for per-agent isolation
const agentProject = baseProjectName;
// Scope search to the current agent's project for per-agent isolation.
// Look up the agent-specific project name (e.g., "openclaw-<agentId>") from
// the channel→project mapping, falling back to the last active project.
const agentProject = sessionProjectNames.get(ctx.channel) || lastActiveProjectName;
const projectParam = `&project=${encodeURIComponent(agentProject)}`;
const data = await workerGetJson(
@@ -1256,8 +1281,10 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
}
const query = parts.join(" ");
// Scope timeline to the current agent's project for per-agent isolation
const agentProject = baseProjectName;
// Scope timeline to the current agent's project for per-agent isolation.
// Look up the agent-specific project name (e.g., "openclaw-<agentId>") from
// the channel→project mapping, falling back to the last active project.
const agentProject = sessionProjectNames.get(ctx.channel) || lastActiveProjectName;
const params = new URLSearchParams({
query,
mode: "auto",