claude-mem 12.1.2 → 12.1.4

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 V="127.0.0.1",Z=["\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 te(e){let s=0;for(let o=0;o<e.length;o++)s=(s<<5)-s+e.charCodeAt(o)|0;return Z[Math.abs(s)%Z.length]}var re="\u{1F99E}",se="\u2328\uFE0F",oe="Claude Code Session",ie="\u{1F980}";function ae(e){let s=e?.primary??re,o=e?.claudeCode??se,a=e?.claudeCodeLabel??oe,c=e?.default??ie,g=e?.agents??{};return function(f){if(!f)return c;if(f.startsWith("openclaw-")){let b=f.slice(9);return b?`${g[b]||te(b)} ${b}`:`${s} openclaw`}if(f==="openclaw")return`${s} openclaw`;let v=a.trim();return v?`${o} ${v} (${f})`:`${o} ${f}`}}var H=V;function T(e){return`http://${H}:${e}`}var ce=3,ee=3e4,h="CLOSED",j=0,G=0,P=!1;function z(e){return h==="CLOSED"?!0:h==="OPEN"?Date.now()-G>=ee?(h="HALF_OPEN",e.info("[claude-mem] Circuit breaker: probing worker connection"),P?!1:(P=!0,!0)):!1:P?!1:(P=!0,!0)}function X(e){h!=="CLOSED"&&e.info("[claude-mem] Worker connection restored \u2014 circuit closed"),h="CLOSED",j=0,P=!1}function O(e){P=!1,j++,(h==="HALF_OPEN"||h==="CLOSED"&&j>=ce)&&(h="OPEN",G=Date.now(),e.warn(`[claude-mem] Worker unreachable \u2014 disabling requests for ${ee/1e3}s`))}function le(){h="CLOSED",j=0,G=0,P=!1}async function Y(e,s,o,a){if(!z(a))return null;try{let c=await fetch(`${T(e)}${s}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)});return c.ok?(X(a),await c.json()):(O(a),a.warn(`[claude-mem] Worker POST ${s} returned ${c.status}`),null)}catch(c){let g=c instanceof Error?c.message:String(c);return O(a),h!=="OPEN"&&a.warn(`[claude-mem] Worker POST ${s} failed: ${g}`),null}}function Q(e,s,o,a){z(a)&&fetch(`${T(e)}${s}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)}).then(c=>{if(!c.ok){O(a),a.warn(`[claude-mem] Worker POST ${s} returned ${c.status}`);return}X(a)}).catch(c=>{let g=c instanceof Error?c.message:String(c);O(a),h!=="OPEN"&&a.warn(`[claude-mem] Worker POST ${s} failed: ${g}`)})}async function J(e,s,o){if(!z(o))return null;try{let a=await fetch(`${T(e)}${s}`);return a.ok?(X(o),await a.text()):(O(o),o.warn(`[claude-mem] Worker GET ${s} returned ${a.status}`),null)}catch(a){let c=a instanceof Error?a.message:String(a);return O(o),h!=="OPEN"&&o.warn(`[claude-mem] Worker GET ${s} failed: ${c}`),null}}async function q(e,s,o){let a=await J(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 ue(e,s){let o=e.title||"Untitled",c=`${s(e.project)}
2
- **${o}**`;return e.subtitle&&(c+=`
3
- ${e.subtitle}`),c}var ge={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 de(e,s,o,a){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:o,parse_mode:"Markdown"})});if(!c.ok){let g=await c.text();a.warn(`[claude-mem] Direct Telegram send failed (${c.status}): ${g}`)}}catch(c){let g=c instanceof Error?c.message:String(c);a.warn(`[claude-mem] Direct Telegram send error: ${g}`)}}function me(e,s,o,a,c){if(c&&s==="telegram")return de(c,o,a,e.logger);let g=ge[s];if(!g)return e.logger.warn(`[claude-mem] Unsupported channel type: ${s}`),Promise.resolve();let w=e.runtime.channel[g.namespace];if(!w)return e.logger.warn(`[claude-mem] Channel "${s}" not available in runtime`),Promise.resolve();let f=w[g.functionName];return f?f(...s==="whatsapp"?[o,a,{verbose:!1}]:[o,a]).catch(b=>{let m=b instanceof Error?b.message:String(b);e.logger.error(`[claude-mem] Failed to send to ${s}: ${m}`)}):(e.logger.warn(`[claude-mem] Channel "${s}" has no ${g.functionName} function`),Promise.resolve())}async function fe(e,s,o,a,c,g,w,f){let v=1e3,b=3e4;for(;!c.signal.aborted;){try{g("reconnecting"),e.logger.info(`[claude-mem] Connecting to SSE stream at ${T(s)}/stream`);let m=await fetch(`${T(s)}/stream`,{signal:c.signal,headers:{Accept:"text/event-stream"}});if(!m.ok)throw new Error(`SSE stream returned HTTP ${m.status}`);if(!m.body)throw new Error("SSE stream response has no body");g("connected"),v=1e3,e.logger.info("[claude-mem] Connected to SSE stream");let A=m.body.getReader(),D=new TextDecoder,k="";for(;;){let{done:B,value:U}=await A.read();if(B)break;k+=D.decode(U,{stream:!0}),k.length>1048576&&(e.logger.warn("[claude-mem] SSE buffer overflow, clearing buffer"),k="");let M=k.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
- `);k=M.pop()||"";for(let E of M){let R=E.split(`
6
- `).filter(S=>S.startsWith("data:")).map(S=>S.slice(5).trim());if(R.length===0)continue;let L=R.join(`
7
- `);if(L)try{let S=JSON.parse(L);if(S.type==="new_observation"&&S.observation){let x=ue(S.observation,w);await me(e,o,a,x,f)}}catch(S){let F=S instanceof Error?S.message:String(S);e.logger.warn(`[claude-mem] Failed to parse SSE frame: ${F}`)}}}}catch(m){if(c.signal.aborted)break;g("reconnecting");let A=m instanceof Error?m.message:String(m);e.logger.warn(`[claude-mem] SSE stream error: ${A}. Reconnecting in ${v/1e3}s`)}if(c.signal.aborted)break;await new Promise(m=>setTimeout(m,v)),v=Math.min(v*2,b)}g("disconnected")}function pe(e){let s=e.pluginConfig||{},o=s.workerPort||37777;H=s.workerHost||V;let a=s.project||"openclaw",c=ae(s.observationFeed?.emojis);function g(r){return r.agentId?`openclaw-${r.agentId}`:a}let w=new Map,f=new Map,v=new Map,b=new Map,m=new Map,A=(()=>{let r=Number(s.completionDelayMs);return Number.isFinite(r)?Math.max(0,r):5e3})(),D=s.syncMemoryFile!==!1,k=new Set(s.syncMemoryFileExclude||[]);function B(r){let n=r||"default";return w.has(n)||w.set(n,`openclaw-${n}-${Date.now()}`),w.get(n)}function U(r){if(!D)return!1;let n=r?.agentId;return!(n&&k.has(n))}function M(r){let n=new Set;for(let t of[r.sessionKey,r.conversationId,r.channelId]){let i=typeof t=="string"?t.trim():"";i&&n.add(i)}return n.size===0&&n.add("default"),Array.from(n)}function E(r){let n=M(r),t=n.find(u=>f.has(u));t=t?f.get(t):n[0];let i=v.get(t);i||(i=new Set([t]),v.set(t,i));for(let u of n)i.add(u),f.set(u,t);let l=B(t);for(let u of i)w.set(u,l);return{canonicalKey:t,contentSessionId:l}}function R(r,n,t){let i=Date.now();for(let[d,p]of m)i-p>2e3&&m.delete(d);let l=`${r}::${n}::${t}`,u=m.get(l);return m.set(l,i),typeof u=="number"&&i-u<=2e3}function L(r){let n=M(r),t=n.map(l=>f.get(l)).find(Boolean)||n[0],i=v.get(t)||new Set([t,...n]);for(let l of i)f.delete(l),w.delete(l);v.delete(t),w.delete(t)}function S(r){let n=b.get(r);n&&clearTimeout(n);let t=setTimeout(()=>{b.delete(r),Q(o,"/api/sessions/complete",{contentSessionId:r},e.logger)},A);b.set(r,t)}let F=6e4,x=new Map;async function ne(r){let n=[a],t=r?g(r):null;t&&t!==a&&n.push(t);let i=n.join(","),l=x.get(i);if(l&&Date.now()-l.fetchedAt<F)return l.text;let u=await J(o,`/api/context/inject?projects=${encodeURIComponent(i)}`,e.logger);if(u&&u.trim().length>0){let d=u.trim();return x.set(i,{text:d,fetchedAt:Date.now()}),d}return null}e.on("session_start",async(r,n)=>{let{contentSessionId:t}=E(n);e.logger.info(`[claude-mem] Session tracking initialized: ${t}`)}),e.on("message_received",async(r,n)=>{let{canonicalKey:t,contentSessionId:i}=E(n);e.logger.info(`[claude-mem] Message received \u2014 prompt capture deferred to before_agent_start: session=${t} contentSessionId=${i} hasContent=${!!r.content}`)}),e.on("after_compaction",async(r,n)=>{let{contentSessionId:t}=E(n);e.logger.info(`[claude-mem] Session preserved after compaction: ${t}`)}),e.on("before_agent_start",async(r,n)=>{let{contentSessionId:t}=E(n),i=g(n),l=r.prompt||"agent run";if(R(t,i,l)){e.logger.info(`[claude-mem] Skipping duplicate prompt init: contentSessionId=${t} project=${i}`);return}await Y(o,"/api/sessions/init",{contentSessionId:t,project:i,prompt:l},e.logger),e.logger.info(`[claude-mem] Session initialized via before_agent_start: contentSessionId=${t} project=${i}`)}),e.on("before_prompt_build",async(r,n)=>{if(!U(n))return;let t=await ne(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:i,contentSessionId:l}=E(n),u="",d=r.message?.content;Array.isArray(d)&&(u=d.filter(C=>(C.type==="tool_result"||C.type==="text")&&"text"in C).map(C=>String(C.text)).join(`
8
- `));let p=1e3;u.length>p&&(u=u.slice(0,p));let y=n.workspaceDir;if(!y){e.logger.warn(`[claude-mem] Skipping observation persist because workspaceDir is unavailable: session=${i} tool=${t}`);return}Q(o,"/api/sessions/observations",{contentSessionId:l,tool_name:t,tool_input:r.params||{},tool_response:u,cwd:y},e.logger)}),e.on("agent_end",async(r,n)=>{let{contentSessionId:t}=E(n),i="";if(Array.isArray(r.messages))for(let l=r.messages.length-1;l>=0;l--){let u=r.messages[l];if(u?.role==="assistant"){typeof u.content=="string"?i=u.content:Array.isArray(u.content)&&(i=u.content.filter(d=>d.type==="text").map(d=>d.text||"").join(`
9
- `));break}}await Y(o,"/api/sessions/summarize",{contentSessionId:t,last_assistant_message:i},e.logger),e.logger.info(`[claude-mem] Scheduling session complete in ${A}ms: ${t}`),S(t)}),e.on("session_end",async(r,n)=>{L(n),e.logger.info("[claude-mem] Session tracking cleaned up")}),e.on("gateway_start",async()=>{le(),w.clear(),x.clear(),m.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 _=null,N="disconnected",$=null;e.registerService({id:"claude-mem-observation-feed",start:async r=>{_&&(_.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}`),_=new AbortController,$=fe(e,o,n.channel,n.to,_,t=>{N=t},c,n.botToken)},stop:async r=>{_&&(_.abort(),_=null),$&&(await $,$=null),N="disconnected",e.logger.info("[claude-mem] Observation feed stopped \u2014 SSE connection closed")}});function K(r,n=5){return!Array.isArray(r)||r.length===0?"No results found.":r.slice(0,n).map((t,i)=>{let l=t,u=String(l.title||l.subtitle||l.text||"Untitled"),d=l.project?` [${String(l.project)}]`:"";return`${i+1}. ${u}${d}`}).join(`
10
- `)}function I(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: ${N}`].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+/),i=t[t.length-1],l=/^\d+$/.test(i),u=l?I(i,10):10,d=l?t.slice(0,-1).join(" "):n,p=await q(o,`/api/search/observations?query=${encodeURIComponent(d)}&limit=${u}`,e.logger);if(!p)return"Claude-Mem search failed (worker unavailable or invalid response).";let y=Array.isArray(p.items)?p.items:[];return[`Claude-Mem Search: "${d}"`,K(y,u)].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+/):[],i=t.length>0?t[t.length-1]:"",l=/^\d+$/.test(i),u=l?I(i,3):3,d=l?t.slice(0,-1).join(" "):n,p=new URLSearchParams;p.set("limit",String(u)),d&&p.set("project",d);let y=await q(o,`/api/context/recent?${p.toString()}`,e.logger);if(!y)return"Claude-Mem recent context failed (worker unavailable or invalid response).";let C=Array.isArray(y.session_summaries)?y.session_summaries:[],W=Array.isArray(y.recent_observations)?y.recent_observations:[];return["Claude-Mem Recent Context",`Project: ${d||"(auto)"}`,`Session summaries: ${C.length}`,`Recent observations: ${W.length}`,K(W,Math.min(5,W.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+/),i=5,l=5;t.length>=2&&/^\d+$/.test(t[t.length-1])&&(i=I(t.pop(),5)),t.length>=2&&/^\d+$/.test(t[t.length-1])&&(l=I(t.pop(),5));let u=t.join(" "),d=new URLSearchParams({query:u,mode:"auto",depth_before:String(l),depth_after:String(i)}),p=await q(o,`/api/timeline/by-query?${d.toString()}`,e.logger);if(!p)return"Claude-Mem timeline lookup failed (worker unavailable or invalid response).";let y=Array.isArray(p.timeline)?p.timeline:[],C=p.anchor?String(p.anchor):"(none)";return[`Claude-Mem Timeline: "${u}"`,`Anchor: ${C}`,K(y,8)].join(`
14
- `)}}),e.registerCommand({name:"claude_mem_status",description:"Check Claude-Mem worker health and session status",handler:async()=>{let r=await J(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: ${w.size}`,`Observation feed: ${N}`].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: ${H}:${o})`)}export{pe 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};
@@ -979,207 +979,3 @@ describe("SSE stream integration", () => {
979
979
  await getService().stop({});
980
980
  });
981
981
  });
982
-
983
- describe("circuit breaker", () => {
984
- // Reset circuit breaker state before each test by firing gateway_start.
985
- // The circuit is module-level state, so tests would otherwise bleed into each other.
986
- beforeEach(async () => {
987
- const { api, fireEvent } = createMockApi({ workerPort: 59999 });
988
- claudeMemPlugin(api);
989
- await fireEvent("gateway_start", {}, {});
990
- });
991
-
992
- it("opens after threshold failures and stops further requests", async () => {
993
- const { api, logs, fireEvent } = createMockApi({ workerPort: 59999 });
994
- claudeMemPlugin(api);
995
- // Reset circuit inside the test body to guard against timers from preceding
996
- // tests (e.g. completionDelayMs timers) that may fire between beforeEach and here.
997
- await fireEvent("gateway_start", {}, {});
998
-
999
- // Fire threshold+1 calls so the circuit is open by the end of the loop
1000
- // regardless of whether a concurrent timer fires at the exact boundary.
1001
- for (let i = 0; i < 4; i++) {
1002
- await fireEvent("before_agent_start", { prompt: "hello" }, { sessionKey: `cb-open-${i}` });
1003
- }
1004
-
1005
- // Circuit is now OPEN. Subsequent calls must be silently dropped.
1006
- const logCountBeforeDrop = logs.length;
1007
- await fireEvent("before_agent_start", { prompt: "hello" }, { sessionKey: "cb-drop" });
1008
- const noisyDropLogs = logs.slice(logCountBeforeDrop).filter(
1009
- (l) => l.includes("failed") || l.includes("disabling")
1010
- );
1011
- assert.equal(noisyDropLogs.length, 0, "calls when circuit is open should be silently dropped");
1012
- });
1013
-
1014
- it("logs individual failures while circuit is closed, then disabling when it opens", async () => {
1015
- const { api, logs, fireEvent } = createMockApi({ workerPort: 59999 });
1016
- claudeMemPlugin(api);
1017
- await fireEvent("gateway_start", {}, {});
1018
- const logsAfterReset = logs.length;
1019
-
1020
- // Fire exactly threshold (3) calls
1021
- for (let i = 0; i < 3; i++) {
1022
- await fireEvent("before_agent_start", { prompt: "hello" }, { sessionKey: `cb-log-${i}` });
1023
- }
1024
-
1025
- const newLogs = logs.slice(logsAfterReset);
1026
- // At least some failures should have been logged (circuit was active)
1027
- assert.ok(newLogs.length > 0, "threshold calls should produce log output");
1028
- // Exactly one disabling warning should appear
1029
- const disablingLogs = newLogs.filter((l) => l.includes("disabling requests"));
1030
- assert.equal(disablingLogs.length, 1, "should emit exactly one disabling warning when circuit opens");
1031
- // The last call (the threshold-crossing one) should NOT log an individual failure
1032
- const failureLogs = newLogs.filter((l) => l.includes("failed:"));
1033
- assert.ok(failureLogs.length < 3, "threshold-crossing call should not log an individual failure");
1034
- });
1035
-
1036
- it("resets on gateway_start, allowing connections again", async () => {
1037
- const { api, logs, fireEvent } = createMockApi({ workerPort: 59999 });
1038
- claudeMemPlugin(api);
1039
- await fireEvent("gateway_start", {}, {});
1040
-
1041
- // Open the circuit by firing threshold+1 calls
1042
- for (let i = 0; i < 4; i++) {
1043
- await fireEvent("before_agent_start", { prompt: "hello" }, { sessionKey: `cb-reset-${i}` });
1044
- }
1045
-
1046
- // Confirm circuit is open (call is silently dropped)
1047
- const logCountWhileOpen = logs.length;
1048
- await fireEvent("before_agent_start", { prompt: "hello" }, { sessionKey: "cb-while-open" });
1049
- assert.equal(
1050
- logs.slice(logCountWhileOpen).filter((l) => l.includes("failed") || l.includes("disabling")).length,
1051
- 0,
1052
- "call while circuit is open should be silently dropped"
1053
- );
1054
-
1055
- // gateway_start resets the circuit
1056
- await fireEvent("gateway_start", {}, {});
1057
-
1058
- // Next call should attempt to connect again (not silently drop)
1059
- const logCountAfterReset = logs.length;
1060
- await fireEvent("before_agent_start", { prompt: "hello" }, { sessionKey: "cb-after-reset" });
1061
- const newLogs = logs.slice(logCountAfterReset);
1062
- assert.ok(
1063
- newLogs.some((l) => l.includes("failed:") || l.includes("disabling")),
1064
- "should attempt worker connection after gateway_start reset"
1065
- );
1066
- });
1067
-
1068
- it("HALF_OPEN allows only a single probe — non-2xx keeps circuit open, 2xx closes it", async () => {
1069
- // ---- Phase 1: open the circuit via network failures (unreachable port) ----
1070
- // Reset circuit state first
1071
- const resetMock = createMockApi({ workerPort: 59999 });
1072
- claudeMemPlugin(resetMock.api);
1073
- await resetMock.fireEvent("gateway_start", {}, {});
1074
-
1075
- // Drive 4 failures to ensure circuit is OPEN
1076
- for (let i = 0; i < 4; i++) {
1077
- await resetMock.fireEvent("before_agent_start", { prompt: "probe-test" }, { sessionKey: `probe-phase1-${i}` });
1078
- }
1079
-
1080
- // ---- Phase 2: advance clock so cooldown has elapsed ----
1081
- // _circuitOpenedAt was set during Phase 1 using the real Date.now().
1082
- // Advancing Date.now by 31s means the next circuitAllow call sees the cooldown elapsed.
1083
- const realDateNow = Date.now.bind(Date);
1084
- Date.now = () => realDateNow() + 31_000;
1085
-
1086
- try {
1087
- // ---- Phase 3: non-2xx probe — circuit should stay OPEN ----
1088
- // Start a server that returns 500 for all requests
1089
- let serverA: Server | null = null;
1090
- const portA: number = await new Promise((resolve) => {
1091
- serverA = createServer((_req: IncomingMessage, res: ServerResponse) => {
1092
- res.writeHead(500);
1093
- res.end();
1094
- });
1095
- serverA!.listen(0, () => {
1096
- const addr = serverA!.address();
1097
- resolve((addr as any).port);
1098
- });
1099
- });
1100
-
1101
- // Reuse the same module-level circuit state — just change the worker port.
1102
- // Create a new mock api instance pointed at server A (500 responder).
1103
- const mockA = createMockApi({ workerPort: portA });
1104
- claudeMemPlugin(mockA.api);
1105
- // Do NOT fire gateway_start here — we want the OPEN circuit state from Phase 1.
1106
-
1107
- // The circuit is OPEN but the mocked clock says cooldown elapsed.
1108
- // The next call should: transition to HALF_OPEN, set _halfOpenProbeInFlight=true,
1109
- // send the probe to server A (which returns 500), then call circuitOnFailure
1110
- // and re-open the circuit.
1111
- const logCountAtProbe = mockA.logs.length;
1112
- await mockA.fireEvent("before_agent_start", { prompt: "probe" }, { sessionKey: "probe-call-non2xx" });
1113
- await new Promise((resolve) => setTimeout(resolve, 100));
1114
-
1115
- const probeALogs = mockA.logs.slice(logCountAtProbe);
1116
- // After a 500 response, circuitOnFailure is called which logs "disabling requests"
1117
- // (because state was HALF_OPEN) and logger.warn logs the 500 status.
1118
- assert.ok(
1119
- probeALogs.some((l) => l.includes("disabling") || l.includes("returned 500") || l.includes("Worker POST")),
1120
- "non-2xx probe should keep circuit open (expected disabling or 500 status log)"
1121
- );
1122
-
1123
- // Verify probe flag resets: a second call with cooldown elapsed should be allowed as a new probe
1124
- // (i.e., _halfOpenProbeInFlight was cleared by circuitOnFailure).
1125
- // But without advancing time further the circuit is OPEN again — so calls are dropped.
1126
- const logCountAfterFailedProbe = mockA.logs.length;
1127
- await mockA.fireEvent("before_agent_start", { prompt: "probe" }, { sessionKey: "probe-concurrent" });
1128
- await new Promise((resolve) => setTimeout(resolve, 100));
1129
- const droppedLogs = mockA.logs.slice(logCountAfterFailedProbe).filter(
1130
- (l) => l.includes("failed") || l.includes("disabling")
1131
- );
1132
- assert.equal(droppedLogs.length, 0, "call should be silently dropped while circuit is OPEN again after failed probe");
1133
-
1134
- serverA!.close();
1135
-
1136
- // ---- Phase 4: 2xx probe — circuit should close ----
1137
- // Re-open the circuit with fresh failures, then probe with a 200-returning server.
1138
- // Reset circuit state first.
1139
- const resetMock2 = createMockApi({ workerPort: 59999 });
1140
- claudeMemPlugin(resetMock2.api);
1141
- await resetMock2.fireEvent("gateway_start", {}, {});
1142
-
1143
- // Drive failures (still using mocked Date.now, but _circuitOpenedAt will be set to
1144
- // the mocked time, so cooldown is NOT elapsed yet from the mocked perspective).
1145
- // We need to temporarily restore real Date.now while opening the circuit, then
1146
- // re-mock it for the probe.
1147
- Date.now = realDateNow;
1148
- for (let i = 0; i < 4; i++) {
1149
- await resetMock2.fireEvent("before_agent_start", { prompt: "probe-test" }, { sessionKey: `probe-phase4-${i}` });
1150
- }
1151
- // Re-advance the clock past cooldown
1152
- Date.now = () => realDateNow() + 31_000;
1153
-
1154
- let serverB: Server | null = null;
1155
- const portB: number = await new Promise((resolve) => {
1156
- serverB = createServer((_req: IncomingMessage, res: ServerResponse) => {
1157
- res.writeHead(200, { "Content-Type": "application/json" });
1158
- res.end(JSON.stringify({ sessionDbId: 1, promptNumber: 1, skipped: false }));
1159
- });
1160
- serverB!.listen(0, () => {
1161
- const addr = serverB!.address();
1162
- resolve((addr as any).port);
1163
- });
1164
- });
1165
-
1166
- const mockB = createMockApi({ workerPort: portB });
1167
- claudeMemPlugin(mockB.api);
1168
- // Do NOT fire gateway_start — reuse OPEN circuit state from resetMock2.
1169
-
1170
- const logCountBeforeSuccessProbe = mockB.logs.length;
1171
- await mockB.fireEvent("before_agent_start", { prompt: "probe" }, { sessionKey: "probe-call-2xx" });
1172
- await new Promise((resolve) => setTimeout(resolve, 150));
1173
-
1174
- const successProbeLogs = mockB.logs.slice(logCountBeforeSuccessProbe);
1175
- assert.ok(
1176
- successProbeLogs.some((l) => l.includes("restored") || l.includes("circuit closed")),
1177
- "2xx probe should close the circuit — expected 'restored' or 'circuit closed' log"
1178
- );
1179
-
1180
- serverB!.close();
1181
- } finally {
1182
- Date.now = realDateNow;
1183
- }
1184
- });
1185
- });
@@ -264,80 +264,12 @@ function workerBaseUrl(port: number): string {
264
264
  return `http://${_workerHost}:${port}`;
265
265
  }
266
266
 
267
- // ============================================================================
268
- // Worker Circuit Breaker
269
- // ============================================================================
270
- // Prevents CPU-spinning retry loops when the worker is unreachable.
271
- // After CIRCUIT_BREAKER_THRESHOLD consecutive network errors, the circuit
272
- // opens and all worker calls are silently dropped for CIRCUIT_BREAKER_COOLDOWN_MS.
273
- // After the cooldown, one probe attempt is allowed to check if the worker recovered.
274
-
275
- const CIRCUIT_BREAKER_THRESHOLD = 3;
276
- const CIRCUIT_BREAKER_COOLDOWN_MS = 30_000;
277
-
278
- type CircuitState = "CLOSED" | "OPEN" | "HALF_OPEN";
279
-
280
- let _circuitState: CircuitState = "CLOSED";
281
- let _circuitFailures = 0;
282
- let _circuitOpenedAt = 0;
283
- let _halfOpenProbeInFlight = false;
284
-
285
- function circuitAllow(logger: PluginLogger): boolean {
286
- if (_circuitState === "CLOSED") return true;
287
- if (_circuitState === "OPEN") {
288
- if (Date.now() - _circuitOpenedAt >= CIRCUIT_BREAKER_COOLDOWN_MS) {
289
- _circuitState = "HALF_OPEN";
290
- logger.info("[claude-mem] Circuit breaker: probing worker connection");
291
- if (_halfOpenProbeInFlight) return false;
292
- _halfOpenProbeInFlight = true;
293
- return true;
294
- }
295
- return false;
296
- }
297
- // HALF_OPEN: allow one probe through
298
- if (_halfOpenProbeInFlight) return false;
299
- _halfOpenProbeInFlight = true;
300
- return true;
301
- }
302
-
303
- function circuitOnSuccess(logger: PluginLogger): void {
304
- if (_circuitState !== "CLOSED") {
305
- logger.info("[claude-mem] Worker connection restored — circuit closed");
306
- }
307
- _circuitState = "CLOSED";
308
- _circuitFailures = 0;
309
- _halfOpenProbeInFlight = false;
310
- }
311
-
312
- function circuitOnFailure(logger: PluginLogger): void {
313
- _halfOpenProbeInFlight = false;
314
- _circuitFailures++;
315
- if (
316
- _circuitState === "HALF_OPEN" ||
317
- (_circuitState === "CLOSED" && _circuitFailures >= CIRCUIT_BREAKER_THRESHOLD)
318
- ) {
319
- _circuitState = "OPEN";
320
- _circuitOpenedAt = Date.now();
321
- logger.warn(
322
- `[claude-mem] Worker unreachable — disabling requests for ${CIRCUIT_BREAKER_COOLDOWN_MS / 1000}s`
323
- );
324
- }
325
- }
326
-
327
- function circuitReset(): void {
328
- _circuitState = "CLOSED";
329
- _circuitFailures = 0;
330
- _circuitOpenedAt = 0;
331
- _halfOpenProbeInFlight = false;
332
- }
333
-
334
267
  async function workerPost(
335
268
  port: number,
336
269
  path: string,
337
270
  body: Record<string, unknown>,
338
271
  logger: PluginLogger
339
272
  ): Promise<Record<string, unknown> | null> {
340
- if (!circuitAllow(logger)) return null;
341
273
  try {
342
274
  const response = await fetch(`${workerBaseUrl(port)}${path}`, {
343
275
  method: "POST",
@@ -345,18 +277,13 @@ async function workerPost(
345
277
  body: JSON.stringify(body),
346
278
  });
347
279
  if (!response.ok) {
348
- circuitOnFailure(logger);
349
280
  logger.warn(`[claude-mem] Worker POST ${path} returned ${response.status}`);
350
281
  return null;
351
282
  }
352
- circuitOnSuccess(logger);
353
283
  return (await response.json()) as Record<string, unknown>;
354
284
  } catch (error: unknown) {
355
285
  const message = error instanceof Error ? error.message : String(error);
356
- circuitOnFailure(logger);
357
- if (_circuitState !== "OPEN") {
358
- logger.warn(`[claude-mem] Worker POST ${path} failed: ${message}`);
359
- }
286
+ logger.warn(`[claude-mem] Worker POST ${path} failed: ${message}`);
360
287
  return null;
361
288
  }
362
289
  }
@@ -367,24 +294,13 @@ function workerPostFireAndForget(
367
294
  body: Record<string, unknown>,
368
295
  logger: PluginLogger
369
296
  ): void {
370
- if (!circuitAllow(logger)) return;
371
297
  fetch(`${workerBaseUrl(port)}${path}`, {
372
298
  method: "POST",
373
299
  headers: { "Content-Type": "application/json" },
374
300
  body: JSON.stringify(body),
375
- }).then((response) => {
376
- if (!response.ok) {
377
- circuitOnFailure(logger);
378
- logger.warn(`[claude-mem] Worker POST ${path} returned ${response.status}`);
379
- return;
380
- }
381
- circuitOnSuccess(logger);
382
301
  }).catch((error: unknown) => {
383
302
  const message = error instanceof Error ? error.message : String(error);
384
- circuitOnFailure(logger);
385
- if (_circuitState !== "OPEN") {
386
- logger.warn(`[claude-mem] Worker POST ${path} failed: ${message}`);
387
- }
303
+ logger.warn(`[claude-mem] Worker POST ${path} failed: ${message}`);
388
304
  });
389
305
  }
390
306
 
@@ -393,22 +309,16 @@ async function workerGetText(
393
309
  path: string,
394
310
  logger: PluginLogger
395
311
  ): Promise<string | null> {
396
- if (!circuitAllow(logger)) return null;
397
312
  try {
398
313
  const response = await fetch(`${workerBaseUrl(port)}${path}`);
399
314
  if (!response.ok) {
400
- circuitOnFailure(logger);
401
315
  logger.warn(`[claude-mem] Worker GET ${path} returned ${response.status}`);
402
316
  return null;
403
317
  }
404
- circuitOnSuccess(logger);
405
318
  return await response.text();
406
319
  } catch (error: unknown) {
407
320
  const message = error instanceof Error ? error.message : String(error);
408
- circuitOnFailure(logger);
409
- if (_circuitState !== "OPEN") {
410
- logger.warn(`[claude-mem] Worker GET ${path} failed: ${message}`);
411
- }
321
+ logger.warn(`[claude-mem] Worker GET ${path} failed: ${message}`);
412
322
  return null;
413
323
  }
414
324
  }
@@ -946,7 +856,6 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
946
856
  // Event: gateway_start — clear session tracking for fresh start
947
857
  // ------------------------------------------------------------------
948
858
  api.on("gateway_start", async () => {
949
- circuitReset();
950
859
  sessionIds.clear();
951
860
  contextCache.clear();
952
861
  recentPromptInits.clear();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem",
3
- "version": "12.1.2",
3
+ "version": "12.1.4",
4
4
  "description": "Memory compression system for Claude Code - persist context across sessions",
5
5
  "keywords": [
6
6
  "claude",
@@ -115,7 +115,7 @@
115
115
  "ansi-to-html": "^0.7.2",
116
116
  "dompurify": "^3.3.1",
117
117
  "express": "^4.18.2",
118
- "glob": "^13.0.0",
118
+ "glob": "^11.0.3",
119
119
  "handlebars": "^4.7.8",
120
120
  "picocolors": "^1.1.1",
121
121
  "react": "^18.3.1",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem",
3
- "version": "12.1.2",
3
+ "version": "12.1.4",
4
4
  "description": "Persistent memory system for Claude Code - seamlessly preserve context across sessions",
5
5
  "author": {
6
6
  "name": "Alex Newman"
@@ -7,7 +7,7 @@
7
7
  "hooks": [
8
8
  {
9
9
  "type": "command",
10
- "command": "export PATH=\"$HOME/.nvm/versions/node/v$(ls \\\"$HOME/.nvm/versions/node\\\" 2>/dev/null | sed 's/^v//' | sort -t. -k1,1n -k2,2n -k3,3n | tail -1)/bin:$HOME/.local/bin:/usr/local/bin:/opt/homebrew/bin:$PATH\"; _R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=$(ls -dt $HOME/.claude/plugins/cache/thedotmack/claude-mem/[0-9]*/ 2>/dev/null | head -1); _R=\"${_R%/}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/thedotmack/plugin\"; node \"$_R/scripts/smart-install.js\"",
10
+ "command": "_R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=$(ls -dt $HOME/.claude/plugins/cache/thedotmack/claude-mem/[0-9]*/ 2>/dev/null | head -1); _R=\"${_R%/}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/thedotmack/plugin\"; \"$_R/scripts/setup.sh\"",
11
11
  "timeout": 300
12
12
  }
13
13
  ]
@@ -19,17 +19,17 @@
19
19
  "hooks": [
20
20
  {
21
21
  "type": "command",
22
- "command": "export PATH=\"$($SHELL -lc 'echo $PATH' 2>/dev/null):$PATH\"; _R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=$(ls -dt $HOME/.claude/plugins/cache/thedotmack/claude-mem/[0-9]*/ 2>/dev/null | head -1); _R=\"${_R%/}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/thedotmack/plugin\"; node \"$_R/scripts/smart-install.js\"",
22
+ "command": "export PATH=\"$HOME/.nvm/versions/node/v$(ls \\\"$HOME/.nvm/versions/node\\\" 2>/dev/null | sed 's/^v//' | sort -t. -k1,1n -k2,2n -k3,3n | tail -1)/bin:$HOME/.local/bin:/usr/local/bin:/opt/homebrew/bin:$PATH\"; _R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=$(ls -dt $HOME/.claude/plugins/cache/thedotmack/claude-mem/[0-9]*/ 2>/dev/null | head -1); _R=\"${_R%/}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/thedotmack/plugin\"; node \"$_R/scripts/smart-install.js\"",
23
23
  "timeout": 300
24
24
  },
25
25
  {
26
26
  "type": "command",
27
- "command": "export PATH=\"$($SHELL -lc 'echo $PATH' 2>/dev/null):$PATH\"; _R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=$(ls -dt $HOME/.claude/plugins/cache/thedotmack/claude-mem/[0-9]*/ 2>/dev/null | head -1); _R=\"${_R%/}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/thedotmack/plugin\"; node \"$_R/scripts/bun-runner.js\" \"$_R/scripts/worker-service.cjs\" start; for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20; do curl -sf http://localhost:37777/health >/dev/null 2>&1 && break; sleep 1; done; curl -sf http://localhost:37777/health >/dev/null 2>&1 || true; echo '{\"continue\":true,\"suppressOutput\":true}'",
27
+ "command": "export PATH=\"$HOME/.nvm/versions/node/v$(ls \\\"$HOME/.nvm/versions/node\\\" 2>/dev/null | sed 's/^v//' | sort -t. -k1,1n -k2,2n -k3,3n | tail -1)/bin:$HOME/.local/bin:/usr/local/bin:/opt/homebrew/bin:$PATH\"; _R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=$(ls -dt $HOME/.claude/plugins/cache/thedotmack/claude-mem/[0-9]*/ 2>/dev/null | head -1); _R=\"${_R%/}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/thedotmack/plugin\"; node \"$_R/scripts/bun-runner.js\" \"$_R/scripts/worker-service.cjs\" start; for i in 1 2 3 4 5 6 7 8; do curl -sf http://localhost:37777/health >/dev/null 2>&1 && break; sleep 1; done; curl -sf http://localhost:37777/health >/dev/null 2>&1 || exit 1; echo '{\"continue\":true,\"suppressOutput\":true}'",
28
28
  "timeout": 60
29
29
  },
30
30
  {
31
31
  "type": "command",
32
- "command": "export PATH=\"$($SHELL -lc 'echo $PATH' 2>/dev/null):$PATH\"; _R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=$(ls -dt $HOME/.claude/plugins/cache/thedotmack/claude-mem/[0-9]*/ 2>/dev/null | head -1); _R=\"${_R%/}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/thedotmack/plugin\"; for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20; do curl -sf http://localhost:37777/health >/dev/null 2>&1 && break; sleep 1; done; if curl -sf http://localhost:37777/health >/dev/null 2>&1; then node \"$_R/scripts/bun-runner.js\" \"$_R/scripts/worker-service.cjs\" hook claude-code context || true; fi",
32
+ "command": "export PATH=\"$HOME/.nvm/versions/node/v$(ls \\\"$HOME/.nvm/versions/node\\\" 2>/dev/null | sed 's/^v//' | sort -t. -k1,1n -k2,2n -k3,3n | tail -1)/bin:$HOME/.local/bin:/usr/local/bin:/opt/homebrew/bin:$PATH\"; _R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=$(ls -dt $HOME/.claude/plugins/cache/thedotmack/claude-mem/[0-9]*/ 2>/dev/null | head -1); _R=\"${_R%/}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/thedotmack/plugin\"; for i in 1 2 3 4 5 6 7 8; do curl -sf http://localhost:37777/health >/dev/null 2>&1 && break; sleep 1; done; node \"$_R/scripts/bun-runner.js\" \"$_R/scripts/worker-service.cjs\" hook claude-code context",
33
33
  "timeout": 60
34
34
  }
35
35
  ]
@@ -40,7 +40,7 @@
40
40
  "hooks": [
41
41
  {
42
42
  "type": "command",
43
- "command": "export PATH=\"$($SHELL -lc 'echo $PATH' 2>/dev/null):$PATH\"; _R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=$(ls -dt $HOME/.claude/plugins/cache/thedotmack/claude-mem/[0-9]*/ 2>/dev/null | head -1); _R=\"${_R%/}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/thedotmack/plugin\"; node \"$_R/scripts/bun-runner.js\" \"$_R/scripts/worker-service.cjs\" hook claude-code session-init",
43
+ "command": "export PATH=\"$HOME/.nvm/versions/node/v$(ls \\\"$HOME/.nvm/versions/node\\\" 2>/dev/null | sed 's/^v//' | sort -t. -k1,1n -k2,2n -k3,3n | tail -1)/bin:$HOME/.local/bin:/usr/local/bin:/opt/homebrew/bin:$PATH\"; _R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=$(ls -dt $HOME/.claude/plugins/cache/thedotmack/claude-mem/[0-9]*/ 2>/dev/null | head -1); _R=\"${_R%/}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/thedotmack/plugin\"; node \"$_R/scripts/bun-runner.js\" \"$_R/scripts/worker-service.cjs\" hook claude-code session-init",
44
44
  "timeout": 60
45
45
  }
46
46
  ]
@@ -52,7 +52,7 @@
52
52
  "hooks": [
53
53
  {
54
54
  "type": "command",
55
- "command": "export PATH=\"$($SHELL -lc 'echo $PATH' 2>/dev/null):$PATH\"; _R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=$(ls -dt $HOME/.claude/plugins/cache/thedotmack/claude-mem/[0-9]*/ 2>/dev/null | head -1); _R=\"${_R%/}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/thedotmack/plugin\"; node \"$_R/scripts/bun-runner.js\" \"$_R/scripts/worker-service.cjs\" hook claude-code observation",
55
+ "command": "export PATH=\"$HOME/.nvm/versions/node/v$(ls \\\"$HOME/.nvm/versions/node\\\" 2>/dev/null | sed 's/^v//' | sort -t. -k1,1n -k2,2n -k3,3n | tail -1)/bin:$HOME/.local/bin:/usr/local/bin:/opt/homebrew/bin:$PATH\"; _R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=$(ls -dt $HOME/.claude/plugins/cache/thedotmack/claude-mem/[0-9]*/ 2>/dev/null | head -1); _R=\"${_R%/}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/thedotmack/plugin\"; node \"$_R/scripts/bun-runner.js\" \"$_R/scripts/worker-service.cjs\" hook claude-code observation",
56
56
  "timeout": 120
57
57
  }
58
58
  ]
@@ -64,7 +64,7 @@
64
64
  "hooks": [
65
65
  {
66
66
  "type": "command",
67
- "command": "export PATH=\"$($SHELL -lc 'echo $PATH' 2>/dev/null):$PATH\"; _R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=$(ls -dt $HOME/.claude/plugins/cache/thedotmack/claude-mem/[0-9]*/ 2>/dev/null | head -1); _R=\"${_R%/}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/thedotmack/plugin\"; node \"$_R/scripts/bun-runner.js\" \"$_R/scripts/worker-service.cjs\" hook claude-code file-context",
67
+ "command": "_R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/thedotmack/plugin\"; node \"$_R/scripts/bun-runner.js\" \"$_R/scripts/worker-service.cjs\" hook claude-code file-context",
68
68
  "timeout": 2000
69
69
  }
70
70
  ]
@@ -75,7 +75,7 @@
75
75
  "hooks": [
76
76
  {
77
77
  "type": "command",
78
- "command": "export PATH=\"$($SHELL -lc 'echo $PATH' 2>/dev/null):$PATH\"; _R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=$(ls -dt $HOME/.claude/plugins/cache/thedotmack/claude-mem/[0-9]*/ 2>/dev/null | head -1); _R=\"${_R%/}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/thedotmack/plugin\"; node \"$_R/scripts/bun-runner.js\" \"$_R/scripts/worker-service.cjs\" hook claude-code summarize",
78
+ "command": "export PATH=\"$HOME/.nvm/versions/node/v$(ls \\\"$HOME/.nvm/versions/node\\\" 2>/dev/null | sed 's/^v//' | sort -t. -k1,1n -k2,2n -k3,3n | tail -1)/bin:$HOME/.local/bin:/usr/local/bin:/opt/homebrew/bin:$PATH\"; _R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=$(ls -dt $HOME/.claude/plugins/cache/thedotmack/claude-mem/[0-9]*/ 2>/dev/null | head -1); _R=\"${_R%/}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/thedotmack/plugin\"; node \"$_R/scripts/bun-runner.js\" \"$_R/scripts/worker-service.cjs\" hook claude-code summarize",
79
79
  "timeout": 120
80
80
  }
81
81
  ]
@@ -86,7 +86,7 @@
86
86
  "hooks": [
87
87
  {
88
88
  "type": "command",
89
- "command": "export PATH=\"$($SHELL -lc 'echo $PATH' 2>/dev/null):$PATH\"; _R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=$(ls -dt $HOME/.claude/plugins/cache/thedotmack/claude-mem/[0-9]*/ 2>/dev/null | head -1); _R=\"${_R%/}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/thedotmack/plugin\"; node \"$_R/scripts/bun-runner.js\" \"$_R/scripts/worker-service.cjs\" hook claude-code session-complete",
89
+ "command": "export PATH=\"$HOME/.nvm/versions/node/v$(ls \\\"$HOME/.nvm/versions/node\\\" 2>/dev/null | sed 's/^v//' | sort -t. -k1,1n -k2,2n -k3,3n | tail -1)/bin:$HOME/.local/bin:/usr/local/bin:/opt/homebrew/bin:$PATH\"; _R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=$(ls -dt $HOME/.claude/plugins/cache/thedotmack/claude-mem/[0-9]*/ 2>/dev/null | head -1); _R=\"${_R%/}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/thedotmack/plugin\"; node \"$_R/scripts/bun-runner.js\" \"$_R/scripts/worker-service.cjs\" hook claude-code session-complete",
90
90
  "timeout": 30
91
91
  }
92
92
  ]
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-plugin",
3
- "version": "12.1.2",
3
+ "version": "12.1.4",
4
4
  "private": true,
5
5
  "description": "Runtime dependencies for claude-mem bundled hooks",
6
6
  "type": "module",
@@ -47,20 +47,12 @@ function fixBrokenScriptPath(argPath) {
47
47
  * Find Bun executable - checks PATH first, then common install locations
48
48
  */
49
49
  function findBun() {
50
- // Try PATH first.
51
- // On Windows, pass a single command string to avoid Node 22+ DEP0190 deprecation warning
52
- // (triggered when an args array is combined with shell:true, as the args are only
53
- // concatenated, not escaped). Fixes #1503.
54
- const pathCheck = IS_WINDOWS
55
- ? spawnSync('where bun', {
56
- encoding: 'utf-8',
57
- stdio: ['pipe', 'pipe', 'pipe'],
58
- shell: true
59
- })
60
- : spawnSync('which', ['bun'], {
61
- encoding: 'utf-8',
62
- stdio: ['pipe', 'pipe', 'pipe']
63
- });
50
+ // Try PATH first
51
+ const pathCheck = spawnSync(IS_WINDOWS ? 'where' : 'which', ['bun'], {
52
+ encoding: 'utf-8',
53
+ stdio: ['pipe', 'pipe', 'pipe'],
54
+ shell: IS_WINDOWS
55
+ });
64
56
 
65
57
  if (pathCheck.status === 0 && pathCheck.stdout.trim()) {
66
58
  // On Windows, prefer bun.cmd over bun (bun is a shell script, bun.cmd is the Windows batch file)