mirror of
https://github.com/thedotmack/claude-mem
synced 2026-04-25 17:15:04 +02:00
feat(openclaw): enable observation feed for OpenClaw agent sessions
Three fixes to make OpenClaw agent observations work end-to-end: 1. Session init in before_agent_start — the worker's privacy check requires a stored user prompt; without calling /api/sessions/init, all observations were skipped as "private" 2. Race condition fix in agent_end — await summarize before sending complete, preventing session deletion before in-flight observation POSTs arrive 3. OAuth token pass-through in buildIsolatedEnv — spawned Claude CLI processes now receive CLAUDE_CODE_OAUTH_TOKEN from the worker's env when no explicit API key is configured Also adds agent-specific emoji mapping and dynamic project naming for the Telegram observation feed. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
"id": "claude-mem",
|
"id": "claude-mem",
|
||||||
"name": "Claude-Mem (Persistent Memory)",
|
"name": "Claude-Mem (Persistent Memory)",
|
||||||
"description": "Official OpenClaw plugin for Claude-Mem. Records observations from embedded runner sessions and streams them to messaging channels.",
|
"description": "Official OpenClaw plugin for Claude-Mem. Records observations from embedded runner sessions and streams them to messaging channels.",
|
||||||
"kind": "memory",
|
"kind": "integration",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"author": "thedotmack",
|
"author": "thedotmack",
|
||||||
"homepage": "https://claude-mem.com",
|
"homepage": "https://claude-mem.com",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "@claude-mem/openclaw-plugin",
|
"name": "@openclaw/claude-mem",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -11,5 +11,10 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^25.2.1",
|
"@types/node": "^25.2.1",
|
||||||
"typescript": "^5.3.0"
|
"typescript": "^5.3.0"
|
||||||
|
},
|
||||||
|
"openclaw": {
|
||||||
|
"extensions": [
|
||||||
|
"./dist/index.js"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -346,7 +346,7 @@ describe("Observation I/O event handlers", () => {
|
|||||||
assert.equal(initRequests.length, 1, "should re-init after compaction");
|
assert.equal(initRequests.length, 1, "should re-init after compaction");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("before_agent_start does not call init", async () => {
|
it("before_agent_start calls init for session privacy check", async () => {
|
||||||
const { api, fireEvent } = createMockApi({ workerPort });
|
const { api, fireEvent } = createMockApi({ workerPort });
|
||||||
claudeMemPlugin(api);
|
claudeMemPlugin(api);
|
||||||
|
|
||||||
@@ -354,7 +354,7 @@ describe("Observation I/O event handlers", () => {
|
|||||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
|
||||||
const initRequests = receivedRequests.filter((r) => r.url === "/api/sessions/init");
|
const initRequests = receivedRequests.filter((r) => r.url === "/api/sessions/init");
|
||||||
assert.equal(initRequests.length, 0, "before_agent_start should not init");
|
assert.equal(initRequests.length, 1, "before_agent_start should init session");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("tool_result_persist sends observation to worker", async () => {
|
it("tool_result_persist sends observation to worker", async () => {
|
||||||
|
|||||||
@@ -160,6 +160,44 @@ const MAX_SSE_BUFFER_SIZE = 1024 * 1024; // 1MB
|
|||||||
const DEFAULT_WORKER_PORT = 37777;
|
const DEFAULT_WORKER_PORT = 37777;
|
||||||
const TOOL_RESULT_MAX_LENGTH = 1000;
|
const TOOL_RESULT_MAX_LENGTH = 1000;
|
||||||
|
|
||||||
|
// Agent emoji map for observation feed messages.
|
||||||
|
// When creating a new OpenClaw agent, add its agentId and emoji here.
|
||||||
|
const AGENT_EMOJI_MAP: Record<string, string> = {
|
||||||
|
"main": "🦞",
|
||||||
|
"openclaw": "🦞",
|
||||||
|
"devops": "🔧",
|
||||||
|
"architect": "📐",
|
||||||
|
"researcher": "🔍",
|
||||||
|
"code-reviewer": "🔎",
|
||||||
|
"coder": "💻",
|
||||||
|
"tester": "🧪",
|
||||||
|
"debugger": "🐛",
|
||||||
|
"opsec": "🛡️",
|
||||||
|
"cloudfarm": "☁️",
|
||||||
|
"extractor": "📦",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Project prefixes that indicate Claude Code sessions (not OpenClaw agents)
|
||||||
|
const CLAUDE_CODE_EMOJI = "⌨️";
|
||||||
|
const OPENCLAW_DEFAULT_EMOJI = "🦀";
|
||||||
|
|
||||||
|
function getSourceLabel(project: string): string {
|
||||||
|
if (!project) return OPENCLAW_DEFAULT_EMOJI;
|
||||||
|
// OpenClaw agent projects are formatted as "openclaw-<agentId>"
|
||||||
|
if (project.startsWith("openclaw-")) {
|
||||||
|
const agentId = project.slice("openclaw-".length);
|
||||||
|
const emoji = AGENT_EMOJI_MAP[agentId] || OPENCLAW_DEFAULT_EMOJI;
|
||||||
|
return `${emoji} ${agentId}`;
|
||||||
|
}
|
||||||
|
// OpenClaw project without agent suffix
|
||||||
|
if (project === "openclaw") {
|
||||||
|
return `🦞 openclaw`;
|
||||||
|
}
|
||||||
|
// Everything else is from Claude Code (project = working directory name)
|
||||||
|
const emoji = CLAUDE_CODE_EMOJI;
|
||||||
|
return `${emoji} ${project}`;
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Worker HTTP Client
|
// Worker HTTP Client
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -233,7 +271,8 @@ async function workerGetText(
|
|||||||
|
|
||||||
function formatObservationMessage(observation: ObservationSSEPayload): string {
|
function formatObservationMessage(observation: ObservationSSEPayload): string {
|
||||||
const title = observation.title || "Untitled";
|
const title = observation.title || "Untitled";
|
||||||
let message = `🧠 Claude-Mem Observation\n**${title}**`;
|
const source = getSourceLabel(observation.project);
|
||||||
|
let message = `${source}\n**${title}**`;
|
||||||
if (observation.subtitle) {
|
if (observation.subtitle) {
|
||||||
message += `\n${observation.subtitle}`;
|
message += `\n${observation.subtitle}`;
|
||||||
}
|
}
|
||||||
@@ -387,7 +426,14 @@ async function connectToSSEStream(
|
|||||||
export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
||||||
const userConfig = (api.pluginConfig || {}) as ClaudeMemPluginConfig;
|
const userConfig = (api.pluginConfig || {}) as ClaudeMemPluginConfig;
|
||||||
const workerPort = userConfig.workerPort || DEFAULT_WORKER_PORT;
|
const workerPort = userConfig.workerPort || DEFAULT_WORKER_PORT;
|
||||||
const projectName = userConfig.project || "openclaw";
|
const baseProjectName = userConfig.project || "openclaw";
|
||||||
|
|
||||||
|
function getProjectName(ctx: EventContext): string {
|
||||||
|
if (ctx.agentId) {
|
||||||
|
return `openclaw-${ctx.agentId}`;
|
||||||
|
}
|
||||||
|
return baseProjectName;
|
||||||
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// Session tracking for observation I/O
|
// Session tracking for observation I/O
|
||||||
@@ -407,7 +453,7 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
|||||||
async function syncMemoryToWorkspace(workspaceDir: string): Promise<void> {
|
async function syncMemoryToWorkspace(workspaceDir: string): Promise<void> {
|
||||||
const contextText = await workerGetText(
|
const contextText = await workerGetText(
|
||||||
workerPort,
|
workerPort,
|
||||||
`/api/context/inject?projects=${encodeURIComponent(projectName)}`,
|
`/api/context/inject?projects=${encodeURIComponent(baseProjectName)}`,
|
||||||
api.logger
|
api.logger
|
||||||
);
|
);
|
||||||
if (contextText && contextText.trim().length > 0) {
|
if (contextText && contextText.trim().length > 0) {
|
||||||
@@ -429,7 +475,7 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
|||||||
|
|
||||||
await workerPost(workerPort, "/api/sessions/init", {
|
await workerPost(workerPort, "/api/sessions/init", {
|
||||||
contentSessionId,
|
contentSessionId,
|
||||||
project: projectName,
|
project: getProjectName(ctx),
|
||||||
prompt: "",
|
prompt: "",
|
||||||
}, api.logger);
|
}, api.logger);
|
||||||
|
|
||||||
@@ -444,7 +490,7 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
|||||||
|
|
||||||
await workerPost(workerPort, "/api/sessions/init", {
|
await workerPost(workerPort, "/api/sessions/init", {
|
||||||
contentSessionId,
|
contentSessionId,
|
||||||
project: projectName,
|
project: getProjectName(ctx),
|
||||||
prompt: "",
|
prompt: "",
|
||||||
}, api.logger);
|
}, api.logger);
|
||||||
|
|
||||||
@@ -452,7 +498,7 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// Event: before_agent_start — sync MEMORY.md + track workspace
|
// Event: before_agent_start — init session + sync MEMORY.md + track workspace
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
api.on("before_agent_start", async (_event, ctx) => {
|
api.on("before_agent_start", async (_event, ctx) => {
|
||||||
// Track workspace dir so tool_result_persist can sync MEMORY.md later
|
// Track workspace dir so tool_result_persist can sync MEMORY.md later
|
||||||
@@ -460,6 +506,15 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
|||||||
workspaceDirsBySessionKey.set(ctx.sessionKey || "default", ctx.workspaceDir);
|
workspaceDirsBySessionKey.set(ctx.sessionKey || "default", ctx.workspaceDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize session in the worker so observations are not skipped
|
||||||
|
// (the privacy check requires a stored user prompt to exist)
|
||||||
|
const contentSessionId = getContentSessionId(ctx.sessionKey);
|
||||||
|
await workerPost(workerPort, "/api/sessions/init", {
|
||||||
|
contentSessionId,
|
||||||
|
project: getProjectName(ctx),
|
||||||
|
prompt: ctx.sessionKey || "agent run",
|
||||||
|
}, api.logger);
|
||||||
|
|
||||||
// Sync MEMORY.md before agent runs (provides context to agent)
|
// Sync MEMORY.md before agent runs (provides context to agent)
|
||||||
if (syncMemoryFile && ctx.workspaceDir) {
|
if (syncMemoryFile && ctx.workspaceDir) {
|
||||||
await syncMemoryToWorkspace(ctx.workspaceDir);
|
await syncMemoryToWorkspace(ctx.workspaceDir);
|
||||||
@@ -470,6 +525,7 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
|||||||
// Event: tool_result_persist — record tool observations + sync MEMORY.md
|
// Event: tool_result_persist — record tool observations + sync MEMORY.md
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
api.on("tool_result_persist", (event, ctx) => {
|
api.on("tool_result_persist", (event, ctx) => {
|
||||||
|
api.logger.info(`[claude-mem] tool_result_persist fired: tool=${event.toolName ?? "unknown"} agent=${ctx.agentId ?? "none"} session=${ctx.sessionKey ?? "none"}`);
|
||||||
const toolName = event.toolName;
|
const toolName = event.toolName;
|
||||||
if (!toolName || toolName.startsWith("memory_")) return;
|
if (!toolName || toolName.startsWith("memory_")) return;
|
||||||
|
|
||||||
@@ -527,7 +583,10 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
workerPostFireAndForget(workerPort, "/api/sessions/summarize", {
|
// Await summarize so the worker receives it before complete.
|
||||||
|
// This also gives in-flight tool_result_persist observations time to arrive
|
||||||
|
// (they use fire-and-forget and may still be in transit).
|
||||||
|
await workerPost(workerPort, "/api/sessions/summarize", {
|
||||||
contentSessionId,
|
contentSessionId,
|
||||||
last_assistant_message: lastAssistantMessage,
|
last_assistant_message: lastAssistantMessage,
|
||||||
}, api.logger);
|
}, api.logger);
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -217,6 +217,14 @@ export function buildIsolatedEnv(includeCredentials: boolean = true): Record<str
|
|||||||
if (credentials.OPENROUTER_API_KEY) {
|
if (credentials.OPENROUTER_API_KEY) {
|
||||||
isolatedEnv.OPENROUTER_API_KEY = credentials.OPENROUTER_API_KEY;
|
isolatedEnv.OPENROUTER_API_KEY = credentials.OPENROUTER_API_KEY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4. Pass through Claude CLI's OAuth token if available (fallback for CLI subscription billing)
|
||||||
|
// When no ANTHROPIC_API_KEY is configured, the spawned CLI uses subscription billing
|
||||||
|
// which requires either ~/.claude/.credentials.json or CLAUDE_CODE_OAUTH_TOKEN.
|
||||||
|
// The worker inherits this token from the Claude Code session that started it.
|
||||||
|
if (!isolatedEnv.ANTHROPIC_API_KEY && process.env.CLAUDE_CODE_OAUTH_TOKEN) {
|
||||||
|
isolatedEnv.CLAUDE_CODE_OAUTH_TOKEN = process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return isolatedEnv;
|
return isolatedEnv;
|
||||||
@@ -257,5 +265,8 @@ export function getAuthMethodDescription(): string {
|
|||||||
if (hasAnthropicApiKey()) {
|
if (hasAnthropicApiKey()) {
|
||||||
return 'API key (from ~/.claude-mem/.env)';
|
return 'API key (from ~/.claude-mem/.env)';
|
||||||
}
|
}
|
||||||
|
if (process.env.CLAUDE_CODE_OAUTH_TOKEN) {
|
||||||
|
return 'Claude Code OAuth token (from parent process)';
|
||||||
|
}
|
||||||
return 'Claude Code CLI (subscription billing)';
|
return 'Claude Code CLI (subscription billing)';
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user