claude-mem 10.7.2 → 12.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,15 +1,15 @@
1
- var j=["\u{1F527}","\u{1F4D0}","\u{1F50D}","\u{1F4BB}","\u{1F9EA}","\u{1F41B}","\u{1F6E1}\uFE0F","\u2601\uFE0F","\u{1F4E6}","\u{1F3AF}","\u{1F52E}","\u26A1","\u{1F30A}","\u{1F3A8}","\u{1F4CA}","\u{1F680}","\u{1F52C}","\u{1F3D7}\uFE0F","\u{1F4DD}","\u{1F3AD}"];function L(e){let s=0;for(let o=0;o<e.length;o++)s=(s<<5)-s+e.charCodeAt(o)|0;return j[Math.abs(s)%j.length]}var I="\u{1F99E}",N="\u2328\uFE0F",B="Claude Code Session",D="\u{1F980}";function U(e){let s=e?.primary??I,o=e?.claudeCode??N,a=e?.claudeCodeLabel??B,i=e?.default??D,l=e?.agents??{};return function(b){if(!b)return i;if(b.startsWith("openclaw-")){let f=b.slice(9);return f?`${l[f]||L(f)} ${f}`:`${s} openclaw`}if(b==="openclaw")return`${s} openclaw`;let y=a.trim();return y?`${o} ${y} (${b})`:`${o} ${b}`}}function M(e){return`http://127.0.0.1:${e}`}async function A(e,s,o,a){try{let i=await fetch(`${M(e)}${s}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)});return i.ok?await i.json():(a.warn(`[claude-mem] Worker POST ${s} returned ${i.status}`),null)}catch(i){let l=i instanceof Error?i.message:String(i);return a.warn(`[claude-mem] Worker POST ${s} failed: ${l}`),null}}function F(e,s,o,a){fetch(`${M(e)}${s}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)}).catch(i=>{let l=i instanceof Error?i.message:String(i);a.warn(`[claude-mem] Worker POST ${s} failed: ${l}`)})}async function O(e,s,o){try{let a=await fetch(`${M(e)}${s}`);return a.ok?await a.text():(o.warn(`[claude-mem] Worker GET ${s} returned ${a.status}`),null)}catch(a){let i=a instanceof Error?a.message:String(a);return o.warn(`[claude-mem] Worker GET ${s} failed: ${i}`),null}}async function R(e,s,o){let a=await O(e,s,o);if(!a)return null;try{return JSON.parse(a)}catch{return o.warn(`[claude-mem] Worker GET ${s} returned non-JSON response`),null}}function K(e,s){let o=e.title||"Untitled",i=`${s(e.project)}
2
- **${o}**`;return e.subtitle&&(i+=`
3
- ${e.subtitle}`),i}var W={telegram:{namespace:"telegram",functionName:"sendMessageTelegram"},whatsapp:{namespace:"whatsapp",functionName:"sendMessageWhatsApp"},discord:{namespace:"discord",functionName:"sendMessageDiscord"},slack:{namespace:"slack",functionName:"sendMessageSlack"},signal:{namespace:"signal",functionName:"sendMessageSignal"},imessage:{namespace:"imessage",functionName:"sendMessageIMessage"},line:{namespace:"line",functionName:"sendMessageLine"}};async function q(e,s,o,a){try{let i=await fetch(`https://api.telegram.org/bot${e}/sendMessage`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({chat_id:s,text:o,parse_mode:"Markdown"})});if(!i.ok){let l=await i.text();a.warn(`[claude-mem] Direct Telegram send failed (${i.status}): ${l}`)}}catch(i){let l=i instanceof Error?i.message:String(i);a.warn(`[claude-mem] Direct Telegram send error: ${l}`)}}function J(e,s,o,a,i){if(i&&s==="telegram")return q(i,o,a,e.logger);let l=W[s];if(!l)return e.logger.warn(`[claude-mem] Unsupported channel type: ${s}`),Promise.resolve();let v=e.runtime.channel[l.namespace];if(!v)return e.logger.warn(`[claude-mem] Channel "${s}" not available in runtime`),Promise.resolve();let b=v[l.functionName];return b?b(...s==="whatsapp"?[o,a,{verbose:!1}]:[o,a]).catch(f=>{let p=f instanceof Error?f.message:String(f);e.logger.error(`[claude-mem] Failed to send to ${s}: ${p}`)}):(e.logger.warn(`[claude-mem] Channel "${s}" has no ${l.functionName} function`),Promise.resolve())}async function G(e,s,o,a,i,l,v,b){let y=1e3,f=3e4;for(;!i.signal.aborted;){try{l("reconnecting"),e.logger.info(`[claude-mem] Connecting to SSE stream at ${M(s)}/stream`);let p=await fetch(`${M(s)}/stream`,{signal:i.signal,headers:{Accept:"text/event-stream"}});if(!p.ok)throw new Error(`SSE stream returned HTTP ${p.status}`);if(!p.body)throw new Error("SSE stream response has no body");l("connected"),y=1e3,e.logger.info("[claude-mem] Connected to SSE stream");let _=p.body.getReader(),$=new TextDecoder,C="";for(;;){let{done:h,value:k}=await _.read();if(h)break;C+=$.decode(k,{stream:!0}),C.length>1048576&&(e.logger.warn("[claude-mem] SSE buffer overflow, clearing buffer"),C="");let w=C.split(`
1
+ var G="127.0.0.1",W=["\u{1F527}","\u{1F4D0}","\u{1F50D}","\u{1F4BB}","\u{1F9EA}","\u{1F41B}","\u{1F6E1}\uFE0F","\u2601\uFE0F","\u{1F4E6}","\u{1F3AF}","\u{1F52E}","\u26A1","\u{1F30A}","\u{1F3A8}","\u{1F4CA}","\u{1F680}","\u{1F52C}","\u{1F3D7}\uFE0F","\u{1F4DD}","\u{1F3AD}"];function z(e){let s=0;for(let i=0;i<e.length;i++)s=(s<<5)-s+e.charCodeAt(i)|0;return W[Math.abs(s)%W.length]}var X="\u{1F99E}",Z="\u2328\uFE0F",Y="Claude Code Session",Q="\u{1F980}";function V(e){let s=e?.primary??X,i=e?.claudeCode??Z,g=e?.claudeCodeLabel??Y,c=e?.default??Q,d=e?.agents??{};return function(f){if(!f)return c;if(f.startsWith("openclaw-")){let b=f.slice(9);return b?`${d[b]||z(b)} ${b}`:`${s} openclaw`}if(f==="openclaw")return`${s} openclaw`;let v=g.trim();return v?`${i} ${v} (${f})`:`${i} ${f}`}}var U=G;function M(e){return`http://${U}:${e}`}async function q(e,s,i,g){try{let c=await fetch(`${M(e)}${s}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(i)});return c.ok?await c.json():(g.warn(`[claude-mem] Worker POST ${s} returned ${c.status}`),null)}catch(c){let d=c instanceof Error?c.message:String(c);return g.warn(`[claude-mem] Worker POST ${s} failed: ${d}`),null}}function J(e,s,i,g){fetch(`${M(e)}${s}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(i)}).catch(c=>{let d=c instanceof Error?c.message:String(c);g.warn(`[claude-mem] Worker POST ${s} failed: ${d}`)})}async function K(e,s,i){try{let g=await fetch(`${M(e)}${s}`);return g.ok?await g.text():(i.warn(`[claude-mem] Worker GET ${s} returned ${g.status}`),null)}catch(g){let c=g instanceof Error?g.message:String(g);return i.warn(`[claude-mem] Worker GET ${s} failed: ${c}`),null}}async function B(e,s,i){let g=await K(e,s,i);if(!g)return null;try{return JSON.parse(g)}catch{return i.warn(`[claude-mem] Worker GET ${s} returned non-JSON response`),null}}function ee(e,s){let i=e.title||"Untitled",c=`${s(e.project)}
2
+ **${i}**`;return e.subtitle&&(c+=`
3
+ ${e.subtitle}`),c}var ne={telegram:{namespace:"telegram",functionName:"sendMessageTelegram"},whatsapp:{namespace:"whatsapp",functionName:"sendMessageWhatsApp"},discord:{namespace:"discord",functionName:"sendMessageDiscord"},slack:{namespace:"slack",functionName:"sendMessageSlack"},signal:{namespace:"signal",functionName:"sendMessageSignal"},imessage:{namespace:"imessage",functionName:"sendMessageIMessage"},line:{namespace:"line",functionName:"sendMessageLine"}};async function te(e,s,i,g){try{let c=await fetch(`https://api.telegram.org/bot${e}/sendMessage`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({chat_id:s,text:i,parse_mode:"Markdown"})});if(!c.ok){let d=await c.text();g.warn(`[claude-mem] Direct Telegram send failed (${c.status}): ${d}`)}}catch(c){let d=c instanceof Error?c.message:String(c);g.warn(`[claude-mem] Direct Telegram send error: ${d}`)}}function re(e,s,i,g,c){if(c&&s==="telegram")return te(c,i,g,e.logger);let d=ne[s];if(!d)return e.logger.warn(`[claude-mem] Unsupported channel type: ${s}`),Promise.resolve();let w=e.runtime.channel[d.namespace];if(!w)return e.logger.warn(`[claude-mem] Channel "${s}" not available in runtime`),Promise.resolve();let f=w[d.functionName];return f?f(...s==="whatsapp"?[i,g,{verbose:!1}]:[i,g]).catch(b=>{let u=b instanceof Error?b.message:String(b);e.logger.error(`[claude-mem] Failed to send to ${s}: ${u}`)}):(e.logger.warn(`[claude-mem] Channel "${s}" has no ${d.functionName} function`),Promise.resolve())}async function se(e,s,i,g,c,d,w,f){let v=1e3,b=3e4;for(;!c.signal.aborted;){try{d("reconnecting"),e.logger.info(`[claude-mem] Connecting to SSE stream at ${M(s)}/stream`);let u=await fetch(`${M(s)}/stream`,{signal:c.signal,headers:{Accept:"text/event-stream"}});if(!u.ok)throw new Error(`SSE stream returned HTTP ${u.status}`);if(!u.body)throw new Error("SSE stream response has no body");d("connected"),v=1e3,e.logger.info("[claude-mem] Connected to SSE stream");let _=u.body.getReader(),j=new TextDecoder,k="";for(;;){let{done:L,value:I}=await _.read();if(L)break;k+=j.decode(I,{stream:!0}),k.length>1048576&&(e.logger.warn("[claude-mem] SSE buffer overflow, clearing buffer"),k="");let P=k.split(`
4
4
 
5
- `);C=w.pop()||"";for(let P of w){let E=P.split(`
6
- `).filter(n=>n.startsWith("data:")).map(n=>n.slice(5).trim());if(E.length===0)continue;let r=E.join(`
7
- `);if(r)try{let n=JSON.parse(r);if(n.type==="new_observation"&&n.observation){let g=K(n.observation,v);await J(e,o,a,g,b)}}catch(n){let t=n instanceof Error?n.message:String(n);e.logger.warn(`[claude-mem] Failed to parse SSE frame: ${t}`)}}}}catch(p){if(i.signal.aborted)break;l("reconnecting");let _=p instanceof Error?p.message:String(p);e.logger.warn(`[claude-mem] SSE stream error: ${_}. Reconnecting in ${y/1e3}s`)}if(i.signal.aborted)break;await new Promise(p=>setTimeout(p,y)),y=Math.min(y*2,f)}l("disconnected")}function z(e){let s=e.pluginConfig||{},o=s.workerPort||37777,a=s.project||"openclaw",i=U(s.observationFeed?.emojis);function l(r){return r.agentId?`openclaw-${r.agentId}`:a}let v=new Map,b=s.syncMemoryFile!==!1,y=new Set(s.syncMemoryFileExclude||[]);function f(r){let n=r||"default";return v.has(n)||v.set(n,`openclaw-${n}-${Date.now()}`),v.get(n)}function p(r){if(!b)return!1;let n=r?.agentId;return!(n&&y.has(n))}let _=6e4,$=new Map;async function C(r){let n=[a],t=r?l(r):null;t&&t!==a&&n.push(t);let g=n.join(","),c=$.get(g);if(c&&Date.now()-c.fetchedAt<_)return c.text;let d=await O(o,`/api/context/inject?projects=${encodeURIComponent(g)}`,e.logger);if(d&&d.trim().length>0){let u=d.trim();return $.set(g,{text:u,fetchedAt:Date.now()}),u}return null}e.on("session_start",async(r,n)=>{let t=f(n.sessionKey);await A(o,"/api/sessions/init",{contentSessionId:t,project:l(n),prompt:""},e.logger),e.logger.info(`[claude-mem] Session initialized: ${t}`)}),e.on("message_received",async(r,n)=>{let t=n.conversationId||n.channelId||"default",g=f(t);await A(o,"/api/sessions/init",{contentSessionId:g,project:a,prompt:r.content||"[media prompt]"},e.logger)}),e.on("after_compaction",async(r,n)=>{let t=f(n.sessionKey);await A(o,"/api/sessions/init",{contentSessionId:t,project:l(n),prompt:""},e.logger),e.logger.info(`[claude-mem] Session re-initialized after compaction: ${t}`)}),e.on("before_agent_start",async(r,n)=>{let t=f(n.sessionKey);await A(o,"/api/sessions/init",{contentSessionId:t,project:l(n),prompt:r.prompt||"agent run"},e.logger)}),e.on("before_prompt_build",async(r,n)=>{if(!p(n))return;let t=await C(n);if(t)return e.logger.info(`[claude-mem] Context injected via system prompt for agent=${n.agentId??"unknown"}`),{appendSystemContext:t}}),e.on("tool_result_persist",(r,n)=>{e.logger.info(`[claude-mem] tool_result_persist fired: tool=${r.toolName??"unknown"} agent=${n.agentId??"none"} session=${n.sessionKey??"none"}`);let t=r.toolName;if(!t||t.startsWith("memory_"))return;let g=f(n.sessionKey),c="",d=r.message?.content;Array.isArray(d)&&(c=d.filter(m=>(m.type==="tool_result"||m.type==="text")&&"text"in m).map(m=>String(m.text)).join(`
8
- `));let u=1e3;c.length>u&&(c=c.slice(0,u)),F(o,"/api/sessions/observations",{contentSessionId:g,tool_name:t,tool_input:r.params||{},tool_response:c,cwd:""},e.logger)}),e.on("agent_end",async(r,n)=>{let t=f(n.sessionKey),g="";if(Array.isArray(r.messages))for(let c=r.messages.length-1;c>=0;c--){let d=r.messages[c];if(d?.role==="assistant"){typeof d.content=="string"?g=d.content:Array.isArray(d.content)&&(g=d.content.filter(u=>u.type==="text").map(u=>u.text||"").join(`
9
- `));break}}await A(o,"/api/sessions/summarize",{contentSessionId:t,last_assistant_message:g},e.logger),F(o,"/api/sessions/complete",{contentSessionId:t},e.logger)}),e.on("session_end",async(r,n)=>{let t=n.sessionKey||"default";v.delete(t)}),e.on("gateway_start",async()=>{v.clear(),$.clear(),e.logger.info("[claude-mem] Gateway started \u2014 session tracking reset")});let h=null,k="disconnected",w=null;e.registerService({id:"claude-mem-observation-feed",start:async r=>{h&&(h.abort(),w&&(await w,w=null));let n=s.observationFeed;if(!n?.enabled){e.logger.info("[claude-mem] Observation feed disabled");return}if(!n.channel||!n.to){e.logger.warn("[claude-mem] Observation feed misconfigured \u2014 channel or target missing");return}e.logger.info(`[claude-mem] Observation feed starting \u2014 channel: ${n.channel}, target: ${n.to}`),h=new AbortController,w=G(e,o,n.channel,n.to,h,t=>{k=t},i,n.botToken)},stop:async r=>{h&&(h.abort(),h=null),w&&(await w,w=null),k="disconnected",e.logger.info("[claude-mem] Observation feed stopped \u2014 SSE connection closed")}});function P(r,n=5){return!Array.isArray(r)||r.length===0?"No results found.":r.slice(0,n).map((t,g)=>{let c=t,d=String(c.title||c.subtitle||c.text||"Untitled"),u=c.project?` [${String(c.project)}]`:"";return`${g+1}. ${d}${u}`}).join(`
10
- `)}function E(r,n=10){let t=Number(r);return Number.isFinite(t)?Math.max(1,Math.min(50,Math.trunc(t))):n}e.registerCommand({name:"claude_mem_feed",description:"Show or toggle Claude-Mem observation feed status",acceptsArgs:!0,handler:async r=>{let n=s.observationFeed;if(!n)return{text:"Observation feed not configured. Add observationFeed to your plugin config."};let t=r.args?.trim();return t==="on"?(e.logger.info("[claude-mem] Feed enable requested via command"),{text:"Feed enable requested. Update observationFeed.enabled in your plugin config to persist."}):t==="off"?(e.logger.info("[claude-mem] Feed disable requested via command"),{text:"Feed disable requested. Update observationFeed.enabled in your plugin config to persist."}):{text:["Claude-Mem Observation Feed",`Enabled: ${n.enabled?"yes":"no"}`,`Channel: ${n.channel||"not set"}`,`Target: ${n.to||"not set"}`,`Connection: ${k}`].join(`
11
- `)}}}),e.registerCommand({name:"claude-mem-search",description:"Search Claude-Mem observations by query",acceptsArgs:!0,handler:async r=>{let n=r.args?.trim()||"";if(!n)return"Usage: /claude-mem-search <query> [limit]";let t=n.split(/\s+/),g=t[t.length-1],c=/^\d+$/.test(g),d=c?E(g,10):10,u=c?t.slice(0,-1).join(" "):n,m=await R(o,`/api/search/observations?query=${encodeURIComponent(u)}&limit=${d}`,e.logger);if(!m)return"Claude-Mem search failed (worker unavailable or invalid response).";let S=Array.isArray(m.items)?m.items:[];return[`Claude-Mem Search: "${u}"`,P(S,d)].join(`
12
- `)}}),e.registerCommand({name:"claude-mem-recent",description:"Show recent Claude-Mem context for a project",acceptsArgs:!0,handler:async r=>{let n=r.args?.trim()||"",t=n?n.split(/\s+/):[],g=t.length>0?t[t.length-1]:"",c=/^\d+$/.test(g),d=c?E(g,3):3,u=c?t.slice(0,-1).join(" "):n,m=new URLSearchParams;m.set("limit",String(d)),u&&m.set("project",u);let S=await R(o,`/api/context/recent?${m.toString()}`,e.logger);if(!S)return"Claude-Mem recent context failed (worker unavailable or invalid response).";let x=Array.isArray(S.session_summaries)?S.session_summaries:[],T=Array.isArray(S.recent_observations)?S.recent_observations:[];return["Claude-Mem Recent Context",`Project: ${u||"(auto)"}`,`Session summaries: ${x.length}`,`Recent observations: ${T.length}`,P(T,Math.min(5,T.length||5))].join(`
13
- `)}}),e.registerCommand({name:"claude-mem-timeline",description:"Find best memory match and show nearby timeline events",acceptsArgs:!0,handler:async r=>{let n=r.args?.trim()||"";if(!n)return"Usage: /claude-mem-timeline <query> [depthBefore] [depthAfter]";let t=n.split(/\s+/),g=5,c=5;t.length>=2&&/^\d+$/.test(t[t.length-1])&&(g=E(t.pop(),5)),t.length>=2&&/^\d+$/.test(t[t.length-1])&&(c=E(t.pop(),5));let d=t.join(" "),u=new URLSearchParams({query:d,mode:"auto",depth_before:String(c),depth_after:String(g)}),m=await R(o,`/api/timeline/by-query?${u.toString()}`,e.logger);if(!m)return"Claude-Mem timeline lookup failed (worker unavailable or invalid response).";let S=Array.isArray(m.timeline)?m.timeline:[],x=m.anchor?String(m.anchor):"(none)";return[`Claude-Mem Timeline: "${d}"`,`Anchor: ${x}`,P(S,8)].join(`
14
- `)}}),e.registerCommand({name:"claude_mem_status",description:"Check Claude-Mem worker health and session status",handler:async()=>{let r=await O(o,"/api/health",e.logger);if(!r)return{text:`Claude-Mem worker unreachable at port ${o}`};try{return{text:["Claude-Mem Worker Status",`Status: ${JSON.parse(r).status||"unknown"}`,`Port: ${o}`,`Active sessions: ${v.size}`,`Observation feed: ${k}`].join(`
15
- `)}}catch{return{text:"Claude-Mem worker responded but returned unexpected data"}}}}),e.logger.info(`[claude-mem] OpenClaw plugin loaded \u2014 v1.0.0 (worker: 127.0.0.1:${o})`)}export{z as default};
5
+ `);k=P.pop()||"";for(let C of P){let x=C.split(`
6
+ `).filter(y=>y.startsWith("data:")).map(y=>y.slice(5).trim());if(x.length===0)continue;let T=x.join(`
7
+ `);if(T)try{let y=JSON.parse(T);if(y.type==="new_observation"&&y.observation){let A=ee(y.observation,w);await re(e,i,g,A,f)}}catch(y){let R=y instanceof Error?y.message:String(y);e.logger.warn(`[claude-mem] Failed to parse SSE frame: ${R}`)}}}}catch(u){if(c.signal.aborted)break;d("reconnecting");let _=u instanceof Error?u.message:String(u);e.logger.warn(`[claude-mem] SSE stream error: ${_}. Reconnecting in ${v/1e3}s`)}if(c.signal.aborted)break;await new Promise(u=>setTimeout(u,v)),v=Math.min(v*2,b)}d("disconnected")}function oe(e){let s=e.pluginConfig||{},i=s.workerPort||37777;U=s.workerHost||G;let g=s.project||"openclaw",c=V(s.observationFeed?.emojis);function d(r){return r.agentId?`openclaw-${r.agentId}`:g}let w=new Map,f=new Map,v=new Map,b=new Map,u=new Map,_=(()=>{let r=Number(s.completionDelayMs);return Number.isFinite(r)?Math.max(0,r):5e3})(),j=s.syncMemoryFile!==!1,k=new Set(s.syncMemoryFileExclude||[]);function L(r){let n=r||"default";return w.has(n)||w.set(n,`openclaw-${n}-${Date.now()}`),w.get(n)}function I(r){if(!j)return!1;let n=r?.agentId;return!(n&&k.has(n))}function P(r){let n=new Set;for(let t of[r.sessionKey,r.conversationId,r.channelId]){let o=typeof t=="string"?t.trim():"";o&&n.add(o)}return n.size===0&&n.add("default"),Array.from(n)}function C(r){let n=P(r),t=n.find(l=>f.has(l));t=t?f.get(t):n[0];let o=v.get(t);o||(o=new Set([t]),v.set(t,o));for(let l of n)o.add(l),f.set(l,t);let a=L(t);for(let l of o)w.set(l,a);return{canonicalKey:t,contentSessionId:a}}function x(r,n,t){let o=Date.now();for(let[m,p]of u)o-p>2e3&&u.delete(m);let a=`${r}::${n}::${t}`,l=u.get(a);return u.set(a,o),typeof l=="number"&&o-l<=2e3}function T(r){let n=P(r),t=n.map(a=>f.get(a)).find(Boolean)||n[0],o=v.get(t)||new Set([t,...n]);for(let a of o)f.delete(a),w.delete(a);v.delete(t),w.delete(t)}function y(r){let n=b.get(r);n&&clearTimeout(n);let t=setTimeout(()=>{b.delete(r),J(i,"/api/sessions/complete",{contentSessionId:r},e.logger)},_);b.set(r,t)}let R=6e4,A=new Map;async function H(r){let n=[g],t=r?d(r):null;t&&t!==g&&n.push(t);let o=n.join(","),a=A.get(o);if(a&&Date.now()-a.fetchedAt<R)return a.text;let l=await K(i,`/api/context/inject?projects=${encodeURIComponent(o)}`,e.logger);if(l&&l.trim().length>0){let m=l.trim();return A.set(o,{text:m,fetchedAt:Date.now()}),m}return null}e.on("session_start",async(r,n)=>{let{contentSessionId:t}=C(n);e.logger.info(`[claude-mem] Session tracking initialized: ${t}`)}),e.on("message_received",async(r,n)=>{let{canonicalKey:t,contentSessionId:o}=C(n);e.logger.info(`[claude-mem] Message received \u2014 prompt capture deferred to before_agent_start: session=${t} contentSessionId=${o} hasContent=${!!r.content}`)}),e.on("after_compaction",async(r,n)=>{let{contentSessionId:t}=C(n);e.logger.info(`[claude-mem] Session preserved after compaction: ${t}`)}),e.on("before_agent_start",async(r,n)=>{let{contentSessionId:t}=C(n),o=d(n),a=r.prompt||"agent run";if(x(t,o,a)){e.logger.info(`[claude-mem] Skipping duplicate prompt init: contentSessionId=${t} project=${o}`);return}await q(i,"/api/sessions/init",{contentSessionId:t,project:o,prompt:a},e.logger),e.logger.info(`[claude-mem] Session initialized via before_agent_start: contentSessionId=${t} project=${o}`)}),e.on("before_prompt_build",async(r,n)=>{if(!I(n))return;let t=await H(n);if(t)return e.logger.info(`[claude-mem] Context injected via system prompt for agent=${n.agentId??"unknown"}`),{appendSystemContext:t}}),e.on("tool_result_persist",(r,n)=>{e.logger.info(`[claude-mem] tool_result_persist fired: tool=${r.toolName??"unknown"} agent=${n.agentId??"none"} session=${n.sessionKey??"none"}`);let t=r.toolName;if(!t||t.startsWith("memory_"))return;let{canonicalKey:o,contentSessionId:a}=C(n),l="",m=r.message?.content;Array.isArray(m)&&(l=m.filter(h=>(h.type==="tool_result"||h.type==="text")&&"text"in h).map(h=>String(h.text)).join(`
8
+ `));let p=1e3;l.length>p&&(l=l.slice(0,p));let S=n.workspaceDir;if(!S){e.logger.warn(`[claude-mem] Skipping observation persist because workspaceDir is unavailable: session=${o} tool=${t}`);return}J(i,"/api/sessions/observations",{contentSessionId:a,tool_name:t,tool_input:r.params||{},tool_response:l,cwd:S},e.logger)}),e.on("agent_end",async(r,n)=>{let{contentSessionId:t}=C(n),o="";if(Array.isArray(r.messages))for(let a=r.messages.length-1;a>=0;a--){let l=r.messages[a];if(l?.role==="assistant"){typeof l.content=="string"?o=l.content:Array.isArray(l.content)&&(o=l.content.filter(m=>m.type==="text").map(m=>m.text||"").join(`
9
+ `));break}}await q(i,"/api/sessions/summarize",{contentSessionId:t,last_assistant_message:o},e.logger),e.logger.info(`[claude-mem] Scheduling session complete in ${_}ms: ${t}`),y(t)}),e.on("session_end",async(r,n)=>{T(n),e.logger.info("[claude-mem] Session tracking cleaned up")}),e.on("gateway_start",async()=>{w.clear(),A.clear(),u.clear(),f.clear(),v.clear();for(let r of b.values())clearTimeout(r);b.clear(),e.logger.info("[claude-mem] Gateway started \u2014 session tracking reset")});let E=null,O="disconnected",$=null;e.registerService({id:"claude-mem-observation-feed",start:async r=>{E&&(E.abort(),$&&(await $,$=null));let n=s.observationFeed;if(!n?.enabled){e.logger.info("[claude-mem] Observation feed disabled");return}if(!n.channel||!n.to){e.logger.warn("[claude-mem] Observation feed misconfigured \u2014 channel or target missing");return}e.logger.info(`[claude-mem] Observation feed starting \u2014 channel: ${n.channel}, target: ${n.to}`),E=new AbortController,$=se(e,i,n.channel,n.to,E,t=>{O=t},c,n.botToken)},stop:async r=>{E&&(E.abort(),E=null),$&&(await $,$=null),O="disconnected",e.logger.info("[claude-mem] Observation feed stopped \u2014 SSE connection closed")}});function N(r,n=5){return!Array.isArray(r)||r.length===0?"No results found.":r.slice(0,n).map((t,o)=>{let a=t,l=String(a.title||a.subtitle||a.text||"Untitled"),m=a.project?` [${String(a.project)}]`:"";return`${o+1}. ${l}${m}`}).join(`
10
+ `)}function F(r,n=10){let t=Number(r);return Number.isFinite(t)?Math.max(1,Math.min(50,Math.trunc(t))):n}e.registerCommand({name:"claude_mem_feed",description:"Show or toggle Claude-Mem observation feed status",acceptsArgs:!0,handler:async r=>{let n=s.observationFeed;if(!n)return{text:"Observation feed not configured. Add observationFeed to your plugin config."};let t=r.args?.trim();return t==="on"?(e.logger.info("[claude-mem] Feed enable requested via command"),{text:"Feed enable requested. Update observationFeed.enabled in your plugin config to persist."}):t==="off"?(e.logger.info("[claude-mem] Feed disable requested via command"),{text:"Feed disable requested. Update observationFeed.enabled in your plugin config to persist."}):{text:["Claude-Mem Observation Feed",`Enabled: ${n.enabled?"yes":"no"}`,`Channel: ${n.channel||"not set"}`,`Target: ${n.to||"not set"}`,`Connection: ${O}`].join(`
11
+ `)}}}),e.registerCommand({name:"claude-mem-search",description:"Search Claude-Mem observations by query",acceptsArgs:!0,handler:async r=>{let n=r.args?.trim()||"";if(!n)return"Usage: /claude-mem-search <query> [limit]";let t=n.split(/\s+/),o=t[t.length-1],a=/^\d+$/.test(o),l=a?F(o,10):10,m=a?t.slice(0,-1).join(" "):n,p=await B(i,`/api/search/observations?query=${encodeURIComponent(m)}&limit=${l}`,e.logger);if(!p)return"Claude-Mem search failed (worker unavailable or invalid response).";let S=Array.isArray(p.items)?p.items:[];return[`Claude-Mem Search: "${m}"`,N(S,l)].join(`
12
+ `)}}),e.registerCommand({name:"claude-mem-recent",description:"Show recent Claude-Mem context for a project",acceptsArgs:!0,handler:async r=>{let n=r.args?.trim()||"",t=n?n.split(/\s+/):[],o=t.length>0?t[t.length-1]:"",a=/^\d+$/.test(o),l=a?F(o,3):3,m=a?t.slice(0,-1).join(" "):n,p=new URLSearchParams;p.set("limit",String(l)),m&&p.set("project",m);let S=await B(i,`/api/context/recent?${p.toString()}`,e.logger);if(!S)return"Claude-Mem recent context failed (worker unavailable or invalid response).";let h=Array.isArray(S.session_summaries)?S.session_summaries:[],D=Array.isArray(S.recent_observations)?S.recent_observations:[];return["Claude-Mem Recent Context",`Project: ${m||"(auto)"}`,`Session summaries: ${h.length}`,`Recent observations: ${D.length}`,N(D,Math.min(5,D.length||5))].join(`
13
+ `)}}),e.registerCommand({name:"claude-mem-timeline",description:"Find best memory match and show nearby timeline events",acceptsArgs:!0,handler:async r=>{let n=r.args?.trim()||"";if(!n)return"Usage: /claude-mem-timeline <query> [depthBefore] [depthAfter]";let t=n.split(/\s+/),o=5,a=5;t.length>=2&&/^\d+$/.test(t[t.length-1])&&(o=F(t.pop(),5)),t.length>=2&&/^\d+$/.test(t[t.length-1])&&(a=F(t.pop(),5));let l=t.join(" "),m=new URLSearchParams({query:l,mode:"auto",depth_before:String(a),depth_after:String(o)}),p=await B(i,`/api/timeline/by-query?${m.toString()}`,e.logger);if(!p)return"Claude-Mem timeline lookup failed (worker unavailable or invalid response).";let S=Array.isArray(p.timeline)?p.timeline:[],h=p.anchor?String(p.anchor):"(none)";return[`Claude-Mem Timeline: "${l}"`,`Anchor: ${h}`,N(S,8)].join(`
14
+ `)}}),e.registerCommand({name:"claude_mem_status",description:"Check Claude-Mem worker health and session status",handler:async()=>{let r=await K(i,"/api/health",e.logger);if(!r)return{text:`Claude-Mem worker unreachable at port ${i}`};try{return{text:["Claude-Mem Worker Status",`Status: ${JSON.parse(r).status||"unknown"}`,`Port: ${i}`,`Active sessions: ${w.size}`,`Observation feed: ${O}`].join(`
15
+ `)}}catch{return{text:"Claude-Mem worker responded but returned unexpected data"}}}}),e.logger.info(`[claude-mem] OpenClaw plugin loaded \u2014 v1.0.0 (worker: ${U}:${i})`)}export{oe as default};
@@ -80,17 +80,18 @@ setup_tty() {
80
80
  if [[ -t 0 ]]; then
81
81
  # stdin IS a terminal — use it directly
82
82
  TTY_FD=0
83
- elif [[ -e /dev/tty ]]; then
84
- # stdin is piped (curl | bash) but /dev/tty is available
83
+ elif [[ "$NON_INTERACTIVE" == "true" ]]; then
84
+ # In non-interactive mode, do not require /dev/tty
85
+ TTY_FD=0
86
+ elif [[ -r /dev/tty ]]; then
87
+ # stdin is piped (curl | bash) but /dev/tty is available and readable
85
88
  exec 3</dev/tty
86
89
  TTY_FD=3
87
90
  else
88
91
  # No terminal available at all
89
- if [[ "$NON_INTERACTIVE" != "true" ]]; then
90
- echo "Error: No terminal available for interactive prompts." >&2
91
- echo "Use --non-interactive or run directly: bash install.sh" >&2
92
- exit 1
93
- fi
92
+ echo "Error: No terminal available for interactive prompts." >&2
93
+ echo "Use --non-interactive or run directly: bash install.sh" >&2
94
+ exit 1
94
95
  fi
95
96
  }
