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
+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?`
${o.message}
${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+`
`,"utf8")}catch(D){process.stderr.write(`[LOGGER] Failed to write to log file: ${D}
`+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(m){process.stderr.write(`[LOGGER] Failed to write to log file: ${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 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}
@@ -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}
${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)})}