feat: Enhanced logging and SDK prompt improvements (#94)
* Initial plan * Initial analysis: Found root cause of double entries bug Co-authored-by: thedotmack <683968+thedotmack@users.noreply.github.com> * Fix double entries by assigning generatorPromise in handleSessionInit Co-authored-by: thedotmack <683968+thedotmack@users.noreply.github.com> * feat(logging): Enhance HTTP request logging and session management - Added middleware for logging HTTP requests and responses, excluding static assets and health checks. - Introduced a method to summarize request bodies for specific endpoints. - Improved logging for user prompt synchronization with Chroma, including duration tracking. - Enhanced session initialization logging to include additional session details. - Updated observation and summary logging to provide more context and error handling during Chroma synchronization. - Refactored tool name formatting for logging in the SessionManager. - Expanded logger component types to include 'HTTP', 'SESSION', and 'CHROMA'. * Refactor SDK prompts and logging for improved clarity and functionality - Updated buildInitPrompt to clarify the observer's role and what to record. - Enhanced buildSummaryPrompt with clearer instructions for summarizing ongoing sessions. - Improved buildContinuationPrompt to emphasize the focus on deliverables and capabilities. - Refactored WorkerService to utilize a centralized tool formatting function for logging. - Added truncation for logged responses and observations to improve readability. - Updated SessionManager to log the queuing of summarize actions with session details. - Enhanced App and Sidebar components to support refreshing stats on sidebar open. - Refactored useStats hook to allow manual refreshing of stats while maintaining automatic loading on mount. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: thedotmack <683968+thedotmack@users.noreply.github.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
import{stdin as I}from"process";import w from"better-sqlite3";import{join as E,dirname as k,basename as W}from"path";import{homedir as O}from"os";import{existsSync as K,mkdirSync as x}from"fs";import{fileURLToPath as U}from"url";function M(){return typeof __dirname<"u"?__dirname:k(U(import.meta.url))}var q=M(),l=process.env.CLAUDE_MEM_DATA_DIR||E(O(),".claude-mem"),R=process.env.CLAUDE_CONFIG_DIR||E(O(),".claude"),J=E(l,"archives"),Q=E(l,"logs"),z=E(l,"trash"),Z=E(l,"backups"),ee=E(l,"settings.json"),f=E(l,"claude-mem.db"),se=E(l,"vector-db"),te=E(R,"settings.json"),re=E(R,"commands"),ne=E(R,"CLAUDE.md");function L(p){x(p,{recursive:!0})}var h=(n=>(n[n.DEBUG=0]="DEBUG",n[n.INFO=1]="INFO",n[n.WARN=2]="WARN",n[n.ERROR=3]="ERROR",n[n.SILENT=4]="SILENT",n))(h||{}),N=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=h[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,n){if(e<this.level)return;let o=new Date().toISOString().replace("T"," ").substring(0,23),i=h[e].padEnd(5),d=s.padEnd(6),_="";r?.correlationId?_=`[${r.correlationId}] `:r?.sessionId&&(_=`[session-${r.sessionId}] `);let u="";n!=null&&(this.level===0&&typeof n=="object"?u=`
|
||||
`+JSON.stringify(n,null,2):u=" "+this.formatData(n));let T="";if(r){let{sessionId:m,sdkSessionId:b,correlationId:c,...a}=r;Object.keys(a).length>0&&(T=` {${Object.entries(a).map(([y,D])=>`${y}=${D}`).join(", ")}}`)}let g=`[${o}] [${i}] [${d}] ${_}${t}${T}${u}`;e===3?console.error(g):console.log(g)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},A=new N;var S=class{db;constructor(){L(l),this.db=new w(f),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable()}initializeSchema(){try{this.db.exec(`
|
||||
`+JSON.stringify(n,null,2):u=" "+this.formatData(n));let T="";if(r){let{sessionId:m,sdkSessionId:S,correlationId:c,...a}=r;Object.keys(a).length>0&&(T=` {${Object.entries(a).map(([y,D])=>`${y}=${D}`).join(", ")}}`)}let b=`[${o}] [${i}] [${d}] ${_}${t}${T}${u}`;e===3?console.error(b):console.log(b)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},A=new N;var g=class{db;constructor(){L(l),this.db=new w(f),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable()}initializeSchema(){try{this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS schema_versions (
|
||||
id INTEGER PRIMARY KEY,
|
||||
version INTEGER UNIQUE NOT NULL,
|
||||
@@ -363,25 +363,25 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
WHERE id <= ? ${o}
|
||||
ORDER BY id DESC
|
||||
LIMIT ?
|
||||
`,b=`
|
||||
`,S=`
|
||||
SELECT id, created_at_epoch
|
||||
FROM observations
|
||||
WHERE id >= ? ${o}
|
||||
ORDER BY id ASC
|
||||
LIMIT ?
|
||||
`;try{let c=this.db.prepare(m).all(e,...i,t+1),a=this.db.prepare(b).all(e,...i,r+1);if(c.length===0&&a.length===0)return{observations:[],sessions:[],prompts:[]};d=c.length>0?c[c.length-1].created_at_epoch:s,_=a.length>0?a[a.length-1].created_at_epoch:s}catch(c){return console.error("[SessionStore] Error getting boundary observations:",c.message),{observations:[],sessions:[],prompts:[]}}}else{let m=`
|
||||
`;try{let c=this.db.prepare(m).all(e,...i,t+1),a=this.db.prepare(S).all(e,...i,r+1);if(c.length===0&&a.length===0)return{observations:[],sessions:[],prompts:[]};d=c.length>0?c[c.length-1].created_at_epoch:s,_=a.length>0?a[a.length-1].created_at_epoch:s}catch(c){return console.error("[SessionStore] Error getting boundary observations:",c.message),{observations:[],sessions:[],prompts:[]}}}else{let m=`
|
||||
SELECT created_at_epoch
|
||||
FROM observations
|
||||
WHERE created_at_epoch <= ? ${o}
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`,b=`
|
||||
`,S=`
|
||||
SELECT created_at_epoch
|
||||
FROM observations
|
||||
WHERE created_at_epoch >= ? ${o}
|
||||
ORDER BY created_at_epoch ASC
|
||||
LIMIT ?
|
||||
`;try{let c=this.db.prepare(m).all(s,...i,t),a=this.db.prepare(b).all(s,...i,r+1);if(c.length===0&&a.length===0)return{observations:[],sessions:[],prompts:[]};d=c.length>0?c[c.length-1].created_at_epoch:s,_=a.length>0?a[a.length-1].created_at_epoch:s}catch(c){return console.error("[SessionStore] Error getting boundary timestamps:",c.message),{observations:[],sessions:[],prompts:[]}}}let u=`
|
||||
`;try{let c=this.db.prepare(m).all(s,...i,t),a=this.db.prepare(S).all(s,...i,r+1);if(c.length===0&&a.length===0)return{observations:[],sessions:[],prompts:[]};d=c.length>0?c[c.length-1].created_at_epoch:s,_=a.length>0?a[a.length-1].created_at_epoch:s}catch(c){return console.error("[SessionStore] Error getting boundary timestamps:",c.message),{observations:[],sessions:[],prompts:[]}}}let u=`
|
||||
SELECT *
|
||||
FROM observations
|
||||
WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${o}
|
||||
@@ -391,11 +391,11 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
FROM session_summaries
|
||||
WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${o}
|
||||
ORDER BY created_at_epoch ASC
|
||||
`,g=`
|
||||
`,b=`
|
||||
SELECT up.*, s.project, s.sdk_session_id
|
||||
FROM user_prompts up
|
||||
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
|
||||
WHERE up.created_at_epoch >= ? AND up.created_at_epoch <= ? ${o.replace("project","s.project")}
|
||||
ORDER BY up.created_at_epoch ASC
|
||||
`;try{let m=this.db.prepare(u).all(d,_,...i),b=this.db.prepare(T).all(d,_,...i),c=this.db.prepare(g).all(d,_,...i);return{observations:m,sessions:b.map(a=>({id:a.id,sdk_session_id:a.sdk_session_id,project:a.project,request:a.request,completed:a.completed,next_steps:a.next_steps,created_at:a.created_at,created_at_epoch:a.created_at_epoch})),prompts:c.map(a=>({id:a.id,claude_session_id:a.claude_session_id,project:a.project,prompt:a.prompt_text,created_at:a.created_at,created_at_epoch:a.created_at_epoch}))}}catch(m){return console.error("[SessionStore] Error querying timeline records:",m.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};import X from"path";import{homedir as F}from"os";import{existsSync as B,readFileSync as j}from"fs";function C(){try{let p=X.join(F(),".claude-mem","settings.json");if(B(p)){let e=JSON.parse(j(p,"utf-8")),s=parseInt(e.env?.CLAUDE_MEM_WORKER_PORT,10);if(!isNaN(s))return s}}catch{}return parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10)}async function v(p){console.error("[claude-mem cleanup] Hook fired",{input:p?{session_id:p.session_id,cwd:p.cwd,reason:p.reason}:null}),p||(console.log("No input provided - this script is designed to run as a Claude Code SessionEnd hook"),console.log(`
|
||||
Expected input format:`),console.log(JSON.stringify({session_id:"string",cwd:"string",transcript_path:"string",hook_event_name:"SessionEnd",reason:"exit"},null,2)),process.exit(0));let{session_id:e,reason:s}=p;console.error("[claude-mem cleanup] Searching for active SDK session",{session_id:e,reason:s});let t=new S,r=t.findActiveSDKSession(e);r||(console.error("[claude-mem cleanup] No active SDK session found",{session_id:e}),t.close(),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)),console.error("[claude-mem cleanup] Active SDK session found",{session_id:r.id,sdk_session_id:r.sdk_session_id,project:r.project,worker_port:r.worker_port}),t.markSessionCompleted(r.id),console.error("[claude-mem cleanup] Session marked as completed in database"),t.close();try{let n=r.worker_port||C();await fetch(`http://127.0.0.1:${n}/sessions/${r.id}/complete`,{method:"POST",signal:AbortSignal.timeout(1e3)}),console.error("[claude-mem cleanup] Worker notified to stop processing indicator")}catch(n){console.error("[claude-mem cleanup] Failed to notify worker (non-critical):",n)}console.error("[claude-mem cleanup] Cleanup completed successfully"),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}if(I.isTTY)v(void 0);else{let p="";I.on("data",e=>p+=e),I.on("end",async()=>{let e=p?JSON.parse(p):void 0;await v(e)})}
|
||||
`;try{let m=this.db.prepare(u).all(d,_,...i),S=this.db.prepare(T).all(d,_,...i),c=this.db.prepare(b).all(d,_,...i);return{observations:m,sessions:S.map(a=>({id:a.id,sdk_session_id:a.sdk_session_id,project:a.project,request:a.request,completed:a.completed,next_steps:a.next_steps,created_at:a.created_at,created_at_epoch:a.created_at_epoch})),prompts:c.map(a=>({id:a.id,claude_session_id:a.claude_session_id,project:a.project,prompt:a.prompt_text,created_at:a.created_at,created_at_epoch:a.created_at_epoch}))}}catch(m){return console.error("[SessionStore] Error querying timeline records:",m.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};import X from"path";import{homedir as F}from"os";import{existsSync as B,readFileSync as j}from"fs";function C(){try{let p=X.join(F(),".claude-mem","settings.json");if(B(p)){let e=JSON.parse(j(p,"utf-8")),s=parseInt(e.env?.CLAUDE_MEM_WORKER_PORT,10);if(!isNaN(s))return s}}catch{}return parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10)}async function v(p){console.error("[claude-mem cleanup] Hook fired",{input:p?{session_id:p.session_id,cwd:p.cwd,reason:p.reason}:null}),p||(console.log("No input provided - this script is designed to run as a Claude Code SessionEnd hook"),console.log(`
|
||||
Expected input format:`),console.log(JSON.stringify({session_id:"string",cwd:"string",transcript_path:"string",hook_event_name:"SessionEnd",reason:"exit"},null,2)),process.exit(0));let{session_id:e,reason:s}=p;console.error("[claude-mem cleanup] Searching for active SDK session",{session_id:e,reason:s});let t=new g,r=t.findActiveSDKSession(e);r||(console.error("[claude-mem cleanup] No active SDK session found",{session_id:e}),t.close(),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)),console.error("[claude-mem cleanup] Active SDK session found",{session_id:r.id,sdk_session_id:r.sdk_session_id,project:r.project,worker_port:r.worker_port}),t.markSessionCompleted(r.id),console.error("[claude-mem cleanup] Session marked as completed in database"),t.close();try{let n=r.worker_port||C();await fetch(`http://127.0.0.1:${n}/sessions/${r.id}/complete`,{method:"POST",signal:AbortSignal.timeout(1e3)}),console.error("[claude-mem cleanup] Worker notified to stop processing indicator")}catch(n){console.error("[claude-mem cleanup] Failed to notify worker (non-critical):",n)}console.error("[claude-mem cleanup] Cleanup completed successfully"),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}if(I.isTTY)v(void 0);else{let p="";I.on("data",e=>p+=e),I.on("end",async()=>{let e=p?JSON.parse(p):void 0;await v(e)})}
|
||||
|
||||
@@ -397,7 +397,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
|
||||
WHERE up.created_at_epoch >= ? AND up.created_at_epoch <= ? ${o.replace("project","s.project")}
|
||||
ORDER BY up.created_at_epoch ASC
|
||||
`;try{let l=this.db.prepare(E).all(d,c,...i),b=this.db.prepare(T).all(d,c,...i),_=this.db.prepare(S).all(d,c,...i);return{observations:l,sessions:b.map(a=>({id:a.id,sdk_session_id:a.sdk_session_id,project:a.project,request:a.request,completed:a.completed,next_steps:a.next_steps,created_at:a.created_at,created_at_epoch:a.created_at_epoch})),prompts:_.map(a=>({id:a.id,claude_session_id:a.claude_session_id,project:a.project,prompt:a.prompt_text,created_at:a.created_at,created_at_epoch:a.created_at_epoch}))}}catch(l){return console.error("[SessionStore] Error querying timeline records:",l.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};function P(p,e,s){return p==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:p==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:p==="UserPromptSubmit"||p==="PostToolUse"?{continue:!0,suppressOutput:!0}:p==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function v(p,e,s={}){let t=P(p,e,s);return JSON.stringify(t)}import B from"path";import{homedir as H}from"os";import{existsSync as j,readFileSync as $}from"fs";var W=100;function R(){try{let p=B.join(H(),".claude-mem","settings.json");if(j(p)){let e=JSON.parse($(p,"utf-8")),s=parseInt(e.env?.CLAUDE_MEM_WORKER_PORT,10);if(!isNaN(s))return s}}catch{}return parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10)}async function G(){try{let p=R();return(await fetch(`http://127.0.0.1:${p}/health`,{signal:AbortSignal.timeout(W)})).ok}catch{return!1}}async function y(){if(await G())return;let p=R();throw new Error(`Worker service is not responding on port ${p}.
|
||||
`;try{let l=this.db.prepare(E).all(d,c,...i),b=this.db.prepare(T).all(d,c,...i),_=this.db.prepare(S).all(d,c,...i);return{observations:l,sessions:b.map(a=>({id:a.id,sdk_session_id:a.sdk_session_id,project:a.project,request:a.request,completed:a.completed,next_steps:a.next_steps,created_at:a.created_at,created_at_epoch:a.created_at_epoch})),prompts:_.map(a=>({id:a.id,claude_session_id:a.claude_session_id,project:a.project,prompt:a.prompt_text,created_at:a.created_at,created_at_epoch:a.created_at_epoch}))}}catch(l){return console.error("[SessionStore] Error querying timeline records:",l.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};function P(p,e,s){return p==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:p==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:p==="UserPromptSubmit"||p==="PostToolUse"?{continue:!0,suppressOutput:!0}:p==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function v(p,e,s={}){let t=P(p,e,s);return JSON.stringify(t)}import H from"path";import{homedir as B}from"os";import{existsSync as j,readFileSync as $}from"fs";var W=100;function R(){try{let p=H.join(B(),".claude-mem","settings.json");if(j(p)){let e=JSON.parse($(p,"utf-8")),s=parseInt(e.env?.CLAUDE_MEM_WORKER_PORT,10);if(!isNaN(s))return s}}catch{}return parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10)}async function G(){try{let p=R();return(await fetch(`http://127.0.0.1:${p}/health`,{signal:AbortSignal.timeout(W)})).ok}catch{return!1}}async function y(){if(await G())return;let p=R();throw new Error(`Worker service is not responding on port ${p}.
|
||||
|
||||
If you just updated the plugin, PM2's watch mode should restart automatically.
|
||||
If the problem persists, run: pm2 restart claude-mem-worker`)}async function K(p){if(!p)throw new Error("newHook requires input");let{session_id:e,cwd:s,prompt:t}=p,r=Y.basename(s);await y();let n=new g,o=n.createSDKSession(e,r,t),i=n.incrementPromptCounter(o);n.saveUserPrompt(e,i,t),console.error(`[new-hook] Session ${o}, prompt #${i}`),n.close();let d=R();try{let c=await fetch(`http://127.0.0.1:${d}/sessions/${o}/init`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({project:r,userPrompt:t}),signal:AbortSignal.timeout(5e3)});if(!c.ok){let E=await c.text();throw new Error(`Failed to initialize session: ${c.status} ${E}`)}}catch(c){throw c.cause?.code==="ECONNREFUSED"||c.name==="TimeoutError"||c.message.includes("fetch failed")?new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue"):c}console.log(v("UserPromptSubmit",!0))}var I="";D.on("data",p=>I+=p);D.on("end",async()=>{let p=I?JSON.parse(I):void 0;await K(p)});
|
||||
|
||||
@@ -397,7 +397,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
|
||||
WHERE up.created_at_epoch >= ? AND up.created_at_epoch <= ? ${o.replace("project","s.project")}
|
||||
ORDER BY up.created_at_epoch ASC
|
||||
`;try{let T=this.db.prepare(E).all(p,u,...i),g=this.db.prepare(c).all(p,u,...i),_=this.db.prepare(b).all(p,u,...i);return{observations:T,sessions:g.map(a=>({id:a.id,sdk_session_id:a.sdk_session_id,project:a.project,request:a.request,completed:a.completed,next_steps:a.next_steps,created_at:a.created_at,created_at_epoch:a.created_at_epoch})),prompts:_.map(a=>({id:a.id,claude_session_id:a.claude_session_id,project:a.project,prompt:a.prompt_text,created_at:a.created_at,created_at_epoch:a.created_at_epoch}))}}catch(T){return console.error("[SessionStore] Error querying timeline records:",T.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};function P(d,e,s){return d==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:d==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:d==="UserPromptSubmit"||d==="PostToolUse"?{continue:!0,suppressOutput:!0}:d==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function f(d,e,s={}){let t=P(d,e,s);return JSON.stringify(t)}import H from"path";import{homedir as B}from"os";import{existsSync as j,readFileSync as $}from"fs";var W=100;function h(){try{let d=H.join(B(),".claude-mem","settings.json");if(j(d)){let e=JSON.parse($(d,"utf-8")),s=parseInt(e.env?.CLAUDE_MEM_WORKER_PORT,10);if(!isNaN(s))return s}}catch{}return parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10)}async function G(){try{let d=h();return(await fetch(`http://127.0.0.1:${d}/health`,{signal:AbortSignal.timeout(W)})).ok}catch{return!1}}async function y(){if(await G())return;let d=h();throw new Error(`Worker service is not responding on port ${d}.
|
||||
`;try{let T=this.db.prepare(E).all(p,u,...i),g=this.db.prepare(c).all(p,u,...i),_=this.db.prepare(b).all(p,u,...i);return{observations:T,sessions:g.map(a=>({id:a.id,sdk_session_id:a.sdk_session_id,project:a.project,request:a.request,completed:a.completed,next_steps:a.next_steps,created_at:a.created_at,created_at_epoch:a.created_at_epoch})),prompts:_.map(a=>({id:a.id,claude_session_id:a.claude_session_id,project:a.project,prompt:a.prompt_text,created_at:a.created_at,created_at_epoch:a.created_at_epoch}))}}catch(T){return console.error("[SessionStore] Error querying timeline records:",T.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};function H(d,e,s){return d==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:d==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:d==="UserPromptSubmit"||d==="PostToolUse"?{continue:!0,suppressOutput:!0}:d==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function f(d,e,s={}){let t=H(d,e,s);return JSON.stringify(t)}import P from"path";import{homedir as B}from"os";import{existsSync as j,readFileSync as $}from"fs";var W=100;function h(){try{let d=P.join(B(),".claude-mem","settings.json");if(j(d)){let e=JSON.parse($(d,"utf-8")),s=parseInt(e.env?.CLAUDE_MEM_WORKER_PORT,10);if(!isNaN(s))return s}}catch{}return parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10)}async function G(){try{let d=h();return(await fetch(`http://127.0.0.1:${d}/health`,{signal:AbortSignal.timeout(W)})).ok}catch{return!1}}async function y(){if(await G())return;let d=h();throw new Error(`Worker service is not responding on port ${d}.
|
||||
|
||||
If you just updated the plugin, PM2's watch mode should restart automatically.
|
||||
If the problem persists, run: pm2 restart claude-mem-worker`)}var Y=new Set(["ListMcpResourcesTool"]);async function K(d){if(!d)throw new Error("saveHook requires input");let{session_id:e,cwd:s,tool_name:t,tool_input:r,tool_response:n}=d;if(Y.has(t)){console.log(f("PostToolUse",!0));return}await y();let o=new R,i=o.createSDKSession(e,"",""),p=o.getPromptCounter(i);o.close();let u=S.formatTool(t,r),E=h();S.dataIn("HOOK",`PostToolUse: ${u}`,{sessionId:i,workerPort:E});try{let c=await fetch(`http://127.0.0.1:${E}/sessions/${i}/observations`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({tool_name:t,tool_input:r!==void 0?JSON.stringify(r):"{}",tool_response:n!==void 0?JSON.stringify(n):"{}",prompt_number:p,cwd:s||""}),signal:AbortSignal.timeout(2e3)});if(!c.ok){let b=await c.text();throw S.failure("HOOK","Failed to send observation",{sessionId:i,status:c.status},b),new Error(`Failed to send observation to worker: ${c.status} ${b}`)}S.debug("HOOK","Observation sent successfully",{sessionId:i,toolName:t})}catch(c){throw c.cause?.code==="ECONNREFUSED"||c.name==="TimeoutError"||c.message.includes("fetch failed")?new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue"):c}console.log(f("PostToolUse",!0))}var L="";D.on("data",d=>L+=d);D.on("end",async()=>{let d=L?JSON.parse(L):void 0;await K(d)});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
import{stdin as D}from"process";import X from"better-sqlite3";import{join as u,dirname as U,basename as V}from"path";import{homedir as L}from"os";import{existsSync as Z,mkdirSync as M}from"fs";import{fileURLToPath as w}from"url";function F(){return typeof __dirname<"u"?__dirname:U(w(import.meta.url))}var se=F(),m=process.env.CLAUDE_MEM_DATA_DIR||u(L(),".claude-mem"),N=process.env.CLAUDE_CONFIG_DIR||u(L(),".claude"),te=u(m,"archives"),re=u(m,"logs"),ne=u(m,"trash"),oe=u(m,"backups"),ie=u(m,"settings.json"),A=u(m,"claude-mem.db"),ae=u(m,"vector-db"),de=u(N,"settings.json"),pe=u(N,"commands"),ce=u(N,"CLAUDE.md");function C(d){M(d,{recursive:!0})}var O=(n=>(n[n.DEBUG=0]="DEBUG",n[n.INFO=1]="INFO",n[n.WARN=2]="WARN",n[n.ERROR=3]="ERROR",n[n.SILENT=4]="SILENT",n))(O||{}),I=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=O[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,n){if(e<this.level)return;let o=new Date().toISOString().replace("T"," ").substring(0,23),i=O[e].padEnd(5),p=s.padEnd(6),_="";r?.correlationId?_=`[${r.correlationId}] `:r?.sessionId&&(_=`[session-${r.sessionId}] `);let E="";n!=null&&(this.level===0&&typeof n=="object"?E=`
|
||||
`+JSON.stringify(n,null,2):E=" "+this.formatData(n));let T="";if(r){let{sessionId:l,sdkSessionId:b,correlationId:c,...a}=r;Object.keys(a).length>0&&(T=` {${Object.entries(a).map(([k,x])=>`${k}=${x}`).join(", ")}}`)}let g=`[${o}] [${i}] [${p}] ${_}${t}${T}${E}`;e===3?console.error(g):console.log(g)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},S=new I;var R=class{db;constructor(){C(m),this.db=new X(A),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable()}initializeSchema(){try{this.db.exec(`
|
||||
`+JSON.stringify(n,null,2):E=" "+this.formatData(n));let T="";if(r){let{sessionId:l,sdkSessionId:S,correlationId:c,...a}=r;Object.keys(a).length>0&&(T=` {${Object.entries(a).map(([k,x])=>`${k}=${x}`).join(", ")}}`)}let g=`[${o}] [${i}] [${p}] ${_}${t}${T}${E}`;e===3?console.error(g):console.log(g)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},b=new I;var R=class{db;constructor(){C(m),this.db=new X(A),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable()}initializeSchema(){try{this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS schema_versions (
|
||||
id INTEGER PRIMARY KEY,
|
||||
version INTEGER UNIQUE NOT NULL,
|
||||
@@ -299,7 +299,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
UPDATE sdk_sessions
|
||||
SET sdk_session_id = ?
|
||||
WHERE id = ? AND sdk_session_id IS NULL
|
||||
`).run(s,e).changes===0?(S.debug("DB","sdk_session_id already set, skipping update",{sessionId:e,sdkSessionId:s}),!1):!0}setWorkerPort(e,s){this.db.prepare(`
|
||||
`).run(s,e).changes===0?(b.debug("DB","sdk_session_id already set, skipping update",{sessionId:e,sdkSessionId:s}),!1):!0}setWorkerPort(e,s){this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET worker_port = ?
|
||||
WHERE id = ?
|
||||
@@ -363,25 +363,25 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
WHERE id <= ? ${o}
|
||||
ORDER BY id DESC
|
||||
LIMIT ?
|
||||
`,b=`
|
||||
`,S=`
|
||||
SELECT id, created_at_epoch
|
||||
FROM observations
|
||||
WHERE id >= ? ${o}
|
||||
ORDER BY id ASC
|
||||
LIMIT ?
|
||||
`;try{let c=this.db.prepare(l).all(e,...i,t+1),a=this.db.prepare(b).all(e,...i,r+1);if(c.length===0&&a.length===0)return{observations:[],sessions:[],prompts:[]};p=c.length>0?c[c.length-1].created_at_epoch:s,_=a.length>0?a[a.length-1].created_at_epoch:s}catch(c){return console.error("[SessionStore] Error getting boundary observations:",c.message),{observations:[],sessions:[],prompts:[]}}}else{let l=`
|
||||
`;try{let c=this.db.prepare(l).all(e,...i,t+1),a=this.db.prepare(S).all(e,...i,r+1);if(c.length===0&&a.length===0)return{observations:[],sessions:[],prompts:[]};p=c.length>0?c[c.length-1].created_at_epoch:s,_=a.length>0?a[a.length-1].created_at_epoch:s}catch(c){return console.error("[SessionStore] Error getting boundary observations:",c.message),{observations:[],sessions:[],prompts:[]}}}else{let l=`
|
||||
SELECT created_at_epoch
|
||||
FROM observations
|
||||
WHERE created_at_epoch <= ? ${o}
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`,b=`
|
||||
`,S=`
|
||||
SELECT created_at_epoch
|
||||
FROM observations
|
||||
WHERE created_at_epoch >= ? ${o}
|
||||
ORDER BY created_at_epoch ASC
|
||||
LIMIT ?
|
||||
`;try{let c=this.db.prepare(l).all(s,...i,t),a=this.db.prepare(b).all(s,...i,r+1);if(c.length===0&&a.length===0)return{observations:[],sessions:[],prompts:[]};p=c.length>0?c[c.length-1].created_at_epoch:s,_=a.length>0?a[a.length-1].created_at_epoch:s}catch(c){return console.error("[SessionStore] Error getting boundary timestamps:",c.message),{observations:[],sessions:[],prompts:[]}}}let E=`
|
||||
`;try{let c=this.db.prepare(l).all(s,...i,t),a=this.db.prepare(S).all(s,...i,r+1);if(c.length===0&&a.length===0)return{observations:[],sessions:[],prompts:[]};p=c.length>0?c[c.length-1].created_at_epoch:s,_=a.length>0?a[a.length-1].created_at_epoch:s}catch(c){return console.error("[SessionStore] Error getting boundary timestamps:",c.message),{observations:[],sessions:[],prompts:[]}}}let E=`
|
||||
SELECT *
|
||||
FROM observations
|
||||
WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${o}
|
||||
@@ -397,7 +397,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
|
||||
WHERE up.created_at_epoch >= ? AND up.created_at_epoch <= ? ${o.replace("project","s.project")}
|
||||
ORDER BY up.created_at_epoch ASC
|
||||
`;try{let l=this.db.prepare(E).all(p,_,...i),b=this.db.prepare(T).all(p,_,...i),c=this.db.prepare(g).all(p,_,...i);return{observations:l,sessions:b.map(a=>({id:a.id,sdk_session_id:a.sdk_session_id,project:a.project,request:a.request,completed:a.completed,next_steps:a.next_steps,created_at:a.created_at,created_at_epoch:a.created_at_epoch})),prompts:c.map(a=>({id:a.id,claude_session_id:a.claude_session_id,project:a.project,prompt:a.prompt_text,created_at:a.created_at,created_at_epoch:a.created_at_epoch}))}}catch(l){return console.error("[SessionStore] Error querying timeline records:",l.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};function H(d,e,s){return d==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:d==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:d==="UserPromptSubmit"||d==="PostToolUse"?{continue:!0,suppressOutput:!0}:d==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function y(d,e,s={}){let t=H(d,e,s);return JSON.stringify(t)}import B from"path";import{homedir as P}from"os";import{existsSync as j,readFileSync as $}from"fs";var W=100;function h(){try{let d=B.join(P(),".claude-mem","settings.json");if(j(d)){let e=JSON.parse($(d,"utf-8")),s=parseInt(e.env?.CLAUDE_MEM_WORKER_PORT,10);if(!isNaN(s))return s}}catch{}return parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10)}async function G(){try{let d=h();return(await fetch(`http://127.0.0.1:${d}/health`,{signal:AbortSignal.timeout(W)})).ok}catch{return!1}}async function v(){if(await G())return;let d=h();throw new Error(`Worker service is not responding on port ${d}.
|
||||
`;try{let l=this.db.prepare(E).all(p,_,...i),S=this.db.prepare(T).all(p,_,...i),c=this.db.prepare(g).all(p,_,...i);return{observations:l,sessions:S.map(a=>({id:a.id,sdk_session_id:a.sdk_session_id,project:a.project,request:a.request,completed:a.completed,next_steps:a.next_steps,created_at:a.created_at,created_at_epoch:a.created_at_epoch})),prompts:c.map(a=>({id:a.id,claude_session_id:a.claude_session_id,project:a.project,prompt:a.prompt_text,created_at:a.created_at,created_at_epoch:a.created_at_epoch}))}}catch(l){return console.error("[SessionStore] Error querying timeline records:",l.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};function H(d,e,s){return d==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:d==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:d==="UserPromptSubmit"||d==="PostToolUse"?{continue:!0,suppressOutput:!0}:d==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function y(d,e,s={}){let t=H(d,e,s);return JSON.stringify(t)}import B from"path";import{homedir as P}from"os";import{existsSync as j,readFileSync as $}from"fs";var W=100;function h(){try{let d=B.join(P(),".claude-mem","settings.json");if(j(d)){let e=JSON.parse($(d,"utf-8")),s=parseInt(e.env?.CLAUDE_MEM_WORKER_PORT,10);if(!isNaN(s))return s}}catch{}return parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10)}async function G(){try{let d=h();return(await fetch(`http://127.0.0.1:${d}/health`,{signal:AbortSignal.timeout(W)})).ok}catch{return!1}}async function v(){if(await G())return;let d=h();throw new Error(`Worker service is not responding on port ${d}.
|
||||
|
||||
If you just updated the plugin, PM2's watch mode should restart automatically.
|
||||
If the problem persists, run: pm2 restart claude-mem-worker`)}async function Y(d){if(!d)throw new Error("summaryHook requires input");let{session_id:e}=d;await v();let s=new R,t=s.createSDKSession(e,"",""),r=s.getPromptCounter(t);s.close();let n=h();S.dataIn("HOOK","Stop: Requesting summary",{sessionId:t,workerPort:n,promptNumber:r});try{let o=await fetch(`http://127.0.0.1:${n}/sessions/${t}/summarize`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({prompt_number:r}),signal:AbortSignal.timeout(2e3)});if(!o.ok){let i=await o.text();throw S.failure("HOOK","Failed to generate summary",{sessionId:t,status:o.status},i),new Error(`Failed to request summary from worker: ${o.status} ${i}`)}S.debug("HOOK","Summary request sent successfully",{sessionId:t})}catch(o){throw o.cause?.code==="ECONNREFUSED"||o.name==="TimeoutError"||o.message.includes("fetch failed")?new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue"):o}finally{await fetch(`http://127.0.0.1:${n}/api/processing`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({isProcessing:!1})})}console.log(y("Stop",!0))}var f="";D.on("data",d=>f+=d);D.on("end",async()=>{let d=f?JSON.parse(f):void 0;await Y(d)});
|
||||
If the problem persists, run: pm2 restart claude-mem-worker`)}async function Y(d){if(!d)throw new Error("summaryHook requires input");let{session_id:e}=d;await v();let s=new R,t=s.createSDKSession(e,"",""),r=s.getPromptCounter(t);s.close();let n=h();b.dataIn("HOOK","Stop: Requesting summary",{sessionId:t,workerPort:n,promptNumber:r});try{let o=await fetch(`http://127.0.0.1:${n}/sessions/${t}/summarize`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({prompt_number:r}),signal:AbortSignal.timeout(2e3)});if(!o.ok){let i=await o.text();throw b.failure("HOOK","Failed to generate summary",{sessionId:t,status:o.status},i),new Error(`Failed to request summary from worker: ${o.status} ${i}`)}b.debug("HOOK","Summary request sent successfully",{sessionId:t})}catch(o){throw o.cause?.code==="ECONNREFUSED"||o.name==="TimeoutError"||o.message.includes("fetch failed")?new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue"):o}finally{await fetch(`http://127.0.0.1:${n}/api/processing`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({isProcessing:!1})})}console.log(y("Stop",!0))}var f="";D.on("data",d=>f+=d);D.on("end",async()=>{let d=f?JSON.parse(f):void 0;await Y(d)});
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+112
-11
@@ -23,14 +23,14 @@ export interface SDKSession {
|
||||
* Build initial prompt to initialize the SDK agent
|
||||
*/
|
||||
export function buildInitPrompt(project: string, sessionId: string, userPrompt: string): string {
|
||||
return `You are observing a development session to create searchable memory FOR FUTURE SESSIONS.
|
||||
return `You are a Claude-Mem, a specialized observer tool for creating searchable memory FOR FUTURE SESSIONS.
|
||||
|
||||
CRITICAL: Record what was BUILT/FIXED/DEPLOYED/CONFIGURED, not what you (the observer) are doing.
|
||||
CRITICAL: Record what was LEARNED/BUILT/FIXED/DEPLOYED/CONFIGURED, not what you (the observer) are doing.
|
||||
|
||||
User's Goal: ${userPrompt}
|
||||
Date: ${new Date().toISOString().split('T')[0]}
|
||||
|
||||
SESSION LIFECYCLE: You will observe tool executions, create observations, generate progress summaries when requested, and receive continuation prompts as the session progresses.
|
||||
Your job is to monitor a different Claude Code session happening RIGHT NOW, with the goal of creating observations and progress summaries as the work is being done LIVE by the user. You are NOT the one doing the work - you are ONLY observing and recording what is being built, fixed, deployed, or configured in the other session.
|
||||
|
||||
SPATIAL AWARENESS: Tool executions include the working directory (tool_cwd) to help you understand:
|
||||
- Which repository/project is being worked on
|
||||
@@ -127,10 +127,10 @@ Output observations using this XML structure:
|
||||
</observation>
|
||||
\`\`\`
|
||||
|
||||
Process the following tool executions.
|
||||
IMPORTANT! DO NOT do any work other than generate the OBSERVATIONS or PROGRESS SUMMARIES - and remember that you are a memory agent designed to summarize a DIFFERENT claude code session, not this one. Never reference yourself or your own actions. Never output anything other than the XML structures defined for observations and summaries. All other output is ignored and would be better left unsaid.
|
||||
|
||||
MEMORY PROCESSING SESSION START
|
||||
===============================`;
|
||||
MEMORY PROCESSING START
|
||||
=======================`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -167,9 +167,7 @@ export function buildObservationPrompt(obs: Observation): string {
|
||||
export function buildSummaryPrompt(session: SDKSession): string {
|
||||
return `PROGRESS SUMMARY CHECKPOINT
|
||||
===========================
|
||||
Write progress notes of what was done, what was learned, and what's next. This is a checkpoint to capture progress so far.
|
||||
|
||||
IMPORTANT! DO NOT summarize the observation process itself - you are summarizing a DIFFERENT claude code session, not this one.
|
||||
Write progress notes of what was done, what was learned, and what's next. This is a checkpoint to capture progress so far. The session is ongoing - you may receive more requests and tool executions after this summary. Write "next_steps" as the current trajectory of work (what's actively being worked on or coming up next), not as post-session future work. Always write at least a minimal summary explaining current progress, even if work is still in early stages, so that users see a summary output tied to each request.
|
||||
|
||||
Respond in this XML format:
|
||||
<summary>
|
||||
@@ -181,12 +179,115 @@ Respond in this XML format:
|
||||
<notes>[Additional insights or observations about the current progress]</notes>
|
||||
</summary>
|
||||
|
||||
FRAMING: This is a mid-session progress checkpoint. The session is ongoing - you may receive more requests and tool executions after this summary. Write "next_steps" as the current trajectory of work (what's actively being worked on or coming up next), not as post-session future work. Always write at least a minimal summary explaining current progress, even if work is still in early stages.`;
|
||||
IMPORTANT! DO NOT do any work other than generate the PROGRESS SUMMARY - and remember that you are a memory agent designed to summarize a DIFFERENT claude code session, not this one. Never reference yourself or your own actions. Never output anything other than the XML structures defined for observations and summaries. All other output is ignored and would be better left unsaid.`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build prompt for continuation of existing session
|
||||
*/
|
||||
export function buildContinuationPrompt(userPrompt: string, promptNumber: number): string {
|
||||
return `User's request #${promptNumber}: ${userPrompt}`;
|
||||
return `This is the next prompt from the user for the session you're observing.
|
||||
|
||||
CRITICAL: Record what was LEARNED/BUILT/FIXED/DEPLOYED/CONFIGURED, not what you (the observer) are doing.
|
||||
|
||||
User's Goal: ${userPrompt}
|
||||
Date: ${new Date().toISOString().split('T')[0]}
|
||||
|
||||
Your job is to continue monitoring the different Claude Code session happening RIGHT NOW, with the goal of creating observations and a progress summary as the work is being done LIVE by the user. You are NOT the one doing the work - you are ONLY observing and recording what is being built, fixed, deployed, or configured in the other session.
|
||||
|
||||
WHAT TO RECORD
|
||||
--------------
|
||||
Focus on deliverables and capabilities:
|
||||
- What the system NOW DOES differently (new capabilities)
|
||||
- What shipped to users/production (features, fixes, configs, docs)
|
||||
- Changes in technical domains (auth, data, UI, infra, DevOps, docs)
|
||||
|
||||
Use verbs like: implemented, fixed, deployed, configured, migrated, optimized, added, refactored
|
||||
|
||||
✅ GOOD EXAMPLES (describes what was built):
|
||||
- "Authentication now supports OAuth2 with PKCE flow"
|
||||
- "Deployment pipeline runs canary releases with auto-rollback"
|
||||
- "Database indexes optimized for common query patterns"
|
||||
|
||||
❌ BAD EXAMPLES (describes observation process - DO NOT DO THIS):
|
||||
- "Analyzed authentication implementation and stored findings"
|
||||
- "Tracked deployment steps and logged outcomes"
|
||||
- "Monitored database performance and recorded metrics"
|
||||
|
||||
WHEN TO SKIP
|
||||
------------
|
||||
Skip routine operations:
|
||||
- Empty status checks
|
||||
- Package installations with no errors
|
||||
- Simple file listings
|
||||
- Repetitive operations you've already documented
|
||||
- If file related research comes back as empty or not found
|
||||
- **No output necessary if skipping.**
|
||||
|
||||
OUTPUT FORMAT
|
||||
-------------
|
||||
Output observations using this XML structure:
|
||||
|
||||
\`\`\`xml
|
||||
<observation>
|
||||
<type>[ bugfix | feature | refactor | change | discovery | decision ]</type>
|
||||
<!--
|
||||
**type**: MUST be EXACTLY one of these 6 options (no other values allowed):
|
||||
- bugfix: something was broken, now fixed
|
||||
- feature: new capability or functionality added
|
||||
- refactor: code restructured, behavior unchanged
|
||||
- change: generic modification (docs, config, misc)
|
||||
- discovery: learning about existing system
|
||||
- decision: architectural/design choice with rationale
|
||||
-->
|
||||
<title>[**title**: Short title capturing the core action or topic]</title>
|
||||
<subtitle>[**subtitle**: One sentence explanation (max 24 words)]</subtitle>
|
||||
<facts>
|
||||
<fact>[Concise, self-contained statement]</fact>
|
||||
<fact>[Concise, self-contained statement]</fact>
|
||||
<fact>[Concise, self-contained statement]</fact>
|
||||
</facts>
|
||||
<!--
|
||||
**facts**: Concise, self-contained statements
|
||||
Each fact is ONE piece of information
|
||||
No pronouns - each fact must stand alone
|
||||
Include specific details: filenames, functions, values
|
||||
-->
|
||||
<narrative>[**narrative**: Full context: What was done, how it works, why it matters]</narrative>
|
||||
<concepts>
|
||||
<concept>[knowledge-type-category]</concept>
|
||||
<concept>[knowledge-type-category]</concept>
|
||||
</concepts>
|
||||
<!--
|
||||
**concepts**: 2-5 knowledge-type categories. MUST use ONLY these exact keywords:
|
||||
- how-it-works: understanding mechanisms
|
||||
- why-it-exists: purpose or rationale
|
||||
- what-changed: modifications made
|
||||
- problem-solution: issues and their fixes
|
||||
- gotcha: traps or edge cases
|
||||
- pattern: reusable approach
|
||||
- trade-off: pros/cons of a decision
|
||||
|
||||
IMPORTANT: Do NOT include the observation type (change/discovery/decision) as a concept.
|
||||
Types and concepts are separate dimensions.
|
||||
-->
|
||||
<files_read>
|
||||
<file>[path/to/file]</file>
|
||||
<file>[path/to/file]</file>
|
||||
</files_read>
|
||||
<files_modified>
|
||||
<file>[path/to/file]</file>
|
||||
<file>[path/to/file]</file>
|
||||
</files_modified>
|
||||
<!--
|
||||
**files**: All files touched (full paths from project root)
|
||||
-->
|
||||
</observation>
|
||||
\`\`\`
|
||||
|
||||
IMPORTANT! DO NOT do any work other than generate the OBSERVATIONS or PROGRESS SUMMARIES - and remember that you are a memory agent designed to summarize a DIFFERENT claude code session, not this one. Never reference yourself or your own actions. Never output anything other than the XML structures defined for observations and summaries. All other output is ignored and would be better left unsaid.
|
||||
|
||||
MEMORY PROCESSING START
|
||||
=======================`;
|
||||
|
||||
}
|
||||
@@ -66,12 +66,64 @@ export class WorkerService {
|
||||
this.app.use(express.json({ limit: '50mb' }));
|
||||
this.app.use(cors());
|
||||
|
||||
// HTTP request/response logging middleware
|
||||
this.app.use((req, res, next) => {
|
||||
// Skip logging for static assets and health checks
|
||||
if (req.path.startsWith('/health') || req.path === '/' || req.path.includes('.')) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const start = Date.now();
|
||||
const requestId = `${req.method}-${Date.now()}`;
|
||||
|
||||
// Log incoming request with body summary
|
||||
const bodySummary = this.summarizeRequestBody(req.method, req.path, req.body);
|
||||
logger.info('HTTP', `→ ${req.method} ${req.path}`, { requestId }, bodySummary);
|
||||
|
||||
// Capture response
|
||||
const originalSend = res.send.bind(res);
|
||||
res.send = function(body: any) {
|
||||
const duration = Date.now() - start;
|
||||
logger.info('HTTP', `← ${res.statusCode} ${req.path}`, { requestId, duration: `${duration}ms` });
|
||||
return originalSend(body);
|
||||
};
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
// Serve static files for web UI (viewer-bundle.js, logos, fonts, etc.)
|
||||
const packageRoot = getPackageRoot();
|
||||
const uiDir = path.join(packageRoot, 'plugin', 'ui');
|
||||
this.app.use(express.static(uiDir));
|
||||
}
|
||||
|
||||
/**
|
||||
* Summarize request body for logging
|
||||
*/
|
||||
private summarizeRequestBody(method: string, path: string, body: any): string {
|
||||
if (!body || Object.keys(body).length === 0) return '';
|
||||
|
||||
// Session init
|
||||
if (path.includes('/init')) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Observations
|
||||
if (path.includes('/observations')) {
|
||||
const toolName = body.tool_name || '?';
|
||||
const toolInput = body.tool_input;
|
||||
const toolSummary = logger.formatTool(toolName, toolInput);
|
||||
return `tool=${toolSummary}`;
|
||||
}
|
||||
|
||||
// Summarize request
|
||||
if (path.includes('/summarize')) {
|
||||
return 'requesting summary';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup HTTP routes
|
||||
*/
|
||||
@@ -245,16 +297,31 @@ export class WorkerService {
|
||||
}
|
||||
});
|
||||
|
||||
// Sync user prompt to Chroma (fire-and-forget)
|
||||
// Sync user prompt to Chroma with error logging
|
||||
const chromaStart = Date.now();
|
||||
const promptText = latestPrompt.prompt_text;
|
||||
this.dbManager.getChromaSync().syncUserPrompt(
|
||||
latestPrompt.id,
|
||||
latestPrompt.sdk_session_id,
|
||||
latestPrompt.project,
|
||||
latestPrompt.prompt_text,
|
||||
promptText,
|
||||
latestPrompt.prompt_number,
|
||||
latestPrompt.created_at_epoch
|
||||
).catch(err => {
|
||||
logger.error('WORKER', 'Failed to sync user_prompt to Chroma', { promptId: latestPrompt.id }, err);
|
||||
).then(() => {
|
||||
const chromaDuration = Date.now() - chromaStart;
|
||||
const truncatedPrompt = promptText.length > 60
|
||||
? promptText.substring(0, 60) + '...'
|
||||
: promptText;
|
||||
logger.debug('CHROMA', 'User prompt synced', {
|
||||
promptId: latestPrompt.id,
|
||||
duration: `${chromaDuration}ms`,
|
||||
prompt: truncatedPrompt
|
||||
});
|
||||
}).catch(err => {
|
||||
logger.error('CHROMA', 'Failed to sync user_prompt', {
|
||||
promptId: latestPrompt.id,
|
||||
sessionId: sessionDbId
|
||||
}, err);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -262,8 +329,14 @@ export class WorkerService {
|
||||
this.broadcastProcessingStatus(true);
|
||||
|
||||
// Start SDK agent in background (pass worker ref for spinner control)
|
||||
logger.info('SESSION', 'Generator starting', {
|
||||
sessionId: sessionDbId,
|
||||
project: session.project,
|
||||
promptNum: session.lastPromptNumber
|
||||
});
|
||||
|
||||
session.generatorPromise = this.sdkAgent.startSession(session, this).catch(err => {
|
||||
logger.failure('WORKER', 'SDK agent error', { sessionId: sessionDbId }, err);
|
||||
logger.failure('SDK', 'SDK agent error', { sessionId: sessionDbId }, err);
|
||||
});
|
||||
|
||||
// Broadcast SSE event
|
||||
@@ -300,8 +373,13 @@ export class WorkerService {
|
||||
// CRITICAL: Ensure SDK agent is running to consume the queue
|
||||
const session = this.sessionManager.getSession(sessionDbId);
|
||||
if (session && !session.generatorPromise) {
|
||||
logger.info('SESSION', 'Generator auto-starting (observation)', {
|
||||
sessionId: sessionDbId,
|
||||
queueDepth: session.pendingMessages.length
|
||||
});
|
||||
|
||||
session.generatorPromise = this.sdkAgent.startSession(session, this).catch(err => {
|
||||
logger.failure('WORKER', 'SDK agent error', { sessionId: sessionDbId }, err);
|
||||
logger.failure('SDK', 'SDK agent error', { sessionId: sessionDbId }, err);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -330,8 +408,13 @@ export class WorkerService {
|
||||
// CRITICAL: Ensure SDK agent is running to consume the queue
|
||||
const session = this.sessionManager.getSession(sessionDbId);
|
||||
if (session && !session.generatorPromise) {
|
||||
logger.info('SESSION', 'Generator auto-starting (summarize)', {
|
||||
sessionId: sessionDbId,
|
||||
queueDepth: session.pendingMessages.length
|
||||
});
|
||||
|
||||
session.generatorPromise = this.sdkAgent.startSession(session, this).catch(err => {
|
||||
logger.failure('WORKER', 'SDK agent error', { sessionId: sessionDbId }, err);
|
||||
logger.failure('SDK', 'SDK agent error', { sessionId: sessionDbId }, err);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -69,13 +69,20 @@ export class SDKAgent {
|
||||
: typeof content === 'string' ? content : '';
|
||||
|
||||
const responseSize = textContent.length;
|
||||
logger.dataOut('SDK', `Response received (${responseSize} chars)`, {
|
||||
sessionId: session.sessionDbId,
|
||||
promptNumber: session.lastPromptNumber
|
||||
});
|
||||
|
||||
// Parse and process response
|
||||
await this.processSDKResponse(session, textContent, worker);
|
||||
// Only log non-empty responses (filter out noise)
|
||||
if (responseSize > 0) {
|
||||
const truncatedResponse = responseSize > 100
|
||||
? textContent.substring(0, 100) + '...'
|
||||
: textContent;
|
||||
logger.dataOut('SDK', `Response received (${responseSize} chars)`, {
|
||||
sessionId: session.sessionDbId,
|
||||
promptNumber: session.lastPromptNumber
|
||||
}, truncatedResponse);
|
||||
|
||||
// Parse and process response
|
||||
await this.processSDKResponse(session, textContent, worker);
|
||||
}
|
||||
}
|
||||
|
||||
// Log result messages
|
||||
@@ -185,7 +192,20 @@ export class SDKAgent {
|
||||
session.lastPromptNumber
|
||||
);
|
||||
|
||||
// Sync to Chroma (fire-and-forget)
|
||||
// Log observation details
|
||||
logger.info('SDK', 'Observation saved', {
|
||||
sessionId: session.sessionDbId,
|
||||
obsId,
|
||||
type: obs.type,
|
||||
title: obs.title.substring(0, 60) + (obs.title.length > 60 ? '...' : ''),
|
||||
files: obs.files?.length || 0,
|
||||
concepts: obs.concepts?.length || 0
|
||||
});
|
||||
|
||||
// Sync to Chroma with error logging
|
||||
const chromaStart = Date.now();
|
||||
const obsType = obs.type;
|
||||
const obsTitle = obs.title;
|
||||
this.dbManager.getChromaSync().syncObservation(
|
||||
obsId,
|
||||
session.claudeSessionId,
|
||||
@@ -193,7 +213,25 @@ export class SDKAgent {
|
||||
obs,
|
||||
session.lastPromptNumber,
|
||||
createdAtEpoch
|
||||
).catch(() => {});
|
||||
).then(() => {
|
||||
const chromaDuration = Date.now() - chromaStart;
|
||||
const truncatedTitle = obsTitle.length > 50
|
||||
? obsTitle.substring(0, 50) + '...'
|
||||
: obsTitle;
|
||||
logger.debug('CHROMA', 'Observation synced', {
|
||||
obsId,
|
||||
duration: `${chromaDuration}ms`,
|
||||
type: obsType,
|
||||
title: truncatedTitle
|
||||
});
|
||||
}).catch(err => {
|
||||
logger.error('CHROMA', 'Failed to sync observation', {
|
||||
obsId,
|
||||
sessionId: session.sessionDbId,
|
||||
type: obsType,
|
||||
title: obsTitle.substring(0, 50)
|
||||
}, err);
|
||||
});
|
||||
|
||||
// Broadcast to SSE clients (for web UI)
|
||||
if (worker && worker.sseBroadcaster) {
|
||||
@@ -218,8 +256,6 @@ export class SDKAgent {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
logger.info('SDK', 'Observation saved', { obsId, type: obs.type });
|
||||
}
|
||||
|
||||
// Parse summary
|
||||
@@ -234,7 +270,18 @@ export class SDKAgent {
|
||||
session.lastPromptNumber
|
||||
);
|
||||
|
||||
// Sync to Chroma (fire-and-forget)
|
||||
// Log summary details
|
||||
logger.info('SDK', 'Summary saved', {
|
||||
sessionId: session.sessionDbId,
|
||||
summaryId,
|
||||
request: summary.request.substring(0, 60) + (summary.request.length > 60 ? '...' : ''),
|
||||
hasCompleted: !!summary.completed,
|
||||
hasNextSteps: !!summary.next_steps
|
||||
});
|
||||
|
||||
// Sync to Chroma with error logging
|
||||
const chromaStart = Date.now();
|
||||
const summaryRequest = summary.request;
|
||||
this.dbManager.getChromaSync().syncSummary(
|
||||
summaryId,
|
||||
session.claudeSessionId,
|
||||
@@ -242,7 +289,23 @@ export class SDKAgent {
|
||||
summary,
|
||||
session.lastPromptNumber,
|
||||
createdAtEpoch
|
||||
).catch(() => {});
|
||||
).then(() => {
|
||||
const chromaDuration = Date.now() - chromaStart;
|
||||
const truncatedRequest = summaryRequest.length > 50
|
||||
? summaryRequest.substring(0, 50) + '...'
|
||||
: summaryRequest;
|
||||
logger.debug('CHROMA', 'Summary synced', {
|
||||
summaryId,
|
||||
duration: `${chromaDuration}ms`,
|
||||
request: truncatedRequest
|
||||
});
|
||||
}).catch(err => {
|
||||
logger.error('CHROMA', 'Failed to sync summary', {
|
||||
summaryId,
|
||||
sessionId: session.sessionDbId,
|
||||
request: summaryRequest.substring(0, 50)
|
||||
}, err);
|
||||
});
|
||||
|
||||
// Broadcast to SSE clients (for web UI)
|
||||
if (worker && worker.sseBroadcaster) {
|
||||
@@ -263,8 +326,6 @@ export class SDKAgent {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
logger.info('SDK', 'Summary saved', { summaryId });
|
||||
}
|
||||
|
||||
// Check and stop spinner after processing (debounced)
|
||||
|
||||
@@ -55,7 +55,13 @@ export class SessionManager {
|
||||
const emitter = new EventEmitter();
|
||||
this.sessionQueues.set(sessionDbId, emitter);
|
||||
|
||||
logger.info('WORKER', 'Session initialized', { sessionDbId, project: session.project });
|
||||
logger.info('SESSION', 'Session initialized', {
|
||||
sessionId: sessionDbId,
|
||||
project: session.project,
|
||||
claudeSessionId: session.claudeSessionId,
|
||||
queueDepth: 0,
|
||||
hasGenerator: false
|
||||
});
|
||||
|
||||
return session;
|
||||
}
|
||||
@@ -78,6 +84,8 @@ export class SessionManager {
|
||||
session = this.initializeSession(sessionDbId);
|
||||
}
|
||||
|
||||
const beforeDepth = session.pendingMessages.length;
|
||||
|
||||
session.pendingMessages.push({
|
||||
type: 'observation',
|
||||
tool_name: data.tool_name,
|
||||
@@ -87,13 +95,19 @@ export class SessionManager {
|
||||
cwd: data.cwd
|
||||
});
|
||||
|
||||
const afterDepth = session.pendingMessages.length;
|
||||
|
||||
// Notify generator immediately (zero latency)
|
||||
const emitter = this.sessionQueues.get(sessionDbId);
|
||||
emitter?.emit('message');
|
||||
|
||||
logger.debug('WORKER', 'Observation queued', {
|
||||
sessionDbId,
|
||||
queueLength: session.pendingMessages.length
|
||||
// Format tool name for logging
|
||||
const toolSummary = logger.formatTool(data.tool_name, data.tool_input);
|
||||
|
||||
logger.info('SESSION', `Observation queued (${beforeDepth}→${afterDepth})`, {
|
||||
sessionId: sessionDbId,
|
||||
tool: toolSummary,
|
||||
hasGenerator: !!session.generatorPromise
|
||||
});
|
||||
}
|
||||
|
||||
@@ -108,12 +122,19 @@ export class SessionManager {
|
||||
session = this.initializeSession(sessionDbId);
|
||||
}
|
||||
|
||||
const beforeDepth = session.pendingMessages.length;
|
||||
|
||||
session.pendingMessages.push({ type: 'summarize' });
|
||||
|
||||
const afterDepth = session.pendingMessages.length;
|
||||
|
||||
const emitter = this.sessionQueues.get(sessionDbId);
|
||||
emitter?.emit('message');
|
||||
|
||||
logger.debug('WORKER', 'Summarize queued', { sessionDbId });
|
||||
logger.info('SESSION', `Summarize queued (${beforeDepth}→${afterDepth})`, {
|
||||
sessionId: sessionDbId,
|
||||
hasGenerator: !!session.generatorPromise
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -125,6 +146,8 @@ export class SessionManager {
|
||||
return; // Already deleted
|
||||
}
|
||||
|
||||
const sessionDuration = Date.now() - session.startTime;
|
||||
|
||||
// Abort the SDK agent
|
||||
session.abortController.abort();
|
||||
|
||||
@@ -137,7 +160,11 @@ export class SessionManager {
|
||||
this.sessions.delete(sessionDbId);
|
||||
this.sessionQueues.delete(sessionDbId);
|
||||
|
||||
logger.info('WORKER', 'Session deleted', { sessionDbId });
|
||||
logger.info('SESSION', 'Session deleted', {
|
||||
sessionId: sessionDbId,
|
||||
duration: `${(sessionDuration / 1000).toFixed(1)}s`,
|
||||
project: session.project
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -19,7 +19,7 @@ export function App() {
|
||||
|
||||
const { observations, summaries, prompts, projects, isProcessing, isConnected } = useSSE();
|
||||
const { settings, saveSettings, isSaving, saveStatus } = useSettings();
|
||||
const { stats } = useStats();
|
||||
const { stats, refreshStats } = useStats();
|
||||
const { preference, resolvedTheme, setThemePreference } = useTheme();
|
||||
const pagination = usePagination(currentFilter);
|
||||
|
||||
@@ -118,6 +118,7 @@ export function App() {
|
||||
isConnected={isConnected}
|
||||
onSave={saveSettings}
|
||||
onClose={toggleSidebar}
|
||||
onRefreshStats={refreshStats}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -12,9 +12,10 @@ interface SidebarProps {
|
||||
isConnected: boolean;
|
||||
onSave: (settings: Settings) => void;
|
||||
onClose: () => void;
|
||||
onRefreshStats: () => void;
|
||||
}
|
||||
|
||||
export function Sidebar({ isOpen, settings, stats, isSaving, saveStatus, isConnected, onSave, onClose }: SidebarProps) {
|
||||
export function Sidebar({ isOpen, settings, stats, isSaving, saveStatus, isConnected, onSave, onClose, onRefreshStats }: SidebarProps) {
|
||||
// Settings form state
|
||||
const [model, setModel] = useState(settings.CLAUDE_MEM_MODEL || DEFAULT_SETTINGS.CLAUDE_MEM_MODEL);
|
||||
const [contextObs, setContextObs] = useState(settings.CLAUDE_MEM_CONTEXT_OBSERVATIONS || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_OBSERVATIONS);
|
||||
@@ -40,6 +41,13 @@ export function Sidebar({ isOpen, settings, stats, isSaving, saveStatus, isConne
|
||||
.catch(error => console.error('Failed to load MCP status:', error));
|
||||
}, []);
|
||||
|
||||
// Refresh stats when sidebar opens
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
onRefreshStats();
|
||||
}
|
||||
}, [isOpen, onRefreshStats]);
|
||||
|
||||
const handleSave = () => {
|
||||
onSave({
|
||||
CLAUDE_MEM_MODEL: model,
|
||||
|
||||
@@ -1,30 +1,24 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Stats } from '../types';
|
||||
import { API_ENDPOINTS } from '../constants/api';
|
||||
import { TIMING } from '../constants/timing';
|
||||
|
||||
export function useStats() {
|
||||
const [stats, setStats] = useState<Stats>({});
|
||||
|
||||
useEffect(() => {
|
||||
const loadStats = async () => {
|
||||
try {
|
||||
const response = await fetch(API_ENDPOINTS.STATS);
|
||||
const data = await response.json();
|
||||
setStats(data);
|
||||
} catch (error) {
|
||||
console.error('Failed to load stats:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Load immediately
|
||||
loadStats();
|
||||
|
||||
// Refresh periodically
|
||||
const interval = setInterval(loadStats, TIMING.STATS_REFRESH_INTERVAL_MS);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
const loadStats = useCallback(async () => {
|
||||
try {
|
||||
const response = await fetch(API_ENDPOINTS.STATS);
|
||||
const data = await response.json();
|
||||
setStats(data);
|
||||
} catch (error) {
|
||||
console.error('Failed to load stats:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return { stats };
|
||||
useEffect(() => {
|
||||
// Load once on mount
|
||||
loadStats();
|
||||
}, [loadStats]);
|
||||
|
||||
return { stats, refreshStats: loadStats };
|
||||
}
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ export enum LogLevel {
|
||||
SILENT = 4
|
||||
}
|
||||
|
||||
export type Component = 'HOOK' | 'WORKER' | 'SDK' | 'PARSER' | 'DB' | 'SYSTEM';
|
||||
export type Component = 'HOOK' | 'WORKER' | 'SDK' | 'PARSER' | 'DB' | 'SYSTEM' | 'HTTP' | 'SESSION' | 'CHROMA';
|
||||
|
||||
interface LogContext {
|
||||
sessionId?: number;
|
||||
|
||||
Reference in New Issue
Block a user