96
97
 
@@ -787,11 +788,16 @@ install_plugin() {
787
788
  const configPath = process.env.INSTALLER_CONFIG_FILE;
788
789
  const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
789
790
  const entry = config?.plugins?.entries?.['claude-mem'];
790
- if (entry || config?.plugins?.slots?.memory === 'claude-mem') {
791
+ const allowHasClaudeMem = Array.isArray(config?.plugins?.allow) && config.plugins.allow.includes('claude-mem');
792
+ if (entry || config?.plugins?.slots?.memory === 'claude-mem' || allowHasClaudeMem) {
791
793
  // Save the config block so we can restore it after install
792
794
  process.stdout.write(JSON.stringify(entry?.config || {}));
793
795
  // Remove the stale entry so OpenClaw CLI can run
794
796
  if (entry) delete config.plugins.entries['claude-mem'];
797
+ // Also remove stale allowlist reference — this alone can block ALL CLI commands
798
+ if (Array.isArray(config?.plugins?.allow)) {
799
+ config.plugins.allow = config.plugins.allow.filter((x) => x !== 'claude-mem');
800
+ }
795
801
  // Also remove the slot reference — if the slot points to a plugin
796
802
  // that isn't in entries, OpenClaw's config validator rejects ALL commands
797
803
  if (config?.plugins?.slots?.memory === 'claude-mem') {
@@ -818,6 +824,49 @@ install_plugin() {
818
824
  exit 1
819
825
  fi
820
826
 
827
+ # Ensure claude-mem is present in plugins.allow after successful install+enable.
828
+ # Some OpenClaw environments require explicit allowlisting for local plugins.
829
+ # This write is guaranteed: if config doesn't exist, configure_memory_slot() will create it.
830
+ if [[ -f "$oc_config" ]]; then
831
+ if ! INSTALLER_CONFIG_FILE="$oc_config" node -e "
832
+ const fs = require('fs');
833
+ const configPath = process.env.INSTALLER_CONFIG_FILE;
834
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
835
+ if (!config.plugins) config.plugins = {};
836
+ if (!Array.isArray(config.plugins.allow)) config.plugins.allow = [];
837
+ if (!config.plugins.allow.includes('claude-mem')) {
838
+ config.plugins.allow.push('claude-mem');
839
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
840
+ console.log('Added claude-mem to plugins.allow');
841
+ } else {
842
+ console.log('claude-mem already in plugins.allow');
843
+ }
844
+ " 2>&1; then
845
+ warn "Failed to write plugins.allow — claude-mem may need manual allowlisting"
846
+ fi
847
+ else
848
+ # Config doesn't exist yet; configure_memory_slot() will create it with plugins.allow
849
+ # We'll add claude-mem to the allowlist in a follow-up step after config is materialized
850
+ info "OpenClaw config not yet materialized; will ensure allowlist in post-install"
851
+ # Force config materialization by running a harmless OpenClaw command
852
+ if run_openclaw status --json >/dev/null 2>&1 && [[ -f "$oc_config" ]]; then
853
+ if ! INSTALLER_CONFIG_FILE="$oc_config" node -e "
854
+ const fs = require('fs');
855
+ const configPath = process.env.INSTALLER_CONFIG_FILE;
856
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
857
+ if (!config.plugins) config.plugins = {};
858
+ if (!Array.isArray(config.plugins.allow)) config.plugins.allow = [];
859
+ if (!config.plugins.allow.includes('claude-mem')) {
860
+ config.plugins.allow.push('claude-mem');
861
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
862
+ console.log('Added claude-mem to plugins.allow (post-materialization)');
863
+ }
864
+ " 2>&1; then
865
+ warn "Failed to write plugins.allow after materialization — configure manually"
866
+ fi
867
+ fi
868
+ fi
869
+
821
870
  # Restore saved plugin config (workerPort, syncMemoryFile, observationFeed, etc.)
822
871
  # from any pre-existing installation that was temporarily removed above.
823
872
  if [[ -n "$saved_plugin_config" && "$saved_plugin_config" != "{}" ]]; then
@@ -1101,7 +1150,7 @@ write_settings() {
1101
1150
 
1102
1151
  // All defaults from SettingsDefaultsManager.ts
1103
1152
  const defaults = {
1104
- CLAUDE_MEM_MODEL: 'claude-sonnet-4-5',
1153
+ CLAUDE_MEM_MODEL: 'claude-sonnet-4-6',
1105
1154
  CLAUDE_MEM_CONTEXT_OBSERVATIONS: '50',
1106
1155
  CLAUDE_MEM_WORKER_PORT: '37777',
1107
1156
  CLAUDE_MEM_WORKER_HOST: '127.0.0.1',
@@ -27,6 +27,11 @@
27
27
  "default": 37777,
28
28
  "description": "Port for Claude-Mem worker service"
29
29
  },
30
+ "workerHost": {
31
+ "type": "string",
32
+ "default": "127.0.0.1",
33
+ "description": "Hostname for Claude-Mem worker service. Set to host.docker.internal when the gateway runs in Docker and the worker runs on the host."
34
+ },
30
35
  "project": {
31
36
  "type": "string",
32
37
  "default": "openclaw",
@@ -183,6 +183,7 @@ interface ClaudeMemPluginConfig {
183
183
  syncMemoryFileExclude?: string[];
184
184
  project?: string;
185
185
  workerPort?: number;
186
+ workerHost?: string;
186
187
  observationFeed?: {
187
188
  enabled?: boolean;
188
189
  channel?: string;
@@ -198,6 +199,7 @@ interface ClaudeMemPluginConfig {
198
199
 
199
200
  const MAX_SSE_BUFFER_SIZE = 1024 * 1024; // 1MB
200
201
  const DEFAULT_WORKER_PORT = 37777;
202
+ const DEFAULT_WORKER_HOST = "127.0.0.1";
201
203
 
202
204
  // Emoji pool for deterministic auto-assignment to unknown agents.
203
205
  // Uses a hash of the agentId to pick a consistent emoji — no persistent state needed.
@@ -256,8 +258,10 @@ function buildGetSourceLabel(
256
258
  // Worker HTTP Client
257
259
  // ============================================================================
258
260
 
261
+ let _workerHost = DEFAULT_WORKER_HOST;
262
+
259
263
  function workerBaseUrl(port: number): string {
260
- return `http://127.0.0.1:${port}`;
264
+ return `http://${_workerHost}:${port}`;
261
265
  }
262
266
 
263
267
  async function workerPost(
@@ -533,6 +537,7 @@ async function connectToSSEStream(
533
537
  export default function claudeMemPlugin(api: OpenClawPluginApi): void {
534
538
  const userConfig = (api.pluginConfig || {}) as ClaudeMemPluginConfig;
535
539
  const workerPort = userConfig.workerPort || DEFAULT_WORKER_PORT;
540
+ _workerHost = userConfig.workerHost || DEFAULT_WORKER_HOST;
536
541
  const baseProjectName = userConfig.project || "openclaw";
537
542
  const getSourceLabel = buildGetSourceLabel(userConfig.observationFeed?.emojis);
538
543
 
@@ -547,6 +552,14 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
547
552
  // Session tracking for observation I/O
548
553
  // ------------------------------------------------------------------
549
554
  const sessionIds = new Map<string, string>();
555
+ const canonicalSessionKeys = new Map<string, string>();
556
+ const sessionAliasesByCanonicalKey = new Map<string, Set<string>>();
557
+ const pendingCompletionTimers = new Map<string, ReturnType<typeof setTimeout>>();
558
+ const recentPromptInits = new Map<string, number>();
559
+ const completionDelayMs = (() => {
560
+ const val = Number((userConfig as Record<string, unknown>).completionDelayMs);
561
+ return Number.isFinite(val) ? Math.max(0, val) : 5000;
562
+ })();
550
563
  const syncMemoryFile = userConfig.syncMemoryFile !== false; // default true
551
564
  const syncMemoryFileExclude = new Set(userConfig.syncMemoryFileExclude || []);
552
565
 
@@ -565,6 +578,83 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
565
578
  return true;
566
579
  }
567
580
 
581
+ type SessionTrackingContext = {
582
+ sessionKey?: string;
583
+ workspaceDir?: string;
584
+ channelId?: string;
585
+ conversationId?: string;
586
+ };
587
+
588
+ function getSessionAliases(ctx: SessionTrackingContext): string[] {
589
+ const aliases = new Set<string>();
590
+ for (const rawKey of [ctx.sessionKey, ctx.conversationId, ctx.channelId]) {
591
+ const key = typeof rawKey === "string" ? rawKey.trim() : "";
592
+ if (key) aliases.add(key);
593
+ }
594
+ if (aliases.size === 0) aliases.add("default");
595
+ return Array.from(aliases);
596
+ }
597
+
598
+ function rememberSessionContext(ctx: SessionTrackingContext): { canonicalKey: string; contentSessionId: string } {
599
+ const aliases = getSessionAliases(ctx);
600
+ let canonicalKey = aliases.find((alias) => canonicalSessionKeys.has(alias));
601
+ canonicalKey = canonicalKey ? canonicalSessionKeys.get(canonicalKey)! : aliases[0];
602
+ let aliasSet = sessionAliasesByCanonicalKey.get(canonicalKey);
603
+ if (!aliasSet) {
604
+ aliasSet = new Set([canonicalKey]);
605
+ sessionAliasesByCanonicalKey.set(canonicalKey, aliasSet);
606
+ }
607
+ for (const alias of aliases) {
608
+ aliasSet.add(alias);
609
+ canonicalSessionKeys.set(alias, canonicalKey);
610
+ }
611
+ const contentSessionId = getContentSessionId(canonicalKey);
612
+ for (const alias of aliasSet) {
613
+ sessionIds.set(alias, contentSessionId);
614
+ }
615
+ return { canonicalKey, contentSessionId };
616
+ }
617
+
618
+ function shouldSkipDuplicatePromptInit(contentSessionId: string, project: string, prompt: string): boolean {
619
+ const now = Date.now();
620
+ for (const [key, timestamp] of recentPromptInits) {
621
+ if (now - timestamp > 2000) recentPromptInits.delete(key);
622
+ }
623
+ const cacheKey = `${contentSessionId}::${project}::${prompt}`;
624
+ const lastSeenAt = recentPromptInits.get(cacheKey);
625
+ // Note: cache is set unconditionally before return. If workerPost fails
626
+ // after this check, a retry within 2s would be incorrectly skipped.
627
+ // Acceptable because before_agent_start is not retried by the runtime.
628
+ recentPromptInits.set(cacheKey, now);
629
+ return typeof lastSeenAt === "number" && now - lastSeenAt <= 2000;
630
+ }
631
+
632
+ function clearSessionContext(ctx: SessionTrackingContext): void {
633
+ const aliases = getSessionAliases(ctx);
634
+ const canonicalKey = aliases
635
+ .map((alias) => canonicalSessionKeys.get(alias))
636
+ .find(Boolean) || aliases[0];
637
+ const knownAliases = sessionAliasesByCanonicalKey.get(canonicalKey) || new Set([canonicalKey, ...aliases]);
638
+ for (const alias of knownAliases) {
639
+ canonicalSessionKeys.delete(alias);
640
+ sessionIds.delete(alias);
641
+ }
642
+ sessionAliasesByCanonicalKey.delete(canonicalKey);
643
+ sessionIds.delete(canonicalKey);
644
+ }
645
+
646
+ function scheduleSessionComplete(contentSessionId: string): void {
647
+ const existingTimer = pendingCompletionTimers.get(contentSessionId);
648
+ if (existingTimer) clearTimeout(existingTimer);
649
+ const timer = setTimeout(() => {
650
+ pendingCompletionTimers.delete(contentSessionId);
651
+ workerPostFireAndForget(workerPort, "/api/sessions/complete", {
652
+ contentSessionId,
653
+ }, api.logger);
654
+ }, completionDelayMs);
655
+ pendingCompletionTimers.set(contentSessionId, timer);
656
+ }
657
+
568
658
  // TTL cache for context injection to avoid re-fetching on every LLM turn.
569
659
  // before_prompt_build fires on every turn; caching for 60s keeps the worker
570
660
  // load manageable while still picking up new observations reasonably quickly.
@@ -600,61 +690,54 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
600
690
  }
601
691
 
602
692
  // ------------------------------------------------------------------
603
- // Event: session_start — init claude-mem session (fires on /new, /reset)
693
+ // Event: session_start — track session (fires on /new, /reset)
694
+ // Init is deferred to before_agent_start to avoid duplicate prompt records.
604
695
  // ------------------------------------------------------------------
605
696
  api.on("session_start", async (_event, ctx) => {
606
- const contentSessionId = getContentSessionId(ctx.sessionKey);
607
-
608
- await workerPost(workerPort, "/api/sessions/init", {
609
- contentSessionId,
610
- project: getProjectName(ctx),
611
- prompt: "",
612
- }, api.logger);
613
-
614
- api.logger.info(`[claude-mem] Session initialized: ${contentSessionId}`);
697
+ const { contentSessionId } = rememberSessionContext(ctx);
698
+ api.logger.info(`[claude-mem] Session tracking initialized: ${contentSessionId}`);
615
699
  });
616
700
 
617
701
  // ------------------------------------------------------------------
618
- // Event: message_received — capture inbound user prompts from channels
702
+ // Event: message_received — alias tracking only; init deferred to before_agent_start
619
703
  // ------------------------------------------------------------------
620
704
  api.on("message_received", async (event, ctx) => {
621
- const sessionKey = ctx.conversationId || ctx.channelId || "default";
622
- const contentSessionId = getContentSessionId(sessionKey);
623
-
624
- await workerPost(workerPort, "/api/sessions/init", {
625
- contentSessionId,
626
- project: baseProjectName,
627
- prompt: event.content || "[media prompt]",
628
- }, api.logger);
705
+ const { canonicalKey, contentSessionId } = rememberSessionContext(ctx);
706
+ api.logger.info(`[claude-mem] Message received — prompt capture deferred to before_agent_start: session=${canonicalKey} contentSessionId=${contentSessionId} hasContent=${Boolean(event.content)}`);
629
707
  });
630
708
 
631
709
  // ------------------------------------------------------------------
632
- // Event: after_compaction — re-init session after context compaction
710
+ // Event: after_compaction — preserve session tracking after context compaction.
711
+ // Re-init is intentionally NOT called here; the worker retains session state
712
+ // independently and re-initializing would create duplicate prompt records.
633
713
  // ------------------------------------------------------------------
634
714
  api.on("after_compaction", async (_event, ctx) => {
635
- const contentSessionId = getContentSessionId(ctx.sessionKey);
636
-
637
- await workerPost(workerPort, "/api/sessions/init", {
638
- contentSessionId,
639
- project: getProjectName(ctx),
640
- prompt: "",
641
- }, api.logger);
642
-
643
- api.logger.info(`[claude-mem] Session re-initialized after compaction: ${contentSessionId}`);
715
+ const { contentSessionId } = rememberSessionContext(ctx);
716
+ api.logger.info(`[claude-mem] Session preserved after compaction: ${contentSessionId}`);
644
717
  });
645
718
 
646
719
  // ------------------------------------------------------------------
647
- // Event: before_agent_start — init session
720
+ // Event: before_agent_start — single init point with dedup guard
648
721
  // ------------------------------------------------------------------
649
722
  api.on("before_agent_start", async (event, ctx) => {
723
+ const { contentSessionId } = rememberSessionContext(ctx);
724
+ const projectName = getProjectName(ctx);
725
+ const promptText = event.prompt || "agent run";
726
+
727
+ if (shouldSkipDuplicatePromptInit(contentSessionId, projectName, promptText)) {
728
+ api.logger.info(`[claude-mem] Skipping duplicate prompt init: contentSessionId=${contentSessionId} project=${projectName}`);
729
+ return;
730
+ }
731
+
650
732
  // Initialize session in the worker so observations are not skipped
651
733
  // (the privacy check requires a stored user prompt to exist)
652
- const contentSessionId = getContentSessionId(ctx.sessionKey);
653
734
  await workerPost(workerPort, "/api/sessions/init", {
654
735
  contentSessionId,
655
- project: getProjectName(ctx),
656
- prompt: event.prompt || "agent run",
736
+ project: projectName,
737
+ prompt: promptText,
657
738
  }, api.logger);
739
+
740
+ api.logger.info(`[claude-mem] Session initialized via before_agent_start: contentSessionId=${contentSessionId} project=${projectName}`);
658
741
  });
659
742
 
660
743
  // ------------------------------------------------------------------
@@ -686,7 +769,7 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
686
769
  // Skip memory_ tools to prevent recursive observation loops
687
770
  if (toolName.startsWith("memory_")) return;
688
771
 
689
- const contentSessionId = getContentSessionId(ctx.sessionKey);
772
+ const { canonicalKey, contentSessionId } = rememberSessionContext(ctx);
690
773
 
691
774
  // Extract result text from all content blocks
692
775
  let toolResponseText = "";
@@ -704,13 +787,23 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
704
787
  toolResponseText = toolResponseText.slice(0, MAX_TOOL_RESPONSE_LENGTH);
705
788
  }
706
789
 
790
+ // Resolve workspaceDir with fallback chain.
791
+ // Empty cwd causes worker-side observation queueing failures,
792
+ // so we drop the observation rather than sending cwd: "".
793
+ const workspaceDir = ctx.workspaceDir;
794
+
795
+ if (!workspaceDir) {
796
+ api.logger.warn(`[claude-mem] Skipping observation persist because workspaceDir is unavailable: session=${canonicalKey} tool=${toolName}`);
797
+ return;
798
+ }
799
+
707
800
  // Fire-and-forget: send observation to worker
708
801
  workerPostFireAndForget(workerPort, "/api/sessions/observations", {
709
802
  contentSessionId,
710
803
  tool_name: toolName,
711
804
  tool_input: event.params || {},
712
805
  tool_response: toolResponseText,
713
- cwd: "",
806
+ cwd: workspaceDir,
714
807
  }, api.logger);
715
808
  });
716
809
 
@@ -718,7 +811,7 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
718
811
  // Event: agent_end — summarize and complete session
719
812
  // ------------------------------------------------------------------
720
813
  api.on("agent_end", async (event, ctx) => {
721
- const contentSessionId = getContentSessionId(ctx.sessionKey);
814
+ const { contentSessionId } = rememberSessionContext(ctx);
722
815
 
723
816
  // Extract last assistant message for summarization
724
817
  let lastAssistantMessage = "";
@@ -747,17 +840,16 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
747
840
  last_assistant_message: lastAssistantMessage,
748
841
  }, api.logger);
749
842
 
750
- workerPostFireAndForget(workerPort, "/api/sessions/complete", {
751
- contentSessionId,
752
- }, api.logger);
843
+ api.logger.info(`[claude-mem] Scheduling session complete in ${completionDelayMs}ms: ${contentSessionId}`);
844
+ scheduleSessionComplete(contentSessionId);
753
845
  });
754
846
 
755
847
  // ------------------------------------------------------------------
756
848
  // Event: session_end — clean up session tracking to prevent unbounded growth
757
849
  // ------------------------------------------------------------------
758
850
  api.on("session_end", async (_event, ctx) => {
759
- const key = ctx.sessionKey || "default";
760
- sessionIds.delete(key);
851
+ clearSessionContext(ctx);
852
+ api.logger.info(`[claude-mem] Session tracking cleaned up`);
761
853
  });
762
854
 
763
855
  // ------------------------------------------------------------------
@@ -766,6 +858,13 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
766
858
  api.on("gateway_start", async () => {
767
859
  sessionIds.clear();
768
860
  contextCache.clear();
861
+ recentPromptInits.clear();
862
+ canonicalSessionKeys.clear();
863
+ sessionAliasesByCanonicalKey.clear();
864
+ for (const timer of pendingCompletionTimers.values()) {
865
+ clearTimeout(timer);
866
+ }
867
+ pendingCompletionTimers.clear();
769
868
  api.logger.info("[claude-mem] Gateway started — session tracking reset");
770
869
  });
771
870
 
@@ -1047,5 +1146,5 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
1047
1146
  },
1048
1147
  });
1049
1148
 
1050
- api.logger.info(`[claude-mem] OpenClaw plugin loaded — v1.0.0 (worker: 127.0.0.1:${workerPort})`);
1149
+ api.logger.info(`[claude-mem] OpenClaw plugin loaded — v1.0.0 (worker: ${_workerHost}:${workerPort})`);
1051
1150
  }
@@ -643,7 +643,7 @@ test_write_settings_new_file() {
643
643
 
644
644
  local model
645
645
  model="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_MODEL);")"
646
- assert_eq "claude-sonnet-4-5" "$model" "CLAUDE_MEM_MODEL defaults to claude-sonnet-4-5"
646
+ assert_eq "claude-sonnet-4-6" "$model" "CLAUDE_MEM_MODEL defaults to claude-sonnet-4-6"
647
647
 
648
648
  HOME="$ORIGINAL_HOME"
649
649
  rm -rf "$fake_home"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem",
3
- "version": "10.7.2",
3
+ "version": "12.0.0",
4
4
  "description": "Memory compression system for Claude Code - persist context across sessions",
5
5
  "keywords": [
6
6
  "claude",
@@ -60,7 +60,7 @@
60
60
  },
61
61
  "scripts": {
62
62
  "dev": "npm run build-and-sync",
63
- "build": "node scripts/build-hooks.js",
63
+ "build": "node scripts/sync-plugin-manifests.js && node scripts/build-hooks.js",
64
64
  "build-and-sync": "npm run build && npm run sync-marketplace && sleep 1 && cd ~/.claude/plugins/marketplaces/thedotmack && npm run worker:restart",
65
65
  "sync-marketplace": "node scripts/sync-marketplace.cjs",
66
66
  "sync-marketplace:force": "node scripts/sync-marketplace.cjs --force",
@@ -124,6 +124,12 @@
124
124
  "zod-to-json-schema": "^3.24.6"
125
125
  },
126
126
  "devDependencies": {
127
+ "@derekstride/tree-sitter-sql": "^0.3.11",
128
+ "@tree-sitter-grammars/tree-sitter-lua": "^0.4.1",
129
+ "@tree-sitter-grammars/tree-sitter-markdown": "^0.3.2",
130
+ "@tree-sitter-grammars/tree-sitter-toml": "^0.7.0",
131
+ "@tree-sitter-grammars/tree-sitter-yaml": "^0.7.1",
132
+ "@tree-sitter-grammars/tree-sitter-zig": "^1.1.2",
127
133
  "@types/cors": "^2.8.19",
128
134
  "@types/dompurify": "^3.0.5",
129
135
  "@types/express": "^4.17.21",
@@ -132,15 +138,24 @@
132
138
  "@types/react-dom": "^18.3.0",
133
139
  "esbuild": "^0.27.2",
134
140
  "np": "^11.0.2",
141
+ "tree-sitter-bash": "^0.25.1",
135
142
  "tree-sitter-c": "^0.24.1",
136
143
  "tree-sitter-cli": "^0.26.5",
137
144
  "tree-sitter-cpp": "^0.23.4",
145
+ "tree-sitter-css": "^0.25.0",
146
+ "tree-sitter-elixir": "^0.3.5",
138
147
  "tree-sitter-go": "^0.25.0",
148
+ "tree-sitter-haskell": "^0.23.1",
139
149
  "tree-sitter-java": "^0.23.5",
140
150
  "tree-sitter-javascript": "^0.25.0",
151
+ "tree-sitter-kotlin": "^0.3.8",
152
+ "tree-sitter-php": "^0.24.2",
141
153
  "tree-sitter-python": "^0.25.0",
142
154
  "tree-sitter-ruby": "^0.23.1",
143
155
  "tree-sitter-rust": "^0.24.0",
156
+ "tree-sitter-scala": "^0.24.0",
157
+ "tree-sitter-scss": "^1.0.0",
158
+ "tree-sitter-swift": "^0.7.1",
144
159
  "tree-sitter-typescript": "^0.23.2",
145
160
  "tsx": "^4.20.6",
146
161
  "typescript": "^5.3.0"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem",
3
- "version": "10.7.2",
3
+ "version": "12.0.0",
4
4
  "description": "Persistent memory system for Claude Code - seamlessly preserve context across sessions",
5
5
  "author": {
6
6
  "name": "Alex Newman"