fix(session): Semantic renaming and memory session ID capture for resume

This commit fixes the session ID confusion identified in PR #475:

PROBLEM:
- Using contentSessionId (user's Claude Code session) for SDK resume was wrong
- Memory agent conversation should persist across the entire user session
- Each SDK call was starting fresh, losing memory agent continuity

SOLUTION:
1. Semantic Renaming (clarity):
   - claudeSessionId → contentSessionId (user's observed session)
   - sdkSessionId → memorySessionId (memory agent's session for resume)
   - Database migration 17 renames columns accordingly

2. Memory Session ID Capture:
   - SDKAgent captures session_id from first SDK message
   - Persists to database via updateMemorySessionId()
   - SessionManager loads memorySessionId on session init

3. Resume Logic Fixed:
   - Only resume if memorySessionId captured from previous interaction
   - Enables memory agent continuity across user prompts

Files changed: 33 (types, database, agents, hooks, routes)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Alex Newman
2025-12-28 22:19:57 -05:00
parent b8ce27bd31
commit 30b142d318
33 changed files with 809 additions and 718 deletions
File diff suppressed because one or more lines are too long
+3 -3
View File
@@ -3,8 +3,8 @@ import{stdin as L}from"process";import A from"path";import{homedir as K}from"os"
${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let r=Object.keys(t);return r.length===0?"{}":r.length<=3?JSON.stringify(t):`{${r.length} keys: ${r.slice(0,3).join(", ")}...}`}return String(t)}formatTool(t,r){if(!r)return t;let e=typeof r=="string"?JSON.parse(r):r;if(t==="Bash"&&e.command)return`${t}(${e.command})`;if(e.file_path)return`${t}(${e.file_path})`;if(e.notebook_path)return`${t}(${e.notebook_path})`;if(t==="Glob"&&e.pattern)return`${t}(${e.pattern})`;if(t==="Grep"&&e.pattern)return`${t}(${e.pattern})`;if(e.url)return`${t}(${e.url})`;if(e.query)return`${t}(${e.query})`;if(t==="Task"){if(e.subagent_type)return`${t}(${e.subagent_type})`;if(e.description)return`${t}(${e.description})`}return t==="Skill"&&e.skill?`${t}(${e.skill})`:t==="LSP"&&e.operation?`${t}(${e.operation})`:t}formatTimestamp(t){let r=t.getFullYear(),e=String(t.getMonth()+1).padStart(2,"0"),n=String(t.getDate()).padStart(2,"0"),o=String(t.getHours()).padStart(2,"0"),s=String(t.getMinutes()).padStart(2,"0"),E=String(t.getSeconds()).padStart(2,"0"),T=String(t.getMilliseconds()).padStart(3,"0");return`${r}-${e}-${n} ${o}:${s}:${E}.${T}`}log(t,r,e,n,o){if(t<this.getLevel())return;let s=this.formatTimestamp(new Date),E=f[t].padEnd(5),T=r.padEnd(6),l="";n?.correlationId?l=`[${n.correlationId}] `:n?.sessionId&&(l=`[session-${n.sessionId}] `);let c="";o!=null&&(o instanceof Error?c=this.getLevel()===0?` ${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let r=Object.keys(t);return r.length===0?"{}":r.length<=3?JSON.stringify(t):`{${r.length} keys: ${r.slice(0,3).join(", ")}...}`}return String(t)}formatTool(t,r){if(!r)return t;let e=typeof r=="string"?JSON.parse(r):r;if(t==="Bash"&&e.command)return`${t}(${e.command})`;if(e.file_path)return`${t}(${e.file_path})`;if(e.notebook_path)return`${t}(${e.notebook_path})`;if(t==="Glob"&&e.pattern)return`${t}(${e.pattern})`;if(t==="Grep"&&e.pattern)return`${t}(${e.pattern})`;if(e.url)return`${t}(${e.url})`;if(e.query)return`${t}(${e.query})`;if(t==="Task"){if(e.subagent_type)return`${t}(${e.subagent_type})`;if(e.description)return`${t}(${e.description})`}return t==="Skill"&&e.skill?`${t}(${e.skill})`:t==="LSP"&&e.operation?`${t}(${e.operation})`:t}formatTimestamp(t){let r=t.getFullYear(),e=String(t.getMonth()+1).padStart(2,"0"),n=String(t.getDate()).padStart(2,"0"),o=String(t.getHours()).padStart(2,"0"),s=String(t.getMinutes()).padStart(2,"0"),E=String(t.getSeconds()).padStart(2,"0"),T=String(t.getMilliseconds()).padStart(3,"0");return`${r}-${e}-${n} ${o}:${s}:${E}.${T}`}log(t,r,e,n,o){if(t<this.getLevel())return;let s=this.formatTimestamp(new Date),E=f[t].padEnd(5),T=r.padEnd(6),l="";n?.correlationId?l=`[${n.correlationId}] `:n?.sessionId&&(l=`[session-${n.sessionId}] `);let c="";o!=null&&(o instanceof Error?c=this.getLevel()===0?`
${o.message} ${o.message}
${o.stack}`:` ${o.message}`:this.getLevel()===0&&typeof o=="object"?c=` ${o.stack}`:` ${o.message}`:this.getLevel()===0&&typeof o=="object"?c=`
`+JSON.stringify(o,null,2):c=" "+this.formatData(o));let O="";if(n){let{sessionId:D,sdkSessionId:Q,correlationId:Z,...m}=n;Object.keys(m).length>0&&(O=` {${Object.entries(m).map(([P,$])=>`${P}=${$}`).join(", ")}}`)}let C=`[${s}] [${E}] [${T}] ${l}${e}${O}${c}`;if(this.logFilePath)try{b(this.logFilePath,C+` `+JSON.stringify(o,null,2):c=" "+this.formatData(o));let O="";if(n){let{sessionId:m,memorySessionId:Q,correlationId:Z,...D}=n;Object.keys(D).length>0&&(O=` {${Object.entries(D).map(([k,$])=>`${k}=${$}`).join(", ")}}`)}let C=`[${s}] [${E}] [${T}] ${l}${e}${O}${c}`;if(this.logFilePath)try{b(this.logFilePath,C+`
`,"utf8")}catch(D){process.stderr.write(`[LOGGER] Failed to write to log file: ${D} `,"utf8")}catch(m){process.stderr.write(`[LOGGER] Failed to write to log file: ${m}
`)}else process.stderr.write(C+` `)}else process.stderr.write(C+`
`)}debug(t,r,e,n){this.log(0,t,r,e,n)}info(t,r,e,n){this.log(1,t,r,e,n)}warn(t,r,e,n){this.log(2,t,r,e,n)}error(t,r,e,n){this.log(3,t,r,e,n)}dataIn(t,r,e,n){this.info(t,`\u2192 ${r}`,e,n)}dataOut(t,r,e,n){this.info(t,`\u2190 ${r}`,e,n)}success(t,r,e,n){this.info(t,`\u2713 ${r}`,e,n)}failure(t,r,e,n){this.error(t,`\u2717 ${r}`,e,n)}timing(t,r,e,n){this.info(t,`\u23F1 ${r}`,n,{duration:`${e}ms`})}happyPathError(t,r,e,n,o=""){let l=((new Error().stack||"").split(` `)}debug(t,r,e,n){this.log(0,t,r,e,n)}info(t,r,e,n){this.log(1,t,r,e,n)}warn(t,r,e,n){this.log(2,t,r,e,n)}error(t,r,e,n){this.log(3,t,r,e,n)}dataIn(t,r,e,n){this.info(t,`\u2192 ${r}`,e,n)}dataOut(t,r,e,n){this.info(t,`\u2190 ${r}`,e,n)}success(t,r,e,n){this.info(t,`\u2713 ${r}`,e,n)}failure(t,r,e,n){this.error(t,`\u2717 ${r}`,e,n)}timing(t,r,e,n){this.info(t,`\u23F1 ${r}`,n,{duration:`${e}ms`})}happyPathError(t,r,e,n,o=""){let l=((new Error().stack||"").split(`
`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),c=l?`${l[1].split("/").pop()}:${l[2]}`:"unknown",O={...e,location:c};return this.warn(t,`[HAPPY-PATH] ${r}`,O,n),o}},_=new p;var g={DEFAULT:3e5,HEALTH_CHECK:3e4,WORKER_STARTUP_WAIT:1e3,WORKER_STARTUP_RETRIES:300,PRE_RESTART_SETTLE_DELAY:2e3,WINDOWS_MULTIPLIER:1.5};function d(i){return process.platform==="win32"?Math.round(i*g.WINDOWS_MULTIPLIER):i}function h(i={}){let{port:t,includeSkillFallback:r=!1,customPrefix:e,actualError:n}=i,o=e||"Worker service connection failed.",s=t?` (port ${t})`:"",E=`${o}${s} `)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),c=l?`${l[1].split("/").pop()}:${l[2]}`:"unknown",O={...e,location:c};return this.warn(t,`[HAPPY-PATH] ${r}`,O,n),o}},_=new p;var g={DEFAULT:3e5,HEALTH_CHECK:3e4,WORKER_STARTUP_WAIT:1e3,WORKER_STARTUP_RETRIES:300,PRE_RESTART_SETTLE_DELAY:2e3,WINDOWS_MULTIPLIER:1.5};function d(i){return process.platform==="win32"?Math.round(i*g.WINDOWS_MULTIPLIER):i}function h(i={}){let{port:t,includeSkillFallback:r=!1,customPrefix:e,actualError:n}=i,o=e||"Worker service connection failed.",s=t?` (port ${t})`:"",E=`${o}${s}
@@ -16,4 +16,4 @@ ${o.stack}`:` ${o.message}`:this.getLevel()===0&&typeof o=="object"?c=`
If that doesn't work, try: /troubleshoot`),n&&(E=`Worker Error: ${n} If that doesn't work, try: /troubleshoot`),n&&(E=`Worker Error: ${n}
${E}`),E}var j=A.join(K(),".claude","plugins","marketplaces","thedotmack"),I=d(g.HEALTH_CHECK),M=null;function u(){if(M!==null)return M;let i=A.join(a.get("CLAUDE_MEM_DATA_DIR"),"settings.json"),t=a.loadFromFile(i);return M=parseInt(t.CLAUDE_MEM_WORKER_PORT,10),M}async function V(){let i=u();return(await fetch(`http://127.0.0.1:${i}/api/readiness`,{signal:AbortSignal.timeout(I)})).ok}function B(){let i=A.join(j,"package.json");return JSON.parse(X(i,"utf-8")).version}async function Y(){let i=u(),t=await fetch(`http://127.0.0.1:${i}/api/version`,{signal:AbortSignal.timeout(I)});if(!t.ok)throw new Error(`Failed to get worker version: ${t.status}`);return(await t.json()).version}async function J(){let i=B(),t=await Y();i!==t&&_.warn("SYSTEM","Worker version mismatch",{pluginVersion:i,workerVersion:t,hint:"Restart worker with: claude-mem worker restart"})}async function N(){for(let r=0;r<25;r++){try{if(await V()){await J();return}}catch{}await new Promise(e=>setTimeout(e,200))}throw new Error(h({port:u(),customPrefix:"Worker did not become ready within 5 seconds."}))}import z from"path";function k(i){if(!i||i.trim()==="")return _.warn("PROJECT_NAME","Empty cwd provided, using fallback",{cwd:i}),"unknown-project";let t=z.basename(i);if(t===""){if(process.platform==="win32"){let e=i.match(/^([A-Z]):\\/i);if(e){let o=`drive-${e[1].toUpperCase()}`;return _.info("PROJECT_NAME","Drive root detected",{cwd:i,projectName:o}),o}}return _.warn("PROJECT_NAME","Root directory detected, using fallback",{cwd:i}),"unknown-project"}return t}async function y(i){await N();let t=i?.cwd??process.cwd(),r=k(t),n=`http://127.0.0.1:${u()}/api/context/inject?project=${encodeURIComponent(r)}`,o=await fetch(n,{signal:AbortSignal.timeout(g.DEFAULT)});if(!o.ok)throw new Error(`Context generation failed: ${o.status}`);return(await o.text()).trim()}var q=process.argv.includes("--colors");if(L.isTTY||q)y(void 0).then(i=>{console.log(i),process.exit(0)});else{let i="";L.on("data",t=>i+=t),L.on("end",async()=>{let t;try{t=i.trim()?JSON.parse(i):void 0}catch(e){throw new Error(`Failed to parse hook input: ${e instanceof Error?e.message:String(e)}`)}let r=await y(t);console.log(JSON.stringify({hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:r}})),process.exit(0)})} ${E}`),E}var j=A.join(K(),".claude","plugins","marketplaces","thedotmack"),I=d(g.HEALTH_CHECK),M=null;function u(){if(M!==null)return M;let i=A.join(a.get("CLAUDE_MEM_DATA_DIR"),"settings.json"),t=a.loadFromFile(i);return M=parseInt(t.CLAUDE_MEM_WORKER_PORT,10),M}async function V(){let i=u();return(await fetch(`http://127.0.0.1:${i}/api/readiness`,{signal:AbortSignal.timeout(I)})).ok}function B(){let i=A.join(j,"package.json");return JSON.parse(X(i,"utf-8")).version}async function Y(){let i=u(),t=await fetch(`http://127.0.0.1:${i}/api/version`,{signal:AbortSignal.timeout(I)});if(!t.ok)throw new Error(`Failed to get worker version: ${t.status}`);return(await t.json()).version}async function J(){let i=B(),t=await Y();i!==t&&_.warn("SYSTEM","Worker version mismatch",{pluginVersion:i,workerVersion:t,hint:"Restart worker with: claude-mem worker restart"})}async function N(){for(let r=0;r<25;r++){try{if(await V()){await J();return}}catch{}await new Promise(e=>setTimeout(e,200))}throw new Error(h({port:u(),customPrefix:"Worker did not become ready within 5 seconds."}))}import z from"path";function y(i){if(!i||i.trim()==="")return _.warn("PROJECT_NAME","Empty cwd provided, using fallback",{cwd:i}),"unknown-project";let t=z.basename(i);if(t===""){if(process.platform==="win32"){let e=i.match(/^([A-Z]):\\/i);if(e){let o=`drive-${e[1].toUpperCase()}`;return _.info("PROJECT_NAME","Drive root detected",{cwd:i,projectName:o}),o}}return _.warn("PROJECT_NAME","Root directory detected, using fallback",{cwd:i}),"unknown-project"}return t}async function P(i){await N();let t=i?.cwd??process.cwd(),r=y(t),n=`http://127.0.0.1:${u()}/api/context/inject?project=${encodeURIComponent(r)}`,o=await fetch(n,{signal:AbortSignal.timeout(g.DEFAULT)});if(!o.ok)throw new Error(`Context generation failed: ${o.status}`);return(await o.text()).trim()}var q=process.argv.includes("--colors");if(L.isTTY||q)P(void 0).then(i=>{console.log(i),process.exit(0)});else{let i="";L.on("data",t=>i+=t),L.on("end",async()=>{let t;try{t=i.trim()?JSON.parse(i):void 0}catch(e){throw new Error(`Failed to parse hook input: ${e instanceof Error?e.message:String(e)}`)}let r=await P(t);console.log(JSON.stringify({hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:r}})),process.exit(0)})}
+1 -1
View File
@@ -3,7 +3,7 @@
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let t=Object.keys(e);return t.length===0?"{}":t.length<=3?JSON.stringify(e):`{${t.length} keys: ${t.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,t){if(!t)return e;let s=typeof t=="string"?JSON.parse(t):t;if(e==="Bash"&&s.command)return`${e}(${s.command})`;if(s.file_path)return`${e}(${s.file_path})`;if(s.notebook_path)return`${e}(${s.notebook_path})`;if(e==="Glob"&&s.pattern)return`${e}(${s.pattern})`;if(e==="Grep"&&s.pattern)return`${e}(${s.pattern})`;if(s.url)return`${e}(${s.url})`;if(s.query)return`${e}(${s.query})`;if(e==="Task"){if(s.subagent_type)return`${e}(${s.subagent_type})`;if(s.description)return`${e}(${s.description})`}return e==="Skill"&&s.skill?`${e}(${s.skill})`:e==="LSP"&&s.operation?`${e}(${s.operation})`:e}formatTimestamp(e){let t=e.getFullYear(),s=String(e.getMonth()+1).padStart(2,"0"),r=String(e.getDate()).padStart(2,"0"),n=String(e.getHours()).padStart(2,"0"),l=String(e.getMinutes()).padStart(2,"0"),i=String(e.getSeconds()).padStart(2,"0"),f=String(e.getMilliseconds()).padStart(3,"0");return`${t}-${s}-${r} ${n}:${l}:${i}.${f}`}log(e,t,s,r,n){if(e<this.getLevel())return;let l=this.formatTimestamp(new Date),i=Ea[e].padEnd(5),f=t.padEnd(6),d="";r?.correlationId?d=`[${r.correlationId}] `:r?.sessionId&&(d=`[session-${r.sessionId}] `);let h="";n!=null&&(n instanceof Error?h=this.getLevel()===0?` ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let t=Object.keys(e);return t.length===0?"{}":t.length<=3?JSON.stringify(e):`{${t.length} keys: ${t.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,t){if(!t)return e;let s=typeof t=="string"?JSON.parse(t):t;if(e==="Bash"&&s.command)return`${e}(${s.command})`;if(s.file_path)return`${e}(${s.file_path})`;if(s.notebook_path)return`${e}(${s.notebook_path})`;if(e==="Glob"&&s.pattern)return`${e}(${s.pattern})`;if(e==="Grep"&&s.pattern)return`${e}(${s.pattern})`;if(s.url)return`${e}(${s.url})`;if(s.query)return`${e}(${s.query})`;if(e==="Task"){if(s.subagent_type)return`${e}(${s.subagent_type})`;if(s.description)return`${e}(${s.description})`}return e==="Skill"&&s.skill?`${e}(${s.skill})`:e==="LSP"&&s.operation?`${e}(${s.operation})`:e}formatTimestamp(e){let t=e.getFullYear(),s=String(e.getMonth()+1).padStart(2,"0"),r=String(e.getDate()).padStart(2,"0"),n=String(e.getHours()).padStart(2,"0"),l=String(e.getMinutes()).padStart(2,"0"),i=String(e.getSeconds()).padStart(2,"0"),f=String(e.getMilliseconds()).padStart(3,"0");return`${t}-${s}-${r} ${n}:${l}:${i}.${f}`}log(e,t,s,r,n){if(e<this.getLevel())return;let l=this.formatTimestamp(new Date),i=Ea[e].padEnd(5),f=t.padEnd(6),d="";r?.correlationId?d=`[${r.correlationId}] `:r?.sessionId&&(d=`[session-${r.sessionId}] `);let h="";n!=null&&(n instanceof Error?h=this.getLevel()===0?`
${n.message} ${n.message}
${n.stack}`:` ${n.message}`:this.getLevel()===0&&typeof n=="object"?h=` ${n.stack}`:` ${n.message}`:this.getLevel()===0&&typeof n=="object"?h=`
`+JSON.stringify(n,null,2):h=" "+this.formatData(n));let m="";if(r){let{sessionId:c,sdkSessionId:y,correlationId:_,...v}=r;Object.keys(v).length>0&&(m=` {${Object.entries(v).map(([O,x])=>`${O}=${x}`).join(", ")}}`)}let E=`[${l}] [${i}] [${f}] ${d}${s}${m}${h}`;if(this.logFilePath)try{(0,Yr.appendFileSync)(this.logFilePath,E+` `+JSON.stringify(n,null,2):h=" "+this.formatData(n));let m="";if(r){let{sessionId:c,memorySessionId:y,correlationId:_,...v}=r;Object.keys(v).length>0&&(m=` {${Object.entries(v).map(([O,x])=>`${O}=${x}`).join(", ")}}`)}let E=`[${l}] [${i}] [${f}] ${d}${s}${m}${h}`;if(this.logFilePath)try{(0,Yr.appendFileSync)(this.logFilePath,E+`
`,"utf8")}catch(c){process.stderr.write(`[LOGGER] Failed to write to log file: ${c} `,"utf8")}catch(c){process.stderr.write(`[LOGGER] Failed to write to log file: ${c}
`)}else process.stderr.write(E+` `)}else process.stderr.write(E+`
`)}debug(e,t,s,r){this.log(0,e,t,s,r)}info(e,t,s,r){this.log(1,e,t,s,r)}warn(e,t,s,r){this.log(2,e,t,s,r)}error(e,t,s,r){this.log(3,e,t,s,r)}dataIn(e,t,s,r){this.info(e,`\u2192 ${t}`,s,r)}dataOut(e,t,s,r){this.info(e,`\u2190 ${t}`,s,r)}success(e,t,s,r){this.info(e,`\u2713 ${t}`,s,r)}failure(e,t,s,r){this.error(e,`\u2717 ${t}`,s,r)}timing(e,t,s,r){this.info(e,`\u23F1 ${t}`,r,{duration:`${s}ms`})}happyPathError(e,t,s,r,n=""){let d=((new Error().stack||"").split(` `)}debug(e,t,s,r){this.log(0,e,t,s,r)}info(e,t,s,r){this.log(1,e,t,s,r)}warn(e,t,s,r){this.log(2,e,t,s,r)}error(e,t,s,r){this.log(3,e,t,s,r)}dataIn(e,t,s,r){this.info(e,`\u2192 ${t}`,s,r)}dataOut(e,t,s,r){this.info(e,`\u2190 ${t}`,s,r)}success(e,t,s,r){this.info(e,`\u2713 ${t}`,s,r)}failure(e,t,s,r){this.error(e,`\u2717 ${t}`,s,r)}timing(e,t,s,r){this.info(e,`\u23F1 ${t}`,r,{duration:`${s}ms`})}happyPathError(e,t,s,r,n=""){let d=((new Error().stack||"").split(`
+4 -4
View File
@@ -1,11 +1,11 @@
#!/usr/bin/env bun #!/usr/bin/env bun
import{stdin as y}from"process";var S=JSON.stringify({continue:!0,suppressOutput:!0});import L from"path";import{homedir as X}from"os";import{readFileSync as j}from"fs";import{readFileSync as w,writeFileSync as b,existsSync as F}from"fs";import{join as H}from"path";import{homedir as W}from"os";var R="bugfix,feature,refactor,discovery,decision,change",U="how-it-works,why-it-exists,what-changed,problem-solution,gotcha,pattern,trade-off";var g=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-sonnet-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_WORKER_HOST:"127.0.0.1",CLAUDE_MEM_SKIP_TOOLS:"ListMcpResourcesTool,SlashCommand,Skill,TodoWrite,AskUserQuestion",CLAUDE_MEM_PROVIDER:"claude",CLAUDE_MEM_GEMINI_API_KEY:"",CLAUDE_MEM_GEMINI_MODEL:"gemini-2.5-flash-lite",CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED:"true",CLAUDE_MEM_OPENROUTER_API_KEY:"",CLAUDE_MEM_OPENROUTER_MODEL:"xiaomi/mimo-v2-flash:free",CLAUDE_MEM_OPENROUTER_SITE_URL:"",CLAUDE_MEM_OPENROUTER_APP_NAME:"claude-mem",CLAUDE_MEM_OPENROUTER_MAX_CONTEXT_MESSAGES:"20",CLAUDE_MEM_OPENROUTER_MAX_TOKENS:"100000",CLAUDE_MEM_DATA_DIR:H(W(),".claude-mem"),CLAUDE_MEM_LOG_LEVEL:"INFO",CLAUDE_MEM_PYTHON_VERSION:"3.13",CLAUDE_CODE_PATH:"",CLAUDE_MEM_MODE:"code",CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT:"true",CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES:R,CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS:U,CLAUDE_MEM_CONTEXT_FULL_COUNT:"5",CLAUDE_MEM_CONTEXT_FULL_FIELD:"narrative",CLAUDE_MEM_CONTEXT_SESSION_COUNT:"10",CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY:"true",CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE:"false"};static getAllDefaults(){return{...this.DEFAULTS}}static get(t){return this.DEFAULTS[t]}static getInt(t){let r=this.get(t);return parseInt(r,10)}static getBool(t){return this.get(t)==="true"}static loadFromFile(t){try{if(!F(t))return this.getAllDefaults();let r=w(t,"utf-8"),e=JSON.parse(r),n=e;if(e.env&&typeof e.env=="object"){n=e.env;try{b(t,JSON.stringify(n,null,2),"utf-8"),E.info("SETTINGS","Migrated settings file from nested to flat schema",{settingsPath:t})}catch(a){E.warn("SETTINGS","Failed to auto-migrate settings file",{settingsPath:t},a)}}let o={...this.DEFAULTS};for(let a of Object.keys(this.DEFAULTS))n[a]!==void 0&&(o[a]=n[a]);return o}catch(r){return E.warn("SETTINGS","Failed to load settings, using defaults",{settingsPath:t},r),this.getAllDefaults()}}};import{appendFileSync as K,existsSync as x,mkdirSync as G}from"fs";import{join as T}from"path";var f=(o=>(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(f||{}),M=class{level=null;useColor;logFilePath=null;constructor(){this.useColor=process.stdout.isTTY??!1,this.initializeLogFile()}initializeLogFile(){try{let t=g.get("CLAUDE_MEM_DATA_DIR"),r=T(t,"logs");x(r)||G(r,{recursive:!0});let e=new Date().toISOString().split("T")[0];this.logFilePath=T(r,`claude-mem-${e}.log`)}catch(t){console.error("[LOGGER] Failed to initialize log file:",t),this.logFilePath=null}}getLevel(){if(this.level===null)try{let t=g.get("CLAUDE_MEM_DATA_DIR"),r=T(t,"settings.json"),n=g.loadFromFile(r).CLAUDE_MEM_LOG_LEVEL.toUpperCase();this.level=f[n]??1}catch(t){console.error("[LOGGER] Failed to load settings, using INFO level:",t),this.level=1}return this.level}correlationId(t,r){return`obs-${t}-${r}`}sessionId(t){return`session-${t}`}formatData(t){if(t==null)return"";if(typeof t=="string")return t;if(typeof t=="number"||typeof t=="boolean")return t.toString();if(typeof t=="object"){if(t instanceof Error)return this.getLevel()===0?`${t.message} import{stdin as y}from"process";var S=JSON.stringify({continue:!0,suppressOutput:!0});import m from"path";import{homedir as X}from"os";import{readFileSync as j}from"fs";import{readFileSync as w,writeFileSync as b,existsSync as F}from"fs";import{join as H}from"path";import{homedir as W}from"os";var R="bugfix,feature,refactor,discovery,decision,change",U="how-it-works,why-it-exists,what-changed,problem-solution,gotcha,pattern,trade-off";var g=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-sonnet-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_WORKER_HOST:"127.0.0.1",CLAUDE_MEM_SKIP_TOOLS:"ListMcpResourcesTool,SlashCommand,Skill,TodoWrite,AskUserQuestion",CLAUDE_MEM_PROVIDER:"claude",CLAUDE_MEM_GEMINI_API_KEY:"",CLAUDE_MEM_GEMINI_MODEL:"gemini-2.5-flash-lite",CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED:"true",CLAUDE_MEM_OPENROUTER_API_KEY:"",CLAUDE_MEM_OPENROUTER_MODEL:"xiaomi/mimo-v2-flash:free",CLAUDE_MEM_OPENROUTER_SITE_URL:"",CLAUDE_MEM_OPENROUTER_APP_NAME:"claude-mem",CLAUDE_MEM_OPENROUTER_MAX_CONTEXT_MESSAGES:"20",CLAUDE_MEM_OPENROUTER_MAX_TOKENS:"100000",CLAUDE_MEM_DATA_DIR:H(W(),".claude-mem"),CLAUDE_MEM_LOG_LEVEL:"INFO",CLAUDE_MEM_PYTHON_VERSION:"3.13",CLAUDE_CODE_PATH:"",CLAUDE_MEM_MODE:"code",CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT:"true",CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES:R,CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS:U,CLAUDE_MEM_CONTEXT_FULL_COUNT:"5",CLAUDE_MEM_CONTEXT_FULL_FIELD:"narrative",CLAUDE_MEM_CONTEXT_SESSION_COUNT:"10",CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY:"true",CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE:"false"};static getAllDefaults(){return{...this.DEFAULTS}}static get(t){return this.DEFAULTS[t]}static getInt(t){let r=this.get(t);return parseInt(r,10)}static getBool(t){return this.get(t)==="true"}static loadFromFile(t){try{if(!F(t))return this.getAllDefaults();let r=w(t,"utf-8"),e=JSON.parse(r),n=e;if(e.env&&typeof e.env=="object"){n=e.env;try{b(t,JSON.stringify(n,null,2),"utf-8"),E.info("SETTINGS","Migrated settings file from nested to flat schema",{settingsPath:t})}catch(a){E.warn("SETTINGS","Failed to auto-migrate settings file",{settingsPath:t},a)}}let o={...this.DEFAULTS};for(let a of Object.keys(this.DEFAULTS))n[a]!==void 0&&(o[a]=n[a]);return o}catch(r){return E.warn("SETTINGS","Failed to load settings, using defaults",{settingsPath:t},r),this.getAllDefaults()}}};import{appendFileSync as K,existsSync as x,mkdirSync as G}from"fs";import{join as T}from"path";var f=(o=>(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(f||{}),M=class{level=null;useColor;logFilePath=null;constructor(){this.useColor=process.stdout.isTTY??!1,this.initializeLogFile()}initializeLogFile(){try{let t=g.get("CLAUDE_MEM_DATA_DIR"),r=T(t,"logs");x(r)||G(r,{recursive:!0});let e=new Date().toISOString().split("T")[0];this.logFilePath=T(r,`claude-mem-${e}.log`)}catch(t){console.error("[LOGGER] Failed to initialize log file:",t),this.logFilePath=null}}getLevel(){if(this.level===null)try{let t=g.get("CLAUDE_MEM_DATA_DIR"),r=T(t,"settings.json"),n=g.loadFromFile(r).CLAUDE_MEM_LOG_LEVEL.toUpperCase();this.level=f[n]??1}catch(t){console.error("[LOGGER] Failed to load settings, using INFO level:",t),this.level=1}return this.level}correlationId(t,r){return`obs-${t}-${r}`}sessionId(t){return`session-${t}`}formatData(t){if(t==null)return"";if(typeof t=="string")return t;if(typeof t=="number"||typeof t=="boolean")return t.toString();if(typeof t=="object"){if(t instanceof Error)return this.getLevel()===0?`${t.message}
${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let r=Object.keys(t);return r.length===0?"{}":r.length<=3?JSON.stringify(t):`{${r.length} keys: ${r.slice(0,3).join(", ")}...}`}return String(t)}formatTool(t,r){if(!r)return t;let e=typeof r=="string"?JSON.parse(r):r;if(t==="Bash"&&e.command)return`${t}(${e.command})`;if(e.file_path)return`${t}(${e.file_path})`;if(e.notebook_path)return`${t}(${e.notebook_path})`;if(t==="Glob"&&e.pattern)return`${t}(${e.pattern})`;if(t==="Grep"&&e.pattern)return`${t}(${e.pattern})`;if(e.url)return`${t}(${e.url})`;if(e.query)return`${t}(${e.query})`;if(t==="Task"){if(e.subagent_type)return`${t}(${e.subagent_type})`;if(e.description)return`${t}(${e.description})`}return t==="Skill"&&e.skill?`${t}(${e.skill})`:t==="LSP"&&e.operation?`${t}(${e.operation})`:t}formatTimestamp(t){let r=t.getFullYear(),e=String(t.getMonth()+1).padStart(2,"0"),n=String(t.getDate()).padStart(2,"0"),o=String(t.getHours()).padStart(2,"0"),a=String(t.getMinutes()).padStart(2,"0"),s=String(t.getSeconds()).padStart(2,"0"),l=String(t.getMilliseconds()).padStart(3,"0");return`${r}-${e}-${n} ${o}:${a}:${s}.${l}`}log(t,r,e,n,o){if(t<this.getLevel())return;let a=this.formatTimestamp(new Date),s=f[t].padEnd(5),l=r.padEnd(6),_="";n?.correlationId?_=`[${n.correlationId}] `:n?.sessionId&&(_=`[session-${n.sessionId}] `);let c="";o!=null&&(o instanceof Error?c=this.getLevel()===0?` ${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let r=Object.keys(t);return r.length===0?"{}":r.length<=3?JSON.stringify(t):`{${r.length} keys: ${r.slice(0,3).join(", ")}...}`}return String(t)}formatTool(t,r){if(!r)return t;let e=typeof r=="string"?JSON.parse(r):r;if(t==="Bash"&&e.command)return`${t}(${e.command})`;if(e.file_path)return`${t}(${e.file_path})`;if(e.notebook_path)return`${t}(${e.notebook_path})`;if(t==="Glob"&&e.pattern)return`${t}(${e.pattern})`;if(t==="Grep"&&e.pattern)return`${t}(${e.pattern})`;if(e.url)return`${t}(${e.url})`;if(e.query)return`${t}(${e.query})`;if(t==="Task"){if(e.subagent_type)return`${t}(${e.subagent_type})`;if(e.description)return`${t}(${e.description})`}return t==="Skill"&&e.skill?`${t}(${e.skill})`:t==="LSP"&&e.operation?`${t}(${e.operation})`:t}formatTimestamp(t){let r=t.getFullYear(),e=String(t.getMonth()+1).padStart(2,"0"),n=String(t.getDate()).padStart(2,"0"),o=String(t.getHours()).padStart(2,"0"),a=String(t.getMinutes()).padStart(2,"0"),s=String(t.getSeconds()).padStart(2,"0"),l=String(t.getMilliseconds()).padStart(3,"0");return`${r}-${e}-${n} ${o}:${a}:${s}.${l}`}log(t,r,e,n,o){if(t<this.getLevel())return;let a=this.formatTimestamp(new Date),s=f[t].padEnd(5),l=r.padEnd(6),_="";n?.correlationId?_=`[${n.correlationId}] `:n?.sessionId&&(_=`[session-${n.sessionId}] `);let c="";o!=null&&(o instanceof Error?c=this.getLevel()===0?`
${o.message} ${o.message}
${o.stack}`:` ${o.message}`:this.getLevel()===0&&typeof o=="object"?c=` ${o.stack}`:` ${o.message}`:this.getLevel()===0&&typeof o=="object"?c=`
`+JSON.stringify(o,null,2):c=" "+this.formatData(o));let u="";if(n){let{sessionId:D,sdkSessionId:Z,correlationId:tt,...d}=n;Object.keys(d).length>0&&(u=` {${Object.entries(d).map(([$,v])=>`${$}=${v}`).join(", ")}}`)}let m=`[${a}] [${s}] [${l}] ${_}${e}${u}${c}`;if(this.logFilePath)try{K(this.logFilePath,m+` `+JSON.stringify(o,null,2):c=" "+this.formatData(o));let u="";if(n){let{sessionId:D,memorySessionId:Z,correlationId:tt,...d}=n;Object.keys(d).length>0&&(u=` {${Object.entries(d).map(([$,v])=>`${$}=${v}`).join(", ")}}`)}let C=`[${a}] [${s}] [${l}] ${_}${e}${u}${c}`;if(this.logFilePath)try{K(this.logFilePath,C+`
`,"utf8")}catch(D){process.stderr.write(`[LOGGER] Failed to write to log file: ${D} `,"utf8")}catch(D){process.stderr.write(`[LOGGER] Failed to write to log file: ${D}
`)}else process.stderr.write(m+` `)}else process.stderr.write(C+`
`)}debug(t,r,e,n){this.log(0,t,r,e,n)}info(t,r,e,n){this.log(1,t,r,e,n)}warn(t,r,e,n){this.log(2,t,r,e,n)}error(t,r,e,n){this.log(3,t,r,e,n)}dataIn(t,r,e,n){this.info(t,`\u2192 ${r}`,e,n)}dataOut(t,r,e,n){this.info(t,`\u2190 ${r}`,e,n)}success(t,r,e,n){this.info(t,`\u2713 ${r}`,e,n)}failure(t,r,e,n){this.error(t,`\u2717 ${r}`,e,n)}timing(t,r,e,n){this.info(t,`\u23F1 ${r}`,n,{duration:`${e}ms`})}happyPathError(t,r,e,n,o=""){let _=((new Error().stack||"").split(` `)}debug(t,r,e,n){this.log(0,t,r,e,n)}info(t,r,e,n){this.log(1,t,r,e,n)}warn(t,r,e,n){this.log(2,t,r,e,n)}error(t,r,e,n){this.log(3,t,r,e,n)}dataIn(t,r,e,n){this.info(t,`\u2192 ${r}`,e,n)}dataOut(t,r,e,n){this.info(t,`\u2190 ${r}`,e,n)}success(t,r,e,n){this.info(t,`\u2713 ${r}`,e,n)}failure(t,r,e,n){this.error(t,`\u2717 ${r}`,e,n)}timing(t,r,e,n){this.info(t,`\u23F1 ${r}`,n,{duration:`${e}ms`})}happyPathError(t,r,e,n,o=""){let _=((new Error().stack||"").split(`
`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),c=_?`${_[1].split("/").pop()}:${_[2]}`:"unknown",u={...e,location:c};return this.warn(t,`[HAPPY-PATH] ${r}`,u,n),o}},E=new M;var A={DEFAULT:3e5,HEALTH_CHECK:3e4,WORKER_STARTUP_WAIT:1e3,WORKER_STARTUP_RETRIES:300,PRE_RESTART_SETTLE_DELAY:2e3,WINDOWS_MULTIPLIER:1.5};function h(i){return process.platform==="win32"?Math.round(i*A.WINDOWS_MULTIPLIER):i}function N(i={}){let{port:t,includeSkillFallback:r=!1,customPrefix:e,actualError:n}=i,o=e||"Worker service connection failed.",a=t?` (port ${t})`:"",s=`${o}${a} `)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),c=_?`${_[1].split("/").pop()}:${_[2]}`:"unknown",u={...e,location:c};return this.warn(t,`[HAPPY-PATH] ${r}`,u,n),o}},E=new M;var A={DEFAULT:3e5,HEALTH_CHECK:3e4,WORKER_STARTUP_WAIT:1e3,WORKER_STARTUP_RETRIES:300,PRE_RESTART_SETTLE_DELAY:2e3,WINDOWS_MULTIPLIER:1.5};function h(i){return process.platform==="win32"?Math.round(i*A.WINDOWS_MULTIPLIER):i}function N(i={}){let{port:t,includeSkillFallback:r=!1,customPrefix:e,actualError:n}=i,o=e||"Worker service connection failed.",a=t?` (port ${t})`:"",s=`${o}${a}
@@ -16,4 +16,4 @@ ${o.stack}`:` ${o.message}`:this.getLevel()===0&&typeof o=="object"?c=`
If that doesn't work, try: /troubleshoot`),n&&(s=`Worker Error: ${n} If that doesn't work, try: /troubleshoot`),n&&(s=`Worker Error: ${n}
${s}`),s}var V=L.join(X(),".claude","plugins","marketplaces","thedotmack"),I=h(A.HEALTH_CHECK),O=null;function p(){if(O!==null)return O;let i=L.join(g.get("CLAUDE_MEM_DATA_DIR"),"settings.json"),t=g.loadFromFile(i);return O=parseInt(t.CLAUDE_MEM_WORKER_PORT,10),O}async function B(){let i=p();return(await fetch(`http://127.0.0.1:${i}/api/readiness`,{signal:AbortSignal.timeout(I)})).ok}function Y(){let i=L.join(V,"package.json");return JSON.parse(j(i,"utf-8")).version}async function J(){let i=p(),t=await fetch(`http://127.0.0.1:${i}/api/version`,{signal:AbortSignal.timeout(I)});if(!t.ok)throw new Error(`Failed to get worker version: ${t.status}`);return(await t.json()).version}async function z(){let i=Y(),t=await J();i!==t&&E.warn("SYSTEM","Worker version mismatch",{pluginVersion:i,workerVersion:t,hint:"Restart worker with: claude-mem worker restart"})}async function k(){for(let r=0;r<25;r++){try{if(await B()){await z();return}}catch{}await new Promise(e=>setTimeout(e,200))}throw new Error(N({port:p(),customPrefix:"Worker did not become ready within 5 seconds."}))}import q from"path";function P(i){if(!i||i.trim()==="")return E.warn("PROJECT_NAME","Empty cwd provided, using fallback",{cwd:i}),"unknown-project";let t=q.basename(i);if(t===""){if(process.platform==="win32"){let e=i.match(/^([A-Z]):\\/i);if(e){let o=`drive-${e[1].toUpperCase()}`;return E.info("PROJECT_NAME","Drive root detected",{cwd:i,projectName:o}),o}}return E.warn("PROJECT_NAME","Root directory detected, using fallback",{cwd:i}),"unknown-project"}return t}async function Q(i){if(await k(),!i)throw new Error("newHook requires input");let{session_id:t,cwd:r,prompt:e}=i,n=P(r);E.info("HOOK","new-hook: Received hook input",{session_id:t,has_prompt:!!e,cwd:r});let o=p();E.info("HOOK","new-hook: Calling /api/sessions/init",{claudeSessionId:t,project:n,prompt_length:e?.length});let a=await fetch(`http://127.0.0.1:${o}/api/sessions/init`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({claudeSessionId:t,project:n,prompt:e}),signal:AbortSignal.timeout(5e3)});if(!a.ok)throw new Error(`Session initialization failed: ${a.status}`);let s=await a.json(),l=s.sessionDbId,_=s.promptNumber;if(E.info("HOOK","new-hook: Received from /api/sessions/init",{sessionDbId:l,promptNumber:_,skipped:s.skipped}),s.skipped&&s.reason==="private"){E.info("HOOK",`new-hook: Session ${l}, prompt #${_} (fully private - skipped)`),console.log(S);return}E.info("HOOK",`new-hook: Session ${l}, prompt #${_}`);let c=e.startsWith("/")?e.substring(1):e;E.info("HOOK","new-hook: Calling /sessions/{sessionDbId}/init",{sessionDbId:l,promptNumber:_,userPrompt_length:c?.length});let u=await fetch(`http://127.0.0.1:${o}/sessions/${l}/init`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({userPrompt:c,promptNumber:_}),signal:AbortSignal.timeout(5e3)});if(!u.ok)throw new Error(`SDK agent start failed: ${u.status}`);console.log(S)}var C="";y.on("data",i=>C+=i);y.on("end",async()=>{let i;try{i=C?JSON.parse(C):void 0}catch(t){throw new Error(`Failed to parse hook input: ${t instanceof Error?t.message:String(t)}`)}await Q(i)}); ${s}`),s}var V=m.join(X(),".claude","plugins","marketplaces","thedotmack"),I=h(A.HEALTH_CHECK),O=null;function p(){if(O!==null)return O;let i=m.join(g.get("CLAUDE_MEM_DATA_DIR"),"settings.json"),t=g.loadFromFile(i);return O=parseInt(t.CLAUDE_MEM_WORKER_PORT,10),O}async function B(){let i=p();return(await fetch(`http://127.0.0.1:${i}/api/readiness`,{signal:AbortSignal.timeout(I)})).ok}function Y(){let i=m.join(V,"package.json");return JSON.parse(j(i,"utf-8")).version}async function J(){let i=p(),t=await fetch(`http://127.0.0.1:${i}/api/version`,{signal:AbortSignal.timeout(I)});if(!t.ok)throw new Error(`Failed to get worker version: ${t.status}`);return(await t.json()).version}async function z(){let i=Y(),t=await J();i!==t&&E.warn("SYSTEM","Worker version mismatch",{pluginVersion:i,workerVersion:t,hint:"Restart worker with: claude-mem worker restart"})}async function P(){for(let r=0;r<25;r++){try{if(await B()){await z();return}}catch{}await new Promise(e=>setTimeout(e,200))}throw new Error(N({port:p(),customPrefix:"Worker did not become ready within 5 seconds."}))}import q from"path";function k(i){if(!i||i.trim()==="")return E.warn("PROJECT_NAME","Empty cwd provided, using fallback",{cwd:i}),"unknown-project";let t=q.basename(i);if(t===""){if(process.platform==="win32"){let e=i.match(/^([A-Z]):\\/i);if(e){let o=`drive-${e[1].toUpperCase()}`;return E.info("PROJECT_NAME","Drive root detected",{cwd:i,projectName:o}),o}}return E.warn("PROJECT_NAME","Root directory detected, using fallback",{cwd:i}),"unknown-project"}return t}async function Q(i){if(await P(),!i)throw new Error("newHook requires input");let{session_id:t,cwd:r,prompt:e}=i,n=k(r);E.info("HOOK","new-hook: Received hook input",{session_id:t,has_prompt:!!e,cwd:r});let o=p();E.info("HOOK","new-hook: Calling /api/sessions/init",{contentSessionId:t,project:n,prompt_length:e?.length});let a=await fetch(`http://127.0.0.1:${o}/api/sessions/init`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({contentSessionId:t,project:n,prompt:e}),signal:AbortSignal.timeout(5e3)});if(!a.ok)throw new Error(`Session initialization failed: ${a.status}`);let s=await a.json(),l=s.sessionDbId,_=s.promptNumber;if(E.info("HOOK","new-hook: Received from /api/sessions/init",{sessionDbId:l,promptNumber:_,skipped:s.skipped}),s.skipped&&s.reason==="private"){E.info("HOOK",`new-hook: Session ${l}, prompt #${_} (fully private - skipped)`),console.log(S);return}E.info("HOOK",`new-hook: Session ${l}, prompt #${_}`);let c=e.startsWith("/")?e.substring(1):e;E.info("HOOK","new-hook: Calling /sessions/{sessionDbId}/init",{sessionDbId:l,promptNumber:_,userPrompt_length:c?.length});let u=await fetch(`http://127.0.0.1:${o}/sessions/${l}/init`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({userPrompt:c,promptNumber:_}),signal:AbortSignal.timeout(5e3)});if(!u.ok)throw new Error(`SDK agent start failed: ${u.status}`);console.log(S)}var L="";y.on("data",i=>L+=i);y.on("end",async()=>{let i;try{i=L?JSON.parse(L):void 0}catch(t){throw new Error(`Failed to parse hook input: ${t instanceof Error?t.message:String(t)}`)}await Q(i)});
+2 -2
View File
@@ -3,7 +3,7 @@ import{stdin as P}from"process";var U=JSON.stringify({continue:!0,suppressOutput
${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let r=Object.keys(t);return r.length===0?"{}":r.length<=3?JSON.stringify(t):`{${r.length} keys: ${r.slice(0,3).join(", ")}...}`}return String(t)}formatTool(t,r){if(!r)return t;let e=typeof r=="string"?JSON.parse(r):r;if(t==="Bash"&&e.command)return`${t}(${e.command})`;if(e.file_path)return`${t}(${e.file_path})`;if(e.notebook_path)return`${t}(${e.notebook_path})`;if(t==="Glob"&&e.pattern)return`${t}(${e.pattern})`;if(t==="Grep"&&e.pattern)return`${t}(${e.pattern})`;if(e.url)return`${t}(${e.url})`;if(e.query)return`${t}(${e.query})`;if(t==="Task"){if(e.subagent_type)return`${t}(${e.subagent_type})`;if(e.description)return`${t}(${e.description})`}return t==="Skill"&&e.skill?`${t}(${e.skill})`:t==="LSP"&&e.operation?`${t}(${e.operation})`:t}formatTimestamp(t){let r=t.getFullYear(),e=String(t.getMonth()+1).padStart(2,"0"),n=String(t.getDate()).padStart(2,"0"),o=String(t.getHours()).padStart(2,"0"),i=String(t.getMinutes()).padStart(2,"0"),E=String(t.getSeconds()).padStart(2,"0"),l=String(t.getMilliseconds()).padStart(3,"0");return`${r}-${e}-${n} ${o}:${i}:${E}.${l}`}log(t,r,e,n,o){if(t<this.getLevel())return;let i=this.formatTimestamp(new Date),E=f[t].padEnd(5),l=r.padEnd(6),g="";n?.correlationId?g=`[${n.correlationId}] `:n?.sessionId&&(g=`[session-${n.sessionId}] `);let c="";o!=null&&(o instanceof Error?c=this.getLevel()===0?` ${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let r=Object.keys(t);return r.length===0?"{}":r.length<=3?JSON.stringify(t):`{${r.length} keys: ${r.slice(0,3).join(", ")}...}`}return String(t)}formatTool(t,r){if(!r)return t;let e=typeof r=="string"?JSON.parse(r):r;if(t==="Bash"&&e.command)return`${t}(${e.command})`;if(e.file_path)return`${t}(${e.file_path})`;if(e.notebook_path)return`${t}(${e.notebook_path})`;if(t==="Glob"&&e.pattern)return`${t}(${e.pattern})`;if(t==="Grep"&&e.pattern)return`${t}(${e.pattern})`;if(e.url)return`${t}(${e.url})`;if(e.query)return`${t}(${e.query})`;if(t==="Task"){if(e.subagent_type)return`${t}(${e.subagent_type})`;if(e.description)return`${t}(${e.description})`}return t==="Skill"&&e.skill?`${t}(${e.skill})`:t==="LSP"&&e.operation?`${t}(${e.operation})`:t}formatTimestamp(t){let r=t.getFullYear(),e=String(t.getMonth()+1).padStart(2,"0"),n=String(t.getDate()).padStart(2,"0"),o=String(t.getHours()).padStart(2,"0"),i=String(t.getMinutes()).padStart(2,"0"),E=String(t.getSeconds()).padStart(2,"0"),l=String(t.getMilliseconds()).padStart(3,"0");return`${r}-${e}-${n} ${o}:${i}:${E}.${l}`}log(t,r,e,n,o){if(t<this.getLevel())return;let i=this.formatTimestamp(new Date),E=f[t].padEnd(5),l=r.padEnd(6),g="";n?.correlationId?g=`[${n.correlationId}] `:n?.sessionId&&(g=`[session-${n.sessionId}] `);let c="";o!=null&&(o instanceof Error?c=this.getLevel()===0?`
${o.message} ${o.message}
${o.stack}`:` ${o.message}`:this.getLevel()===0&&typeof o=="object"?c=` ${o.stack}`:` ${o.message}`:this.getLevel()===0&&typeof o=="object"?c=`
`+JSON.stringify(o,null,2):c=" "+this.formatData(o));let T="";if(n){let{sessionId:D,sdkSessionId:z,correlationId:Q,...m}=n;Object.keys(m).length>0&&(T=` {${Object.entries(m).map(([$,k])=>`${$}=${k}`).join(", ")}}`)}let C=`[${i}] [${E}] [${l}] ${g}${e}${T}${c}`;if(this.logFilePath)try{b(this.logFilePath,C+` `+JSON.stringify(o,null,2):c=" "+this.formatData(o));let T="";if(n){let{sessionId:D,memorySessionId:z,correlationId:Q,...m}=n;Object.keys(m).length>0&&(T=` {${Object.entries(m).map(([$,k])=>`${$}=${k}`).join(", ")}}`)}let C=`[${i}] [${E}] [${l}] ${g}${e}${T}${c}`;if(this.logFilePath)try{b(this.logFilePath,C+`
`,"utf8")}catch(D){process.stderr.write(`[LOGGER] Failed to write to log file: ${D} `,"utf8")}catch(D){process.stderr.write(`[LOGGER] Failed to write to log file: ${D}
`)}else process.stderr.write(C+` `)}else process.stderr.write(C+`
`)}debug(t,r,e,n){this.log(0,t,r,e,n)}info(t,r,e,n){this.log(1,t,r,e,n)}warn(t,r,e,n){this.log(2,t,r,e,n)}error(t,r,e,n){this.log(3,t,r,e,n)}dataIn(t,r,e,n){this.info(t,`\u2192 ${r}`,e,n)}dataOut(t,r,e,n){this.info(t,`\u2190 ${r}`,e,n)}success(t,r,e,n){this.info(t,`\u2713 ${r}`,e,n)}failure(t,r,e,n){this.error(t,`\u2717 ${r}`,e,n)}timing(t,r,e,n){this.info(t,`\u23F1 ${r}`,n,{duration:`${e}ms`})}happyPathError(t,r,e,n,o=""){let g=((new Error().stack||"").split(` `)}debug(t,r,e,n){this.log(0,t,r,e,n)}info(t,r,e,n){this.log(1,t,r,e,n)}warn(t,r,e,n){this.log(2,t,r,e,n)}error(t,r,e,n){this.log(3,t,r,e,n)}dataIn(t,r,e,n){this.info(t,`\u2192 ${r}`,e,n)}dataOut(t,r,e,n){this.info(t,`\u2190 ${r}`,e,n)}success(t,r,e,n){this.info(t,`\u2713 ${r}`,e,n)}failure(t,r,e,n){this.error(t,`\u2717 ${r}`,e,n)}timing(t,r,e,n){this.info(t,`\u23F1 ${r}`,n,{duration:`${e}ms`})}happyPathError(t,r,e,n,o=""){let g=((new Error().stack||"").split(`
@@ -16,4 +16,4 @@ ${o.stack}`:` ${o.message}`:this.getLevel()===0&&typeof o=="object"?c=`
If that doesn't work, try: /troubleshoot`),n&&(E=`Worker Error: ${n} If that doesn't work, try: /troubleshoot`),n&&(E=`Worker Error: ${n}
${E}`),E}var V=A.join(K(),".claude","plugins","marketplaces","thedotmack"),N=I(u.HEALTH_CHECK),S=null;function O(){if(S!==null)return S;let s=A.join(a.get("CLAUDE_MEM_DATA_DIR"),"settings.json"),t=a.loadFromFile(s);return S=parseInt(t.CLAUDE_MEM_WORKER_PORT,10),S}async function j(){let s=O();return(await fetch(`http://127.0.0.1:${s}/api/readiness`,{signal:AbortSignal.timeout(N)})).ok}function B(){let s=A.join(V,"package.json");return JSON.parse(X(s,"utf-8")).version}async function Y(){let s=O(),t=await fetch(`http://127.0.0.1:${s}/api/version`,{signal:AbortSignal.timeout(N)});if(!t.ok)throw new Error(`Failed to get worker version: ${t.status}`);return(await t.json()).version}async function J(){let s=B(),t=await Y();s!==t&&_.warn("SYSTEM","Worker version mismatch",{pluginVersion:s,workerVersion:t,hint:"Restart worker with: claude-mem worker restart"})}async function y(){for(let r=0;r<25;r++){try{if(await j()){await J();return}}catch{}await new Promise(e=>setTimeout(e,200))}throw new Error(h({port:O(),customPrefix:"Worker did not become ready within 5 seconds."}))}async function q(s){if(await y(),!s)throw new Error("saveHook requires input");let{session_id:t,cwd:r,tool_name:e,tool_input:n,tool_response:o}=s,i=O(),E=_.formatTool(e,n);if(_.dataIn("HOOK",`PostToolUse: ${E}`,{workerPort:i}),!r)throw new Error(`Missing cwd in PostToolUse hook input for session ${t}, tool ${e}`);let l=await fetch(`http://127.0.0.1:${i}/api/sessions/observations`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({claudeSessionId:t,tool_name:e,tool_input:n,tool_response:o,cwd:r}),signal:AbortSignal.timeout(u.DEFAULT)});if(!l.ok)throw new Error(`Observation storage failed: ${l.status}`);_.debug("HOOK","Observation sent successfully",{toolName:e}),console.log(U)}var L="";P.on("data",s=>L+=s);P.on("end",async()=>{let s;try{s=L?JSON.parse(L):void 0}catch(t){throw new Error(`Failed to parse hook input: ${t instanceof Error?t.message:String(t)}`)}await q(s)}); ${E}`),E}var V=A.join(K(),".claude","plugins","marketplaces","thedotmack"),N=I(u.HEALTH_CHECK),S=null;function O(){if(S!==null)return S;let s=A.join(a.get("CLAUDE_MEM_DATA_DIR"),"settings.json"),t=a.loadFromFile(s);return S=parseInt(t.CLAUDE_MEM_WORKER_PORT,10),S}async function j(){let s=O();return(await fetch(`http://127.0.0.1:${s}/api/readiness`,{signal:AbortSignal.timeout(N)})).ok}function B(){let s=A.join(V,"package.json");return JSON.parse(X(s,"utf-8")).version}async function Y(){let s=O(),t=await fetch(`http://127.0.0.1:${s}/api/version`,{signal:AbortSignal.timeout(N)});if(!t.ok)throw new Error(`Failed to get worker version: ${t.status}`);return(await t.json()).version}async function J(){let s=B(),t=await Y();s!==t&&_.warn("SYSTEM","Worker version mismatch",{pluginVersion:s,workerVersion:t,hint:"Restart worker with: claude-mem worker restart"})}async function y(){for(let r=0;r<25;r++){try{if(await j()){await J();return}}catch{}await new Promise(e=>setTimeout(e,200))}throw new Error(h({port:O(),customPrefix:"Worker did not become ready within 5 seconds."}))}async function q(s){if(await y(),!s)throw new Error("saveHook requires input");let{session_id:t,cwd:r,tool_name:e,tool_input:n,tool_response:o}=s,i=O(),E=_.formatTool(e,n);if(_.dataIn("HOOK",`PostToolUse: ${E}`,{workerPort:i}),!r)throw new Error(`Missing cwd in PostToolUse hook input for session ${t}, tool ${e}`);let l=await fetch(`http://127.0.0.1:${i}/api/sessions/observations`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({contentSessionId:t,tool_name:e,tool_input:n,tool_response:o,cwd:r}),signal:AbortSignal.timeout(u.DEFAULT)});if(!l.ok)throw new Error(`Observation storage failed: ${l.status}`);_.debug("HOOK","Observation sent successfully",{toolName:e}),console.log(U)}var L="";P.on("data",s=>L+=s);P.on("end",async()=>{let s;try{s=L?JSON.parse(L):void 0}catch(t){throw new Error(`Failed to parse hook input: ${t instanceof Error?t.message:String(t)}`)}await q(s)});
+3 -3
View File
@@ -1,9 +1,9 @@
#!/usr/bin/env bun #!/usr/bin/env bun
import{stdin as k}from"process";var f=JSON.stringify({continue:!0,suppressOutput:!0});import{readFileSync as v,writeFileSync as F,existsSync as x}from"fs";import{join as H}from"path";import{homedir as W}from"os";var d="bugfix,feature,refactor,discovery,decision,change",h="how-it-works,why-it-exists,what-changed,problem-solution,gotcha,pattern,trade-off";var c=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-sonnet-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_WORKER_HOST:"127.0.0.1",CLAUDE_MEM_SKIP_TOOLS:"ListMcpResourcesTool,SlashCommand,Skill,TodoWrite,AskUserQuestion",CLAUDE_MEM_PROVIDER:"claude",CLAUDE_MEM_GEMINI_API_KEY:"",CLAUDE_MEM_GEMINI_MODEL:"gemini-2.5-flash-lite",CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED:"true",CLAUDE_MEM_OPENROUTER_API_KEY:"",CLAUDE_MEM_OPENROUTER_MODEL:"xiaomi/mimo-v2-flash:free",CLAUDE_MEM_OPENROUTER_SITE_URL:"",CLAUDE_MEM_OPENROUTER_APP_NAME:"claude-mem",CLAUDE_MEM_OPENROUTER_MAX_CONTEXT_MESSAGES:"20",CLAUDE_MEM_OPENROUTER_MAX_TOKENS:"100000",CLAUDE_MEM_DATA_DIR:H(W(),".claude-mem"),CLAUDE_MEM_LOG_LEVEL:"INFO",CLAUDE_MEM_PYTHON_VERSION:"3.13",CLAUDE_CODE_PATH:"",CLAUDE_MEM_MODE:"code",CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT:"true",CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES:d,CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS:h,CLAUDE_MEM_CONTEXT_FULL_COUNT:"5",CLAUDE_MEM_CONTEXT_FULL_FIELD:"narrative",CLAUDE_MEM_CONTEXT_SESSION_COUNT:"10",CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY:"true",CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE:"false"};static getAllDefaults(){return{...this.DEFAULTS}}static get(t){return this.DEFAULTS[t]}static getInt(t){let r=this.get(t);return parseInt(r,10)}static getBool(t){return this.get(t)==="true"}static loadFromFile(t){try{if(!x(t))return this.getAllDefaults();let r=v(t,"utf-8"),e=JSON.parse(r),n=e;if(e.env&&typeof e.env=="object"){n=e.env;try{F(t,JSON.stringify(n,null,2),"utf-8"),g.info("SETTINGS","Migrated settings file from nested to flat schema",{settingsPath:t})}catch(E){g.warn("SETTINGS","Failed to auto-migrate settings file",{settingsPath:t},E)}}let o={...this.DEFAULTS};for(let E of Object.keys(this.DEFAULTS))n[E]!==void 0&&(o[E]=n[E]);return o}catch(r){return g.warn("SETTINGS","Failed to load settings, using defaults",{settingsPath:t},r),this.getAllDefaults()}}};import{appendFileSync as b,existsSync as G,mkdirSync as K}from"fs";import{join as M}from"path";var p=(o=>(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(p||{}),A=class{level=null;useColor;logFilePath=null;constructor(){this.useColor=process.stdout.isTTY??!1,this.initializeLogFile()}initializeLogFile(){try{let t=c.get("CLAUDE_MEM_DATA_DIR"),r=M(t,"logs");G(r)||K(r,{recursive:!0});let e=new Date().toISOString().split("T")[0];this.logFilePath=M(r,`claude-mem-${e}.log`)}catch(t){console.error("[LOGGER] Failed to initialize log file:",t),this.logFilePath=null}}getLevel(){if(this.level===null)try{let t=c.get("CLAUDE_MEM_DATA_DIR"),r=M(t,"settings.json"),n=c.loadFromFile(r).CLAUDE_MEM_LOG_LEVEL.toUpperCase();this.level=p[n]??1}catch(t){console.error("[LOGGER] Failed to load settings, using INFO level:",t),this.level=1}return this.level}correlationId(t,r){return`obs-${t}-${r}`}sessionId(t){return`session-${t}`}formatData(t){if(t==null)return"";if(typeof t=="string")return t;if(typeof t=="number"||typeof t=="boolean")return t.toString();if(typeof t=="object"){if(t instanceof Error)return this.getLevel()===0?`${t.message} import{stdin as k}from"process";var f=JSON.stringify({continue:!0,suppressOutput:!0});import{readFileSync as v,writeFileSync as F,existsSync as x}from"fs";import{join as H}from"path";import{homedir as W}from"os";var h="bugfix,feature,refactor,discovery,decision,change",d="how-it-works,why-it-exists,what-changed,problem-solution,gotcha,pattern,trade-off";var c=class{static DEFAULTS={CLAUDE_MEM_MODEL:"claude-sonnet-4-5",CLAUDE_MEM_CONTEXT_OBSERVATIONS:"50",CLAUDE_MEM_WORKER_PORT:"37777",CLAUDE_MEM_WORKER_HOST:"127.0.0.1",CLAUDE_MEM_SKIP_TOOLS:"ListMcpResourcesTool,SlashCommand,Skill,TodoWrite,AskUserQuestion",CLAUDE_MEM_PROVIDER:"claude",CLAUDE_MEM_GEMINI_API_KEY:"",CLAUDE_MEM_GEMINI_MODEL:"gemini-2.5-flash-lite",CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED:"true",CLAUDE_MEM_OPENROUTER_API_KEY:"",CLAUDE_MEM_OPENROUTER_MODEL:"xiaomi/mimo-v2-flash:free",CLAUDE_MEM_OPENROUTER_SITE_URL:"",CLAUDE_MEM_OPENROUTER_APP_NAME:"claude-mem",CLAUDE_MEM_OPENROUTER_MAX_CONTEXT_MESSAGES:"20",CLAUDE_MEM_OPENROUTER_MAX_TOKENS:"100000",CLAUDE_MEM_DATA_DIR:H(W(),".claude-mem"),CLAUDE_MEM_LOG_LEVEL:"INFO",CLAUDE_MEM_PYTHON_VERSION:"3.13",CLAUDE_CODE_PATH:"",CLAUDE_MEM_MODE:"code",CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT:"true",CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT:"true",CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES:h,CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS:d,CLAUDE_MEM_CONTEXT_FULL_COUNT:"5",CLAUDE_MEM_CONTEXT_FULL_FIELD:"narrative",CLAUDE_MEM_CONTEXT_SESSION_COUNT:"10",CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY:"true",CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE:"false"};static getAllDefaults(){return{...this.DEFAULTS}}static get(t){return this.DEFAULTS[t]}static getInt(t){let r=this.get(t);return parseInt(r,10)}static getBool(t){return this.get(t)==="true"}static loadFromFile(t){try{if(!x(t))return this.getAllDefaults();let r=v(t,"utf-8"),e=JSON.parse(r),n=e;if(e.env&&typeof e.env=="object"){n=e.env;try{F(t,JSON.stringify(n,null,2),"utf-8"),g.info("SETTINGS","Migrated settings file from nested to flat schema",{settingsPath:t})}catch(E){g.warn("SETTINGS","Failed to auto-migrate settings file",{settingsPath:t},E)}}let o={...this.DEFAULTS};for(let E of Object.keys(this.DEFAULTS))n[E]!==void 0&&(o[E]=n[E]);return o}catch(r){return g.warn("SETTINGS","Failed to load settings, using defaults",{settingsPath:t},r),this.getAllDefaults()}}};import{appendFileSync as b,existsSync as G,mkdirSync as K}from"fs";import{join as M}from"path";var p=(o=>(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(p||{}),A=class{level=null;useColor;logFilePath=null;constructor(){this.useColor=process.stdout.isTTY??!1,this.initializeLogFile()}initializeLogFile(){try{let t=c.get("CLAUDE_MEM_DATA_DIR"),r=M(t,"logs");G(r)||K(r,{recursive:!0});let e=new Date().toISOString().split("T")[0];this.logFilePath=M(r,`claude-mem-${e}.log`)}catch(t){console.error("[LOGGER] Failed to initialize log file:",t),this.logFilePath=null}}getLevel(){if(this.level===null)try{let t=c.get("CLAUDE_MEM_DATA_DIR"),r=M(t,"settings.json"),n=c.loadFromFile(r).CLAUDE_MEM_LOG_LEVEL.toUpperCase();this.level=p[n]??1}catch(t){console.error("[LOGGER] Failed to load settings, using INFO level:",t),this.level=1}return this.level}correlationId(t,r){return`obs-${t}-${r}`}sessionId(t){return`session-${t}`}formatData(t){if(t==null)return"";if(typeof t=="string")return t;if(typeof t=="number"||typeof t=="boolean")return t.toString();if(typeof t=="object"){if(t instanceof Error)return this.getLevel()===0?`${t.message}
${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let r=Object.keys(t);return r.length===0?"{}":r.length<=3?JSON.stringify(t):`{${r.length} keys: ${r.slice(0,3).join(", ")}...}`}return String(t)}formatTool(t,r){if(!r)return t;let e=typeof r=="string"?JSON.parse(r):r;if(t==="Bash"&&e.command)return`${t}(${e.command})`;if(e.file_path)return`${t}(${e.file_path})`;if(e.notebook_path)return`${t}(${e.notebook_path})`;if(t==="Glob"&&e.pattern)return`${t}(${e.pattern})`;if(t==="Grep"&&e.pattern)return`${t}(${e.pattern})`;if(e.url)return`${t}(${e.url})`;if(e.query)return`${t}(${e.query})`;if(t==="Task"){if(e.subagent_type)return`${t}(${e.subagent_type})`;if(e.description)return`${t}(${e.description})`}return t==="Skill"&&e.skill?`${t}(${e.skill})`:t==="LSP"&&e.operation?`${t}(${e.operation})`:t}formatTimestamp(t){let r=t.getFullYear(),e=String(t.getMonth()+1).padStart(2,"0"),n=String(t.getDate()).padStart(2,"0"),o=String(t.getHours()).padStart(2,"0"),E=String(t.getMinutes()).padStart(2,"0"),i=String(t.getSeconds()).padStart(2,"0"),_=String(t.getMilliseconds()).padStart(3,"0");return`${r}-${e}-${n} ${o}:${E}:${i}.${_}`}log(t,r,e,n,o){if(t<this.getLevel())return;let E=this.formatTimestamp(new Date),i=p[t].padEnd(5),_=r.padEnd(6),a="";n?.correlationId?a=`[${n.correlationId}] `:n?.sessionId&&(a=`[session-${n.sessionId}] `);let l="";o!=null&&(o instanceof Error?l=this.getLevel()===0?` ${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let r=Object.keys(t);return r.length===0?"{}":r.length<=3?JSON.stringify(t):`{${r.length} keys: ${r.slice(0,3).join(", ")}...}`}return String(t)}formatTool(t,r){if(!r)return t;let e=typeof r=="string"?JSON.parse(r):r;if(t==="Bash"&&e.command)return`${t}(${e.command})`;if(e.file_path)return`${t}(${e.file_path})`;if(e.notebook_path)return`${t}(${e.notebook_path})`;if(t==="Glob"&&e.pattern)return`${t}(${e.pattern})`;if(t==="Grep"&&e.pattern)return`${t}(${e.pattern})`;if(e.url)return`${t}(${e.url})`;if(e.query)return`${t}(${e.query})`;if(t==="Task"){if(e.subagent_type)return`${t}(${e.subagent_type})`;if(e.description)return`${t}(${e.description})`}return t==="Skill"&&e.skill?`${t}(${e.skill})`:t==="LSP"&&e.operation?`${t}(${e.operation})`:t}formatTimestamp(t){let r=t.getFullYear(),e=String(t.getMonth()+1).padStart(2,"0"),n=String(t.getDate()).padStart(2,"0"),o=String(t.getHours()).padStart(2,"0"),E=String(t.getMinutes()).padStart(2,"0"),i=String(t.getSeconds()).padStart(2,"0"),_=String(t.getMilliseconds()).padStart(3,"0");return`${r}-${e}-${n} ${o}:${E}:${i}.${_}`}log(t,r,e,n,o){if(t<this.getLevel())return;let E=this.formatTimestamp(new Date),i=p[t].padEnd(5),_=r.padEnd(6),a="";n?.correlationId?a=`[${n.correlationId}] `:n?.sessionId&&(a=`[session-${n.sessionId}] `);let l="";o!=null&&(o instanceof Error?l=this.getLevel()===0?`
${o.message} ${o.message}
${o.stack}`:` ${o.message}`:this.getLevel()===0&&typeof o=="object"?l=` ${o.stack}`:` ${o.message}`:this.getLevel()===0&&typeof o=="object"?l=`
`+JSON.stringify(o,null,2):l=" "+this.formatData(o));let S="";if(n){let{sessionId:U,sdkSessionId:tt,correlationId:et,...R}=n;Object.keys(R).length>0&&(S=` {${Object.entries(R).map(([P,w])=>`${P}=${w}`).join(", ")}}`)}let D=`[${E}] [${i}] [${_}] ${a}${e}${S}${l}`;if(this.logFilePath)try{b(this.logFilePath,D+` `+JSON.stringify(o,null,2):l=" "+this.formatData(o));let S="";if(n){let{sessionId:U,memorySessionId:tt,correlationId:et,...R}=n;Object.keys(R).length>0&&(S=` {${Object.entries(R).map(([P,w])=>`${P}=${w}`).join(", ")}}`)}let D=`[${E}] [${i}] [${_}] ${a}${e}${S}${l}`;if(this.logFilePath)try{b(this.logFilePath,D+`
`,"utf8")}catch(U){process.stderr.write(`[LOGGER] Failed to write to log file: ${U} `,"utf8")}catch(U){process.stderr.write(`[LOGGER] Failed to write to log file: ${U}
`)}else process.stderr.write(D+` `)}else process.stderr.write(D+`
`)}debug(t,r,e,n){this.log(0,t,r,e,n)}info(t,r,e,n){this.log(1,t,r,e,n)}warn(t,r,e,n){this.log(2,t,r,e,n)}error(t,r,e,n){this.log(3,t,r,e,n)}dataIn(t,r,e,n){this.info(t,`\u2192 ${r}`,e,n)}dataOut(t,r,e,n){this.info(t,`\u2190 ${r}`,e,n)}success(t,r,e,n){this.info(t,`\u2713 ${r}`,e,n)}failure(t,r,e,n){this.error(t,`\u2717 ${r}`,e,n)}timing(t,r,e,n){this.info(t,`\u23F1 ${r}`,n,{duration:`${e}ms`})}happyPathError(t,r,e,n,o=""){let a=((new Error().stack||"").split(` `)}debug(t,r,e,n){this.log(0,t,r,e,n)}info(t,r,e,n){this.log(1,t,r,e,n)}warn(t,r,e,n){this.log(2,t,r,e,n)}error(t,r,e,n){this.log(3,t,r,e,n)}dataIn(t,r,e,n){this.info(t,`\u2192 ${r}`,e,n)}dataOut(t,r,e,n){this.info(t,`\u2190 ${r}`,e,n)}success(t,r,e,n){this.info(t,`\u2713 ${r}`,e,n)}failure(t,r,e,n){this.error(t,`\u2717 ${r}`,e,n)}timing(t,r,e,n){this.info(t,`\u23F1 ${r}`,n,{duration:`${e}ms`})}happyPathError(t,r,e,n,o=""){let a=((new Error().stack||"").split(`
@@ -20,4 +20,4 @@ ${i}`),i}var j=L.join(X(),".claude","plugins","marketplaces","thedotmack"),y=I(u
`),o=!1;for(let E=n.length-1;E>=0;E--){let i=JSON.parse(n[E]);if(i.type===t&&(o=!0,i.message?.content)){let _="",a=i.message.content;if(typeof a=="string")_=a;else if(Array.isArray(a))_=a.filter(l=>l.type==="text").map(l=>l.text).join(` `),o=!1;for(let E=n.length-1;E>=0;E--){let i=JSON.parse(n[E]);if(i.type===t&&(o=!0,i.message?.content)){let _="",a=i.message.content;if(typeof a=="string")_=a;else if(Array.isArray(a))_=a.filter(l=>l.type==="text").map(l=>l.text).join(`
`);else throw new Error(`Unknown message content format in transcript. Type: ${typeof a}`);return r&&(_=_.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g,""),_=_.replace(/\n{3,}/g,` `);else throw new Error(`Unknown message content format in transcript. Type: ${typeof a}`);return r&&(_=_.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g,""),_=_.replace(/\n{3,}/g,`
`).trim()),_}}if(!o)throw new Error(`No message found for role '${t}' in transcript: ${s}`);return""}async function Z(s){if(await $(),!s)throw new Error("summaryHook requires input");let{session_id:t}=s,r=O();if(!s.transcript_path)throw new Error(`Missing transcript_path in Stop hook input for session ${t}`);let e=m(s.transcript_path,"user"),n=m(s.transcript_path,"assistant",!0);g.dataIn("HOOK","Stop: Requesting summary",{workerPort:r,hasLastUserMessage:!!e,hasLastAssistantMessage:!!n});let o=await fetch(`http://127.0.0.1:${r}/api/sessions/summarize`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({claudeSessionId:t,last_user_message:e,last_assistant_message:n}),signal:AbortSignal.timeout(u.DEFAULT)});if(!o.ok)throw console.log(f),new Error(`Summary generation failed: ${o.status}`);g.debug("HOOK","Summary request sent successfully"),console.log(f)}var C="";k.on("data",s=>C+=s);k.on("end",async()=>{let s;try{s=C?JSON.parse(C):void 0}catch(t){throw new Error(`Failed to parse hook input: ${t instanceof Error?t.message:String(t)}`)}await Z(s)}); `).trim()),_}}if(!o)throw new Error(`No message found for role '${t}' in transcript: ${s}`);return""}async function Z(s){if(await $(),!s)throw new Error("summaryHook requires input");let{session_id:t}=s,r=O();if(!s.transcript_path)throw new Error(`Missing transcript_path in Stop hook input for session ${t}`);let e=m(s.transcript_path,"user"),n=m(s.transcript_path,"assistant",!0);g.dataIn("HOOK","Stop: Requesting summary",{workerPort:r,hasLastUserMessage:!!e,hasLastAssistantMessage:!!n});let o=await fetch(`http://127.0.0.1:${r}/api/sessions/summarize`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({contentSessionId:t,last_user_message:e,last_assistant_message:n}),signal:AbortSignal.timeout(u.DEFAULT)});if(!o.ok)throw console.log(f),new Error(`Summary generation failed: ${o.status}`);g.debug("HOOK","Summary request sent successfully"),console.log(f)}var C="";k.on("data",s=>C+=s);k.on("end",async()=>{let s;try{s=C?JSON.parse(C):void 0}catch(t){throw new Error(`Failed to parse hook input: ${t instanceof Error?t.message:String(t)}`)}await Z(s)});
+3 -3
View File
@@ -3,11 +3,11 @@ import{basename as z}from"path";import L from"path";import{homedir as K}from"os"
${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let r=Object.keys(t);return r.length===0?"{}":r.length<=3?JSON.stringify(t):`{${r.length} keys: ${r.slice(0,3).join(", ")}...}`}return String(t)}formatTool(t,r){if(!r)return t;let e=typeof r=="string"?JSON.parse(r):r;if(t==="Bash"&&e.command)return`${t}(${e.command})`;if(e.file_path)return`${t}(${e.file_path})`;if(e.notebook_path)return`${t}(${e.notebook_path})`;if(t==="Glob"&&e.pattern)return`${t}(${e.pattern})`;if(t==="Grep"&&e.pattern)return`${t}(${e.pattern})`;if(e.url)return`${t}(${e.url})`;if(e.query)return`${t}(${e.query})`;if(t==="Task"){if(e.subagent_type)return`${t}(${e.subagent_type})`;if(e.description)return`${t}(${e.description})`}return t==="Skill"&&e.skill?`${t}(${e.skill})`:t==="LSP"&&e.operation?`${t}(${e.operation})`:t}formatTimestamp(t){let r=t.getFullYear(),e=String(t.getMonth()+1).padStart(2,"0"),n=String(t.getDate()).padStart(2,"0"),o=String(t.getHours()).padStart(2,"0"),E=String(t.getMinutes()).padStart(2,"0"),s=String(t.getSeconds()).padStart(2,"0"),T=String(t.getMilliseconds()).padStart(3,"0");return`${r}-${e}-${n} ${o}:${E}:${s}.${T}`}log(t,r,e,n,o){if(t<this.getLevel())return;let E=this.formatTimestamp(new Date),s=S[t].padEnd(5),T=r.padEnd(6),a="";n?.correlationId?a=`[${n.correlationId}] `:n?.sessionId&&(a=`[session-${n.sessionId}] `);let l="";o!=null&&(o instanceof Error?l=this.getLevel()===0?` ${t.stack}`:t.message;if(Array.isArray(t))return`[${t.length} items]`;let r=Object.keys(t);return r.length===0?"{}":r.length<=3?JSON.stringify(t):`{${r.length} keys: ${r.slice(0,3).join(", ")}...}`}return String(t)}formatTool(t,r){if(!r)return t;let e=typeof r=="string"?JSON.parse(r):r;if(t==="Bash"&&e.command)return`${t}(${e.command})`;if(e.file_path)return`${t}(${e.file_path})`;if(e.notebook_path)return`${t}(${e.notebook_path})`;if(t==="Glob"&&e.pattern)return`${t}(${e.pattern})`;if(t==="Grep"&&e.pattern)return`${t}(${e.pattern})`;if(e.url)return`${t}(${e.url})`;if(e.query)return`${t}(${e.query})`;if(t==="Task"){if(e.subagent_type)return`${t}(${e.subagent_type})`;if(e.description)return`${t}(${e.description})`}return t==="Skill"&&e.skill?`${t}(${e.skill})`:t==="LSP"&&e.operation?`${t}(${e.operation})`:t}formatTimestamp(t){let r=t.getFullYear(),e=String(t.getMonth()+1).padStart(2,"0"),n=String(t.getDate()).padStart(2,"0"),o=String(t.getHours()).padStart(2,"0"),E=String(t.getMinutes()).padStart(2,"0"),s=String(t.getSeconds()).padStart(2,"0"),T=String(t.getMilliseconds()).padStart(3,"0");return`${r}-${e}-${n} ${o}:${E}:${s}.${T}`}log(t,r,e,n,o){if(t<this.getLevel())return;let E=this.formatTimestamp(new Date),s=S[t].padEnd(5),T=r.padEnd(6),a="";n?.correlationId?a=`[${n.correlationId}] `:n?.sessionId&&(a=`[session-${n.sessionId}] `);let l="";o!=null&&(o instanceof Error?l=this.getLevel()===0?`
${o.message} ${o.message}
${o.stack}`:` ${o.message}`:this.getLevel()===0&&typeof o=="object"?l=` ${o.stack}`:` ${o.message}`:this.getLevel()===0&&typeof o=="object"?l=`
`+JSON.stringify(o,null,2):l=" "+this.formatData(o));let u="";if(n){let{sessionId:D,sdkSessionId:Z,correlationId:tt,...m}=n;Object.keys(m).length>0&&(u=` {${Object.entries(m).map(([P,k])=>`${P}=${k}`).join(", ")}}`)}let C=`[${E}] [${s}] [${T}] ${a}${e}${u}${l}`;if(this.logFilePath)try{x(this.logFilePath,C+` `+JSON.stringify(o,null,2):l=" "+this.formatData(o));let u="";if(n){let{sessionId:D,memorySessionId:Z,correlationId:tt,...m}=n;Object.keys(m).length>0&&(u=` {${Object.entries(m).map(([P,k])=>`${P}=${k}`).join(", ")}}`)}let C=`[${E}] [${s}] [${T}] ${a}${e}${u}${l}`;if(this.logFilePath)try{x(this.logFilePath,C+`
`,"utf8")}catch(D){process.stderr.write(`[LOGGER] Failed to write to log file: ${D} `,"utf8")}catch(D){process.stderr.write(`[LOGGER] Failed to write to log file: ${D}
`)}else process.stderr.write(C+` `)}else process.stderr.write(C+`
`)}debug(t,r,e,n){this.log(0,t,r,e,n)}info(t,r,e,n){this.log(1,t,r,e,n)}warn(t,r,e,n){this.log(2,t,r,e,n)}error(t,r,e,n){this.log(3,t,r,e,n)}dataIn(t,r,e,n){this.info(t,`\u2192 ${r}`,e,n)}dataOut(t,r,e,n){this.info(t,`\u2190 ${r}`,e,n)}success(t,r,e,n){this.info(t,`\u2713 ${r}`,e,n)}failure(t,r,e,n){this.error(t,`\u2717 ${r}`,e,n)}timing(t,r,e,n){this.info(t,`\u23F1 ${r}`,n,{duration:`${e}ms`})}happyPathError(t,r,e,n,o=""){let a=((new Error().stack||"").split(` `)}debug(t,r,e,n){this.log(0,t,r,e,n)}info(t,r,e,n){this.log(1,t,r,e,n)}warn(t,r,e,n){this.log(2,t,r,e,n)}error(t,r,e,n){this.log(3,t,r,e,n)}dataIn(t,r,e,n){this.info(t,`\u2192 ${r}`,e,n)}dataOut(t,r,e,n){this.info(t,`\u2190 ${r}`,e,n)}success(t,r,e,n){this.info(t,`\u2713 ${r}`,e,n)}failure(t,r,e,n){this.error(t,`\u2717 ${r}`,e,n)}timing(t,r,e,n){this.info(t,`\u23F1 ${r}`,n,{duration:`${e}ms`})}happyPathError(t,r,e,n,o=""){let a=((new Error().stack||"").split(`
`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),l=a?`${a[1].split("/").pop()}:${a[2]}`:"unknown",u={...e,location:l};return this.warn(t,`[HAPPY-PATH] ${r}`,u,n),o}},c=new p;var A={DEFAULT:3e5,HEALTH_CHECK:3e4,WORKER_STARTUP_WAIT:1e3,WORKER_STARTUP_RETRIES:300,PRE_RESTART_SETTLE_DELAY:2e3,WINDOWS_MULTIPLIER:1.5},h={SUCCESS:0,FAILURE:1,USER_MESSAGE_ONLY:3};function I(i){return process.platform==="win32"?Math.round(i*A.WINDOWS_MULTIPLIER):i}function d(i={}){let{port:t,includeSkillFallback:r=!1,customPrefix:e,actualError:n}=i,o=e||"Worker service connection failed.",E=t?` (port ${t})`:"",s=`${o}${E} `)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),l=a?`${a[1].split("/").pop()}:${a[2]}`:"unknown",u={...e,location:l};return this.warn(t,`[HAPPY-PATH] ${r}`,u,n),o}},c=new p;var A={DEFAULT:3e5,HEALTH_CHECK:3e4,WORKER_STARTUP_WAIT:1e3,WORKER_STARTUP_RETRIES:300,PRE_RESTART_SETTLE_DELAY:2e3,WINDOWS_MULTIPLIER:1.5},h={SUCCESS:0,FAILURE:1,USER_MESSAGE_ONLY:3};function I(i){return process.platform==="win32"?Math.round(i*A.WINDOWS_MULTIPLIER):i}function N(i={}){let{port:t,includeSkillFallback:r=!1,customPrefix:e,actualError:n}=i,o=e||"Worker service connection failed.",E=t?` (port ${t})`:"",s=`${o}${E}
`;return s+=`To restart the worker: `;return s+=`To restart the worker:
`,s+=`1. Exit Claude Code completely `,s+=`1. Exit Claude Code completely
@@ -16,7 +16,7 @@ ${o.stack}`:` ${o.message}`:this.getLevel()===0&&typeof o=="object"?l=`
If that doesn't work, try: /troubleshoot`),n&&(s=`Worker Error: ${n} If that doesn't work, try: /troubleshoot`),n&&(s=`Worker Error: ${n}
${s}`),s}var V=L.join(K(),".claude","plugins","marketplaces","thedotmack"),N=I(A.HEALTH_CHECK),M=null;function g(){if(M!==null)return M;let i=L.join(_.get("CLAUDE_MEM_DATA_DIR"),"settings.json"),t=_.loadFromFile(i);return M=parseInt(t.CLAUDE_MEM_WORKER_PORT,10),M}async function j(){let i=g();return(await fetch(`http://127.0.0.1:${i}/api/readiness`,{signal:AbortSignal.timeout(N)})).ok}function B(){let i=L.join(V,"package.json");return JSON.parse(X(i,"utf-8")).version}async function Y(){let i=g(),t=await fetch(`http://127.0.0.1:${i}/api/version`,{signal:AbortSignal.timeout(N)});if(!t.ok)throw new Error(`Failed to get worker version: ${t.status}`);return(await t.json()).version}async function J(){let i=B(),t=await Y();i!==t&&c.warn("SYSTEM","Worker version mismatch",{pluginVersion:i,workerVersion:t,hint:"Restart worker with: claude-mem worker restart"})}async function y(){for(let r=0;r<25;r++){try{if(await j()){await J();return}}catch{}await new Promise(e=>setTimeout(e,200))}throw new Error(d({port:g(),customPrefix:"Worker did not become ready within 5 seconds."}))}await y();var $=g(),q=z(process.cwd()),f=await fetch(`http://127.0.0.1:${$}/api/context/inject?project=${encodeURIComponent(q)}&colors=true`,{method:"GET",signal:AbortSignal.timeout(5e3)});if(!f.ok)throw new Error(`Failed to fetch context: ${f.status}`);var Q=await f.text();console.error(` ${s}`),s}var V=L.join(K(),".claude","plugins","marketplaces","thedotmack"),d=I(A.HEALTH_CHECK),M=null;function g(){if(M!==null)return M;let i=L.join(_.get("CLAUDE_MEM_DATA_DIR"),"settings.json"),t=_.loadFromFile(i);return M=parseInt(t.CLAUDE_MEM_WORKER_PORT,10),M}async function j(){let i=g();return(await fetch(`http://127.0.0.1:${i}/api/readiness`,{signal:AbortSignal.timeout(d)})).ok}function B(){let i=L.join(V,"package.json");return JSON.parse(X(i,"utf-8")).version}async function Y(){let i=g(),t=await fetch(`http://127.0.0.1:${i}/api/version`,{signal:AbortSignal.timeout(d)});if(!t.ok)throw new Error(`Failed to get worker version: ${t.status}`);return(await t.json()).version}async function J(){let i=B(),t=await Y();i!==t&&c.warn("SYSTEM","Worker version mismatch",{pluginVersion:i,workerVersion:t,hint:"Restart worker with: claude-mem worker restart"})}async function y(){for(let r=0;r<25;r++){try{if(await j()){await J();return}}catch{}await new Promise(e=>setTimeout(e,200))}throw new Error(N({port:g(),customPrefix:"Worker did not become ready within 5 seconds."}))}await y();var $=g(),q=z(process.cwd()),f=await fetch(`http://127.0.0.1:${$}/api/context/inject?project=${encodeURIComponent(q)}&colors=true`,{method:"GET",signal:AbortSignal.timeout(5e3)});if(!f.ok)throw new Error(`Failed to fetch context: ${f.status}`);var Q=await f.text();console.error(`
\u{1F4DD} Claude-Mem Context Loaded \u{1F4DD} Claude-Mem Context Loaded
\u2139\uFE0F Note: This appears as stderr but is informational only \u2139\uFE0F Note: This appears as stderr but is informational only
File diff suppressed because one or more lines are too long
+17 -17
View File
@@ -216,18 +216,18 @@ function main() {
// Try to find existing session first // Try to find existing session first
const existingQuery = db['db'].prepare(` const existingQuery = db['db'].prepare(`
SELECT sdk_session_id SELECT memory_session_id
FROM sdk_sessions FROM sdk_sessions
WHERE claude_session_id = ? WHERE content_session_id = ?
`); `);
const existing = existingQuery.get(sessionMeta.sessionId) as { sdk_session_id: string | null } | undefined; const existing = existingQuery.get(sessionMeta.sessionId) as { memory_session_id: string | null } | undefined;
if (existing && existing.sdk_session_id) { if (existing && existing.memory_session_id) {
// Use existing SDK session ID // Use existing SDK session ID
claudeSessionToSdkSession.set(sessionMeta.sessionId, existing.sdk_session_id); claudeSessionToSdkSession.set(sessionMeta.sessionId, existing.memory_session_id);
} else if (existing && !existing.sdk_session_id) { } else if (existing && !existing.memory_session_id) {
// Session exists but sdk_session_id is NULL, update it // Session exists but memory_session_id is NULL, update it
db['db'].prepare('UPDATE sdk_sessions SET sdk_session_id = ? WHERE claude_session_id = ?') db['db'].prepare('UPDATE sdk_sessions SET memory_session_id = ? WHERE content_session_id = ?')
.run(syntheticSdkSessionId, sessionMeta.sessionId); .run(syntheticSdkSessionId, sessionMeta.sessionId);
claudeSessionToSdkSession.set(sessionMeta.sessionId, syntheticSdkSessionId); claudeSessionToSdkSession.set(sessionMeta.sessionId, syntheticSdkSessionId);
} else { } else {
@@ -239,7 +239,7 @@ function main() {
); );
// Update with synthetic SDK session ID // Update with synthetic SDK session ID
db['db'].prepare('UPDATE sdk_sessions SET sdk_session_id = ? WHERE claude_session_id = ?') db['db'].prepare('UPDATE sdk_sessions SET memory_session_id = ? WHERE content_session_id = ?')
.run(syntheticSdkSessionId, sessionMeta.sessionId); .run(syntheticSdkSessionId, sessionMeta.sessionId);
claudeSessionToSdkSession.set(sessionMeta.sessionId, syntheticSdkSessionId); claudeSessionToSdkSession.set(sessionMeta.sessionId, syntheticSdkSessionId);
@@ -289,8 +289,8 @@ function main() {
} }
// Get SDK session ID // Get SDK session ID
const sdkSessionId = claudeSessionToSdkSession.get(sessionMeta.sessionId); const memorySessionId = claudeSessionToSdkSession.get(sessionMeta.sessionId);
if (!sdkSessionId) { if (!memorySessionId) {
skipped++; skipped++;
continue; continue;
} }
@@ -301,8 +301,8 @@ function main() {
// Check for duplicate // Check for duplicate
const existingObs = db['db'].prepare(` const existingObs = db['db'].prepare(`
SELECT id FROM observations SELECT id FROM observations
WHERE sdk_session_id = ? AND title = ? AND subtitle = ? AND type = ? WHERE memory_session_id = ? AND title = ? AND subtitle = ? AND type = ?
`).get(sdkSessionId, observation.title, observation.subtitle, observation.type); `).get(memorySessionId, observation.title, observation.subtitle, observation.type);
if (existingObs) { if (existingObs) {
duplicateObs++; duplicateObs++;
@@ -311,7 +311,7 @@ function main() {
try { try {
db.storeObservation( db.storeObservation(
sdkSessionId, memorySessionId,
sessionMeta.project, sessionMeta.project,
observation observation
); );
@@ -333,8 +333,8 @@ function main() {
// Check for duplicate // Check for duplicate
const existingSum = db['db'].prepare(` const existingSum = db['db'].prepare(`
SELECT id FROM session_summaries SELECT id FROM session_summaries
WHERE sdk_session_id = ? AND request = ? AND completed = ? AND learned = ? WHERE memory_session_id = ? AND request = ? AND completed = ? AND learned = ?
`).get(sdkSessionId, summary.request, summary.completed, summary.learned); `).get(memorySessionId, summary.request, summary.completed, summary.learned);
if (existingSum) { if (existingSum) {
duplicateSum++; duplicateSum++;
@@ -343,7 +343,7 @@ function main() {
try { try {
db.storeSummary( db.storeSummary(
sdkSessionId, memorySessionId,
sessionMeta.project, sessionMeta.project,
summary summary
); );
+2 -2
View File
@@ -29,14 +29,14 @@ async function newHook(input?: UserPromptSubmitInput): Promise<void> {
const port = getWorkerPort(); const port = getWorkerPort();
logger.info('HOOK', 'new-hook: Calling /api/sessions/init', { claudeSessionId: session_id, project, prompt_length: prompt?.length }); logger.info('HOOK', 'new-hook: Calling /api/sessions/init', { contentSessionId: session_id, project, prompt_length: prompt?.length });
// Initialize session via HTTP - handles DB operations and privacy checks // Initialize session via HTTP - handles DB operations and privacy checks
const initResponse = await fetch(`http://127.0.0.1:${port}/api/sessions/init`, { const initResponse = await fetch(`http://127.0.0.1:${port}/api/sessions/init`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
claudeSessionId: session_id, contentSessionId: session_id,
project, project,
prompt prompt
}), }),
+1 -1
View File
@@ -51,7 +51,7 @@ async function saveHook(input?: PostToolUseInput): Promise<void> {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
claudeSessionId: session_id, contentSessionId: session_id,
tool_name, tool_name,
tool_input, tool_input,
tool_response, tool_response,
+1 -1
View File
@@ -57,7 +57,7 @@ async function summaryHook(input?: StopInput): Promise<void> {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
claudeSessionId: session_id, contentSessionId: session_id,
last_user_message: lastUserMessage, last_user_message: lastUserMessage,
last_assistant_message: lastAssistantMessage last_assistant_message: lastAssistantMessage
}), }),
+5 -5
View File
@@ -17,7 +17,7 @@ export interface Observation {
export interface SDKSession { export interface SDKSession {
id: number; id: number;
sdk_session_id: string | null; memory_session_id: string | null;
project: string; project: string;
user_prompt: string; user_prompt: string;
last_user_message?: string; last_user_message?: string;
@@ -148,14 +148,14 @@ ${mode.prompts.summary_footer}`;
/** /**
* Build prompt for continuation of existing session * Build prompt for continuation of existing session
* *
* CRITICAL: Why claudeSessionId Parameter is Required * CRITICAL: Why contentSessionId Parameter is Required
* ==================================================== * ====================================================
* This function receives claudeSessionId from SDKAgent.ts, which comes from: * This function receives contentSessionId from SDKAgent.ts, which comes from:
* - SessionManager.initializeSession (fetched from database) * - SessionManager.initializeSession (fetched from database)
* - SessionStore.createSDKSession (stored by new-hook.ts) * - SessionStore.createSDKSession (stored by new-hook.ts)
* - new-hook.ts receives it from Claude Code's hook context * - new-hook.ts receives it from Claude Code's hook context
* *
* The claudeSessionId is the SAME session_id used by: * The contentSessionId is the SAME session_id used by:
* - NEW hook (to create/fetch session) * - NEW hook (to create/fetch session)
* - SAVE hook (to store observations) * - SAVE hook (to store observations)
* - This continuation prompt (to maintain session context) * - This continuation prompt (to maintain session context)
@@ -166,7 +166,7 @@ ${mode.prompts.summary_footer}`;
* Called when: promptNumber > 1 (see SDKAgent.ts line 150) * Called when: promptNumber > 1 (see SDKAgent.ts line 150)
* First prompt: Uses buildInitPrompt instead (promptNumber === 1) * First prompt: Uses buildInitPrompt instead (promptNumber === 1)
*/ */
export function buildContinuationPrompt(userPrompt: string, promptNumber: number, claudeSessionId: string, mode: ModeConfig): string { export function buildContinuationPrompt(userPrompt: string, promptNumber: number, contentSessionId: string, mode: ModeConfig): string {
return `${mode.prompts.continuation_greeting} return `${mode.prompts.continuation_greeting}
<observed_from_primary_session> <observed_from_primary_session>
+10 -10
View File
@@ -8,7 +8,7 @@ import { logger } from '../../utils/logger.js';
export interface PersistentPendingMessage { export interface PersistentPendingMessage {
id: number; id: number;
session_db_id: number; session_db_id: number;
claude_session_id: string; content_session_id: string;
message_type: 'observation' | 'summarize'; message_type: 'observation' | 'summarize';
tool_name: string | null; tool_name: string | null;
tool_input: string | null; tool_input: string | null;
@@ -53,11 +53,11 @@ export class PendingMessageStore {
* Enqueue a new message (persist before processing) * Enqueue a new message (persist before processing)
* @returns The database ID of the persisted message * @returns The database ID of the persisted message
*/ */
enqueue(sessionDbId: number, claudeSessionId: string, message: PendingMessage): number { enqueue(sessionDbId: number, contentSessionId: string, message: PendingMessage): number {
const now = Date.now(); const now = Date.now();
const stmt = this.db.prepare(` const stmt = this.db.prepare(`
INSERT INTO pending_messages ( INSERT INTO pending_messages (
session_db_id, claude_session_id, message_type, session_db_id, content_session_id, message_type,
tool_name, tool_input, tool_response, cwd, tool_name, tool_input, tool_response, cwd,
last_user_message, last_assistant_message, last_user_message, last_assistant_message,
prompt_number, status, retry_count, created_at_epoch prompt_number, status, retry_count, created_at_epoch
@@ -66,7 +66,7 @@ export class PendingMessageStore {
const result = stmt.run( const result = stmt.run(
sessionDbId, sessionDbId,
claudeSessionId, contentSessionId,
message.type, message.type,
message.tool_name || null, message.tool_name || null,
message.tool_input ? JSON.stringify(message.tool_input) : null, message.tool_input ? JSON.stringify(message.tool_input) : null,
@@ -140,7 +140,7 @@ export class PendingMessageStore {
const stmt = this.db.prepare(` const stmt = this.db.prepare(`
SELECT pm.*, ss.project SELECT pm.*, ss.project
FROM pending_messages pm FROM pending_messages pm
LEFT JOIN sdk_sessions ss ON pm.claude_session_id = ss.claude_session_id LEFT JOIN sdk_sessions ss ON pm.content_session_id = ss.content_session_id
WHERE pm.status IN ('pending', 'processing', 'failed') WHERE pm.status IN ('pending', 'processing', 'failed')
ORDER BY ORDER BY
CASE pm.status CASE pm.status
@@ -226,7 +226,7 @@ export class PendingMessageStore {
const stmt = this.db.prepare(` const stmt = this.db.prepare(`
SELECT pm.*, ss.project SELECT pm.*, ss.project
FROM pending_messages pm FROM pending_messages pm
LEFT JOIN sdk_sessions ss ON pm.claude_session_id = ss.claude_session_id LEFT JOIN sdk_sessions ss ON pm.content_session_id = ss.content_session_id
WHERE pm.status = 'processed' AND pm.completed_at_epoch > ? WHERE pm.status = 'processed' AND pm.completed_at_epoch > ?
ORDER BY pm.completed_at_epoch DESC ORDER BY pm.completed_at_epoch DESC
LIMIT ? LIMIT ?
@@ -354,12 +354,12 @@ export class PendingMessageStore {
/** /**
* Get session info for a pending message (for recovery) * Get session info for a pending message (for recovery)
*/ */
getSessionInfoForMessage(messageId: number): { sessionDbId: number; claudeSessionId: string } | null { getSessionInfoForMessage(messageId: number): { sessionDbId: number; contentSessionId: string } | null {
const stmt = this.db.prepare(` const stmt = this.db.prepare(`
SELECT session_db_id, claude_session_id FROM pending_messages WHERE id = ? SELECT session_db_id, content_session_id FROM pending_messages WHERE id = ?
`); `);
const result = stmt.get(messageId) as { session_db_id: number; claude_session_id: string } | undefined; const result = stmt.get(messageId) as { session_db_id: number; content_session_id: string } | undefined;
return result ? { sessionDbId: result.session_db_id, claudeSessionId: result.claude_session_id } : null; return result ? { sessionDbId: result.session_db_id, contentSessionId: result.content_session_id } : null;
} }
/** /**
+6 -6
View File
@@ -481,7 +481,7 @@ export class SessionSearch {
const sql = ` const sql = `
SELECT up.* SELECT up.*
FROM user_prompts up FROM user_prompts up
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id JOIN sdk_sessions s ON up.content_session_id = s.content_session_id
${whereClause} ${whereClause}
${orderClause} ${orderClause}
LIMIT ? OFFSET ? LIMIT ? OFFSET ?
@@ -498,23 +498,23 @@ export class SessionSearch {
} }
/** /**
* Get all prompts for a session by claude_session_id * Get all prompts for a session by content_session_id
*/ */
getUserPromptsBySession(claudeSessionId: string): UserPromptRow[] { getUserPromptsBySession(contentSessionId: string): UserPromptRow[] {
const stmt = this.db.prepare(` const stmt = this.db.prepare(`
SELECT SELECT
id, id,
claude_session_id, content_session_id,
prompt_number, prompt_number,
prompt_text, prompt_text,
created_at, created_at,
created_at_epoch created_at_epoch
FROM user_prompts FROM user_prompts
WHERE claude_session_id = ? WHERE content_session_id = ?
ORDER BY prompt_number ASC ORDER BY prompt_number ASC
`); `);
return stmt.all(claudeSessionId) as UserPromptRow[]; return stmt.all(contentSessionId) as UserPromptRow[];
} }
/** /**
+189 -127
View File
@@ -43,6 +43,7 @@ export class SessionStore {
this.createUserPromptsTable(); this.createUserPromptsTable();
this.ensureDiscoveryTokensColumn(); this.ensureDiscoveryTokensColumn();
this.createPendingMessagesTable(); this.createPendingMessagesTable();
this.renameSessionIdColumns();
} }
/** /**
@@ -73,8 +74,8 @@ export class SessionStore {
this.db.run(` this.db.run(`
CREATE TABLE IF NOT EXISTS sdk_sessions ( CREATE TABLE IF NOT EXISTS sdk_sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
claude_session_id TEXT UNIQUE NOT NULL, content_session_id TEXT UNIQUE NOT NULL,
sdk_session_id TEXT UNIQUE, memory_session_id TEXT UNIQUE,
project TEXT NOT NULL, project TEXT NOT NULL,
user_prompt TEXT, user_prompt TEXT,
started_at TEXT NOT NULL, started_at TEXT NOT NULL,
@@ -84,31 +85,31 @@ export class SessionStore {
status TEXT CHECK(status IN ('active', 'completed', 'failed')) NOT NULL DEFAULT 'active' status TEXT CHECK(status IN ('active', 'completed', 'failed')) NOT NULL DEFAULT 'active'
); );
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_claude_id ON sdk_sessions(claude_session_id); CREATE INDEX IF NOT EXISTS idx_sdk_sessions_claude_id ON sdk_sessions(content_session_id);
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_sdk_id ON sdk_sessions(sdk_session_id); CREATE INDEX IF NOT EXISTS idx_sdk_sessions_sdk_id ON sdk_sessions(memory_session_id);
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_project ON sdk_sessions(project); CREATE INDEX IF NOT EXISTS idx_sdk_sessions_project ON sdk_sessions(project);
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_status ON sdk_sessions(status); CREATE INDEX IF NOT EXISTS idx_sdk_sessions_status ON sdk_sessions(status);
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_started ON sdk_sessions(started_at_epoch DESC); CREATE INDEX IF NOT EXISTS idx_sdk_sessions_started ON sdk_sessions(started_at_epoch DESC);
CREATE TABLE IF NOT EXISTS observations ( CREATE TABLE IF NOT EXISTS observations (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
sdk_session_id TEXT NOT NULL, memory_session_id TEXT NOT NULL,
project TEXT NOT NULL, project TEXT NOT NULL,
text TEXT NOT NULL, text TEXT NOT NULL,
type TEXT NOT NULL CHECK(type IN ('decision', 'bugfix', 'feature', 'refactor', 'discovery')), type TEXT NOT NULL CHECK(type IN ('decision', 'bugfix', 'feature', 'refactor', 'discovery')),
created_at TEXT NOT NULL, created_at TEXT NOT NULL,
created_at_epoch INTEGER NOT NULL, created_at_epoch INTEGER NOT NULL,
FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id) ON DELETE CASCADE FOREIGN KEY(memory_session_id) REFERENCES sdk_sessions(memory_session_id) ON DELETE CASCADE
); );
CREATE INDEX IF NOT EXISTS idx_observations_sdk_session ON observations(sdk_session_id); CREATE INDEX IF NOT EXISTS idx_observations_sdk_session ON observations(memory_session_id);
CREATE INDEX IF NOT EXISTS idx_observations_project ON observations(project); CREATE INDEX IF NOT EXISTS idx_observations_project ON observations(project);
CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type); CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type);
CREATE INDEX IF NOT EXISTS idx_observations_created ON observations(created_at_epoch DESC); CREATE INDEX IF NOT EXISTS idx_observations_created ON observations(created_at_epoch DESC);
CREATE TABLE IF NOT EXISTS session_summaries ( CREATE TABLE IF NOT EXISTS session_summaries (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
sdk_session_id TEXT UNIQUE NOT NULL, memory_session_id TEXT UNIQUE NOT NULL,
project TEXT NOT NULL, project TEXT NOT NULL,
request TEXT, request TEXT,
investigated TEXT, investigated TEXT,
@@ -120,10 +121,10 @@ export class SessionStore {
notes TEXT, notes TEXT,
created_at TEXT NOT NULL, created_at TEXT NOT NULL,
created_at_epoch INTEGER NOT NULL, created_at_epoch INTEGER NOT NULL,
FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id) ON DELETE CASCADE FOREIGN KEY(memory_session_id) REFERENCES sdk_sessions(memory_session_id) ON DELETE CASCADE
); );
CREATE INDEX IF NOT EXISTS idx_session_summaries_sdk_session ON session_summaries(sdk_session_id); CREATE INDEX IF NOT EXISTS idx_session_summaries_sdk_session ON session_summaries(memory_session_id);
CREATE INDEX IF NOT EXISTS idx_session_summaries_project ON session_summaries(project); CREATE INDEX IF NOT EXISTS idx_session_summaries_project ON session_summaries(project);
CREATE INDEX IF NOT EXISTS idx_session_summaries_created ON session_summaries(created_at_epoch DESC); CREATE INDEX IF NOT EXISTS idx_session_summaries_created ON session_summaries(created_at_epoch DESC);
`); `);
@@ -200,7 +201,7 @@ export class SessionStore {
} }
/** /**
* Remove UNIQUE constraint from session_summaries.sdk_session_id (migration 7) * Remove UNIQUE constraint from session_summaries.memory_session_id (migration 7)
*/ */
private removeSessionSummariesUniqueConstraint(): void { private removeSessionSummariesUniqueConstraint(): void {
// Check if migration already applied // Check if migration already applied
@@ -217,7 +218,7 @@ export class SessionStore {
return; return;
} }
logger.info('DB', 'Removing UNIQUE constraint from session_summaries.sdk_session_id'); logger.info('DB', 'Removing UNIQUE constraint from session_summaries.memory_session_id');
// Begin transaction // Begin transaction
this.db.run('BEGIN TRANSACTION'); this.db.run('BEGIN TRANSACTION');
@@ -227,7 +228,7 @@ export class SessionStore {
this.db.run(` this.db.run(`
CREATE TABLE session_summaries_new ( CREATE TABLE session_summaries_new (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
sdk_session_id TEXT NOT NULL, memory_session_id TEXT NOT NULL,
project TEXT NOT NULL, project TEXT NOT NULL,
request TEXT, request TEXT,
investigated TEXT, investigated TEXT,
@@ -240,14 +241,14 @@ export class SessionStore {
prompt_number INTEGER, prompt_number INTEGER,
created_at TEXT NOT NULL, created_at TEXT NOT NULL,
created_at_epoch INTEGER NOT NULL, created_at_epoch INTEGER NOT NULL,
FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id) ON DELETE CASCADE FOREIGN KEY(memory_session_id) REFERENCES sdk_sessions(memory_session_id) ON DELETE CASCADE
) )
`); `);
// Copy data from old table // Copy data from old table
this.db.run(` this.db.run(`
INSERT INTO session_summaries_new INSERT INTO session_summaries_new
SELECT id, sdk_session_id, project, request, investigated, learned, SELECT id, memory_session_id, project, request, investigated, learned,
completed, next_steps, files_read, files_edited, notes, completed, next_steps, files_read, files_edited, notes,
prompt_number, created_at, created_at_epoch prompt_number, created_at, created_at_epoch
FROM session_summaries FROM session_summaries
@@ -261,7 +262,7 @@ export class SessionStore {
// Recreate indexes // Recreate indexes
this.db.run(` this.db.run(`
CREATE INDEX idx_session_summaries_sdk_session ON session_summaries(sdk_session_id); CREATE INDEX idx_session_summaries_sdk_session ON session_summaries(memory_session_id);
CREATE INDEX idx_session_summaries_project ON session_summaries(project); CREATE INDEX idx_session_summaries_project ON session_summaries(project);
CREATE INDEX idx_session_summaries_created ON session_summaries(created_at_epoch DESC); CREATE INDEX idx_session_summaries_created ON session_summaries(created_at_epoch DESC);
`); `);
@@ -272,7 +273,7 @@ export class SessionStore {
// Record migration // Record migration
this.db.prepare('INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)').run(7, new Date().toISOString()); this.db.prepare('INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)').run(7, new Date().toISOString());
logger.info('DB', 'Successfully removed UNIQUE constraint from session_summaries.sdk_session_id'); logger.info('DB', 'Successfully removed UNIQUE constraint from session_summaries.memory_session_id');
} catch (error: any) { } catch (error: any) {
// Rollback on error // Rollback on error
this.db.run('ROLLBACK'); this.db.run('ROLLBACK');
@@ -346,7 +347,7 @@ export class SessionStore {
this.db.run(` this.db.run(`
CREATE TABLE observations_new ( CREATE TABLE observations_new (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
sdk_session_id TEXT NOT NULL, memory_session_id TEXT NOT NULL,
project TEXT NOT NULL, project TEXT NOT NULL,
text TEXT, text TEXT,
type TEXT NOT NULL CHECK(type IN ('decision', 'bugfix', 'feature', 'refactor', 'discovery', 'change')), type TEXT NOT NULL CHECK(type IN ('decision', 'bugfix', 'feature', 'refactor', 'discovery', 'change')),
@@ -360,14 +361,14 @@ export class SessionStore {
prompt_number INTEGER, prompt_number INTEGER,
created_at TEXT NOT NULL, created_at TEXT NOT NULL,
created_at_epoch INTEGER NOT NULL, created_at_epoch INTEGER NOT NULL,
FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id) ON DELETE CASCADE FOREIGN KEY(memory_session_id) REFERENCES sdk_sessions(memory_session_id) ON DELETE CASCADE
) )
`); `);
// Copy data from old table (all existing columns) // Copy data from old table (all existing columns)
this.db.run(` this.db.run(`
INSERT INTO observations_new INSERT INTO observations_new
SELECT id, sdk_session_id, project, text, type, title, subtitle, facts, SELECT id, memory_session_id, project, text, type, title, subtitle, facts,
narrative, concepts, files_read, files_modified, prompt_number, narrative, concepts, files_read, files_modified, prompt_number,
created_at, created_at_epoch created_at, created_at_epoch
FROM observations FROM observations
@@ -381,7 +382,7 @@ export class SessionStore {
// Recreate indexes // Recreate indexes
this.db.run(` this.db.run(`
CREATE INDEX idx_observations_sdk_session ON observations(sdk_session_id); CREATE INDEX idx_observations_sdk_session ON observations(memory_session_id);
CREATE INDEX idx_observations_project ON observations(project); CREATE INDEX idx_observations_project ON observations(project);
CREATE INDEX idx_observations_type ON observations(type); CREATE INDEX idx_observations_type ON observations(type);
CREATE INDEX idx_observations_created ON observations(created_at_epoch DESC); CREATE INDEX idx_observations_created ON observations(created_at_epoch DESC);
@@ -423,22 +424,22 @@ export class SessionStore {
this.db.run('BEGIN TRANSACTION'); this.db.run('BEGIN TRANSACTION');
try { try {
// Create main table (using claude_session_id since sdk_session_id is set asynchronously by worker) // Create main table (using content_session_id since memory_session_id is set asynchronously by worker)
this.db.run(` this.db.run(`
CREATE TABLE user_prompts ( CREATE TABLE user_prompts (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
claude_session_id TEXT NOT NULL, content_session_id TEXT NOT NULL,
prompt_number INTEGER NOT NULL, prompt_number INTEGER NOT NULL,
prompt_text TEXT NOT NULL, prompt_text TEXT NOT NULL,
created_at TEXT NOT NULL, created_at TEXT NOT NULL,
created_at_epoch INTEGER NOT NULL, created_at_epoch INTEGER NOT NULL,
FOREIGN KEY(claude_session_id) REFERENCES sdk_sessions(claude_session_id) ON DELETE CASCADE FOREIGN KEY(content_session_id) REFERENCES sdk_sessions(content_session_id) ON DELETE CASCADE
); );
CREATE INDEX idx_user_prompts_claude_session ON user_prompts(claude_session_id); CREATE INDEX idx_user_prompts_claude_session ON user_prompts(content_session_id);
CREATE INDEX idx_user_prompts_created ON user_prompts(created_at_epoch DESC); CREATE INDEX idx_user_prompts_created ON user_prompts(created_at_epoch DESC);
CREATE INDEX idx_user_prompts_prompt_number ON user_prompts(prompt_number); CREATE INDEX idx_user_prompts_prompt_number ON user_prompts(prompt_number);
CREATE INDEX idx_user_prompts_lookup ON user_prompts(claude_session_id, prompt_number); CREATE INDEX idx_user_prompts_lookup ON user_prompts(content_session_id, prompt_number);
`); `);
// Create FTS5 virtual table // Create FTS5 virtual table
@@ -545,7 +546,7 @@ export class SessionStore {
CREATE TABLE pending_messages ( CREATE TABLE pending_messages (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
session_db_id INTEGER NOT NULL, session_db_id INTEGER NOT NULL,
claude_session_id TEXT NOT NULL, content_session_id TEXT NOT NULL,
message_type TEXT NOT NULL CHECK(message_type IN ('observation', 'summarize')), message_type TEXT NOT NULL CHECK(message_type IN ('observation', 'summarize')),
tool_name TEXT, tool_name TEXT,
tool_input TEXT, tool_input TEXT,
@@ -565,7 +566,7 @@ export class SessionStore {
this.db.run('CREATE INDEX IF NOT EXISTS idx_pending_messages_session ON pending_messages(session_db_id)'); this.db.run('CREATE INDEX IF NOT EXISTS idx_pending_messages_session ON pending_messages(session_db_id)');
this.db.run('CREATE INDEX IF NOT EXISTS idx_pending_messages_status ON pending_messages(status)'); this.db.run('CREATE INDEX IF NOT EXISTS idx_pending_messages_status ON pending_messages(status)');
this.db.run('CREATE INDEX IF NOT EXISTS idx_pending_messages_claude_session ON pending_messages(claude_session_id)'); this.db.run('CREATE INDEX IF NOT EXISTS idx_pending_messages_claude_session ON pending_messages(content_session_id)');
this.db.prepare('INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)').run(16, new Date().toISOString()); this.db.prepare('INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)').run(16, new Date().toISOString());
@@ -576,6 +577,67 @@ export class SessionStore {
} }
} }
/**
* Rename session ID columns for semantic clarity (migration 17)
* - claude_session_id content_session_id (user's observed session)
* - sdk_session_id memory_session_id (memory agent's session for resume)
*/
private renameSessionIdColumns(): void {
try {
const applied = this.db.prepare('SELECT version FROM schema_versions WHERE version = ?').get(17) as SchemaVersion | undefined;
if (applied) return;
logger.info('DB', 'Renaming session ID columns for semantic clarity');
// Check if columns are already renamed (idempotent check)
const sessionsInfo = this.db.query('PRAGMA table_info(sdk_sessions)').all() as TableColumnInfo[];
const hasContentSessionId = sessionsInfo.some(col => col.name === 'content_session_id');
if (hasContentSessionId) {
// Already renamed, just record migration
this.db.prepare('INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)').run(17, new Date().toISOString());
return;
}
// SQLite 3.25+ supports ALTER TABLE RENAME COLUMN
// Rename in sdk_sessions table
this.db.run('ALTER TABLE sdk_sessions RENAME COLUMN claude_session_id TO content_session_id');
this.db.run('ALTER TABLE sdk_sessions RENAME COLUMN sdk_session_id TO memory_session_id');
// Rename in pending_messages table
this.db.run('ALTER TABLE pending_messages RENAME COLUMN claude_session_id TO content_session_id');
// Rename in observations table
this.db.run('ALTER TABLE observations RENAME COLUMN sdk_session_id TO memory_session_id');
// Rename in session_summaries table
this.db.run('ALTER TABLE session_summaries RENAME COLUMN sdk_session_id TO memory_session_id');
// Rename in user_prompts table
this.db.run('ALTER TABLE user_prompts RENAME COLUMN claude_session_id TO content_session_id');
// Record migration
this.db.prepare('INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)').run(17, new Date().toISOString());
logger.info('DB', 'Successfully renamed session ID columns');
} catch (error: any) {
logger.error('DB', 'Session ID column rename migration error', undefined, error);
throw error;
}
}
/**
* Update the memory session ID for a session
* Called by SDKAgent when it captures the session ID from the first SDK message
*/
updateMemorySessionId(sessionDbId: number, memorySessionId: string): void {
this.db.prepare(`
UPDATE sdk_sessions
SET memory_session_id = ?
WHERE id = ?
`).run(memorySessionId, sessionDbId);
}
/** /**
* Get recent session summaries for a project * Get recent session summaries for a project
*/ */
@@ -608,7 +670,7 @@ export class SessionStore {
* Get recent summaries with session info for context display * Get recent summaries with session info for context display
*/ */
getRecentSummariesWithSessionInfo(project: string, limit: number = 3): Array<{ getRecentSummariesWithSessionInfo(project: string, limit: number = 3): Array<{
sdk_session_id: string; memory_session_id: string;
request: string | null; request: string | null;
learned: string | null; learned: string | null;
completed: string | null; completed: string | null;
@@ -618,7 +680,7 @@ export class SessionStore {
}> { }> {
const stmt = this.db.prepare(` const stmt = this.db.prepare(`
SELECT SELECT
sdk_session_id, request, learned, completed, next_steps, memory_session_id, request, learned, completed, next_steps,
prompt_number, created_at prompt_number, created_at
FROM session_summaries FROM session_summaries
WHERE project = ? WHERE project = ?
@@ -708,7 +770,7 @@ export class SessionStore {
*/ */
getAllRecentUserPrompts(limit: number = 100): Array<{ getAllRecentUserPrompts(limit: number = 100): Array<{
id: number; id: number;
claude_session_id: string; content_session_id: string;
project: string; project: string;
prompt_number: number; prompt_number: number;
prompt_text: string; prompt_text: string;
@@ -718,14 +780,14 @@ export class SessionStore {
const stmt = this.db.prepare(` const stmt = this.db.prepare(`
SELECT SELECT
up.id, up.id,
up.claude_session_id, up.content_session_id,
s.project, s.project,
up.prompt_number, up.prompt_number,
up.prompt_text, up.prompt_text,
up.created_at, up.created_at,
up.created_at_epoch up.created_at_epoch
FROM user_prompts up FROM user_prompts up
LEFT JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id LEFT JOIN sdk_sessions s ON up.content_session_id = s.content_session_id
ORDER BY up.created_at_epoch DESC ORDER BY up.created_at_epoch DESC
LIMIT ? LIMIT ?
`); `);
@@ -752,10 +814,10 @@ export class SessionStore {
* Get latest user prompt with session info for a Claude session * Get latest user prompt with session info for a Claude session
* Used for syncing prompts to Chroma during session initialization * Used for syncing prompts to Chroma during session initialization
*/ */
getLatestUserPrompt(claudeSessionId: string): { getLatestUserPrompt(contentSessionId: string): {
id: number; id: number;
claude_session_id: string; content_session_id: string;
sdk_session_id: string; memory_session_id: string;
project: string; project: string;
prompt_number: number; prompt_number: number;
prompt_text: string; prompt_text: string;
@@ -764,23 +826,23 @@ export class SessionStore {
const stmt = this.db.prepare(` const stmt = this.db.prepare(`
SELECT SELECT
up.*, up.*,
s.sdk_session_id, s.memory_session_id,
s.project s.project
FROM user_prompts up FROM user_prompts up
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id JOIN sdk_sessions s ON up.content_session_id = s.content_session_id
WHERE up.claude_session_id = ? WHERE up.content_session_id = ?
ORDER BY up.created_at_epoch DESC ORDER BY up.created_at_epoch DESC
LIMIT 1 LIMIT 1
`); `);
return stmt.get(claudeSessionId) as LatestPromptResult | undefined; return stmt.get(contentSessionId) as LatestPromptResult | undefined;
} }
/** /**
* Get recent sessions with their status and summary info * Get recent sessions with their status and summary info
*/ */
getRecentSessionsWithStatus(project: string, limit: number = 3): Array<{ getRecentSessionsWithStatus(project: string, limit: number = 3): Array<{
sdk_session_id: string | null; memory_session_id: string | null;
status: string; status: string;
started_at: string; started_at: string;
user_prompt: string | null; user_prompt: string | null;
@@ -789,16 +851,16 @@ export class SessionStore {
const stmt = this.db.prepare(` const stmt = this.db.prepare(`
SELECT * FROM ( SELECT * FROM (
SELECT SELECT
s.sdk_session_id, s.memory_session_id,
s.status, s.status,
s.started_at, s.started_at,
s.started_at_epoch, s.started_at_epoch,
s.user_prompt, s.user_prompt,
CASE WHEN sum.sdk_session_id IS NOT NULL THEN 1 ELSE 0 END as has_summary CASE WHEN sum.memory_session_id IS NOT NULL THEN 1 ELSE 0 END as has_summary
FROM sdk_sessions s FROM sdk_sessions s
LEFT JOIN session_summaries sum ON s.sdk_session_id = sum.sdk_session_id LEFT JOIN session_summaries sum ON s.memory_session_id = sum.memory_session_id
WHERE s.project = ? AND s.sdk_session_id IS NOT NULL WHERE s.project = ? AND s.memory_session_id IS NOT NULL
GROUP BY s.sdk_session_id GROUP BY s.memory_session_id
ORDER BY s.started_at_epoch DESC ORDER BY s.started_at_epoch DESC
LIMIT ? LIMIT ?
) )
@@ -811,7 +873,7 @@ export class SessionStore {
/** /**
* Get observations for a specific session * Get observations for a specific session
*/ */
getObservationsForSession(sdkSessionId: string): Array<{ getObservationsForSession(memorySessionId: string): Array<{
title: string; title: string;
subtitle: string; subtitle: string;
type: string; type: string;
@@ -820,11 +882,11 @@ export class SessionStore {
const stmt = this.db.prepare(` const stmt = this.db.prepare(`
SELECT title, subtitle, type, prompt_number SELECT title, subtitle, type, prompt_number
FROM observations FROM observations
WHERE sdk_session_id = ? WHERE memory_session_id = ?
ORDER BY created_at_epoch ASC ORDER BY created_at_epoch ASC
`); `);
return stmt.all(sdkSessionId); return stmt.all(memorySessionId);
} }
/** /**
@@ -916,7 +978,7 @@ export class SessionStore {
/** /**
* Get summary for a specific session * Get summary for a specific session
*/ */
getSummaryForSession(sdkSessionId: string): { getSummaryForSession(memorySessionId: string): {
request: string | null; request: string | null;
investigated: string | null; investigated: string | null;
learned: string | null; learned: string | null;
@@ -935,28 +997,28 @@ export class SessionStore {
files_read, files_edited, notes, prompt_number, created_at, files_read, files_edited, notes, prompt_number, created_at,
created_at_epoch created_at_epoch
FROM session_summaries FROM session_summaries
WHERE sdk_session_id = ? WHERE memory_session_id = ?
ORDER BY created_at_epoch DESC ORDER BY created_at_epoch DESC
LIMIT 1 LIMIT 1
`); `);
return stmt.get(sdkSessionId) || null; return stmt.get(memorySessionId) || null;
} }
/** /**
* Get aggregated files from all observations for a session * Get aggregated files from all observations for a session
*/ */
getFilesForSession(sdkSessionId: string): { getFilesForSession(memorySessionId: string): {
filesRead: string[]; filesRead: string[];
filesModified: string[]; filesModified: string[];
} { } {
const stmt = this.db.prepare(` const stmt = this.db.prepare(`
SELECT files_read, files_modified SELECT files_read, files_modified
FROM observations FROM observations
WHERE sdk_session_id = ? WHERE memory_session_id = ?
`); `);
const rows = stmt.all(sdkSessionId) as Array<{ const rows = stmt.all(memorySessionId) as Array<{
files_read: string | null; files_read: string | null;
files_modified: string | null; files_modified: string | null;
}>; }>;
@@ -993,13 +1055,13 @@ export class SessionStore {
*/ */
getSessionById(id: number): { getSessionById(id: number): {
id: number; id: number;
claude_session_id: string; content_session_id: string;
sdk_session_id: string | null; memory_session_id: string | null;
project: string; project: string;
user_prompt: string; user_prompt: string;
} | null { } | null {
const stmt = this.db.prepare(` const stmt = this.db.prepare(`
SELECT id, claude_session_id, sdk_session_id, project, user_prompt SELECT id, content_session_id, memory_session_id, project, user_prompt
FROM sdk_sessions FROM sdk_sessions
WHERE id = ? WHERE id = ?
LIMIT 1 LIMIT 1
@@ -1012,10 +1074,10 @@ export class SessionStore {
* Get SDK sessions by SDK session IDs * Get SDK sessions by SDK session IDs
* Used for exporting session metadata * Used for exporting session metadata
*/ */
getSdkSessionsBySessionIds(sdkSessionIds: string[]): { getSdkSessionsBySessionIds(memorySessionIds: string[]): {
id: number; id: number;
claude_session_id: string; content_session_id: string;
sdk_session_id: string; memory_session_id: string;
project: string; project: string;
user_prompt: string; user_prompt: string;
started_at: string; started_at: string;
@@ -1024,18 +1086,18 @@ export class SessionStore {
completed_at_epoch: number | null; completed_at_epoch: number | null;
status: string; status: string;
}[] { }[] {
if (sdkSessionIds.length === 0) return []; if (memorySessionIds.length === 0) return [];
const placeholders = sdkSessionIds.map(() => '?').join(','); const placeholders = memorySessionIds.map(() => '?').join(',');
const stmt = this.db.prepare(` const stmt = this.db.prepare(`
SELECT id, claude_session_id, sdk_session_id, project, user_prompt, SELECT id, content_session_id, memory_session_id, project, user_prompt,
started_at, started_at_epoch, completed_at, completed_at_epoch, status started_at, started_at_epoch, completed_at, completed_at_epoch, status
FROM sdk_sessions FROM sdk_sessions
WHERE sdk_session_id IN (${placeholders}) WHERE memory_session_id IN (${placeholders})
ORDER BY started_at_epoch DESC ORDER BY started_at_epoch DESC
`); `);
return stmt.all(...sdkSessionIds) as any[]; return stmt.all(...memorySessionIds) as any[];
} }
@@ -1047,10 +1109,10 @@ export class SessionStore {
* Get current prompt number by counting user_prompts for this session * Get current prompt number by counting user_prompts for this session
* Replaces the prompt_counter column which is no longer maintained * Replaces the prompt_counter column which is no longer maintained
*/ */
getPromptNumberFromUserPrompts(claudeSessionId: string): number { getPromptNumberFromUserPrompts(contentSessionId: string): number {
const result = this.db.prepare(` const result = this.db.prepare(`
SELECT COUNT(*) as count FROM user_prompts WHERE claude_session_id = ? SELECT COUNT(*) as count FROM user_prompts WHERE content_session_id = ?
`).get(claudeSessionId) as { count: number }; `).get(contentSessionId) as { count: number };
return result.count; return result.count;
} }
@@ -1080,20 +1142,20 @@ export class SessionStore {
* This is KISS in action: Trust the database UNIQUE constraint and * This is KISS in action: Trust the database UNIQUE constraint and
* INSERT OR IGNORE to handle both creation and lookup elegantly. * INSERT OR IGNORE to handle both creation and lookup elegantly.
*/ */
createSDKSession(claudeSessionId: string, project: string, userPrompt: string): number { createSDKSession(contentSessionId: string, project: string, userPrompt: string): number {
const now = new Date(); const now = new Date();
const nowEpoch = now.getTime(); const nowEpoch = now.getTime();
// Pure INSERT OR IGNORE - no updates, no complexity // Pure INSERT OR IGNORE - no updates, no complexity
this.db.prepare(` this.db.prepare(`
INSERT OR IGNORE INTO sdk_sessions INSERT OR IGNORE INTO sdk_sessions
(claude_session_id, sdk_session_id, project, user_prompt, started_at, started_at_epoch, status) (content_session_id, memory_session_id, project, user_prompt, started_at, started_at_epoch, status)
VALUES (?, ?, ?, ?, ?, ?, 'active') VALUES (?, ?, ?, ?, ?, ?, 'active')
`).run(claudeSessionId, claudeSessionId, project, userPrompt, now.toISOString(), nowEpoch); `).run(contentSessionId, contentSessionId, project, userPrompt, now.toISOString(), nowEpoch);
// Return existing or new ID // Return existing or new ID
const row = this.db.prepare('SELECT id FROM sdk_sessions WHERE claude_session_id = ?') const row = this.db.prepare('SELECT id FROM sdk_sessions WHERE content_session_id = ?')
.get(claudeSessionId) as { id: number }; .get(contentSessionId) as { id: number };
return row.id; return row.id;
} }
@@ -1103,17 +1165,17 @@ export class SessionStore {
/** /**
* Save a user prompt * Save a user prompt
*/ */
saveUserPrompt(claudeSessionId: string, promptNumber: number, promptText: string): number { saveUserPrompt(contentSessionId: string, promptNumber: number, promptText: string): number {
const now = new Date(); const now = new Date();
const nowEpoch = now.getTime(); const nowEpoch = now.getTime();
const stmt = this.db.prepare(` const stmt = this.db.prepare(`
INSERT INTO user_prompts INSERT INTO user_prompts
(claude_session_id, prompt_number, prompt_text, created_at, created_at_epoch) (content_session_id, prompt_number, prompt_text, created_at, created_at_epoch)
VALUES (?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?)
`); `);
const result = stmt.run(claudeSessionId, promptNumber, promptText, now.toISOString(), nowEpoch); const result = stmt.run(contentSessionId, promptNumber, promptText, now.toISOString(), nowEpoch);
return result.lastInsertRowid as number; return result.lastInsertRowid as number;
} }
@@ -1121,15 +1183,15 @@ export class SessionStore {
* Get user prompt by session ID and prompt number * Get user prompt by session ID and prompt number
* Returns the prompt text, or null if not found * Returns the prompt text, or null if not found
*/ */
getUserPrompt(claudeSessionId: string, promptNumber: number): string | null { getUserPrompt(contentSessionId: string, promptNumber: number): string | null {
const stmt = this.db.prepare(` const stmt = this.db.prepare(`
SELECT prompt_text SELECT prompt_text
FROM user_prompts FROM user_prompts
WHERE claude_session_id = ? AND prompt_number = ? WHERE content_session_id = ? AND prompt_number = ?
LIMIT 1 LIMIT 1
`); `);
const result = stmt.get(claudeSessionId, promptNumber) as { prompt_text: string } | undefined; const result = stmt.get(contentSessionId, promptNumber) as { prompt_text: string } | undefined;
return result?.prompt_text ?? null; return result?.prompt_text ?? null;
} }
@@ -1138,7 +1200,7 @@ export class SessionStore {
* Assumes session already exists (created by hook) * Assumes session already exists (created by hook)
*/ */
storeObservation( storeObservation(
sdkSessionId: string, memorySessionId: string,
project: string, project: string,
observation: { observation: {
type: string; type: string;
@@ -1160,13 +1222,13 @@ export class SessionStore {
const stmt = this.db.prepare(` const stmt = this.db.prepare(`
INSERT INTO observations INSERT INTO observations
(sdk_session_id, project, type, title, subtitle, facts, narrative, concepts, (memory_session_id, project, type, title, subtitle, facts, narrative, concepts,
files_read, files_modified, prompt_number, discovery_tokens, created_at, created_at_epoch) files_read, files_modified, prompt_number, discovery_tokens, created_at, created_at_epoch)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`); `);
const result = stmt.run( const result = stmt.run(
sdkSessionId, memorySessionId,
project, project,
observation.type, observation.type,
observation.title, observation.title,
@@ -1193,7 +1255,7 @@ export class SessionStore {
* Assumes session already exists - will fail with FK error if not * Assumes session already exists - will fail with FK error if not
*/ */
storeSummary( storeSummary(
sdkSessionId: string, memorySessionId: string,
project: string, project: string,
summary: { summary: {
request: string; request: string;
@@ -1213,13 +1275,13 @@ export class SessionStore {
const stmt = this.db.prepare(` const stmt = this.db.prepare(`
INSERT INTO session_summaries INSERT INTO session_summaries
(sdk_session_id, project, request, investigated, learned, completed, (memory_session_id, project, request, investigated, learned, completed,
next_steps, notes, prompt_number, discovery_tokens, created_at, created_at_epoch) next_steps, notes, prompt_number, discovery_tokens, created_at, created_at_epoch)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`); `);
const result = stmt.run( const result = stmt.run(
sdkSessionId, memorySessionId,
project, project,
summary.request, summary.request,
summary.investigated, summary.investigated,
@@ -1302,9 +1364,9 @@ export class SessionStore {
SELECT SELECT
up.*, up.*,
s.project, s.project,
s.sdk_session_id s.memory_session_id
FROM user_prompts up FROM user_prompts up
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id JOIN sdk_sessions s ON up.content_session_id = s.content_session_id
WHERE up.id IN (${placeholders}) ${projectFilter} WHERE up.id IN (${placeholders}) ${projectFilter}
ORDER BY up.created_at_epoch ${orderClause} ORDER BY up.created_at_epoch ${orderClause}
${limitClause} ${limitClause}
@@ -1437,9 +1499,9 @@ export class SessionStore {
`; `;
const promptQuery = ` const promptQuery = `
SELECT up.*, s.project, s.sdk_session_id SELECT up.*, s.project, s.memory_session_id
FROM user_prompts up FROM user_prompts up
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id JOIN sdk_sessions s ON up.content_session_id = s.content_session_id
WHERE up.created_at_epoch >= ? AND up.created_at_epoch <= ? ${projectFilter.replace('project', 's.project')} WHERE up.created_at_epoch >= ? AND up.created_at_epoch <= ? ${projectFilter.replace('project', 's.project')}
ORDER BY up.created_at_epoch ASC ORDER BY up.created_at_epoch ASC
`; `;
@@ -1453,7 +1515,7 @@ export class SessionStore {
observations, observations,
sessions: sessions.map(s => ({ sessions: sessions.map(s => ({
id: s.id, id: s.id,
sdk_session_id: s.sdk_session_id, memory_session_id: s.memory_session_id,
project: s.project, project: s.project,
request: s.request, request: s.request,
completed: s.completed, completed: s.completed,
@@ -1463,7 +1525,7 @@ export class SessionStore {
})), })),
prompts: prompts.map(p => ({ prompts: prompts.map(p => ({
id: p.id, id: p.id,
claude_session_id: p.claude_session_id, content_session_id: p.content_session_id,
prompt_number: p.prompt_number, prompt_number: p.prompt_number,
prompt_text: p.prompt_text, prompt_text: p.prompt_text,
project: p.project, project: p.project,
@@ -1482,7 +1544,7 @@ export class SessionStore {
*/ */
getPromptById(id: number): { getPromptById(id: number): {
id: number; id: number;
claude_session_id: string; content_session_id: string;
prompt_number: number; prompt_number: number;
prompt_text: string; prompt_text: string;
project: string; project: string;
@@ -1492,14 +1554,14 @@ export class SessionStore {
const stmt = this.db.prepare(` const stmt = this.db.prepare(`
SELECT SELECT
p.id, p.id,
p.claude_session_id, p.content_session_id,
p.prompt_number, p.prompt_number,
p.prompt_text, p.prompt_text,
s.project, s.project,
p.created_at, p.created_at,
p.created_at_epoch p.created_at_epoch
FROM user_prompts p FROM user_prompts p
LEFT JOIN sdk_sessions s ON p.claude_session_id = s.claude_session_id LEFT JOIN sdk_sessions s ON p.content_session_id = s.content_session_id
WHERE p.id = ? WHERE p.id = ?
LIMIT 1 LIMIT 1
`); `);
@@ -1512,7 +1574,7 @@ export class SessionStore {
*/ */
getPromptsByIds(ids: number[]): Array<{ getPromptsByIds(ids: number[]): Array<{
id: number; id: number;
claude_session_id: string; content_session_id: string;
prompt_number: number; prompt_number: number;
prompt_text: string; prompt_text: string;
project: string; project: string;
@@ -1525,21 +1587,21 @@ export class SessionStore {
const stmt = this.db.prepare(` const stmt = this.db.prepare(`
SELECT SELECT
p.id, p.id,
p.claude_session_id, p.content_session_id,
p.prompt_number, p.prompt_number,
p.prompt_text, p.prompt_text,
s.project, s.project,
p.created_at, p.created_at,
p.created_at_epoch p.created_at_epoch
FROM user_prompts p FROM user_prompts p
LEFT JOIN sdk_sessions s ON p.claude_session_id = s.claude_session_id LEFT JOIN sdk_sessions s ON p.content_session_id = s.content_session_id
WHERE p.id IN (${placeholders}) WHERE p.id IN (${placeholders})
ORDER BY p.created_at_epoch DESC ORDER BY p.created_at_epoch DESC
`); `);
return stmt.all(...ids) as Array<{ return stmt.all(...ids) as Array<{
id: number; id: number;
claude_session_id: string; content_session_id: string;
prompt_number: number; prompt_number: number;
prompt_text: string; prompt_text: string;
project: string; project: string;
@@ -1553,8 +1615,8 @@ export class SessionStore {
*/ */
getSessionSummaryById(id: number): { getSessionSummaryById(id: number): {
id: number; id: number;
sdk_session_id: string | null; memory_session_id: string | null;
claude_session_id: string; content_session_id: string;
project: string; project: string;
user_prompt: string; user_prompt: string;
request_summary: string | null; request_summary: string | null;
@@ -1566,8 +1628,8 @@ export class SessionStore {
const stmt = this.db.prepare(` const stmt = this.db.prepare(`
SELECT SELECT
id, id,
sdk_session_id, memory_session_id,
claude_session_id, content_session_id,
project, project,
user_prompt, user_prompt,
request_summary, request_summary,
@@ -1599,8 +1661,8 @@ export class SessionStore {
* Returns: { imported: boolean, id: number } * Returns: { imported: boolean, id: number }
*/ */
importSdkSession(session: { importSdkSession(session: {
claude_session_id: string; content_session_id: string;
sdk_session_id: string; memory_session_id: string;
project: string; project: string;
user_prompt: string; user_prompt: string;
started_at: string; started_at: string;
@@ -1611,8 +1673,8 @@ export class SessionStore {
}): { imported: boolean; id: number } { }): { imported: boolean; id: number } {
// Check if session already exists // Check if session already exists
const existing = this.db.prepare( const existing = this.db.prepare(
'SELECT id FROM sdk_sessions WHERE claude_session_id = ?' 'SELECT id FROM sdk_sessions WHERE content_session_id = ?'
).get(session.claude_session_id) as { id: number } | undefined; ).get(session.content_session_id) as { id: number } | undefined;
if (existing) { if (existing) {
return { imported: false, id: existing.id }; return { imported: false, id: existing.id };
@@ -1620,14 +1682,14 @@ export class SessionStore {
const stmt = this.db.prepare(` const stmt = this.db.prepare(`
INSERT INTO sdk_sessions ( INSERT INTO sdk_sessions (
claude_session_id, sdk_session_id, project, user_prompt, content_session_id, memory_session_id, project, user_prompt,
started_at, started_at_epoch, completed_at, completed_at_epoch, status started_at, started_at_epoch, completed_at, completed_at_epoch, status
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`); `);
const result = stmt.run( const result = stmt.run(
session.claude_session_id, session.content_session_id,
session.sdk_session_id, session.memory_session_id,
session.project, session.project,
session.user_prompt, session.user_prompt,
session.started_at, session.started_at,
@@ -1645,7 +1707,7 @@ export class SessionStore {
* Returns: { imported: boolean, id: number } * Returns: { imported: boolean, id: number }
*/ */
importSessionSummary(summary: { importSessionSummary(summary: {
sdk_session_id: string; memory_session_id: string;
project: string; project: string;
request: string | null; request: string | null;
investigated: string | null; investigated: string | null;
@@ -1662,8 +1724,8 @@ export class SessionStore {
}): { imported: boolean; id: number } { }): { imported: boolean; id: number } {
// Check if summary already exists for this session // Check if summary already exists for this session
const existing = this.db.prepare( const existing = this.db.prepare(
'SELECT id FROM session_summaries WHERE sdk_session_id = ?' 'SELECT id FROM session_summaries WHERE memory_session_id = ?'
).get(summary.sdk_session_id) as { id: number } | undefined; ).get(summary.memory_session_id) as { id: number } | undefined;
if (existing) { if (existing) {
return { imported: false, id: existing.id }; return { imported: false, id: existing.id };
@@ -1671,14 +1733,14 @@ export class SessionStore {
const stmt = this.db.prepare(` const stmt = this.db.prepare(`
INSERT INTO session_summaries ( INSERT INTO session_summaries (
sdk_session_id, project, request, investigated, learned, memory_session_id, project, request, investigated, learned,
completed, next_steps, files_read, files_edited, notes, completed, next_steps, files_read, files_edited, notes,
prompt_number, discovery_tokens, created_at, created_at_epoch prompt_number, discovery_tokens, created_at, created_at_epoch
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`); `);
const result = stmt.run( const result = stmt.run(
summary.sdk_session_id, summary.memory_session_id,
summary.project, summary.project,
summary.request, summary.request,
summary.investigated, summary.investigated,
@@ -1699,11 +1761,11 @@ export class SessionStore {
/** /**
* Import observation with duplicate checking * Import observation with duplicate checking
* Duplicates are identified by sdk_session_id + title + created_at_epoch * Duplicates are identified by memory_session_id + title + created_at_epoch
* Returns: { imported: boolean, id: number } * Returns: { imported: boolean, id: number }
*/ */
importObservation(obs: { importObservation(obs: {
sdk_session_id: string; memory_session_id: string;
project: string; project: string;
text: string | null; text: string | null;
type: string; type: string;
@@ -1722,8 +1784,8 @@ export class SessionStore {
// Check if observation already exists // Check if observation already exists
const existing = this.db.prepare(` const existing = this.db.prepare(`
SELECT id FROM observations SELECT id FROM observations
WHERE sdk_session_id = ? AND title = ? AND created_at_epoch = ? WHERE memory_session_id = ? AND title = ? AND created_at_epoch = ?
`).get(obs.sdk_session_id, obs.title, obs.created_at_epoch) as { id: number } | undefined; `).get(obs.memory_session_id, obs.title, obs.created_at_epoch) as { id: number } | undefined;
if (existing) { if (existing) {
return { imported: false, id: existing.id }; return { imported: false, id: existing.id };
@@ -1731,14 +1793,14 @@ export class SessionStore {
const stmt = this.db.prepare(` const stmt = this.db.prepare(`
INSERT INTO observations ( INSERT INTO observations (
sdk_session_id, project, text, type, title, subtitle, memory_session_id, project, text, type, title, subtitle,
facts, narrative, concepts, files_read, files_modified, facts, narrative, concepts, files_read, files_modified,
prompt_number, discovery_tokens, created_at, created_at_epoch prompt_number, discovery_tokens, created_at, created_at_epoch
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`); `);
const result = stmt.run( const result = stmt.run(
obs.sdk_session_id, obs.memory_session_id,
obs.project, obs.project,
obs.text, obs.text,
obs.type, obs.type,
@@ -1760,11 +1822,11 @@ export class SessionStore {
/** /**
* Import user prompt with duplicate checking * Import user prompt with duplicate checking
* Duplicates are identified by claude_session_id + prompt_number * Duplicates are identified by content_session_id + prompt_number
* Returns: { imported: boolean, id: number } * Returns: { imported: boolean, id: number }
*/ */
importUserPrompt(prompt: { importUserPrompt(prompt: {
claude_session_id: string; content_session_id: string;
prompt_number: number; prompt_number: number;
prompt_text: string; prompt_text: string;
created_at: string; created_at: string;
@@ -1773,8 +1835,8 @@ export class SessionStore {
// Check if prompt already exists // Check if prompt already exists
const existing = this.db.prepare(` const existing = this.db.prepare(`
SELECT id FROM user_prompts SELECT id FROM user_prompts
WHERE claude_session_id = ? AND prompt_number = ? WHERE content_session_id = ? AND prompt_number = ?
`).get(prompt.claude_session_id, prompt.prompt_number) as { id: number } | undefined; `).get(prompt.content_session_id, prompt.prompt_number) as { id: number } | undefined;
if (existing) { if (existing) {
return { imported: false, id: existing.id }; return { imported: false, id: existing.id };
@@ -1782,13 +1844,13 @@ export class SessionStore {
const stmt = this.db.prepare(` const stmt = this.db.prepare(`
INSERT INTO user_prompts ( INSERT INTO user_prompts (
claude_session_id, prompt_number, prompt_text, content_session_id, prompt_number, prompt_text,
created_at, created_at_epoch created_at, created_at_epoch
) VALUES (?, ?, ?, ?, ?) ) VALUES (?, ?, ?, ?, ?)
`); `);
const result = stmt.run( const result = stmt.run(
prompt.claude_session_id, prompt.content_session_id,
prompt.prompt_number, prompt.prompt_number,
prompt.prompt_text, prompt.prompt_text,
prompt.created_at, prompt.created_at,
+22 -22
View File
@@ -170,8 +170,8 @@ export const migration003: Migration = {
db.run(` db.run(`
CREATE TABLE IF NOT EXISTS streaming_sessions ( CREATE TABLE IF NOT EXISTS streaming_sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
claude_session_id TEXT UNIQUE NOT NULL, content_session_id TEXT UNIQUE NOT NULL,
sdk_session_id TEXT, memory_session_id TEXT,
project TEXT NOT NULL, project TEXT NOT NULL,
title TEXT, title TEXT,
subtitle TEXT, subtitle TEXT,
@@ -185,8 +185,8 @@ export const migration003: Migration = {
status TEXT NOT NULL DEFAULT 'active' status TEXT NOT NULL DEFAULT 'active'
); );
CREATE INDEX IF NOT EXISTS idx_streaming_sessions_claude_id ON streaming_sessions(claude_session_id); CREATE INDEX IF NOT EXISTS idx_streaming_sessions_claude_id ON streaming_sessions(content_session_id);
CREATE INDEX IF NOT EXISTS idx_streaming_sessions_sdk_id ON streaming_sessions(sdk_session_id); CREATE INDEX IF NOT EXISTS idx_streaming_sessions_sdk_id ON streaming_sessions(memory_session_id);
CREATE INDEX IF NOT EXISTS idx_streaming_sessions_project ON streaming_sessions(project); CREATE INDEX IF NOT EXISTS idx_streaming_sessions_project ON streaming_sessions(project);
CREATE INDEX IF NOT EXISTS idx_streaming_sessions_status ON streaming_sessions(status); CREATE INDEX IF NOT EXISTS idx_streaming_sessions_status ON streaming_sessions(status);
CREATE INDEX IF NOT EXISTS idx_streaming_sessions_started ON streaming_sessions(started_at_epoch DESC); CREATE INDEX IF NOT EXISTS idx_streaming_sessions_started ON streaming_sessions(started_at_epoch DESC);
@@ -213,8 +213,8 @@ export const migration004: Migration = {
db.run(` db.run(`
CREATE TABLE IF NOT EXISTS sdk_sessions ( CREATE TABLE IF NOT EXISTS sdk_sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
claude_session_id TEXT UNIQUE NOT NULL, content_session_id TEXT UNIQUE NOT NULL,
sdk_session_id TEXT UNIQUE, memory_session_id TEXT UNIQUE,
project TEXT NOT NULL, project TEXT NOT NULL,
user_prompt TEXT, user_prompt TEXT,
started_at TEXT NOT NULL, started_at TEXT NOT NULL,
@@ -224,8 +224,8 @@ export const migration004: Migration = {
status TEXT CHECK(status IN ('active', 'completed', 'failed')) NOT NULL DEFAULT 'active' status TEXT CHECK(status IN ('active', 'completed', 'failed')) NOT NULL DEFAULT 'active'
); );
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_claude_id ON sdk_sessions(claude_session_id); CREATE INDEX IF NOT EXISTS idx_sdk_sessions_claude_id ON sdk_sessions(content_session_id);
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_sdk_id ON sdk_sessions(sdk_session_id); CREATE INDEX IF NOT EXISTS idx_sdk_sessions_sdk_id ON sdk_sessions(memory_session_id);
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_project ON sdk_sessions(project); CREATE INDEX IF NOT EXISTS idx_sdk_sessions_project ON sdk_sessions(project);
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_status ON sdk_sessions(status); CREATE INDEX IF NOT EXISTS idx_sdk_sessions_status ON sdk_sessions(status);
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_started ON sdk_sessions(started_at_epoch DESC); CREATE INDEX IF NOT EXISTS idx_sdk_sessions_started ON sdk_sessions(started_at_epoch DESC);
@@ -235,34 +235,34 @@ export const migration004: Migration = {
db.run(` db.run(`
CREATE TABLE IF NOT EXISTS observation_queue ( CREATE TABLE IF NOT EXISTS observation_queue (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
sdk_session_id TEXT NOT NULL, memory_session_id TEXT NOT NULL,
tool_name TEXT NOT NULL, tool_name TEXT NOT NULL,
tool_input TEXT NOT NULL, tool_input TEXT NOT NULL,
tool_output TEXT NOT NULL, tool_output TEXT NOT NULL,
created_at_epoch INTEGER NOT NULL, created_at_epoch INTEGER NOT NULL,
processed_at_epoch INTEGER, processed_at_epoch INTEGER,
FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id) ON DELETE CASCADE FOREIGN KEY(memory_session_id) REFERENCES sdk_sessions(memory_session_id) ON DELETE CASCADE
); );
CREATE INDEX IF NOT EXISTS idx_observation_queue_sdk_session ON observation_queue(sdk_session_id); CREATE INDEX IF NOT EXISTS idx_observation_queue_sdk_session ON observation_queue(memory_session_id);
CREATE INDEX IF NOT EXISTS idx_observation_queue_processed ON observation_queue(processed_at_epoch); CREATE INDEX IF NOT EXISTS idx_observation_queue_processed ON observation_queue(processed_at_epoch);
CREATE INDEX IF NOT EXISTS idx_observation_queue_pending ON observation_queue(sdk_session_id, processed_at_epoch); CREATE INDEX IF NOT EXISTS idx_observation_queue_pending ON observation_queue(memory_session_id, processed_at_epoch);
`); `);
// Observations table - stores extracted observations (what SDK decides is important) // Observations table - stores extracted observations (what SDK decides is important)
db.run(` db.run(`
CREATE TABLE IF NOT EXISTS observations ( CREATE TABLE IF NOT EXISTS observations (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
sdk_session_id TEXT NOT NULL, memory_session_id TEXT NOT NULL,
project TEXT NOT NULL, project TEXT NOT NULL,
text TEXT NOT NULL, text TEXT NOT NULL,
type TEXT NOT NULL CHECK(type IN ('decision', 'bugfix', 'feature', 'refactor', 'discovery')), type TEXT NOT NULL CHECK(type IN ('decision', 'bugfix', 'feature', 'refactor', 'discovery')),
created_at TEXT NOT NULL, created_at TEXT NOT NULL,
created_at_epoch INTEGER NOT NULL, created_at_epoch INTEGER NOT NULL,
FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id) ON DELETE CASCADE FOREIGN KEY(memory_session_id) REFERENCES sdk_sessions(memory_session_id) ON DELETE CASCADE
); );
CREATE INDEX IF NOT EXISTS idx_observations_sdk_session ON observations(sdk_session_id); CREATE INDEX IF NOT EXISTS idx_observations_sdk_session ON observations(memory_session_id);
CREATE INDEX IF NOT EXISTS idx_observations_project ON observations(project); CREATE INDEX IF NOT EXISTS idx_observations_project ON observations(project);
CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type); CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type);
CREATE INDEX IF NOT EXISTS idx_observations_created ON observations(created_at_epoch DESC); CREATE INDEX IF NOT EXISTS idx_observations_created ON observations(created_at_epoch DESC);
@@ -272,7 +272,7 @@ export const migration004: Migration = {
db.run(` db.run(`
CREATE TABLE IF NOT EXISTS session_summaries ( CREATE TABLE IF NOT EXISTS session_summaries (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
sdk_session_id TEXT UNIQUE NOT NULL, memory_session_id TEXT UNIQUE NOT NULL,
project TEXT NOT NULL, project TEXT NOT NULL,
request TEXT, request TEXT,
investigated TEXT, investigated TEXT,
@@ -284,10 +284,10 @@ export const migration004: Migration = {
notes TEXT, notes TEXT,
created_at TEXT NOT NULL, created_at TEXT NOT NULL,
created_at_epoch INTEGER NOT NULL, created_at_epoch INTEGER NOT NULL,
FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id) ON DELETE CASCADE FOREIGN KEY(memory_session_id) REFERENCES sdk_sessions(memory_session_id) ON DELETE CASCADE
); );
CREATE INDEX IF NOT EXISTS idx_session_summaries_sdk_session ON session_summaries(sdk_session_id); CREATE INDEX IF NOT EXISTS idx_session_summaries_sdk_session ON session_summaries(memory_session_id);
CREATE INDEX IF NOT EXISTS idx_session_summaries_project ON session_summaries(project); CREATE INDEX IF NOT EXISTS idx_session_summaries_project ON session_summaries(project);
CREATE INDEX IF NOT EXISTS idx_session_summaries_created ON session_summaries(created_at_epoch DESC); CREATE INDEX IF NOT EXISTS idx_session_summaries_created ON session_summaries(created_at_epoch DESC);
`); `);
@@ -329,8 +329,8 @@ export const migration005: Migration = {
db.run(` db.run(`
CREATE TABLE IF NOT EXISTS streaming_sessions ( CREATE TABLE IF NOT EXISTS streaming_sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
claude_session_id TEXT UNIQUE NOT NULL, content_session_id TEXT UNIQUE NOT NULL,
sdk_session_id TEXT, memory_session_id TEXT,
project TEXT NOT NULL, project TEXT NOT NULL,
title TEXT, title TEXT,
subtitle TEXT, subtitle TEXT,
@@ -348,13 +348,13 @@ export const migration005: Migration = {
db.run(` db.run(`
CREATE TABLE IF NOT EXISTS observation_queue ( CREATE TABLE IF NOT EXISTS observation_queue (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
sdk_session_id TEXT NOT NULL, memory_session_id TEXT NOT NULL,
tool_name TEXT NOT NULL, tool_name TEXT NOT NULL,
tool_input TEXT NOT NULL, tool_input TEXT NOT NULL,
tool_output TEXT NOT NULL, tool_output TEXT NOT NULL,
created_at_epoch INTEGER NOT NULL, created_at_epoch INTEGER NOT NULL,
processed_at_epoch INTEGER, processed_at_epoch INTEGER,
FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id) ON DELETE CASCADE FOREIGN KEY(memory_session_id) REFERENCES sdk_sessions(memory_session_id) ON DELETE CASCADE
) )
`); `);
+5 -5
View File
@@ -188,8 +188,8 @@ export function normalizeTimestamp(timestamp: string | Date | number | undefined
*/ */
export interface SDKSessionRow { export interface SDKSessionRow {
id: number; id: number;
claude_session_id: string; content_session_id: string;
sdk_session_id: string | null; memory_session_id: string | null;
project: string; project: string;
user_prompt: string | null; user_prompt: string | null;
started_at: string; started_at: string;
@@ -203,7 +203,7 @@ export interface SDKSessionRow {
export interface ObservationRow { export interface ObservationRow {
id: number; id: number;
sdk_session_id: string; memory_session_id: string;
project: string; project: string;
text: string | null; text: string | null;
type: 'decision' | 'bugfix' | 'feature' | 'refactor' | 'discovery' | 'change'; type: 'decision' | 'bugfix' | 'feature' | 'refactor' | 'discovery' | 'change';
@@ -222,7 +222,7 @@ export interface ObservationRow {
export interface SessionSummaryRow { export interface SessionSummaryRow {
id: number; id: number;
sdk_session_id: string; memory_session_id: string;
project: string; project: string;
request: string | null; request: string | null;
investigated: string | null; investigated: string | null;
@@ -240,7 +240,7 @@ export interface SessionSummaryRow {
export interface UserPromptRow { export interface UserPromptRow {
id: number; id: number;
claude_session_id: string; content_session_id: string;
prompt_number: number; prompt_number: number;
prompt_text: string; prompt_text: string;
created_at: string; created_at: string;
+17 -17
View File
@@ -26,7 +26,7 @@ interface ChromaDocument {
interface StoredObservation { interface StoredObservation {
id: number; id: number;
sdk_session_id: string; memory_session_id: string;
project: string; project: string;
text: string | null; text: string | null;
type: string; type: string;
@@ -45,7 +45,7 @@ interface StoredObservation {
interface StoredSummary { interface StoredSummary {
id: number; id: number;
sdk_session_id: string; memory_session_id: string;
project: string; project: string;
request: string | null; request: string | null;
investigated: string | null; investigated: string | null;
@@ -61,12 +61,12 @@ interface StoredSummary {
interface StoredUserPrompt { interface StoredUserPrompt {
id: number; id: number;
claude_session_id: string; content_session_id: string;
prompt_number: number; prompt_number: number;
prompt_text: string; prompt_text: string;
created_at: string; created_at: string;
created_at_epoch: number; created_at_epoch: number;
sdk_session_id: string; memory_session_id: string;
project: string; project: string;
} }
@@ -201,7 +201,7 @@ export class ChromaSync {
const baseMetadata: Record<string, string | number> = { const baseMetadata: Record<string, string | number> = {
sqlite_id: obs.id, sqlite_id: obs.id,
doc_type: 'observation', doc_type: 'observation',
sdk_session_id: obs.sdk_session_id, memory_session_id: obs.memory_session_id,
project: obs.project, project: obs.project,
created_at_epoch: obs.created_at_epoch, created_at_epoch: obs.created_at_epoch,
type: obs.type || 'discovery', type: obs.type || 'discovery',
@@ -262,7 +262,7 @@ export class ChromaSync {
const baseMetadata: Record<string, string | number> = { const baseMetadata: Record<string, string | number> = {
sqlite_id: summary.id, sqlite_id: summary.id,
doc_type: 'session_summary', doc_type: 'session_summary',
sdk_session_id: summary.sdk_session_id, memory_session_id: summary.memory_session_id,
project: summary.project, project: summary.project,
created_at_epoch: summary.created_at_epoch, created_at_epoch: summary.created_at_epoch,
prompt_number: summary.prompt_number || 0 prompt_number: summary.prompt_number || 0
@@ -368,7 +368,7 @@ export class ChromaSync {
*/ */
async syncObservation( async syncObservation(
observationId: number, observationId: number,
sdkSessionId: string, memorySessionId: string,
project: string, project: string,
obs: ParsedObservation, obs: ParsedObservation,
promptNumber: number, promptNumber: number,
@@ -378,7 +378,7 @@ export class ChromaSync {
// Convert ParsedObservation to StoredObservation format // Convert ParsedObservation to StoredObservation format
const stored: StoredObservation = { const stored: StoredObservation = {
id: observationId, id: observationId,
sdk_session_id: sdkSessionId, memory_session_id: memorySessionId,
project: project, project: project,
text: null, // Legacy field, not used text: null, // Legacy field, not used
type: obs.type, type: obs.type,
@@ -412,7 +412,7 @@ export class ChromaSync {
*/ */
async syncSummary( async syncSummary(
summaryId: number, summaryId: number,
sdkSessionId: string, memorySessionId: string,
project: string, project: string,
summary: ParsedSummary, summary: ParsedSummary,
promptNumber: number, promptNumber: number,
@@ -422,7 +422,7 @@ export class ChromaSync {
// Convert ParsedSummary to StoredSummary format // Convert ParsedSummary to StoredSummary format
const stored: StoredSummary = { const stored: StoredSummary = {
id: summaryId, id: summaryId,
sdk_session_id: sdkSessionId, memory_session_id: memorySessionId,
project: project, project: project,
request: summary.request, request: summary.request,
investigated: summary.investigated, investigated: summary.investigated,
@@ -458,7 +458,7 @@ export class ChromaSync {
metadata: { metadata: {
sqlite_id: prompt.id, sqlite_id: prompt.id,
doc_type: 'user_prompt', doc_type: 'user_prompt',
sdk_session_id: prompt.sdk_session_id, memory_session_id: prompt.memory_session_id,
project: prompt.project, project: prompt.project,
created_at_epoch: prompt.created_at_epoch, created_at_epoch: prompt.created_at_epoch,
prompt_number: prompt.prompt_number prompt_number: prompt.prompt_number
@@ -472,7 +472,7 @@ export class ChromaSync {
*/ */
async syncUserPrompt( async syncUserPrompt(
promptId: number, promptId: number,
sdkSessionId: string, memorySessionId: string,
project: string, project: string,
promptText: string, promptText: string,
promptNumber: number, promptNumber: number,
@@ -481,12 +481,12 @@ export class ChromaSync {
// Create StoredUserPrompt format // Create StoredUserPrompt format
const stored: StoredUserPrompt = { const stored: StoredUserPrompt = {
id: promptId, id: promptId,
claude_session_id: '', // Not needed for Chroma sync content_session_id: '', // Not needed for Chroma sync
prompt_number: promptNumber, prompt_number: promptNumber,
prompt_text: promptText, prompt_text: promptText,
created_at: new Date(createdAtEpoch * 1000).toISOString(), created_at: new Date(createdAtEpoch * 1000).toISOString(),
created_at_epoch: createdAtEpoch, created_at_epoch: createdAtEpoch,
sdk_session_id: sdkSessionId, memory_session_id: memorySessionId,
project: project project: project
}; };
@@ -697,9 +697,9 @@ export class ChromaSync {
SELECT SELECT
up.*, up.*,
s.project, s.project,
s.sdk_session_id s.memory_session_id
FROM user_prompts up FROM user_prompts up
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id JOIN sdk_sessions s ON up.content_session_id = s.content_session_id
WHERE s.project = ? ${promptExclusionClause} WHERE s.project = ? ${promptExclusionClause}
ORDER BY up.id ASC ORDER BY up.id ASC
`).all(this.project) as StoredUserPrompt[]; `).all(this.project) as StoredUserPrompt[];
@@ -707,7 +707,7 @@ export class ChromaSync {
const totalPromptCount = db.db.prepare(` const totalPromptCount = db.db.prepare(`
SELECT COUNT(*) as count SELECT COUNT(*) as count
FROM user_prompts up FROM user_prompts up
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id JOIN sdk_sessions s ON up.content_session_id = s.content_session_id
WHERE s.project = ? WHERE s.project = ?
`).get(this.project) as { count: number }; `).get(this.project) as { count: number };
+7 -7
View File
@@ -19,8 +19,8 @@ export interface ConversationMessage {
export interface ActiveSession { export interface ActiveSession {
sessionDbId: number; sessionDbId: number;
claudeSessionId: string; contentSessionId: string; // User's Claude Code session being observed
sdkSessionId: string | null; memorySessionId: string | null; // Memory agent's session ID for resume
project: string; project: string;
userPrompt: string; userPrompt: string;
pendingMessages: PendingMessage[]; // Deprecated: now using persistent store, kept for compatibility pendingMessages: PendingMessage[]; // Deprecated: now using persistent store, kept for compatibility
@@ -110,7 +110,7 @@ export interface ViewerSettings {
export interface Observation { export interface Observation {
id: number; id: number;
sdk_session_id: string; memory_session_id: string; // Renamed from sdk_session_id
project: string; project: string;
type: string; type: string;
title: string; title: string;
@@ -128,7 +128,7 @@ export interface Observation {
export interface Summary { export interface Summary {
id: number; id: number;
session_id: string; // claude_session_id (from JOIN) session_id: string; // content_session_id (from JOIN)
project: string; project: string;
request: string | null; request: string | null;
investigated: string | null; investigated: string | null;
@@ -142,7 +142,7 @@ export interface Summary {
export interface UserPrompt { export interface UserPrompt {
id: number; id: number;
claude_session_id: string; content_session_id: string; // Renamed from claude_session_id
project: string; // From JOIN with sdk_sessions project: string; // From JOIN with sdk_sessions
prompt_number: number; prompt_number: number;
prompt_text: string; prompt_text: string;
@@ -152,10 +152,10 @@ export interface UserPrompt {
export interface DBSession { export interface DBSession {
id: number; id: number;
claude_session_id: string; content_session_id: string; // Renamed from claude_session_id
project: string; project: string;
user_prompt: string; user_prompt: string;
sdk_session_id: string | null; memory_session_id: string | null; // Renamed from sdk_session_id
status: 'active' | 'completed' | 'failed'; status: 'active' | 'completed' | 'failed';
started_at: string; started_at: string;
started_at_epoch: number; started_at_epoch: number;
+2 -2
View File
@@ -93,8 +93,8 @@ export class DatabaseManager {
*/ */
getSessionById(sessionDbId: number): { getSessionById(sessionDbId: number): {
id: number; id: number;
claude_session_id: string; content_session_id: string;
sdk_session_id: string | null; memory_session_id: string | null;
project: string; project: string;
user_prompt: string; user_prompt: string;
} { } {
+11 -11
View File
@@ -152,8 +152,8 @@ export class GeminiAgent {
// Build initial prompt // Build initial prompt
const initPrompt = session.lastPromptNumber === 1 const initPrompt = session.lastPromptNumber === 1
? buildInitPrompt(session.project, session.claudeSessionId, session.userPrompt, mode) ? buildInitPrompt(session.project, session.contentSessionId, session.userPrompt, mode)
: buildContinuationPrompt(session.userPrompt, session.lastPromptNumber, session.claudeSessionId, mode); : buildContinuationPrompt(session.userPrompt, session.lastPromptNumber, session.contentSessionId, mode);
// Add to conversation history and query Gemini with full context // Add to conversation history and query Gemini with full context
session.conversationHistory.push({ role: 'user', content: initPrompt }); session.conversationHistory.push({ role: 'user', content: initPrompt });
@@ -224,7 +224,7 @@ export class GeminiAgent {
// Build summary prompt // Build summary prompt
const summaryPrompt = buildSummaryPrompt({ const summaryPrompt = buildSummaryPrompt({
id: session.sessionDbId, id: session.sessionDbId,
sdk_session_id: session.sdkSessionId, memory_session_id: session.memorySessionId,
project: session.project, project: session.project,
user_prompt: session.userPrompt, user_prompt: session.userPrompt,
last_user_message: message.last_user_message || '', last_user_message: message.last_user_message || '',
@@ -374,12 +374,12 @@ export class GeminiAgent {
originalTimestamp: number | null originalTimestamp: number | null
): Promise<void> { ): Promise<void> {
// Parse observations (same XML format) // Parse observations (same XML format)
const observations = parseObservations(text, session.claudeSessionId); const observations = parseObservations(text, session.contentSessionId);
// Store observations with original timestamp (if processing backlog) or current time // Store observations with original timestamp (if processing backlog) or current time
for (const obs of observations) { for (const obs of observations) {
const { id: obsId, createdAtEpoch } = this.dbManager.getSessionStore().storeObservation( const { id: obsId, createdAtEpoch } = this.dbManager.getSessionStore().storeObservation(
session.claudeSessionId, session.contentSessionId,
session.project, session.project,
obs, obs,
session.lastPromptNumber, session.lastPromptNumber,
@@ -397,7 +397,7 @@ export class GeminiAgent {
// Sync to Chroma // Sync to Chroma
this.dbManager.getChromaSync().syncObservation( this.dbManager.getChromaSync().syncObservation(
obsId, obsId,
session.claudeSessionId, session.contentSessionId,
session.project, session.project,
obs, obs,
session.lastPromptNumber, session.lastPromptNumber,
@@ -413,8 +413,8 @@ export class GeminiAgent {
type: 'new_observation', type: 'new_observation',
observation: { observation: {
id: obsId, id: obsId,
sdk_session_id: session.sdkSessionId, memory_session_id: session.memorySessionId,
session_id: session.claudeSessionId, session_id: session.contentSessionId,
type: obs.type, type: obs.type,
title: obs.title, title: obs.title,
subtitle: obs.subtitle, subtitle: obs.subtitle,
@@ -447,7 +447,7 @@ export class GeminiAgent {
}; };
const { id: summaryId, createdAtEpoch } = this.dbManager.getSessionStore().storeSummary( const { id: summaryId, createdAtEpoch } = this.dbManager.getSessionStore().storeSummary(
session.claudeSessionId, session.contentSessionId,
session.project, session.project,
summaryForStore, summaryForStore,
session.lastPromptNumber, session.lastPromptNumber,
@@ -464,7 +464,7 @@ export class GeminiAgent {
// Sync to Chroma // Sync to Chroma
this.dbManager.getChromaSync().syncSummary( this.dbManager.getChromaSync().syncSummary(
summaryId, summaryId,
session.claudeSessionId, session.contentSessionId,
session.project, session.project,
summaryForStore, summaryForStore,
session.lastPromptNumber, session.lastPromptNumber,
@@ -480,7 +480,7 @@ export class GeminiAgent {
type: 'new_summary', type: 'new_summary',
summary: { summary: {
id: summaryId, id: summaryId,
session_id: session.claudeSessionId, session_id: session.contentSessionId,
request: summary.request, request: summary.request,
investigated: summary.investigated, investigated: summary.investigated,
learned: summary.learned, learned: summary.learned,
+11 -11
View File
@@ -112,8 +112,8 @@ export class OpenRouterAgent {
// Build initial prompt // Build initial prompt
const initPrompt = session.lastPromptNumber === 1 const initPrompt = session.lastPromptNumber === 1
? buildInitPrompt(session.project, session.claudeSessionId, session.userPrompt, mode) ? buildInitPrompt(session.project, session.contentSessionId, session.userPrompt, mode)
: buildContinuationPrompt(session.userPrompt, session.lastPromptNumber, session.claudeSessionId, mode); : buildContinuationPrompt(session.userPrompt, session.lastPromptNumber, session.contentSessionId, mode);
// Add to conversation history and query OpenRouter with full context // Add to conversation history and query OpenRouter with full context
session.conversationHistory.push({ role: 'user', content: initPrompt }); session.conversationHistory.push({ role: 'user', content: initPrompt });
@@ -183,7 +183,7 @@ export class OpenRouterAgent {
// Build summary prompt // Build summary prompt
const summaryPrompt = buildSummaryPrompt({ const summaryPrompt = buildSummaryPrompt({
id: session.sessionDbId, id: session.sessionDbId,
sdk_session_id: session.sdkSessionId, memory_session_id: session.memorySessionId,
project: session.project, project: session.project,
user_prompt: session.userPrompt, user_prompt: session.userPrompt,
last_user_message: message.last_user_message || '', last_user_message: message.last_user_message || '',
@@ -417,12 +417,12 @@ export class OpenRouterAgent {
originalTimestamp: number | null originalTimestamp: number | null
): Promise<void> { ): Promise<void> {
// Parse observations (same XML format) // Parse observations (same XML format)
const observations = parseObservations(text, session.claudeSessionId); const observations = parseObservations(text, session.contentSessionId);
// Store observations with original timestamp (if processing backlog) or current time // Store observations with original timestamp (if processing backlog) or current time
for (const obs of observations) { for (const obs of observations) {
const { id: obsId, createdAtEpoch } = this.dbManager.getSessionStore().storeObservation( const { id: obsId, createdAtEpoch } = this.dbManager.getSessionStore().storeObservation(
session.claudeSessionId, session.contentSessionId,
session.project, session.project,
obs, obs,
session.lastPromptNumber, session.lastPromptNumber,
@@ -440,7 +440,7 @@ export class OpenRouterAgent {
// Sync to Chroma // Sync to Chroma
this.dbManager.getChromaSync().syncObservation( this.dbManager.getChromaSync().syncObservation(
obsId, obsId,
session.claudeSessionId, session.contentSessionId,
session.project, session.project,
obs, obs,
session.lastPromptNumber, session.lastPromptNumber,
@@ -456,8 +456,8 @@ export class OpenRouterAgent {
type: 'new_observation', type: 'new_observation',
observation: { observation: {
id: obsId, id: obsId,
sdk_session_id: session.sdkSessionId, memory_session_id: session.memorySessionId,
session_id: session.claudeSessionId, session_id: session.contentSessionId,
type: obs.type, type: obs.type,
title: obs.title, title: obs.title,
subtitle: obs.subtitle, subtitle: obs.subtitle,
@@ -490,7 +490,7 @@ export class OpenRouterAgent {
}; };
const { id: summaryId, createdAtEpoch } = this.dbManager.getSessionStore().storeSummary( const { id: summaryId, createdAtEpoch } = this.dbManager.getSessionStore().storeSummary(
session.claudeSessionId, session.contentSessionId,
session.project, session.project,
summaryForStore, summaryForStore,
session.lastPromptNumber, session.lastPromptNumber,
@@ -507,7 +507,7 @@ export class OpenRouterAgent {
// Sync to Chroma // Sync to Chroma
this.dbManager.getChromaSync().syncSummary( this.dbManager.getChromaSync().syncSummary(
summaryId, summaryId,
session.claudeSessionId, session.contentSessionId,
session.project, session.project,
summaryForStore, summaryForStore,
session.lastPromptNumber, session.lastPromptNumber,
@@ -523,7 +523,7 @@ export class OpenRouterAgent {
type: 'new_summary', type: 'new_summary',
summary: { summary: {
id: summaryId, id: summaryId,
session_id: session.claudeSessionId, session_id: session.contentSessionId,
request: summary.request, request: summary.request,
investigated: summary.investigated, investigated: summary.investigated,
learned: summary.learned, learned: summary.learned,
+5 -5
View File
@@ -74,7 +74,7 @@ export class PaginationHelper {
getObservations(offset: number, limit: number, project?: string): PaginatedResult<Observation> { getObservations(offset: number, limit: number, project?: string): PaginatedResult<Observation> {
const result = this.paginate<Observation>( const result = this.paginate<Observation>(
'observations', 'observations',
'id, sdk_session_id, project, type, title, subtitle, narrative, text, facts, concepts, files_read, files_modified, prompt_number, created_at, created_at_epoch', 'id, memory_session_id, project, type, title, subtitle, narrative, text, facts, concepts, files_read, files_modified, prompt_number, created_at, created_at_epoch',
offset, offset,
limit, limit,
project project
@@ -96,7 +96,7 @@ export class PaginationHelper {
let query = ` let query = `
SELECT SELECT
ss.id, ss.id,
s.claude_session_id as session_id, s.content_session_id as session_id,
ss.request, ss.request,
ss.investigated, ss.investigated,
ss.learned, ss.learned,
@@ -106,7 +106,7 @@ export class PaginationHelper {
ss.created_at, ss.created_at,
ss.created_at_epoch ss.created_at_epoch
FROM session_summaries ss FROM session_summaries ss
JOIN sdk_sessions s ON ss.sdk_session_id = s.sdk_session_id JOIN sdk_sessions s ON ss.memory_session_id = s.memory_session_id
`; `;
const params: any[] = []; const params: any[] = [];
@@ -136,9 +136,9 @@ export class PaginationHelper {
const db = this.dbManager.getSessionStore().db; const db = this.dbManager.getSessionStore().db;
let query = ` let query = `
SELECT up.id, up.claude_session_id, s.project, up.prompt_number, up.prompt_text, up.created_at, up.created_at_epoch SELECT up.id, up.content_session_id, s.project, up.prompt_number, up.prompt_text, up.created_at, up.created_at_epoch
FROM user_prompts up FROM user_prompts up
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id JOIN sdk_sessions s ON up.content_session_id = s.content_session_id
`; `;
const params: any[] = []; const params: any[] = [];
+38 -20
View File
@@ -66,17 +66,20 @@ export class SDKAgent {
logger.info('SDK', 'Starting SDK query', { logger.info('SDK', 'Starting SDK query', {
sessionDbId: session.sessionDbId, sessionDbId: session.sessionDbId,
claudeSessionId: session.claudeSessionId, contentSessionId: session.contentSessionId,
resume_parameter: session.claudeSessionId, memorySessionId: session.memorySessionId,
resume_parameter: session.memorySessionId || '(none - fresh start)',
lastPromptNumber: session.lastPromptNumber lastPromptNumber: session.lastPromptNumber
}); });
// Run Agent SDK query loop // Run Agent SDK query loop
// Use memorySessionId for resume (captured from previous SDK response) if available
const queryResult = query({ const queryResult = query({
prompt: messageGenerator, prompt: messageGenerator,
options: { options: {
model: modelId, model: modelId,
resume: session.claudeSessionId, // Only resume if we have a captured memory session ID from previous SDK interaction
...(session.memorySessionId && { resume: session.memorySessionId }),
disallowedTools, disallowedTools,
abortController: session.abortController, abortController: session.abortController,
pathToClaudeCodeExecutable: claudePath pathToClaudeCodeExecutable: claudePath
@@ -85,6 +88,21 @@ export class SDKAgent {
// Process SDK messages // Process SDK messages
for await (const message of queryResult) { for await (const message of queryResult) {
// Capture memory session ID from first SDK message (any type has session_id)
// This enables resume for subsequent generator starts within the same user session
if (!session.memorySessionId && message.session_id) {
session.memorySessionId = message.session_id;
// Persist to database for cross-restart recovery
this.dbManager.getSessionStore().updateMemorySessionId(
session.sessionDbId,
message.session_id
);
logger.info('SDK', 'Captured memory session ID', {
sessionDbId: session.sessionDbId,
memorySessionId: message.session_id
});
}
// Handle assistant messages // Handle assistant messages
if (message.type === 'assistant') { if (message.type === 'assistant') {
const content = message.message.content; const content = message.message.content;
@@ -184,7 +202,7 @@ export class SDKAgent {
* - Continuation prompt for same session * - Continuation prompt for same session
* - Includes session context and prompt number * - Includes session context and prompt number
* *
* BOTH prompts receive session.claudeSessionId: * BOTH prompts receive session.contentSessionId:
* - This comes from the hook's session_id (see new-hook.ts) * - This comes from the hook's session_id (see new-hook.ts)
* - Same session_id used by SAVE hook to store observations * - Same session_id used by SAVE hook to store observations
* - This is how everything stays connected in one unified session * - This is how everything stays connected in one unified session
@@ -207,28 +225,28 @@ export class SDKAgent {
const isInitPrompt = session.lastPromptNumber === 1; const isInitPrompt = session.lastPromptNumber === 1;
logger.info('SDK', 'Creating message generator', { logger.info('SDK', 'Creating message generator', {
sessionDbId: session.sessionDbId, sessionDbId: session.sessionDbId,
claudeSessionId: session.claudeSessionId, contentSessionId: session.contentSessionId,
lastPromptNumber: session.lastPromptNumber, lastPromptNumber: session.lastPromptNumber,
isInitPrompt, isInitPrompt,
promptType: isInitPrompt ? 'INIT' : 'CONTINUATION' promptType: isInitPrompt ? 'INIT' : 'CONTINUATION'
}); });
const initPrompt = isInitPrompt const initPrompt = isInitPrompt
? buildInitPrompt(session.project, session.claudeSessionId, session.userPrompt, mode) ? buildInitPrompt(session.project, session.contentSessionId, session.userPrompt, mode)
: buildContinuationPrompt(session.userPrompt, session.lastPromptNumber, session.claudeSessionId, mode); : buildContinuationPrompt(session.userPrompt, session.lastPromptNumber, session.contentSessionId, mode);
// Add to shared conversation history for provider interop // Add to shared conversation history for provider interop
session.conversationHistory.push({ role: 'user', content: initPrompt }); session.conversationHistory.push({ role: 'user', content: initPrompt });
// Yield initial user prompt with context (or continuation if prompt #2+) // Yield initial user prompt with context (or continuation if prompt #2+)
// CRITICAL: Both paths use session.claudeSessionId from the hook // CRITICAL: Both paths use session.contentSessionId from the hook
yield { yield {
type: 'user', type: 'user',
message: { message: {
role: 'user', role: 'user',
content: initPrompt content: initPrompt
}, },
session_id: session.claudeSessionId, session_id: session.contentSessionId,
parent_tool_use_id: null, parent_tool_use_id: null,
isSynthetic: true isSynthetic: true
}; };
@@ -259,14 +277,14 @@ export class SDKAgent {
role: 'user', role: 'user',
content: obsPrompt content: obsPrompt
}, },
session_id: session.claudeSessionId, session_id: session.contentSessionId,
parent_tool_use_id: null, parent_tool_use_id: null,
isSynthetic: true isSynthetic: true
}; };
} else if (message.type === 'summarize') { } else if (message.type === 'summarize') {
const summaryPrompt = buildSummaryPrompt({ const summaryPrompt = buildSummaryPrompt({
id: session.sessionDbId, id: session.sessionDbId,
sdk_session_id: session.sdkSessionId, memory_session_id: session.memorySessionId,
project: session.project, project: session.project,
user_prompt: session.userPrompt, user_prompt: session.userPrompt,
last_user_message: message.last_user_message || '', last_user_message: message.last_user_message || '',
@@ -282,7 +300,7 @@ export class SDKAgent {
role: 'user', role: 'user',
content: summaryPrompt content: summaryPrompt
}, },
session_id: session.claudeSessionId, session_id: session.contentSessionId,
parent_tool_use_id: null, parent_tool_use_id: null,
isSynthetic: true isSynthetic: true
}; };
@@ -305,12 +323,12 @@ export class SDKAgent {
} }
// Parse observations // Parse observations
const observations = parseObservations(text, session.claudeSessionId); const observations = parseObservations(text, session.contentSessionId);
// Store observations with original timestamp (if processing backlog) or current time // Store observations with original timestamp (if processing backlog) or current time
for (const obs of observations) { for (const obs of observations) {
const { id: obsId, createdAtEpoch } = this.dbManager.getSessionStore().storeObservation( const { id: obsId, createdAtEpoch } = this.dbManager.getSessionStore().storeObservation(
session.claudeSessionId, session.contentSessionId,
session.project, session.project,
obs, obs,
session.lastPromptNumber, session.lastPromptNumber,
@@ -335,7 +353,7 @@ export class SDKAgent {
const obsTitle = obs.title || '(untitled)'; const obsTitle = obs.title || '(untitled)';
this.dbManager.getChromaSync().syncObservation( this.dbManager.getChromaSync().syncObservation(
obsId, obsId,
session.claudeSessionId, session.contentSessionId,
session.project, session.project,
obs, obs,
session.lastPromptNumber, session.lastPromptNumber,
@@ -363,8 +381,8 @@ export class SDKAgent {
type: 'new_observation', type: 'new_observation',
observation: { observation: {
id: obsId, id: obsId,
sdk_session_id: session.sdkSessionId, memory_session_id: session.memorySessionId,
session_id: session.claudeSessionId, session_id: session.contentSessionId,
type: obs.type, type: obs.type,
title: obs.title, title: obs.title,
subtitle: obs.subtitle, subtitle: obs.subtitle,
@@ -388,7 +406,7 @@ export class SDKAgent {
// Store summary with original timestamp (if processing backlog) or current time // Store summary with original timestamp (if processing backlog) or current time
if (summary) { if (summary) {
const { id: summaryId, createdAtEpoch } = this.dbManager.getSessionStore().storeSummary( const { id: summaryId, createdAtEpoch } = this.dbManager.getSessionStore().storeSummary(
session.claudeSessionId, session.contentSessionId,
session.project, session.project,
summary, summary,
session.lastPromptNumber, session.lastPromptNumber,
@@ -410,7 +428,7 @@ export class SDKAgent {
const summaryRequest = summary.request || '(no request)'; const summaryRequest = summary.request || '(no request)';
this.dbManager.getChromaSync().syncSummary( this.dbManager.getChromaSync().syncSummary(
summaryId, summaryId,
session.claudeSessionId, session.contentSessionId,
session.project, session.project,
summary, summary,
session.lastPromptNumber, session.lastPromptNumber,
@@ -436,7 +454,7 @@ export class SDKAgent {
type: 'new_summary', type: 'new_summary',
summary: { summary: {
id: summaryId, id: summaryId,
session_id: session.claudeSessionId, session_id: session.contentSessionId,
request: summary.request, request: summary.request,
investigated: summary.investigated, investigated: summary.investigated,
learned: summary.learned, learned: summary.learned,
+13 -11
View File
@@ -59,7 +59,7 @@ export class SessionManager {
if (session) { if (session) {
logger.info('SESSION', 'Returning cached session', { logger.info('SESSION', 'Returning cached session', {
sessionDbId, sessionDbId,
claudeSessionId: session.claudeSessionId, contentSessionId: session.contentSessionId,
lastPromptNumber: session.lastPromptNumber lastPromptNumber: session.lastPromptNumber
}); });
@@ -101,8 +101,8 @@ export class SessionManager {
logger.info('SESSION', 'Fetched session from database', { logger.info('SESSION', 'Fetched session from database', {
sessionDbId, sessionDbId,
claude_session_id: dbSession.claude_session_id, content_session_id: dbSession.content_session_id,
sdk_session_id: dbSession.sdk_session_id memory_session_id: dbSession.memory_session_id
}); });
// Use currentUserPrompt if provided, otherwise fall back to database (first prompt) // Use currentUserPrompt if provided, otherwise fall back to database (first prompt)
@@ -123,16 +123,17 @@ export class SessionManager {
} }
// Create active session // Create active session
// Load memorySessionId from database if previously captured (enables resume across restarts)
session = { session = {
sessionDbId, sessionDbId,
claudeSessionId: dbSession.claude_session_id, contentSessionId: dbSession.content_session_id,
sdkSessionId: null, memorySessionId: dbSession.memory_session_id || null,
project: dbSession.project, project: dbSession.project,
userPrompt, userPrompt,
pendingMessages: [], pendingMessages: [],
abortController: new AbortController(), abortController: new AbortController(),
generatorPromise: null, generatorPromise: null,
lastPromptNumber: promptNumber || this.dbManager.getSessionStore().getPromptNumberFromUserPrompts(dbSession.claude_session_id), lastPromptNumber: promptNumber || this.dbManager.getSessionStore().getPromptNumberFromUserPrompts(dbSession.content_session_id),
startTime: Date.now(), startTime: Date.now(),
cumulativeInputTokens: 0, cumulativeInputTokens: 0,
cumulativeOutputTokens: 0, cumulativeOutputTokens: 0,
@@ -144,8 +145,9 @@ export class SessionManager {
logger.info('SESSION', 'Creating new session object', { logger.info('SESSION', 'Creating new session object', {
sessionDbId, sessionDbId,
claudeSessionId: dbSession.claude_session_id, contentSessionId: dbSession.content_session_id,
lastPromptNumber: promptNumber || this.dbManager.getSessionStore().getPromptNumberFromUserPrompts(dbSession.claude_session_id) memorySessionId: dbSession.memory_session_id || '(none - fresh session)',
lastPromptNumber: promptNumber || this.dbManager.getSessionStore().getPromptNumberFromUserPrompts(dbSession.content_session_id)
}); });
this.sessions.set(sessionDbId, session); this.sessions.set(sessionDbId, session);
@@ -157,7 +159,7 @@ export class SessionManager {
logger.info('SESSION', 'Session initialized', { logger.info('SESSION', 'Session initialized', {
sessionId: sessionDbId, sessionId: sessionDbId,
project: session.project, project: session.project,
claudeSessionId: session.claudeSessionId, contentSessionId: session.contentSessionId,
queueDepth: 0, queueDepth: 0,
hasGenerator: false hasGenerator: false
}); });
@@ -197,7 +199,7 @@ export class SessionManager {
}; };
try { try {
const messageId = this.getPendingStore().enqueue(sessionDbId, session.claudeSessionId, message); const messageId = this.getPendingStore().enqueue(sessionDbId, session.contentSessionId, message);
logger.debug('SESSION', `Observation persisted to DB`, { logger.debug('SESSION', `Observation persisted to DB`, {
sessionId: sessionDbId, sessionId: sessionDbId,
messageId, messageId,
@@ -247,7 +249,7 @@ export class SessionManager {
}; };
try { try {
const messageId = this.getPendingStore().enqueue(sessionDbId, session.claudeSessionId, message); const messageId = this.getPendingStore().enqueue(sessionDbId, session.contentSessionId, message);
logger.debug('SESSION', `Summarize persisted to DB`, { logger.debug('SESSION', `Summarize persisted to DB`, {
sessionId: sessionDbId, sessionId: sessionDbId,
messageId messageId
@@ -21,7 +21,7 @@ export class SessionEventBroadcaster {
*/ */
broadcastNewPrompt(prompt: { broadcastNewPrompt(prompt: {
id: number; id: number;
claude_session_id: string; content_session_id: string;
project: string; project: string;
prompt_number: number; prompt_number: number;
prompt_text: string; prompt_text: string;
@@ -158,18 +158,18 @@ export class DataRoutes extends BaseRouteHandler {
/** /**
* Get SDK sessions by SDK session IDs * Get SDK sessions by SDK session IDs
* POST /api/sdk-sessions/batch * POST /api/sdk-sessions/batch
* Body: { sdkSessionIds: string[] } * Body: { memorySessionIds: string[] }
*/ */
private handleGetSdkSessionsByIds = this.wrapHandler((req: Request, res: Response): void => { private handleGetSdkSessionsByIds = this.wrapHandler((req: Request, res: Response): void => {
const { sdkSessionIds } = req.body; const { memorySessionIds } = req.body;
if (!Array.isArray(sdkSessionIds)) { if (!Array.isArray(memorySessionIds)) {
this.badRequest(res, 'sdkSessionIds must be an array'); this.badRequest(res, 'memorySessionIds must be an array');
return; return;
} }
const store = this.dbManager.getSessionStore(); const store = this.dbManager.getSessionStore();
const sessions = store.getSdkSessionsBySessionIds(sdkSessionIds); const sessions = store.getSdkSessionsBySessionIds(memorySessionIds);
res.json(sessions); res.json(sessions);
}); });
@@ -45,7 +45,7 @@ export class SessionRoutes extends BaseRouteHandler {
* Get the appropriate agent based on settings * Get the appropriate agent based on settings
* Throws error if provider is selected but not configured (no silent fallback) * Throws error if provider is selected but not configured (no silent fallback)
* *
* Note: Session linking via claudeSessionId allows provider switching mid-session. * Note: Session linking via contentSessionId allows provider switching mid-session.
* The conversationHistory on ActiveSession maintains context across providers. * The conversationHistory on ActiveSession maintains context across providers.
*/ */
private getActiveAgent(): SDKAgent | GeminiAgent | OpenRouterAgent { private getActiveAgent(): SDKAgent | GeminiAgent | OpenRouterAgent {
@@ -217,7 +217,7 @@ export class SessionRoutes extends BaseRouteHandler {
app.delete('/sessions/:sessionDbId', this.handleSessionDelete.bind(this)); app.delete('/sessions/:sessionDbId', this.handleSessionDelete.bind(this));
app.post('/sessions/:sessionDbId/complete', this.handleSessionComplete.bind(this)); app.post('/sessions/:sessionDbId/complete', this.handleSessionComplete.bind(this));
// New session endpoints (use claudeSessionId) // New session endpoints (use contentSessionId)
app.post('/api/sessions/init', this.handleSessionInitByClaudeId.bind(this)); app.post('/api/sessions/init', this.handleSessionInitByClaudeId.bind(this));
app.post('/api/sessions/observations', this.handleObservationsByClaudeId.bind(this)); app.post('/api/sessions/observations', this.handleObservationsByClaudeId.bind(this));
app.post('/api/sessions/summarize', this.handleSummarizeByClaudeId.bind(this)); app.post('/api/sessions/summarize', this.handleSummarizeByClaudeId.bind(this));
@@ -240,13 +240,13 @@ export class SessionRoutes extends BaseRouteHandler {
const session = this.sessionManager.initializeSession(sessionDbId, userPrompt, promptNumber); const session = this.sessionManager.initializeSession(sessionDbId, userPrompt, promptNumber);
// Get the latest user_prompt for this session to sync to Chroma // Get the latest user_prompt for this session to sync to Chroma
const latestPrompt = this.dbManager.getSessionStore().getLatestUserPrompt(session.claudeSessionId); const latestPrompt = this.dbManager.getSessionStore().getLatestUserPrompt(session.contentSessionId);
// Broadcast new prompt to SSE clients (for web UI) // Broadcast new prompt to SSE clients (for web UI)
if (latestPrompt) { if (latestPrompt) {
this.eventBroadcaster.broadcastNewPrompt({ this.eventBroadcaster.broadcastNewPrompt({
id: latestPrompt.id, id: latestPrompt.id,
claude_session_id: latestPrompt.claude_session_id, content_session_id: latestPrompt.content_session_id,
project: latestPrompt.project, project: latestPrompt.project,
prompt_number: latestPrompt.prompt_number, prompt_number: latestPrompt.prompt_number,
prompt_text: latestPrompt.prompt_text, prompt_text: latestPrompt.prompt_text,
@@ -258,7 +258,7 @@ export class SessionRoutes extends BaseRouteHandler {
const promptText = latestPrompt.prompt_text; const promptText = latestPrompt.prompt_text;
this.dbManager.getChromaSync().syncUserPrompt( this.dbManager.getChromaSync().syncUserPrompt(
latestPrompt.id, latestPrompt.id,
latestPrompt.sdk_session_id, latestPrompt.memory_session_id,
latestPrompt.project, latestPrompt.project,
promptText, promptText,
latestPrompt.prompt_number, latestPrompt.prompt_number,
@@ -387,15 +387,15 @@ export class SessionRoutes extends BaseRouteHandler {
}); });
/** /**
* Queue observations by claudeSessionId (post-tool-use-hook uses this) * Queue observations by contentSessionId (post-tool-use-hook uses this)
* POST /api/sessions/observations * POST /api/sessions/observations
* Body: { claudeSessionId, tool_name, tool_input, tool_response, cwd } * Body: { contentSessionId, tool_name, tool_input, tool_response, cwd }
*/ */
private handleObservationsByClaudeId = this.wrapHandler((req: Request, res: Response): void => { private handleObservationsByClaudeId = this.wrapHandler((req: Request, res: Response): void => {
const { claudeSessionId, tool_name, tool_input, tool_response, cwd } = req.body; const { contentSessionId, tool_name, tool_input, tool_response, cwd } = req.body;
if (!claudeSessionId) { if (!contentSessionId) {
return this.badRequest(res, 'Missing claudeSessionId'); return this.badRequest(res, 'Missing contentSessionId');
} }
// Load skip tools from settings // Load skip tools from settings
@@ -426,13 +426,13 @@ export class SessionRoutes extends BaseRouteHandler {
const store = this.dbManager.getSessionStore(); const store = this.dbManager.getSessionStore();
// Get or create session // Get or create session
const sessionDbId = store.createSDKSession(claudeSessionId, '', ''); const sessionDbId = store.createSDKSession(contentSessionId, '', '');
const promptNumber = store.getPromptNumberFromUserPrompts(claudeSessionId); const promptNumber = store.getPromptNumberFromUserPrompts(contentSessionId);
// Privacy check: skip if user prompt was entirely private // Privacy check: skip if user prompt was entirely private
const userPrompt = PrivacyCheckValidator.checkUserPromptPrivacy( const userPrompt = PrivacyCheckValidator.checkUserPromptPrivacy(
store, store,
claudeSessionId, contentSessionId,
promptNumber, promptNumber,
'observation', 'observation',
sessionDbId, sessionDbId,
@@ -477,29 +477,29 @@ export class SessionRoutes extends BaseRouteHandler {
}); });
/** /**
* Queue summarize by claudeSessionId (summary-hook uses this) * Queue summarize by contentSessionId (summary-hook uses this)
* POST /api/sessions/summarize * POST /api/sessions/summarize
* Body: { claudeSessionId, last_user_message, last_assistant_message } * Body: { contentSessionId, last_user_message, last_assistant_message }
* *
* Checks privacy, queues summarize request for SDK agent * Checks privacy, queues summarize request for SDK agent
*/ */
private handleSummarizeByClaudeId = this.wrapHandler((req: Request, res: Response): void => { private handleSummarizeByClaudeId = this.wrapHandler((req: Request, res: Response): void => {
const { claudeSessionId, last_user_message, last_assistant_message } = req.body; const { contentSessionId, last_user_message, last_assistant_message } = req.body;
if (!claudeSessionId) { if (!contentSessionId) {
return this.badRequest(res, 'Missing claudeSessionId'); return this.badRequest(res, 'Missing contentSessionId');
} }
const store = this.dbManager.getSessionStore(); const store = this.dbManager.getSessionStore();
// Get or create session // Get or create session
const sessionDbId = store.createSDKSession(claudeSessionId, '', ''); const sessionDbId = store.createSDKSession(contentSessionId, '', '');
const promptNumber = store.getPromptNumberFromUserPrompts(claudeSessionId); const promptNumber = store.getPromptNumberFromUserPrompts(contentSessionId);
// Privacy check: skip if user prompt was entirely private // Privacy check: skip if user prompt was entirely private
const userPrompt = PrivacyCheckValidator.checkUserPromptPrivacy( const userPrompt = PrivacyCheckValidator.checkUserPromptPrivacy(
store, store,
claudeSessionId, contentSessionId,
promptNumber, promptNumber,
'summarize', 'summarize',
sessionDbId sessionDbId
@@ -532,9 +532,9 @@ export class SessionRoutes extends BaseRouteHandler {
}); });
/** /**
* Initialize session by claudeSessionId (new-hook uses this) * Initialize session by contentSessionId (new-hook uses this)
* POST /api/sessions/init * POST /api/sessions/init
* Body: { claudeSessionId, project, prompt } * Body: { contentSessionId, project, prompt }
* *
* Performs all session initialization DB operations: * Performs all session initialization DB operations:
* - Creates/gets SDK session (idempotent) * - Creates/gets SDK session (idempotent)
@@ -544,31 +544,31 @@ export class SessionRoutes extends BaseRouteHandler {
* Returns: { sessionDbId, promptNumber, skipped: boolean, reason?: string } * Returns: { sessionDbId, promptNumber, skipped: boolean, reason?: string }
*/ */
private handleSessionInitByClaudeId = this.wrapHandler((req: Request, res: Response): void => { private handleSessionInitByClaudeId = this.wrapHandler((req: Request, res: Response): void => {
const { claudeSessionId, project, prompt } = req.body; const { contentSessionId, project, prompt } = req.body;
logger.info('HTTP', 'SessionRoutes: handleSessionInitByClaudeId called', { logger.info('HTTP', 'SessionRoutes: handleSessionInitByClaudeId called', {
claudeSessionId, contentSessionId,
project, project,
prompt_length: prompt?.length prompt_length: prompt?.length
}); });
// Validate required parameters // Validate required parameters
if (!this.validateRequired(req, res, ['claudeSessionId', 'project', 'prompt'])) { if (!this.validateRequired(req, res, ['contentSessionId', 'project', 'prompt'])) {
return; return;
} }
const store = this.dbManager.getSessionStore(); const store = this.dbManager.getSessionStore();
// Step 1: Create/get SDK session (idempotent INSERT OR IGNORE) // Step 1: Create/get SDK session (idempotent INSERT OR IGNORE)
const sessionDbId = store.createSDKSession(claudeSessionId, project, prompt); const sessionDbId = store.createSDKSession(contentSessionId, project, prompt);
logger.info('HTTP', 'SessionRoutes: createSDKSession returned', { logger.info('HTTP', 'SessionRoutes: createSDKSession returned', {
sessionDbId, sessionDbId,
claudeSessionId contentSessionId
}); });
// Step 2: Get next prompt number from user_prompts count // Step 2: Get next prompt number from user_prompts count
const currentCount = store.getPromptNumberFromUserPrompts(claudeSessionId); const currentCount = store.getPromptNumberFromUserPrompts(contentSessionId);
const promptNumber = currentCount + 1; const promptNumber = currentCount + 1;
logger.info('HTTP', 'SessionRoutes: Calculated promptNumber', { logger.info('HTTP', 'SessionRoutes: Calculated promptNumber', {
@@ -598,7 +598,7 @@ export class SessionRoutes extends BaseRouteHandler {
} }
// Step 5: Save cleaned user prompt // Step 5: Save cleaned user prompt
store.saveUserPrompt(claudeSessionId, promptNumber, cleanedPrompt); store.saveUserPrompt(contentSessionId, promptNumber, cleanedPrompt);
logger.info('SESSION', 'Session initialized via HTTP', { logger.info('SESSION', 'Session initialized via HTTP', {
sessionId: sessionDbId, sessionId: sessionDbId,
@@ -12,20 +12,20 @@ export class PrivacyCheckValidator {
* Check if user prompt is public (not entirely private) * Check if user prompt is public (not entirely private)
* *
* @param store - SessionStore instance * @param store - SessionStore instance
* @param claudeSessionId - Claude session ID * @param contentSessionId - Claude session ID
* @param promptNumber - Prompt number within session * @param promptNumber - Prompt number within session
* @param operationType - Type of operation being validated ('observation' or 'summarize') * @param operationType - Type of operation being validated ('observation' or 'summarize')
* @returns User prompt text if public, null if private * @returns User prompt text if public, null if private
*/ */
static checkUserPromptPrivacy( static checkUserPromptPrivacy(
store: SessionStore, store: SessionStore,
claudeSessionId: string, contentSessionId: string,
promptNumber: number, promptNumber: number,
operationType: 'observation' | 'summarize', operationType: 'observation' | 'summarize',
sessionDbId: number, sessionDbId: number,
additionalContext?: Record<string, any> additionalContext?: Record<string, any>
): string | null { ): string | null {
const userPrompt = store.getUserPrompt(claudeSessionId, promptNumber); const userPrompt = store.getUserPrompt(contentSessionId, promptNumber);
if (!userPrompt || userPrompt.trim() === '') { if (!userPrompt || userPrompt.trim() === '') {
logger.debug('HOOK', `Skipping ${operationType} - user prompt was entirely private`, { logger.debug('HOOK', `Skipping ${operationType} - user prompt was entirely private`, {
+9 -8
View File
@@ -45,8 +45,8 @@ export interface SchemaVersion {
*/ */
export interface SdkSessionRecord { export interface SdkSessionRecord {
id: number; id: number;
claude_session_id: string; content_session_id: string;
sdk_session_id: string | null; memory_session_id: string | null;
project: string; project: string;
user_prompt: string | null; user_prompt: string | null;
started_at: string; started_at: string;
@@ -63,7 +63,7 @@ export interface SdkSessionRecord {
*/ */
export interface ObservationRecord { export interface ObservationRecord {
id: number; id: number;
sdk_session_id: string; memory_session_id: string;
project: string; project: string;
text: string | null; text: string | null;
type: 'decision' | 'bugfix' | 'feature' | 'refactor' | 'discovery' | 'change'; type: 'decision' | 'bugfix' | 'feature' | 'refactor' | 'discovery' | 'change';
@@ -81,7 +81,7 @@ export interface ObservationRecord {
*/ */
export interface SessionSummaryRecord { export interface SessionSummaryRecord {
id: number; id: number;
sdk_session_id: string; memory_session_id: string;
project: string; project: string;
request: string | null; request: string | null;
investigated: string | null; investigated: string | null;
@@ -99,9 +99,10 @@ export interface SessionSummaryRecord {
*/ */
export interface UserPromptRecord { export interface UserPromptRecord {
id: number; id: number;
claude_session_id: string; content_session_id: string;
prompt_number: number; prompt_number: number;
prompt_text: string; prompt_text: string;
project?: string; // From JOIN with sdk_sessions
created_at: string; created_at: string;
created_at_epoch: number; created_at_epoch: number;
} }
@@ -111,8 +112,8 @@ export interface UserPromptRecord {
*/ */
export interface LatestPromptResult { export interface LatestPromptResult {
id: number; id: number;
claude_session_id: string; content_session_id: string;
sdk_session_id: string; memory_session_id: string;
project: string; project: string;
prompt_number: number; prompt_number: number;
prompt_text: string; prompt_text: string;
@@ -124,7 +125,7 @@ export interface LatestPromptResult {
*/ */
export interface ObservationWithContext { export interface ObservationWithContext {
id: number; id: number;
sdk_session_id: string; memory_session_id: string;
project: string; project: string;
text: string | null; text: string | null;
type: string; type: string;
+2 -2
View File
@@ -1,6 +1,6 @@
export interface Observation { export interface Observation {
id: number; id: number;
sdk_session_id: string; memory_session_id: string;
project: string; project: string;
type: string; type: string;
title: string | null; title: string | null;
@@ -30,7 +30,7 @@ export interface Summary {
export interface UserPrompt { export interface UserPrompt {
id: number; id: number;
claude_session_id: string; content_session_id: string;
project: string; project: string;
prompt_number: number; prompt_number: number;
prompt_text: string; prompt_text: string;
+2 -2
View File
@@ -19,7 +19,7 @@ export type Component = 'HOOK' | 'WORKER' | 'SDK' | 'PARSER' | 'DB' | 'SYSTEM' |
interface LogContext { interface LogContext {
sessionId?: number; sessionId?: number;
sdkSessionId?: string; memorySessionId?: string;
correlationId?: string; correlationId?: string;
[key: string]: any; [key: string]: any;
} }
@@ -253,7 +253,7 @@ class Logger {
// Build additional context // Build additional context
let contextStr = ''; let contextStr = '';
if (context) { if (context) {
const { sessionId, sdkSessionId, correlationId, ...rest } = context; const { sessionId, memorySessionId, correlationId, ...rest } = context;
if (Object.keys(rest).length > 0) { if (Object.keys(rest).length > 0) {
const pairs = Object.entries(rest).map(([k, v]) => `${k}=${v}`); const pairs = Object.entries(rest).map(([k, v]) => `${k}=${v}`);
contextStr = ` {${pairs.join(', ')}}`; contextStr = ` {${pairs.join(', ')}}`;