diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index d9c7f4d1..90cae179 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -10,7 +10,7 @@ "plugins": [ { "name": "claude-mem", - "version": "4.2.5", + "version": "4.2.6", "source": "./plugin", "description": "Persistent memory system for Claude Code - context compression across sessions" } diff --git a/CLAUDE.md b/CLAUDE.md index 4d1dacb3..be087f4b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ Claude-mem is a persistent memory compression system that preserves context across Claude Code sessions. It automatically captures tool usage observations, processes them through the Claude Agent SDK, and makes summaries available to future sessions. -**Current Version**: 4.2.5 +**Current Version**: 4.2.6 **License**: AGPL-3.0 **Author**: Alex Newman (@thedotmack) @@ -210,7 +210,32 @@ npm run build && git commit -a -m "Build and update" && git push && cd ~/.claude ## Version History -### v4.2.5 (Current) +### v4.2.6 (Current) +**Breaking Changes**: None (patch version) + +**Critical Bugfix**: +- Fixed overly defensive observation validation that was blocking observations from being saved + - Removed validation requiring title, subtitle, and narrative fields + - Parser now NEVER skips observations - always saves them + - Invalid or missing type defaults to "change" (generic catch-all type) + - Prevents critical data loss - partial observations are better than no observations + +**Impact**: +- Before: Missing title, subtitle, OR narrative caused entire observation to be discarded +- After: ALL observations preserved regardless of field completeness +- Even partial observations contain valuable data: concepts, files_read, files_modified, facts +- LLMs make mistakes - system must be resilient and save everything +- Consistent with v4.2.5 summary fix - partial data is always better than no data + +**Technical Details**: +- Updated `src/sdk/parser.ts:52-67` to never skip observations +- Uses "change" as fallback type for invalid/missing types (no schema change needed) +- Updated ParsedObservation interface to allow null for title, subtitle, narrative +- Database schema already supports nullable fields +- Parser now matches database schema constraints exactly +- Affects `parseObservations()` function used by worker service + +### v4.2.5 **Breaking Changes**: None (patch version) **Critical Bugfix**: diff --git a/package.json b/package.json index ad4b96b1..b498f916 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "claude-mem", - "version": "4.2.5", + "version": "4.2.6", "description": "Memory compression system for Claude Code - persist context across sessions", "keywords": [ "claude", diff --git a/plugin/.claude-plugin/plugin.json b/plugin/.claude-plugin/plugin.json index d1278df6..575aa59d 100644 --- a/plugin/.claude-plugin/plugin.json +++ b/plugin/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "claude-mem", - "version": "4.2.5", + "version": "4.2.6", "description": "Persistent memory system for Claude Code - seamlessly preserve context across sessions", "author": { "name": "Alex Newman" diff --git a/plugin/scripts/context-hook.js b/plugin/scripts/context-hook.js index 1c90cd33..fc2e739d 100755 --- a/plugin/scripts/context-hook.js +++ b/plugin/scripts/context-hook.js @@ -1,7 +1,7 @@ #!/usr/bin/env node -import C from"path";import q from"better-sqlite3";import{join as E,dirname as W,basename as z}from"path";import{homedir as U}from"os";import{existsSync as te,mkdirSync as j}from"fs";import{fileURLToPath as H}from"url";function B(){return typeof __dirname<"u"?__dirname:W(H(import.meta.url))}var Y=B(),u=process.env.CLAUDE_MEM_DATA_DIR||E(U(),".claude-mem"),O=process.env.CLAUDE_CONFIG_DIR||E(U(),".claude"),ne=E(u,"archives"),ie=E(u,"logs"),oe=E(u,"trash"),ae=E(u,"backups"),de=E(u,"settings.json"),w=E(u,"claude-mem.db"),pe=E(O,"settings.json"),ce=E(O,"commands"),Ee=E(O,"CLAUDE.md");function $(p){j(p,{recursive:!0})}function M(){return E(Y,"..","..")}var L=(i=>(i[i.DEBUG=0]="DEBUG",i[i.INFO=1]="INFO",i[i.WARN=2]="WARN",i[i.ERROR=3]="ERROR",i[i.SILENT=4]="SILENT",i))(L||{}),A=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=L[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} +import C from"path";import q from"better-sqlite3";import{join as E,dirname as W,basename as z}from"path";import{homedir as U}from"os";import{existsSync as te,mkdirSync as j}from"fs";import{fileURLToPath as H}from"url";function B(){return typeof __dirname<"u"?__dirname:W(H(import.meta.url))}var Y=B(),m=process.env.CLAUDE_MEM_DATA_DIR||E(U(),".claude-mem"),O=process.env.CLAUDE_CONFIG_DIR||E(U(),".claude"),ne=E(m,"archives"),ie=E(m,"logs"),oe=E(m,"trash"),ae=E(m,"backups"),de=E(m,"settings.json"),w=E(m,"claude-mem.db"),pe=E(O,"settings.json"),ce=E(O,"commands"),Ee=E(O,"CLAUDE.md");function $(p){j(p,{recursive:!0})}function M(){return E(Y,"..","..")}var L=(i=>(i[i.DEBUG=0]="DEBUG",i[i.INFO=1]="INFO",i[i.WARN=2]="WARN",i[i.ERROR=3]="ERROR",i[i.SILENT=4]="SILENT",i))(L||{}),A=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=L[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,i){if(e0&&(l=` {${Object.entries(T).map(([g,b])=>`${g}=${b}`).join(", ")}}`)}let f=`[${d}] [${n}] [${c}] ${_}${t}${l}${a}`;e===3?console.error(f):console.log(f)}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`})}},X=new A;var N=class{db;constructor(){$(u),this.db=new q(w),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(i,null,2):a=" "+this.formatData(i));let l="";if(r){let{sessionId:G,sdkSessionId:k,correlationId:R,...T}=r;Object.keys(T).length>0&&(l=` {${Object.entries(T).map(([g,b])=>`${g}=${b}`).join(", ")}}`)}let f=`[${d}] [${n}] [${c}] ${_}${t}${l}${a}`;e===3?console.error(f):console.log(f)}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`})}},X=new A;var N=class{db;constructor(){$(m),this.db=new q(w),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, @@ -326,5 +326,5 @@ No previous summaries found for this project yet.`;let n=[];e?(n.push(""),n.push SELECT files_read, files_modified FROM observations WHERE sdk_session_id = ? - `).all(a.sdk_session_id),h=new Set,g=new Set,b=m=>{try{return C.isAbsolute(m)?C.relative(t,m):m}catch{return m}};for(let m of T){if(m.files_read)try{let S=JSON.parse(m.files_read);Array.isArray(S)&&S.forEach(I=>h.add(b(I)))}catch{}if(m.files_modified)try{let S=JSON.parse(m.files_modified);Array.isArray(S)&&S.forEach(I=>g.add(b(I)))}catch{}}g.forEach(m=>h.delete(m)),h.size>0&&(e?n.push(`${o.dim}Files Read: ${Array.from(h).join(", ")}${o.reset}`):n.push(`**Files Read:** ${Array.from(h).join(", ")}`)),g.size>0&&(e?n.push(`${o.dim}Files Modified: ${Array.from(g).join(", ")}${o.reset}`):n.push(`**Files Modified:** ${Array.from(g).join(", ")}`))}let R=new Date(a.created_at).toLocaleString();e?n.push(`${o.dim}Date: ${R}${o.reset}`):n.push(`**Date:** ${R}`),e||n.push("")}return e&&(n.push(""),n.push(`${o.gray}${"\u2500".repeat(60)}${o.reset}`)),n.join(` + `).all(a.sdk_session_id),h=new Set,g=new Set,b=u=>{try{return C.isAbsolute(u)?C.relative(t,u):u}catch{return u}};for(let u of T){if(u.files_read)try{let S=JSON.parse(u.files_read);Array.isArray(S)&&S.forEach(I=>h.add(b(I)))}catch{}if(u.files_modified)try{let S=JSON.parse(u.files_modified);Array.isArray(S)&&S.forEach(I=>g.add(b(I)))}catch{}}g.forEach(u=>h.delete(u)),h.size>0&&(e?n.push(`${o.dim}Files Read: ${Array.from(h).join(", ")}${o.reset}`):n.push(`**Files Read:** ${Array.from(h).join(", ")}`)),g.size>0&&(e?n.push(`${o.dim}Files Modified: ${Array.from(g).join(", ")}${o.reset}`):n.push(`**Files Modified:** ${Array.from(g).join(", ")}`))}let R=new Date(a.created_at).toLocaleString();e?n.push(`${o.dim}Date: ${R}${o.reset}`):n.push(`**Date:** ${R}`),e||n.push("")}return e&&(n.push(""),n.push(`${o.gray}${"\u2500".repeat(60)}${o.reset}`)),n.join(` `)}finally{i.close()}}import{stdin as x}from"process";try{let p=process.argv.includes("--index");if(x.isTTY){let e=D(void 0,!0,p);console.log(e),process.exit(0)}else{let e="";x.on("data",s=>e+=s),x.on("end",()=>{let s=e.trim()?JSON.parse(e):void 0,r={hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:D(s,!1,p)}};console.log(JSON.stringify(r)),process.exit(0)})}}catch(p){console.error(`[claude-mem context-hook error: ${p.message}]`),process.exit(0)} diff --git a/plugin/scripts/worker-service.cjs b/plugin/scripts/worker-service.cjs index a6903815..971a28c2 100755 --- a/plugin/scripts/worker-service.cjs +++ b/plugin/scripts/worker-service.cjs @@ -501,7 +501,7 @@ Output this XML: **Optional fields**: notes IMPORTANT: This is not the end of the session. You will receive more requests to process, and more tool usages to observe and record. The summary helps keep track of progress. -`}function xg(a,e){let r=[],t=/([\s\S]*?)<\/observation>/g,i;for(;(i=t.exec(a))!==null;){let s=i[1],n=Vt(s,"type"),o=Vt(s,"title"),p=Vt(s,"subtitle"),c=Vt(s,"narrative"),l=ps(s,"facts","fact"),u=ps(s,"concepts","concept"),d=ps(s,"files_read","file"),m=ps(s,"files_modified","file");if(!n||!o||!p||!c){se.warn("PARSER","Observation missing required fields, skipping",{correlationId:e,hasType:!!n,hasTitle:!!o,hasSubtitle:!!p,hasNarrative:!!c});continue}if(!["bugfix","feature","refactor","change","discovery","decision"].includes(n.trim())){se.warn("PARSER",`Invalid observation type: ${n}, skipping`,{correlationId:e});continue}let f=u.filter(g=>g!==n.trim());f.length!==u.length&&se.warn("PARSER","Removed observation type from concepts array",{correlationId:e,type:n.trim(),originalConcepts:u,cleanedConcepts:f}),r.push({type:n.trim(),title:o,subtitle:p,facts:l,narrative:c,concepts:f,files_read:d,files_modified:m})}return r}function yg(a,e){let t=//.exec(a);if(t)return se.info("PARSER","Summary skipped",{sessionId:e,reason:t[1]}),null;let s=/([\s\S]*?)<\/summary>/.exec(a);if(!s)return null;let n=s[1],o=Vt(n,"request"),p=Vt(n,"investigated"),c=Vt(n,"learned"),l=Vt(n,"completed"),u=Vt(n,"next_steps"),d=Vt(n,"notes");return{request:o,investigated:p,learned:c,completed:l,next_steps:u,notes:d}}function Vt(a,e){let t=new RegExp(`<${e}>([^<]*)`).exec(a);return t?t[1].trim():null}function ps(a,e,r){let t=[],s=new RegExp(`<${e}>(.*?)`,"s").exec(a);if(!s)return t;let n=s[1],o=new RegExp(`<${r}>([^<]+)`,"g"),p;for(;(p=o.exec(n))!==null;)t.push(p[1].trim());return t}var Q2=process.env.CLAUDE_MEM_MODEL||"claude-sonnet-4-5",J2=["Glob","Grep","ListMcpResourcesTool","WebSearch"],ls=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10),us=class{app;port=null;sessions=new Map;constructor(){this.app=(0,Vc.default)(),this.app.use(Vc.default.json({limit:"50mb"})),this.app.get("/health",this.handleHealth.bind(this)),this.app.post("/sessions/:sessionDbId/init",this.handleInit.bind(this)),this.app.post("/sessions/:sessionDbId/observations",this.handleObservation.bind(this)),this.app.post("/sessions/:sessionDbId/summarize",this.handleSummarize.bind(this)),this.app.get("/sessions/:sessionDbId/status",this.handleStatus.bind(this)),this.app.delete("/sessions/:sessionDbId",this.handleDelete.bind(this))}async start(){this.port=ls;let e=new nt,r=e.cleanupOrphanedSessions();return e.close(),r>0&&se.info("SYSTEM",`Cleaned up ${r} orphaned sessions`),new Promise((t,i)=>{this.app.listen(ls,"127.0.0.1",()=>{se.info("SYSTEM","Worker started",{port:ls,pid:process.pid,activeSessions:this.sessions.size}),t()}).on("error",s=>{s.code==="EADDRINUSE"&&se.error("SYSTEM",`Port ${ls} already in use - worker may already be running`),i(s)})})}handleHealth(e,r){r.json({status:"ok",port:this.port,pid:process.pid,activeSessions:this.sessions.size,uptime:process.uptime(),memory:process.memoryUsage()})}async handleInit(e,r){let t=parseInt(e.params.sessionDbId,10),{project:i,userPrompt:s}=e.body,n=se.sessionId(t);se.info("WORKER","Session init",{correlationId:n,project:i});let o=new nt,p=o.getSessionById(t);if(!p){o.close(),r.status(404).json({error:"Session not found in database"});return}let c=p.sdk_session_id||`session-${t}`,l={sessionDbId:t,claudeSessionId:c,sdkSessionId:p.sdk_session_id||null,project:i,userPrompt:s,pendingMessages:[],abortController:new AbortController,generatorPromise:null,lastPromptNumber:0,observationCounter:0,startTime:Date.now()};this.sessions.set(t,l),o.setWorkerPort(t,this.port),o.close(),l.generatorPromise=this.runSDKAgent(l).catch(u=>{se.failure("WORKER","SDK agent error",{sessionId:t},u);let d=new nt;d.markSessionFailed(t),d.close(),this.sessions.delete(t)}),se.success("WORKER","Session initialized",{sessionId:t,port:this.port}),r.json({status:"initialized",sessionDbId:t,port:this.port})}handleObservation(e,r){let t=parseInt(e.params.sessionDbId,10),{tool_name:i,tool_input:s,tool_output:n,prompt_number:o}=e.body,p=this.sessions.get(t);if(!p){let u=new nt,d=u.getSessionById(t);u.close();let m=d?.sdk_session_id||`session-${t}`;p={sessionDbId:t,claudeSessionId:m,sdkSessionId:null,project:d?.project||"",userPrompt:d?.user_prompt||"",pendingMessages:[],abortController:new AbortController,generatorPromise:null,lastPromptNumber:0,observationCounter:0,startTime:Date.now()},this.sessions.set(t,p),p.generatorPromise=this.runSDKAgent(p).catch(v=>{se.failure("WORKER","SDK agent error",{sessionId:t},v);let f=new nt;f.markSessionFailed(t),f.close(),this.sessions.delete(t)})}p.observationCounter++;let c=se.correlationId(t,p.observationCounter),l=se.formatTool(i,s);se.dataIn("WORKER",`Observation queued: ${l}`,{correlationId:c,queue:p.pendingMessages.length+1}),p.pendingMessages.push({type:"observation",tool_name:i,tool_input:s,tool_output:n,prompt_number:o}),r.json({status:"queued",queueLength:p.pendingMessages.length})}handleSummarize(e,r){let t=parseInt(e.params.sessionDbId,10),{prompt_number:i}=e.body,s=this.sessions.get(t);if(!s){let n=new nt,o=n.getSessionById(t);n.close();let p=o?.sdk_session_id||`session-${t}`;s={sessionDbId:t,claudeSessionId:p,sdkSessionId:null,project:o?.project||"",userPrompt:o?.user_prompt||"",pendingMessages:[],abortController:new AbortController,generatorPromise:null,lastPromptNumber:0,observationCounter:0,startTime:Date.now()},this.sessions.set(t,s),s.generatorPromise=this.runSDKAgent(s).catch(c=>{se.failure("WORKER","SDK agent error",{sessionId:t},c);let l=new nt;l.markSessionFailed(t),l.close(),this.sessions.delete(t)})}se.dataIn("WORKER","Summary requested",{sessionId:t,promptNumber:i,queue:s.pendingMessages.length+1}),s.pendingMessages.push({type:"summarize",prompt_number:i}),r.json({status:"queued",queueLength:s.pendingMessages.length})}handleStatus(e,r){let t=parseInt(e.params.sessionDbId,10),i=this.sessions.get(t);if(!i){r.status(404).json({error:"Session not found"});return}r.json({sessionDbId:t,sdkSessionId:i.sdkSessionId,project:i.project,pendingMessages:i.pendingMessages.length})}async handleDelete(e,r){let t=parseInt(e.params.sessionDbId,10),i=this.sessions.get(t);if(!i){r.status(404).json({error:"Session not found"});return}se.warn("WORKER","Session delete requested",{sessionId:t}),i.abortController.abort(),i.generatorPromise&&await Promise.race([i.generatorPromise,new Promise(n=>setTimeout(n,5e3))]);let s=new nt;s.markSessionFailed(t),s.close(),this.sessions.delete(t),se.info("WORKER","Session deleted",{sessionId:t}),r.json({status:"deleted"})}async runSDKAgent(e){se.info("SDK","Agent starting",{sessionId:e.sessionDbId});let r=process.env.CLAUDE_CODE_PATH||"/Users/alexnewman/.nvm/versions/node/v24.5.0/bin/claude";try{let t=lg({prompt:this.createMessageGenerator(e),options:{model:Q2,disallowedTools:J2,abortController:e.abortController,pathToClaudeCodeExecutable:r}});for await(let n of t)if(n.type==="system"&&n.subtype==="init"){let o=n;if(o.session_id){let p=new nt,c=p.updateSDKSessionId(e.sessionDbId,o.session_id);p.close(),c&&(se.success("SDK","Session initialized",{sessionId:e.sessionDbId,sdkSessionId:o.session_id}),e.sdkSessionId=o.session_id)}}else if(n.type==="assistant"){let o=n.message.content,p=Array.isArray(o)?o.filter(l=>l.type==="text").map(l=>l.text).join(` +`}function xg(a,e){let r=[],t=/([\s\S]*?)<\/observation>/g,i;for(;(i=t.exec(a))!==null;){let s=i[1],n=Vt(s,"type"),o=Vt(s,"title"),p=Vt(s,"subtitle"),c=Vt(s,"narrative"),l=ps(s,"facts","fact"),u=ps(s,"concepts","concept"),d=ps(s,"files_read","file"),m=ps(s,"files_modified","file"),v="change";n?["bugfix","feature","refactor","change","discovery","decision"].includes(n.trim())?v=n.trim():se.warn("PARSER",`Invalid observation type: ${n}, using "change"`,{correlationId:e}):se.warn("PARSER",'Observation missing type field, using "change"',{correlationId:e});let f=u.filter(g=>g!==v);f.length!==u.length&&se.warn("PARSER","Removed observation type from concepts array",{correlationId:e,type:v,originalConcepts:u,cleanedConcepts:f}),r.push({type:v,title:o,subtitle:p,facts:l,narrative:c,concepts:f,files_read:d,files_modified:m})}return r}function yg(a,e){let t=//.exec(a);if(t)return se.info("PARSER","Summary skipped",{sessionId:e,reason:t[1]}),null;let s=/([\s\S]*?)<\/summary>/.exec(a);if(!s)return null;let n=s[1],o=Vt(n,"request"),p=Vt(n,"investigated"),c=Vt(n,"learned"),l=Vt(n,"completed"),u=Vt(n,"next_steps"),d=Vt(n,"notes");return{request:o,investigated:p,learned:c,completed:l,next_steps:u,notes:d}}function Vt(a,e){let t=new RegExp(`<${e}>([^<]*)`).exec(a);return t?t[1].trim():null}function ps(a,e,r){let t=[],s=new RegExp(`<${e}>(.*?)`,"s").exec(a);if(!s)return t;let n=s[1],o=new RegExp(`<${r}>([^<]+)`,"g"),p;for(;(p=o.exec(n))!==null;)t.push(p[1].trim());return t}var Q2=process.env.CLAUDE_MEM_MODEL||"claude-sonnet-4-5",J2=["Glob","Grep","ListMcpResourcesTool","WebSearch"],ls=parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10),us=class{app;port=null;sessions=new Map;constructor(){this.app=(0,Vc.default)(),this.app.use(Vc.default.json({limit:"50mb"})),this.app.get("/health",this.handleHealth.bind(this)),this.app.post("/sessions/:sessionDbId/init",this.handleInit.bind(this)),this.app.post("/sessions/:sessionDbId/observations",this.handleObservation.bind(this)),this.app.post("/sessions/:sessionDbId/summarize",this.handleSummarize.bind(this)),this.app.get("/sessions/:sessionDbId/status",this.handleStatus.bind(this)),this.app.delete("/sessions/:sessionDbId",this.handleDelete.bind(this))}async start(){this.port=ls;let e=new nt,r=e.cleanupOrphanedSessions();return e.close(),r>0&&se.info("SYSTEM",`Cleaned up ${r} orphaned sessions`),new Promise((t,i)=>{this.app.listen(ls,"127.0.0.1",()=>{se.info("SYSTEM","Worker started",{port:ls,pid:process.pid,activeSessions:this.sessions.size}),t()}).on("error",s=>{s.code==="EADDRINUSE"&&se.error("SYSTEM",`Port ${ls} already in use - worker may already be running`),i(s)})})}handleHealth(e,r){r.json({status:"ok",port:this.port,pid:process.pid,activeSessions:this.sessions.size,uptime:process.uptime(),memory:process.memoryUsage()})}async handleInit(e,r){let t=parseInt(e.params.sessionDbId,10),{project:i,userPrompt:s}=e.body,n=se.sessionId(t);se.info("WORKER","Session init",{correlationId:n,project:i});let o=new nt,p=o.getSessionById(t);if(!p){o.close(),r.status(404).json({error:"Session not found in database"});return}let c=p.sdk_session_id||`session-${t}`,l={sessionDbId:t,claudeSessionId:c,sdkSessionId:p.sdk_session_id||null,project:i,userPrompt:s,pendingMessages:[],abortController:new AbortController,generatorPromise:null,lastPromptNumber:0,observationCounter:0,startTime:Date.now()};this.sessions.set(t,l),o.setWorkerPort(t,this.port),o.close(),l.generatorPromise=this.runSDKAgent(l).catch(u=>{se.failure("WORKER","SDK agent error",{sessionId:t},u);let d=new nt;d.markSessionFailed(t),d.close(),this.sessions.delete(t)}),se.success("WORKER","Session initialized",{sessionId:t,port:this.port}),r.json({status:"initialized",sessionDbId:t,port:this.port})}handleObservation(e,r){let t=parseInt(e.params.sessionDbId,10),{tool_name:i,tool_input:s,tool_output:n,prompt_number:o}=e.body,p=this.sessions.get(t);if(!p){let u=new nt,d=u.getSessionById(t);u.close();let m=d?.sdk_session_id||`session-${t}`;p={sessionDbId:t,claudeSessionId:m,sdkSessionId:null,project:d?.project||"",userPrompt:d?.user_prompt||"",pendingMessages:[],abortController:new AbortController,generatorPromise:null,lastPromptNumber:0,observationCounter:0,startTime:Date.now()},this.sessions.set(t,p),p.generatorPromise=this.runSDKAgent(p).catch(v=>{se.failure("WORKER","SDK agent error",{sessionId:t},v);let f=new nt;f.markSessionFailed(t),f.close(),this.sessions.delete(t)})}p.observationCounter++;let c=se.correlationId(t,p.observationCounter),l=se.formatTool(i,s);se.dataIn("WORKER",`Observation queued: ${l}`,{correlationId:c,queue:p.pendingMessages.length+1}),p.pendingMessages.push({type:"observation",tool_name:i,tool_input:s,tool_output:n,prompt_number:o}),r.json({status:"queued",queueLength:p.pendingMessages.length})}handleSummarize(e,r){let t=parseInt(e.params.sessionDbId,10),{prompt_number:i}=e.body,s=this.sessions.get(t);if(!s){let n=new nt,o=n.getSessionById(t);n.close();let p=o?.sdk_session_id||`session-${t}`;s={sessionDbId:t,claudeSessionId:p,sdkSessionId:null,project:o?.project||"",userPrompt:o?.user_prompt||"",pendingMessages:[],abortController:new AbortController,generatorPromise:null,lastPromptNumber:0,observationCounter:0,startTime:Date.now()},this.sessions.set(t,s),s.generatorPromise=this.runSDKAgent(s).catch(c=>{se.failure("WORKER","SDK agent error",{sessionId:t},c);let l=new nt;l.markSessionFailed(t),l.close(),this.sessions.delete(t)})}se.dataIn("WORKER","Summary requested",{sessionId:t,promptNumber:i,queue:s.pendingMessages.length+1}),s.pendingMessages.push({type:"summarize",prompt_number:i}),r.json({status:"queued",queueLength:s.pendingMessages.length})}handleStatus(e,r){let t=parseInt(e.params.sessionDbId,10),i=this.sessions.get(t);if(!i){r.status(404).json({error:"Session not found"});return}r.json({sessionDbId:t,sdkSessionId:i.sdkSessionId,project:i.project,pendingMessages:i.pendingMessages.length})}async handleDelete(e,r){let t=parseInt(e.params.sessionDbId,10),i=this.sessions.get(t);if(!i){r.status(404).json({error:"Session not found"});return}se.warn("WORKER","Session delete requested",{sessionId:t}),i.abortController.abort(),i.generatorPromise&&await Promise.race([i.generatorPromise,new Promise(n=>setTimeout(n,5e3))]);let s=new nt;s.markSessionFailed(t),s.close(),this.sessions.delete(t),se.info("WORKER","Session deleted",{sessionId:t}),r.json({status:"deleted"})}async runSDKAgent(e){se.info("SDK","Agent starting",{sessionId:e.sessionDbId});let r=process.env.CLAUDE_CODE_PATH||"/Users/alexnewman/.nvm/versions/node/v24.5.0/bin/claude";try{let t=lg({prompt:this.createMessageGenerator(e),options:{model:Q2,disallowedTools:J2,abortController:e.abortController,pathToClaudeCodeExecutable:r}});for await(let n of t)if(n.type==="system"&&n.subtype==="init"){let o=n;if(o.session_id){let p=new nt,c=p.updateSDKSessionId(e.sessionDbId,o.session_id);p.close(),c&&(se.success("SDK","Session initialized",{sessionId:e.sessionDbId,sdkSessionId:o.session_id}),e.sdkSessionId=o.session_id)}}else if(n.type==="assistant"){let o=n.message.content,p=Array.isArray(o)?o.filter(l=>l.type==="text").map(l=>l.text).join(` `):typeof o=="string"?o:"",c=p.length;se.dataOut("SDK",`Response received (${c} chars)`,{sessionId:e.sessionDbId,promptNumber:e.lastPromptNumber}),se.debug("SDK","Full response",{sessionId:e.sessionDbId},p),this.handleAgentMessage(e,p,e.lastPromptNumber)}let i=Date.now()-e.startTime;se.success("SDK","Agent completed",{sessionId:e.sessionDbId,duration:`${(i/1e3).toFixed(1)}s`});let s=new nt;s.markSessionCompleted(e.sessionDbId),s.close(),this.sessions.delete(e.sessionDbId)}catch(t){throw t.name==="AbortError"?se.warn("SDK","Agent aborted",{sessionId:e.sessionDbId}):se.failure("SDK","Agent error",{sessionId:e.sessionDbId},t),t}}async*createMessageGenerator(e){let r=hg(e.project,e.claudeSessionId,e.userPrompt);for(se.dataIn("SDK",`Init prompt sent (${r.length} chars)`,{sessionId:e.sessionDbId,claudeSessionId:e.claudeSessionId,project:e.project}),se.debug("SDK","Full init prompt",{sessionId:e.sessionDbId},r),yield{type:"user",session_id:e.claudeSessionId,parent_tool_use_id:null,message:{role:"user",content:r}};!e.abortController.signal.aborted;){if(e.pendingMessages.length===0){await new Promise(t=>setTimeout(t,100));continue}for(;e.pendingMessages.length>0;){let t=e.pendingMessages.shift();if(t.type==="summarize"){e.lastPromptNumber=t.prompt_number;let i=new nt,s=i.getSessionById(e.sessionDbId);if(i.close(),s){let n=gg(s);se.dataIn("SDK",`Summary prompt sent (${n.length} chars)`,{sessionId:e.sessionDbId,promptNumber:t.prompt_number}),se.debug("SDK","Full summary prompt",{sessionId:e.sessionDbId},n),yield{type:"user",session_id:e.claudeSessionId,parent_tool_use_id:null,message:{role:"user",content:n}}}}else if(t.type==="observation"){e.lastPromptNumber=t.prompt_number;let i=vg({id:0,tool_name:t.tool_name,tool_input:t.tool_input,tool_output:t.tool_output,created_at_epoch:Date.now()}),s=se.formatTool(t.tool_name,t.tool_input),n=se.correlationId(e.sessionDbId,e.observationCounter);se.dataIn("SDK",`Observation prompt: ${s}`,{correlationId:n,promptNumber:t.prompt_number,size:`${i.length} chars`}),se.debug("SDK","Full observation prompt",{correlationId:n},i),yield{type:"user",session_id:e.claudeSessionId,parent_tool_use_id:null,message:{role:"user",content:i}}}}}}handleAgentMessage(e,r,t){let i=se.correlationId(e.sessionDbId,e.observationCounter),s=xg(r,i);s.length>0&&se.info("PARSER",`Parsed ${s.length} observation(s)`,{correlationId:i,promptNumber:t,types:s.map(p=>p.type).join(", ")});let n=new nt;for(let p of s)e.sdkSessionId&&(n.storeObservation(e.sdkSessionId,e.project,p,t),se.success("DB","Observation stored",{correlationId:i,type:p.type,title:p.title}));let o=yg(r,e.sessionDbId);o&&e.sdkSessionId&&(se.info("PARSER","Summary parsed",{sessionId:e.sessionDbId,promptNumber:t}),n.storeSummary(e.sdkSessionId,e.project,o,t),se.success("DB","Summary stored",{sessionId:e.sessionDbId})),n.close()}};async function Y2(){await new us().start(),process.on("SIGINT",()=>{se.warn("SYSTEM","Shutting down (SIGINT)"),process.exit(0)}),process.on("SIGTERM",()=>{se.warn("SYSTEM","Shutting down (SIGTERM)"),process.exit(0)})}Y2().catch(a=>{se.failure("SYSTEM","Fatal startup error",{},a),process.exit(1)});0&&(module.exports={WorkerService}); /*! Bundled license information: diff --git a/src/sdk/parser.ts b/src/sdk/parser.ts index e89f4162..77237001 100644 --- a/src/sdk/parser.ts +++ b/src/sdk/parser.ts @@ -7,10 +7,10 @@ import { logger } from '../utils/logger.js'; export interface ParsedObservation { type: string; - title: string; - subtitle: string; + title: string | null; + subtitle: string | null; facts: string[]; - narrative: string; + narrative: string | null; concepts: string[]; files_read: string[]; files_modified: string[]; @@ -49,39 +49,39 @@ export function parseObservations(text: string, correlationId?: string): ParsedO const files_read = extractArrayElements(obsContent, 'files_read', 'file'); const files_modified = extractArrayElements(obsContent, 'files_modified', 'file'); - // Validate required fields - if (!type || !title || !subtitle || !narrative) { - logger.warn('PARSER', 'Observation missing required fields, skipping', { - correlationId, - hasType: !!type, - hasTitle: !!title, - hasSubtitle: !!subtitle, - hasNarrative: !!narrative - }); - continue; + // NOTE FROM THEDOTMACK: ALWAYS save observations - never skip. 10/24/2025 + // All fields except type are nullable in schema + // If type is missing or invalid, use "change" as catch-all fallback + + // Determine final type + let finalType = 'change'; // Default catch-all + if (type) { + const validTypes = ['bugfix', 'feature', 'refactor', 'change', 'discovery', 'decision']; + if (validTypes.includes(type.trim())) { + finalType = type.trim(); + } else { + logger.warn('PARSER', `Invalid observation type: ${type}, using "change"`, { correlationId }); + } + } else { + logger.warn('PARSER', 'Observation missing type field, using "change"', { correlationId }); } - // Validate type - const validTypes = ['bugfix', 'feature', 'refactor', 'change', 'discovery', 'decision']; - if (!validTypes.includes(type.trim())) { - logger.warn('PARSER', `Invalid observation type: ${type}, skipping`, { correlationId }); - continue; - } + // All other fields are optional - save whatever we have // Filter out type from concepts array (types and concepts are separate dimensions) - const cleanedConcepts = concepts.filter(c => c !== type.trim()); + const cleanedConcepts = concepts.filter(c => c !== finalType); if (cleanedConcepts.length !== concepts.length) { logger.warn('PARSER', 'Removed observation type from concepts array', { correlationId, - type: type.trim(), + type: finalType, originalConcepts: concepts, cleanedConcepts }); } observations.push({ - type: type.trim(), + type: finalType, title, subtitle, facts, diff --git a/src/services/sqlite/SessionStore.ts b/src/services/sqlite/SessionStore.ts index 8e1b79ff..f4900bf1 100644 --- a/src/services/sqlite/SessionStore.ts +++ b/src/services/sqlite/SessionStore.ts @@ -903,10 +903,10 @@ export class SessionStore { project: string, observation: { type: string; - title: string; - subtitle: string; + title: string | null; + subtitle: string | null; facts: string[]; - narrative: string; + narrative: string | null; concepts: string[]; files_read: string[]; files_modified: string[];