claude-mem 13.3.0 → 13.4.1

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.
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Claude-Mem SDK - TypeScript Declarations
3
+ *
4
+ * Standalone module for external consumers to parse claude-mem observation XML
5
+ * and build prompts for the memory worker.
6
+ */
7
+
8
+ export interface ParsedObservation {
9
+ type: string;
10
+ title: string | null;
11
+ subtitle: string | null;
12
+ facts: string[];
13
+ narrative: string | null;
14
+ concepts: string[];
15
+ files_read: string[];
16
+ files_modified: string[];
17
+ }
18
+
19
+ export interface ParsedSummary {
20
+ request: string | null;
21
+ investigated: string | null;
22
+ learned: string | null;
23
+ completed: string | null;
24
+ next_steps: string | null;
25
+ notes: string | null;
26
+ }
27
+
28
+ export interface Observation {
29
+ id: number;
30
+ tool_name: string;
31
+ tool_input: string;
32
+ tool_output: string;
33
+ created_at_epoch: number;
34
+ cwd?: string;
35
+ }
36
+
37
+ export interface ParseObservationsOptions {
38
+ /** Array of valid observation types. If provided, validates types against this list. */
39
+ validTypes?: string[];
40
+ /** Type to use if type is missing or invalid. Defaults to 'observation'. */
41
+ fallbackType?: string;
42
+ }
43
+
44
+ /**
45
+ * Parse observation XML blocks from text
46
+ * Returns all observations found in the text
47
+ *
48
+ * @param text - The text containing observation XML blocks
49
+ * @param options - Optional configuration for type validation
50
+ * @returns Array of parsed observations
51
+ *
52
+ * @example
53
+ * ```typescript
54
+ * const text = `<observation>
55
+ * <type>code_change</type>
56
+ * <title>Added login feature</title>
57
+ * <facts><fact>New auth module</fact></facts>
58
+ * </observation>`;
59
+ *
60
+ * const observations = parseObservations(text);
61
+ * // => [{ type: 'code_change', title: 'Added login feature', ... }]
62
+ * ```
63
+ */
64
+ export function parseObservations(
65
+ text: string,
66
+ options?: ParseObservationsOptions
67
+ ): ParsedObservation[];
68
+
69
+ /**
70
+ * Parse summary XML block from text
71
+ * Returns null if no valid summary found or if summary was skipped
72
+ *
73
+ * @param text - The text containing summary XML block
74
+ * @returns Parsed summary or null
75
+ *
76
+ * @example
77
+ * ```typescript
78
+ * const text = `<summary>
79
+ * <request>Implement auth</request>
80
+ * <completed>Added JWT tokens</completed>
81
+ * </summary>`;
82
+ *
83
+ * const summary = parseSummary(text);
84
+ * // => { request: 'Implement auth', completed: 'Added JWT tokens', ... }
85
+ * ```
86
+ */
87
+ export function parseSummary(text: string): ParsedSummary | null;
88
+
89
+ /**
90
+ * Build prompt to send tool observation to SDK agent
91
+ *
92
+ * @param obs - The observation object containing tool data
93
+ * @returns Formatted XML prompt string
94
+ *
95
+ * @example
96
+ * ```typescript
97
+ * const obs = {
98
+ * id: 1,
99
+ * tool_name: 'Read',
100
+ * tool_input: '{"file_path": "/src/index.ts"}',
101
+ * tool_output: '{"content": "..."}',
102
+ * created_at_epoch: Date.now()
103
+ * };
104
+ *
105
+ * const prompt = buildObservationPrompt(obs);
106
+ * // => '<observed_from_primary_session>...'
107
+ * ```
108
+ */
109
+ export function buildObservationPrompt(obs: Observation): string;
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Claude-Mem SDK - Standalone module for external consumers
3
+ *
4
+ * This is a self-contained module that exports parsing and prompt utilities
5
+ * without internal dependencies on the main claude-mem codebase.
6
+ *
7
+ * Usage:
8
+ * import { parseObservations, buildObservationPrompt } from 'claude-mem/sdk';
9
+ */
10
+
11
+ // ============================================================================
12
+ // Parser Functions
13
+ // ============================================================================
14
+
15
+ /**
16
+ * Parse observation XML blocks from text
17
+ * Returns all observations found in the text
18
+ *
19
+ * @param {string} text - The text containing observation XML blocks
20
+ * @param {Object} [options] - Optional configuration
21
+ * @param {string[]} [options.validTypes] - Array of valid observation types. If provided, validates types.
22
+ * @param {string} [options.fallbackType='observation'] - Type to use if type is missing or invalid
23
+ * @returns {ParsedObservation[]} Array of parsed observations
24
+ */
25
+ export function parseObservations(text, options = {}) {
26
+ const observations = [];
27
+ const { validTypes, fallbackType = 'observation' } = options;
28
+
29
+ // Match <observation>...</observation> blocks (non-greedy)
30
+ const observationRegex = /<observation>([\s\S]*?)<\/observation>/g;
31
+
32
+ let match;
33
+ while ((match = observationRegex.exec(text)) !== null) {
34
+ const obsContent = match[1];
35
+
36
+ // Extract all fields
37
+ const type = extractField(obsContent, 'type');
38
+ const title = extractField(obsContent, 'title');
39
+ const subtitle = extractField(obsContent, 'subtitle');
40
+ const narrative = extractField(obsContent, 'narrative');
41
+ const facts = extractArrayElements(obsContent, 'facts', 'fact');
42
+ const concepts = extractArrayElements(obsContent, 'concepts', 'concept');
43
+ const files_read = extractArrayElements(obsContent, 'files_read', 'file');
44
+ const files_modified = extractArrayElements(obsContent, 'files_modified', 'file');
45
+
46
+ // Determine final type
47
+ let finalType = fallbackType;
48
+ if (type) {
49
+ const trimmedType = type.trim();
50
+ if (validTypes) {
51
+ finalType = validTypes.includes(trimmedType) ? trimmedType : fallbackType;
52
+ } else {
53
+ finalType = trimmedType;
54
+ }
55
+ }
56
+
57
+ // Filter out type from concepts array (types and concepts are separate dimensions)
58
+ const cleanedConcepts = concepts.filter(c => c !== finalType);
59
+
60
+ observations.push({
61
+ type: finalType,
62
+ title,
63
+ subtitle,
64
+ facts,
65
+ narrative,
66
+ concepts: cleanedConcepts,
67
+ files_read,
68
+ files_modified
69
+ });
70
+ }
71
+
72
+ return observations;
73
+ }
74
+
75
+ /**
76
+ * Parse summary XML block from text
77
+ * Returns null if no valid summary found or if summary was skipped
78
+ *
79
+ * @param {string} text - The text containing summary XML block
80
+ * @returns {ParsedSummary|null} Parsed summary or null
81
+ */
82
+ export function parseSummary(text) {
83
+ // Check for skip_summary first
84
+ const skipRegex = /<skip_summary\s+reason="([^"]+)"\s*\/>/;
85
+ const skipMatch = skipRegex.exec(text);
86
+
87
+ if (skipMatch) {
88
+ return null;
89
+ }
90
+
91
+ // Match <summary>...</summary> block (non-greedy)
92
+ const summaryRegex = /<summary>([\s\S]*?)<\/summary>/;
93
+ const summaryMatch = summaryRegex.exec(text);
94
+
95
+ if (!summaryMatch) {
96
+ return null;
97
+ }
98
+
99
+ const summaryContent = summaryMatch[1];
100
+
101
+ return {
102
+ request: extractField(summaryContent, 'request'),
103
+ investigated: extractField(summaryContent, 'investigated'),
104
+ learned: extractField(summaryContent, 'learned'),
105
+ completed: extractField(summaryContent, 'completed'),
106
+ next_steps: extractField(summaryContent, 'next_steps'),
107
+ notes: extractField(summaryContent, 'notes')
108
+ };
109
+ }
110
+
111
+ /**
112
+ * Extract a simple field value from XML content
113
+ * Returns null for missing or empty/whitespace-only fields
114
+ */
115
+ function extractField(content, fieldName) {
116
+ const regex = new RegExp(`<${fieldName}>([^<]*)</${fieldName}>`);
117
+ const match = regex.exec(content);
118
+ if (!match) return null;
119
+
120
+ const trimmed = match[1].trim();
121
+ return trimmed === '' ? null : trimmed;
122
+ }
123
+
124
+ /**
125
+ * Extract array of elements from XML content
126
+ */
127
+ function extractArrayElements(content, arrayName, elementName) {
128
+ const elements = [];
129
+
130
+ // Match the array block
131
+ const arrayRegex = new RegExp(`<${arrayName}>(.*?)</${arrayName}>`, 's');
132
+ const arrayMatch = arrayRegex.exec(content);
133
+
134
+ if (!arrayMatch) {
135
+ return elements;
136
+ }
137
+
138
+ const arrayContent = arrayMatch[1];
139
+
140
+ // Extract individual elements
141
+ const elementRegex = new RegExp(`<${elementName}>([^<]+)</${elementName}>`, 'g');
142
+ let elementMatch;
143
+ while ((elementMatch = elementRegex.exec(arrayContent)) !== null) {
144
+ elements.push(elementMatch[1].trim());
145
+ }
146
+
147
+ return elements;
148
+ }
149
+
150
+ // ============================================================================
151
+ // Prompt Building Functions
152
+ // ============================================================================
153
+
154
+ /**
155
+ * Build prompt to send tool observation to SDK agent
156
+ *
157
+ * @param {Observation} obs - The observation object containing tool data
158
+ * @returns {string} Formatted XML prompt string
159
+ */
160
+ export function buildObservationPrompt(obs) {
161
+ // Safely parse tool_input and tool_output - they may be JSON strings
162
+ let toolInput;
163
+ let toolOutput;
164
+
165
+ try {
166
+ toolInput = typeof obs.tool_input === 'string' ? JSON.parse(obs.tool_input) : obs.tool_input;
167
+ } catch {
168
+ toolInput = obs.tool_input;
169
+ }
170
+
171
+ try {
172
+ toolOutput = typeof obs.tool_output === 'string' ? JSON.parse(obs.tool_output) : obs.tool_output;
173
+ } catch {
174
+ toolOutput = obs.tool_output;
175
+ }
176
+
177
+ return `<observed_from_primary_session>
178
+ <what_happened>${obs.tool_name}</what_happened>
179
+ <occurred_at>${new Date(obs.created_at_epoch).toISOString()}</occurred_at>${obs.cwd ? `\n <working_directory>${obs.cwd}</working_directory>` : ''}
180
+ <parameters>${JSON.stringify(toolInput, null, 2)}</parameters>
181
+ <outcome>${JSON.stringify(toolOutput, null, 2)}</outcome>
182
+ </observed_from_primary_session>`;
183
+ }
@@ -1,15 +1,15 @@
1
- var Z="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 Q(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 V="\u{1F99E}",ee="\u2328\uFE0F",ne="Claude Code Session",te="\u{1F980}";function re(e){let s=e?.primary??V,o=e?.claudeCode??ee,a=e?.claudeCodeLabel??ne,c=e?.default??te,d=e?.agents??{};return function(m){if(!m)return c;if(m.startsWith("openclaw-")){let p=m.slice(9);return p?`${d[p]||Q(p)} ${p}`:`${s} openclaw`}if(m==="openclaw")return`${s} openclaw`;let b=a.trim();return b?`${o} ${b} (${m})`:`${o} ${m}`}}var W=Z;function T(e){return`http://${W}:${e}`}var se=3,Y=3e4,h="CLOSED",N=0,H=0,P=!1;function J(e){return h==="CLOSED"?!0:h==="OPEN"?Date.now()-H>=Y?(h="HALF_OPEN",e.info("[claude-mem] Circuit breaker: probing worker connection"),P?!1:(P=!0,!0)):!1:P?!1:(P=!0,!0)}function G(e){h!=="CLOSED"&&e.info("[claude-mem] Worker connection restored \u2014 circuit closed"),h="CLOSED",N=0,P=!1}function x(e){P=!1,N++,(h==="HALF_OPEN"||h==="CLOSED"&&N>=se)&&(h="OPEN",H=Date.now(),e.warn(`[claude-mem] Worker unreachable \u2014 disabling requests for ${Y/1e3}s`))}function oe(){h="CLOSED",N=0,H=0,P=!1}async function X(e,s,o,a){if(!J(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?(G(a),await c.json()):(x(a),a.warn(`[claude-mem] Worker POST ${s} returned ${c.status}`),null)}catch(c){let d=c instanceof Error?c.message:String(c);return x(a),h!=="OPEN"&&a.warn(`[claude-mem] Worker POST ${s} failed: ${d}`),null}}function ie(e,s,o,a){J(a)&&fetch(`${T(e)}${s}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)}).then(c=>{if(!c.ok){x(a),a.warn(`[claude-mem] Worker POST ${s} returned ${c.status}`);return}G(a)}).catch(c=>{let d=c instanceof Error?c.message:String(c);x(a),h!=="OPEN"&&a.warn(`[claude-mem] Worker POST ${s} failed: ${d}`)})}async function q(e,s,o){if(!J(o))return null;try{let a=await fetch(`${T(e)}${s}`);return a.ok?(G(o),await a.text()):(x(o),o.warn(`[claude-mem] Worker GET ${s} returned ${a.status}`),null)}catch(a){let c=a instanceof Error?a.message:String(a);return x(o),h!=="OPEN"&&o.warn(`[claude-mem] Worker GET ${s} failed: ${c}`),null}}async function K(e,s,o){let a=await q(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 ae(e,s){let o=e.title||"Untitled",c=`${s(e.project)}
2
- **${o}**`;return e.subtitle&&(c+=`
3
- ${e.subtitle}`),c}var ce={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 le(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 d=await c.text();a.warn(`[claude-mem] Direct Telegram send failed (${c.status}): ${d}`)}}catch(c){let d=c instanceof Error?c.message:String(c);a.warn(`[claude-mem] Direct Telegram send error: ${d}`)}}function ue(e,s,o,a,c){if(c&&s==="telegram")return le(c,o,a,e.logger);let d=ce[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 m=w[d.functionName];return m?m(...s==="whatsapp"?[o,a,{verbose:!1}]:[o,a]).catch(p=>{let v=p instanceof Error?p.message:String(p);e.logger.error(`[claude-mem] Failed to send to ${s}: ${v}`)}):(e.logger.warn(`[claude-mem] Channel "${s}" has no ${d.functionName} function`),Promise.resolve())}async function de(e,s,o,a,c,d,w,m){let b=1e3,p=3e4;for(;!c.signal.aborted;){try{d("reconnecting"),e.logger.info(`[claude-mem] Connecting to SSE stream at ${T(s)}/stream`);let v=await fetch(`${T(s)}/stream`,{signal:c.signal,headers:{Accept:"text/event-stream"}});if(!v.ok)throw new Error(`SSE stream returned HTTP ${v.status}`);if(!v.body)throw new Error("SSE stream response has no body");d("connected"),b=1e3,e.logger.info("[claude-mem] Connected to SSE stream");let M=v.body.getReader(),j=new TextDecoder,_="";for(;;){let{done:R,value:k}=await M.read();if(R)break;_+=j.decode(k,{stream:!0}),_.length>1048576&&(e.logger.warn("[claude-mem] SSE buffer overflow, clearing buffer"),_="");let L=_.split(`
1
+ var Y="127.0.0.1",X=["\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 V(e){let s=0;for(let o=0;o<e.length;o++)s=(s<<5)-s+e.charCodeAt(o)|0;return X[Math.abs(s)%X.length]}var ee="\u{1F99E}",ne="\u2328\uFE0F",te="Claude Code Session",re="\u{1F980}";function se(e){let s=e?.primary??ee,o=e?.claudeCode??ne,a=e?.claudeCodeLabel??te,l=e?.default??re,g=e?.agents??{};return function(m){if(!m)return l;if(m.startsWith("openclaw-")){let p=m.slice(9);return p?`${g[p]||V(p)} ${p}`:`${s} openclaw`}if(m==="openclaw")return`${s} openclaw`;let b=a.trim();return b?`${o} ${b} (${m})`:`${o} ${m}`}}var q=Y;function T(e){return`http://${q}:${e}`}var oe=3,Q=3e4,y="CLOSED",j=0,J=0,$=!1;function G(e){return y==="CLOSED"?!0:y==="OPEN"?Date.now()-J>=Q?(y="HALF_OPEN",e.info("[claude-mem] Circuit breaker: probing worker connection"),$?!1:($=!0,!0)):!1:$?!1:($=!0,!0)}function z(e){y!=="CLOSED"&&e.info("[claude-mem] Worker connection restored \u2014 circuit closed"),y="CLOSED",j=0,$=!1}function M(e){$=!1,j++,(y==="HALF_OPEN"||y==="CLOSED"&&j>=oe)&&(y="OPEN",J=Date.now(),e.warn(`[claude-mem] Worker unreachable \u2014 disabling requests for ${Q/1e3}s`))}function ie(){y="CLOSED",j=0,J=0,$=!1}async function Z(e,s,o,a){if(!G(a))return null;try{let l=await fetch(`${T(e)}${s}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)});return l.ok?(z(a),await l.json()):(M(a),a.warn(`[claude-mem] Worker POST ${s} returned ${l.status}`),null)}catch(l){let g=l instanceof Error?l.message:String(l);return M(a),y!=="OPEN"&&a.warn(`[claude-mem] Worker POST ${s} failed: ${g}`),null}}function ae(e,s,o,a){G(a)&&fetch(`${T(e)}${s}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)}).then(l=>{if(!l.ok){M(a),a.warn(`[claude-mem] Worker POST ${s} returned ${l.status}`);return}z(a)}).catch(l=>{let g=l instanceof Error?l.message:String(l);M(a),y!=="OPEN"&&a.warn(`[claude-mem] Worker POST ${s} failed: ${g}`)})}async function H(e,s,o){if(!G(o))return null;try{let a=await fetch(`${T(e)}${s}`);return a.ok?(z(o),await a.text()):(M(o),o.warn(`[claude-mem] Worker GET ${s} returned ${a.status}`),null)}catch(a){let l=a instanceof Error?a.message:String(a);return M(o),y!=="OPEN"&&o.warn(`[claude-mem] Worker GET ${s} failed: ${l}`),null}}async function W(e,s,o){let a=await H(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 ce(e,s){let o=e.title||"Untitled",l=`${s(e.project)}
2
+ **${o}**`;return e.subtitle&&(l+=`
3
+ ${e.subtitle}`),l}var le={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 ue(e,s,o,a){try{let l=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(!l.ok){let g=await l.text();a.warn(`[claude-mem] Direct Telegram send failed (${l.status}): ${g}`)}}catch(l){let g=l instanceof Error?l.message:String(l);a.warn(`[claude-mem] Direct Telegram send error: ${g}`)}}function ge(e,s,o,a,l){if(l&&s==="telegram")return ue(l,o,a,e.logger);let g=le[s];if(!g)return e.logger.warn(`[claude-mem] Unsupported channel type: ${s}`),Promise.resolve();let v=e.runtime.channel[g.namespace];if(!v)return e.logger.warn(`[claude-mem] Channel "${s}" not available in runtime`),Promise.resolve();let m=v[g.functionName];return m?m(...s==="whatsapp"?[o,a,{verbose:!1}]:[o,a]).catch(p=>{let w=p instanceof Error?p.message:String(p);e.logger.error(`[claude-mem] Failed to send to ${s}: ${w}`)}):(e.logger.warn(`[claude-mem] Channel "${s}" has no ${g.functionName} function`),Promise.resolve())}async function de(e,s,o,a,l,g,v,m){let b=1e3,p=3e4;for(;!l.signal.aborted;){try{g("reconnecting"),e.logger.info(`[claude-mem] Connecting to SSE stream at ${T(s)}/stream`);let w=await fetch(`${T(s)}/stream`,{signal:l.signal,headers:{Accept:"text/event-stream"}});if(!w.ok)throw new Error(`SSE stream returned HTTP ${w.status}`);if(!w.body)throw new Error("SSE stream response has no body");g("connected"),b=1e3,e.logger.info("[claude-mem] Connected to SSE stream");let x=w.body.getReader(),D=new TextDecoder,_="";for(;;){let{done:R,value:P}=await x.read();if(R)break;_+=D.decode(P,{stream:!0}),_.length>1048576&&(e.logger.warn("[claude-mem] SSE buffer overflow, clearing buffer"),_="");let L=_.split(`
4
4
 
5
- `);_=L.pop()||"";for(let D of L){let F=D.split(`
5
+ `);_=L.pop()||"";for(let B of L){let F=B.split(`
6
6
  `).filter(S=>S.startsWith("data:")).map(S=>S.slice(5).trim());if(F.length===0)continue;let A=F.join(`
7
- `);if(A)try{let S=JSON.parse(A);if(S.type==="new_observation"&&S.observation){let O=ae(S.observation,w);await ue(e,o,a,O,m)}}catch(S){let C=S instanceof Error?S.message:String(S);e.logger.warn(`[claude-mem] Failed to parse SSE frame: ${C}`)}}}}catch(v){if(c.signal.aborted)break;d("reconnecting");let M=v instanceof Error?v.message:String(v);e.logger.warn(`[claude-mem] SSE stream error: ${M}. Reconnecting in ${b/1e3}s`)}if(c.signal.aborted)break;await new Promise(v=>setTimeout(v,b)),b=Math.min(b*2,p)}d("disconnected")}function ge(e){let s=e.pluginConfig||{},o=s.workerPort||37777;W=s.workerHost||Z;let a=s.project||"openclaw",c=re(s.observationFeed?.emojis);function d(r){return r.agentId?`openclaw-${r.agentId}`:a}let w=new Map,m=new Map,b=new Map,p=new Map,v=s.syncMemoryFile!==!1,M=new Set(s.syncMemoryFileExclude||[]);function j(r){let n=r||"default";return w.has(n)||w.set(n,`openclaw-${n}-${Date.now()}`),w.get(n)}function _(r){if(!v)return!1;let n=r?.agentId;return!(n&&M.has(n))}function R(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 k(r){let n=R(r),t=n.find(u=>m.has(u));t=t?m.get(t):n[0];let i=b.get(t);i||(i=new Set([t]),b.set(t,i));for(let u of n)i.add(u),m.set(u,t);let l=j(t);for(let u of i)w.set(u,l);return{canonicalKey:t,contentSessionId:l}}function L(r,n,t){let i=Date.now();for(let[g,f]of p)i-f>2e3&&p.delete(g);let l=`${r}::${n}::${t}`,u=p.get(l);return p.set(l,i),typeof u=="number"&&i-u<=2e3}function D(r){let n=R(r),t=n.map(l=>m.get(l)).find(Boolean)||n[0],i=b.get(t)||new Set([t,...n]);for(let l of i)m.delete(l),w.delete(l);b.delete(t),w.delete(t)}let F=6e4,A=new Map;async function S(r){let n=[a],t=r?d(r):null;t&&t!==a&&n.push(t);let i=n.join(","),l=A.get(i);if(l&&Date.now()-l.fetchedAt<F)return l.text;let u=await q(o,`/api/context/inject?projects=${encodeURIComponent(i)}`,e.logger);if(u&&u.trim().length>0){let g=u.trim();return A.set(i,{text:g,fetchedAt:Date.now()}),g}return null}e.on("session_start",async(r,n)=>{let{contentSessionId:t}=k(n);e.logger.info(`[claude-mem] Session tracking initialized: ${t}`)}),e.on("message_received",async(r,n)=>{let{canonicalKey:t,contentSessionId:i}=k(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}=k(n);e.logger.info(`[claude-mem] Session preserved after compaction: ${t}`)}),e.on("before_agent_start",async(r,n)=>{let{contentSessionId:t}=k(n),i=d(n),l=r.prompt||"agent run";if(L(t,i,l)){e.logger.info(`[claude-mem] Skipping duplicate prompt init: contentSessionId=${t} project=${i}`);return}await X(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(!_(n))return;let t=await S(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}=k(n),u="",g=r.message?.content;Array.isArray(g)&&(u=g.filter(E=>(E.type==="tool_result"||E.type==="text")&&"text"in E).map(E=>String(E.text)).join(`
8
- `));let f=1e3;u.length>f&&(u=u.slice(0,f));let y=n.workspaceDir;if(!y){e.logger.warn(`[claude-mem] Skipping observation persist because workspaceDir is unavailable: session=${i} tool=${t}`);return}ie(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}=k(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(g=>g.type==="text").map(g=>g.text||"").join(`
9
- `));break}}await X(o,"/api/sessions/summarize",{contentSessionId:t,last_assistant_message:i},e.logger)}),e.on("session_end",async(r,n)=>{D(n),e.logger.info("[claude-mem] Session tracking cleaned up")}),e.on("gateway_start",async()=>{oe(),w.clear(),A.clear(),p.clear(),m.clear(),b.clear(),e.logger.info("[claude-mem] Gateway started \u2014 session tracking reset")});let C=null,O="disconnected",$=null;e.registerService({id:"claude-mem-observation-feed",start:async r=>{C&&(C.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}`),C=new AbortController,$=de(e,o,n.channel,n.to,C,t=>{O=t},c,n.botToken)},stop:async r=>{C&&(C.abort(),C=null),$&&(await $,$=null),O="disconnected",e.logger.info("[claude-mem] Observation feed stopped \u2014 SSE connection closed")}});function B(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"),g=l.project?` [${String(l.project)}]`:"";return`${i+1}. ${u}${g}`}).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: ${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+/),i=t[t.length-1],l=/^\d+$/.test(i),u=l?I(i,10):10,g=l?t.slice(0,-1).join(" "):n,f=await K(o,`/api/search/observations?query=${encodeURIComponent(g)}&limit=${u}`,e.logger);if(!f)return"Claude-Mem search failed (worker unavailable or invalid response).";let y=Array.isArray(f.items)?f.items:[];return[`Claude-Mem Search: "${g}"`,B(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,g=l?t.slice(0,-1).join(" "):n,f=new URLSearchParams;f.set("limit",String(u)),g&&f.set("project",g);let y=await K(o,`/api/context/recent?${f.toString()}`,e.logger);if(!y)return"Claude-Mem recent context failed (worker unavailable or invalid response).";let E=Array.isArray(y.session_summaries)?y.session_summaries:[],U=Array.isArray(y.recent_observations)?y.recent_observations:[];return["Claude-Mem Recent Context",`Project: ${g||"(auto)"}`,`Session summaries: ${E.length}`,`Recent observations: ${U.length}`,B(U,Math.min(5,U.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(" "),g=new URLSearchParams({query:u,mode:"auto",depth_before:String(l),depth_after:String(i)}),f=await K(o,`/api/timeline/by-query?${g.toString()}`,e.logger);if(!f)return"Claude-Mem timeline lookup failed (worker unavailable or invalid response).";let y=Array.isArray(f.timeline)?f.timeline:[],E=f.anchor?String(f.anchor):"(none)";return[`Claude-Mem Timeline: "${u}"`,`Anchor: ${E}`,B(y,8)].join(`
14
- `)}}),e.registerCommand({name:"claude_mem_status",description:"Check Claude-Mem worker health and session status",handler:async()=>{let r=await q(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: ${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: ${W}:${o})`)}export{ge as default};
7
+ `);if(A)try{let S=JSON.parse(A);if(S.type==="new_observation"&&S.observation){let C=ce(S.observation,v);await ge(e,o,a,C,m)}}catch(S){let O=S instanceof Error?S.message:String(S);e.logger.warn(`[claude-mem] Failed to parse SSE frame: ${O}`)}}}}catch(w){if(l.signal.aborted)break;g("reconnecting");let x=w instanceof Error?w.message:String(w);e.logger.warn(`[claude-mem] SSE stream error: ${x}. Reconnecting in ${b/1e3}s`)}if(l.signal.aborted)break;await new Promise(w=>setTimeout(w,b)),b=Math.min(b*2,p)}g("disconnected")}function me(e){let s=e.pluginConfig||{},o=s.workerPort||37777;q=s.workerHost||Y;let a=s.project||"openclaw",l=se(s.observationFeed?.emojis);function g(r){return r.agentId?`openclaw-${r.agentId}`:a}let v=new Map,m=new Map,b=new Map,p=new Map,w=s.syncMemoryFile!==!1,x=new Set(s.syncMemoryFileExclude||[]);function D(r){let n=r||"default";return v.has(n)||v.set(n,`openclaw-${n}-${Date.now()}`),v.get(n)}function _(r){if(!w)return!1;let n=r?.agentId;return!(n&&x.has(n))}function R(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 P(r){let n=R(r),t=n.find(u=>m.has(u));t=t?m.get(t):n[0];let i=b.get(t);i||(i=new Set([t]),b.set(t,i));for(let u of n)i.add(u),m.set(u,t);let c=D(t);for(let u of i)v.set(u,c);return{canonicalKey:t,contentSessionId:c}}function L(r,n,t){let i=Date.now();for(let[d,f]of p)i-f>2e3&&p.delete(d);let c=`${r}::${n}::${t}`,u=p.get(c);return p.set(c,i),typeof u=="number"&&i-u<=2e3}function B(r){let n=R(r),t=n.map(c=>m.get(c)).find(Boolean)||n[0],i=b.get(t)||new Set([t,...n]);for(let c of i)m.delete(c),v.delete(c);b.delete(t),v.delete(t)}let F=6e4,A=new Map;async function S(r){let n=[a],t=r?g(r):null;t&&t!==a&&n.push(t);let i=n.join(","),c=A.get(i);if(c&&Date.now()-c.fetchedAt<F)return c.text;let u=await H(o,`/api/context/inject?projects=${encodeURIComponent(i)}`,e.logger);if(u&&u.trim().length>0){let d=u.trim();return A.set(i,{text:d,fetchedAt:Date.now()}),d}return null}async function O(r,n,t){let{contentSessionId:i}=P(r),c=g(r);if(L(i,c,n)){e.logger.info(`[claude-mem] Skipping duplicate prompt init: contentSessionId=${i} project=${c} via=${t}`);return}await Z(o,"/api/sessions/init",{contentSessionId:i,project:c,prompt:n},e.logger),e.logger.info(`[claude-mem] Session initialized via ${t}: contentSessionId=${i} project=${c}`)}e.on("session_start",async(r,n)=>{await O(n,"session start","session_start")}),e.on("message_received",async(r,n)=>{let{canonicalKey:t,contentSessionId:i}=P(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)=>{await O(n,"after compaction","after_compaction")}),e.on("before_agent_start",async(r,n)=>{await O(n,r.prompt||"agent run","before_agent_start")}),e.on("before_prompt_build",async(r,n)=>{if(!_(n))return;let t=await S(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:c}=P(n),u="",d=r.message?.content;Array.isArray(d)&&(u=d.filter(E=>(E.type==="tool_result"||E.type==="text")&&"text"in E).map(E=>String(E.text)).join(`
8
+ `));let f=1e3;u.length>f&&(u=u.slice(0,f));let h=n.workspaceDir||process.cwd();n.workspaceDir||e.logger.info(`[claude-mem] tool_result_persist missing workspaceDir; using process.cwd(): session=${i} tool=${t}`),ae(o,"/api/sessions/observations",{contentSessionId:c,tool_name:t,tool_input:r.params||{},tool_response:u,cwd:h},e.logger)}),e.on("agent_end",async(r,n)=>{let{contentSessionId:t}=P(n),i="";if(Array.isArray(r.messages))for(let c=r.messages.length-1;c>=0;c--){let u=r.messages[c];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 Z(o,"/api/sessions/summarize",{contentSessionId:t,last_assistant_message:i},e.logger)}),e.on("session_end",async(r,n)=>{B(n),e.logger.info("[claude-mem] Session tracking cleaned up")}),e.on("gateway_start",async()=>{ie(),v.clear(),A.clear(),p.clear(),m.clear(),b.clear(),e.logger.info("[claude-mem] Gateway started \u2014 session tracking reset")});let C=null,I="disconnected",k=null;e.registerService({id:"claude-mem-observation-feed",start:async r=>{C&&(C.abort(),k&&(await k,k=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}`),C=new AbortController,k=de(e,o,n.channel,n.to,C,t=>{I=t},l,n.botToken)},stop:async r=>{C&&(C.abort(),C=null),k&&(await k,k=null),I="disconnected",e.logger.info("[claude-mem] Observation feed stopped \u2014 SSE connection closed")}});function U(r,n=5){return!Array.isArray(r)||r.length===0?"No results found.":r.slice(0,n).map((t,i)=>{let c=t,u=String(c.title||c.subtitle||c.text||"Untitled"),d=c.project?` [${String(c.project)}]`:"";return`${i+1}. ${u}${d}`}).join(`
10
+ `)}function N(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: ${I}`].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],c=/^\d+$/.test(i),u=c?N(i,10):10,d=c?t.slice(0,-1).join(" "):n,f=await W(o,`/api/search/observations?query=${encodeURIComponent(d)}&limit=${u}`,e.logger);if(!f)return"Claude-Mem search failed (worker unavailable or invalid response).";let h=Array.isArray(f.items)?f.items:[];return[`Claude-Mem Search: "${d}"`,U(h,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]:"",c=/^\d+$/.test(i),u=c?N(i,3):3,d=c?t.slice(0,-1).join(" "):n,f=new URLSearchParams;f.set("limit",String(u)),d&&f.set("project",d);let h=await W(o,`/api/context/recent?${f.toString()}`,e.logger);if(!h)return"Claude-Mem recent context failed (worker unavailable or invalid response).";let E=Array.isArray(h.session_summaries)?h.session_summaries:[],K=Array.isArray(h.recent_observations)?h.recent_observations:[];return["Claude-Mem Recent Context",`Project: ${d||"(auto)"}`,`Session summaries: ${E.length}`,`Recent observations: ${K.length}`,U(K,Math.min(5,K.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,c=5;t.length>=2&&/^\d+$/.test(t[t.length-1])&&(i=N(t.pop(),5)),t.length>=2&&/^\d+$/.test(t[t.length-1])&&(c=N(t.pop(),5));let u=t.join(" "),d=new URLSearchParams({query:u,mode:"auto",depth_before:String(c),depth_after:String(i)}),f=await W(o,`/api/timeline/by-query?${d.toString()}`,e.logger);if(!f)return"Claude-Mem timeline lookup failed (worker unavailable or invalid response).";let h=Array.isArray(f.timeline)?f.timeline:[],E=f.anchor?String(f.anchor):"(none)";return[`Claude-Mem Timeline: "${u}"`,`Anchor: ${E}`,U(h,8)].join(`
14
+ `)}}),e.registerCommand({name:"claude_mem_status",description:"Check Claude-Mem worker health and session status",handler:async()=>{let r=await H(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: ${I}`].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: ${q}:${o})`)}export{me as default};
@@ -3,7 +3,7 @@
3
3
  "name": "Claude-Mem (Persistent Memory)",
4
4
  "description": "OpenClaw plugin for Claude-Mem. Records observations from embedded runner sessions and streams them to messaging channels.",
5
5
  "kind": "memory",
6
- "version": "13.3.0",
6
+ "version": "13.4.1",
7
7
  "license": "Apache-2.0",
8
8
  "author": "thedotmack",
9
9
  "homepage": "https://claude-mem.ai",
@@ -700,28 +700,17 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
700
700
  return null;
701
701
  }
702
702
 
703
- api.on("session_start", async (_event, ctx) => {
704
- const { contentSessionId } = rememberSessionContext(ctx);
705
- api.logger.info(`[claude-mem] Session tracking initialized: ${contentSessionId}`);
706
- });
707
-
708
- api.on("message_received", async (event, ctx) => {
709
- const { canonicalKey, contentSessionId } = rememberSessionContext(ctx);
710
- api.logger.info(`[claude-mem] Message received — prompt capture deferred to before_agent_start: session=${canonicalKey} contentSessionId=${contentSessionId} hasContent=${Boolean(event.content)}`);
711
- });
712
-
713
- api.on("after_compaction", async (_event, ctx) => {
714
- const { contentSessionId } = rememberSessionContext(ctx);
715
- api.logger.info(`[claude-mem] Session preserved after compaction: ${contentSessionId}`);
716
- });
717
-
718
- api.on("before_agent_start", async (event, ctx) => {
703
+ // Centralized session-init POST. session_start, after_compaction, and
704
+ // before_agent_start each call this; the 2s dedup guard
705
+ // (shouldSkipDuplicatePromptInit) collapses the redundant inits a single
706
+ // user-message flow produces into one prompt record, while still ensuring a
707
+ // session is initialized even on flows that never reach before_agent_start.
708
+ async function initSessionOnce(ctx: EventContext, promptText: string, via: string): Promise<void> {
719
709
  const { contentSessionId } = rememberSessionContext(ctx);
720
710
  const projectName = getProjectName(ctx);
721
- const promptText = event.prompt || "agent run";
722
711
 
723
712
  if (shouldSkipDuplicatePromptInit(contentSessionId, projectName, promptText)) {
724
- api.logger.info(`[claude-mem] Skipping duplicate prompt init: contentSessionId=${contentSessionId} project=${projectName}`);
713
+ api.logger.info(`[claude-mem] Skipping duplicate prompt init: contentSessionId=${contentSessionId} project=${projectName} via=${via}`);
725
714
  return;
726
715
  }
727
716
 
@@ -731,7 +720,24 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
731
720
  prompt: promptText,
732
721
  }, api.logger);
733
722
 
734
- api.logger.info(`[claude-mem] Session initialized via before_agent_start: contentSessionId=${contentSessionId} project=${projectName}`);
723
+ api.logger.info(`[claude-mem] Session initialized via ${via}: contentSessionId=${contentSessionId} project=${projectName}`);
724
+ }
725
+
726
+ api.on("session_start", async (_event, ctx) => {
727
+ await initSessionOnce(ctx, "session start", "session_start");
728
+ });
729
+
730
+ api.on("message_received", async (event, ctx) => {
731
+ const { canonicalKey, contentSessionId } = rememberSessionContext(ctx);
732
+ api.logger.info(`[claude-mem] Message received — prompt capture deferred to before_agent_start: session=${canonicalKey} contentSessionId=${contentSessionId} hasContent=${Boolean(event.content)}`);
733
+ });
734
+
735
+ api.on("after_compaction", async (_event, ctx) => {
736
+ await initSessionOnce(ctx, "after compaction", "after_compaction");
737
+ });
738
+
739
+ api.on("before_agent_start", async (event, ctx) => {
740
+ await initSessionOnce(ctx, event.prompt || "agent run", "before_agent_start");
735
741
  });
736
742
 
737
743
  api.on("before_prompt_build", async (_event, ctx) => {
@@ -767,11 +773,11 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
767
773
  toolResponseText = toolResponseText.slice(0, MAX_TOOL_RESPONSE_LENGTH);
768
774
  }
769
775
 
770
- const workspaceDir = ctx.workspaceDir;
771
-
772
- if (!workspaceDir) {
773
- api.logger.warn(`[claude-mem] Skipping observation persist because workspaceDir is unavailable: session=${canonicalKey} tool=${toolName}`);
774
- return;
776
+ // Fall back to the process cwd when the event carries no workspaceDir, so a
777
+ // missing ctx field never silently drops a captured observation.
778
+ const workspaceDir = ctx.workspaceDir || process.cwd();
779
+ if (!ctx.workspaceDir) {
780
+ api.logger.info(`[claude-mem] tool_result_persist missing workspaceDir; using process.cwd(): session=${canonicalKey} tool=${toolName}`);
775
781
  }
776
782
 
777
783
  workerPostFireAndForget(workerPort, "/api/sessions/observations", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem",
3
- "version": "13.3.0",
3
+ "version": "13.4.1",
4
4
  "description": "Memory compression system for Claude Code - persist context across sessions",
5
5
  "keywords": [
6
6
  "claude",
@@ -47,13 +47,12 @@
47
47
  "plugin/.claude-plugin",
48
48
  "plugin/.codex-plugin",
49
49
  "plugin/.mcp.json",
50
- "plugin/CLAUDE.md",
51
50
  "plugin/package.json",
51
+ "plugin/bun.lock",
52
52
  "plugin/hooks",
53
53
  "plugin/modes",
54
54
  "plugin/scripts/*.js",
55
55
  "plugin/scripts/*.cjs",
56
- "plugin/scripts/CLAUDE.md",
57
56
  "plugin/skills",
58
57
  "plugin/ui",
59
58
  "openclaw"
@@ -64,7 +63,7 @@
64
63
  },
65
64
  "scripts": {
66
65
  "dev": "npm run build-and-sync",
67
- "build": "node scripts/sync-plugin-manifests.js && node scripts/build-hooks.js",
66
+ "build": "node scripts/sync-plugin-manifests.js && node scripts/build-hooks.js && node scripts/gen-plugin-lockfile.cjs",
68
67
  "build-and-sync": "npm run build && npm run sync-marketplace && sleep 1 && (cd ~/.claude/plugins/marketplaces/thedotmack && npm run worker:restart)",
69
68
  "sync-marketplace": "node scripts/sync-marketplace.cjs",
70
69
  "sync-marketplace:force": "node scripts/sync-marketplace.cjs --force",
@@ -98,6 +97,8 @@
98
97
  "cursor:uninstall": "bun plugin/scripts/worker-service.cjs cursor uninstall",
99
98
  "cursor:status": "bun plugin/scripts/worker-service.cjs cursor status",
100
99
  "cursor:setup": "bun plugin/scripts/worker-service.cjs cursor setup",
100
+ "lint:hook-io": "node scripts/check-hook-io-discipline.cjs",
101
+ "lint:spawn-env": "node scripts/check-spawn-env-discipline.cjs",
101
102
  "typecheck": "tsc --noEmit && tsc --noEmit -p src/ui/viewer/tsconfig.json",
102
103
  "typecheck:root": "tsc --noEmit",
103
104
  "typecheck:viewer": "tsc --noEmit -p src/ui/viewer/tsconfig.json",
@@ -109,7 +110,9 @@
109
110
  "test:infra": "bun test tests/infrastructure/",
110
111
  "test:server": "bun test tests/server/",
111
112
  "e2e:server-beta:docker": "bash scripts/e2e-server-beta-docker.sh",
112
- "prepublishOnly": "npm run build",
113
+ "check:postinstall-allowlist": "node scripts/check-postinstall-allowlist.js",
114
+ "smoke:clean-room": "node scripts/smoke-clean-room.cjs",
115
+ "prepublishOnly": "npm run build && node scripts/check-postinstall-allowlist.js",
113
116
  "release": "np",
114
117
  "release:patch": "np patch --no-cleanup",
115
118
  "release:minor": "np minor --no-cleanup",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem",
3
- "version": "13.3.0",
3
+ "version": "13.4.1",
4
4
  "description": "Memory compression system for Claude Code - persist context across sessions",
5
5
  "author": {
6
6
  "name": "Alex Newman"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem",
3
- "version": "13.3.0",
3
+ "version": "13.4.1",
4
4
  "description": "Memory compression system for Claude Code - persist context across sessions",
5
5
  "author": {
6
6
  "name": "Alex Newman",
package/plugin/.mcp.json CHANGED
@@ -2,10 +2,10 @@
2
2
  "mcpServers": {
3
3
  "mcp-search": {
4
4
  "type": "stdio",
5
- "command": "sh",
5
+ "command": "node",
6
6
  "args": [
7
- "-c",
8
- "_C=\"${CLAUDE_CONFIG_DIR:-$HOME/.claude}\"; _E=\"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; _P=$({ [ -n \"$_E\" ] && printf '%s\\n' \"$_E\"; printf '%s\\n' \"$PWD/plugin\" \"$PWD\"; ls -dt \"$HOME/.codex/plugins/cache/claude-mem-local/claude-mem\"/[0-9]*/ \"$HOME/.codex/plugins/cache/thedotmack/claude-mem\"/[0-9]*/ \"$_C/plugins/cache/thedotmack/claude-mem\"/[0-9]*/ 2>/dev/null; printf '%s\\n' \"$_C/plugins/marketplaces/thedotmack/plugin\"; } | while IFS= read -r _R; do [ -d \"$_R/plugin/scripts\" ] && _Q=\"$_R/plugin\" || _Q=\"$_R\"; [ -f \"$_Q/scripts/mcp-server.cjs\" ] && { printf '%s\\n' \"$_Q\"; break; }; done); [ -n \"$_P\" ] || { echo \"claude-mem: mcp server not found\" >&2; exit 1; }; exec node \"$_P/scripts/mcp-server.cjs\""
7
+ "-e",
8
+ "const f=require('fs'),p=require('path'),o=require('os'),c=require('child_process');const h=o.homedir();const C=process.env.CLAUDE_CONFIG_DIR||p.join(h,'.claude');const E=process.env.CLAUDE_PLUGIN_ROOT||process.env.PLUGIN_ROOT||'';const d=process.cwd();const L=x=>{try{return f.readdirSync(x).filter(n=>/^\\d/.test(n)).map(n=>p.join(x,n)).filter(z=>{try{return f.statSync(z).isDirectory()}catch{return false}}).sort((a,b)=>f.statSync(b).mtimeMs-f.statSync(a).mtimeMs)}catch{return[]}};const K=[E,p.join(d,\"plugin\"),d,...L(p.join(h,\".codex/plugins/cache/claude-mem-local/claude-mem\")),...L(p.join(h,\".codex/plugins/cache/thedotmack/claude-mem\")),...L(p.join(C,\"plugins/cache/thedotmack/claude-mem\")),p.join(C,\"plugins/marketplaces/thedotmack/plugin\")].filter(Boolean);let R=null;for(const k of K){const r=f.existsSync(p.join(k,'plugin','scripts'))?p.join(k,'plugin'):k;if(f.existsSync(p.join(r,'scripts',\"mcp-server.cjs\"))){R=r;break}}if(!R){process.stderr.write(\"claude-mem: mcp server not found\\n\");process.exit(1)}const ch=c.spawn(process.execPath,[p.join(R,'scripts',\"mcp-server.cjs\")],{stdio:'inherit'});for(const s of ['SIGTERM','SIGINT','SIGHUP'])process.on(s,()=>{try{ch.kill(s)}catch{}});ch.on('exit',(code,sig)=>{if(sig){process.removeAllListeners(sig);try{process.kill(process.pid,sig)}catch{process.exit(1)}}else process.exit(code==null?0:code)})"
9
9
  ]
10
10
  }
11
11
  }