From 711f5455dfff2dccf92805a8ac1722bf2f4084ac Mon Sep 17 00:00:00 2001 From: Alex Newman Date: Fri, 6 Feb 2026 02:04:49 -0500 Subject: [PATCH] fix: generate synthetic memorySessionId for stateless providers (PR #615) Gemini and OpenRouter are stateless APIs that never return session IDs. Without synthetic IDs, PR #693's defensive memorySessionId checks throw errors on every observation processing call for these providers. Generates provider-prefixed IDs (gemini-/openrouter-{contentSessionId}- {timestamp}) before the first API call, persisted to the database via updateMemorySessionId(). Applied from PR #615 (closed due to staleness). Co-Authored-By: Claude Opus 4.6 --- plugin/scripts/worker-service.cjs | 2 +- src/services/worker/GeminiAgent.ts | 8 ++++++++ src/services/worker/OpenRouterAgent.ts | 8 ++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/plugin/scripts/worker-service.cjs b/plugin/scripts/worker-service.cjs index 641374fd..23e9dc83 100755 --- a/plugin/scripts/worker-service.cjs +++ b/plugin/scripts/worker-service.cjs @@ -1173,7 +1173,7 @@ ${n}`}function Cie(t,e){let r=ir.default.join(t,"CLAUDE.md"),n=`${r}.tmp`;if(!(0 `):typeof h=="string"?h:"",g=_.length,m=e.cumulativeInputTokens+e.cumulativeOutputTokens,y=f.message.usage;y&&(e.cumulativeInputTokens+=y.input_tokens||0,e.cumulativeOutputTokens+=y.output_tokens||0,y.cache_creation_input_tokens&&(e.cumulativeInputTokens+=y.cache_creation_input_tokens),E.debug("SDK","Token usage captured",{sessionId:e.sessionDbId,inputTokens:y.input_tokens,outputTokens:y.output_tokens,cacheCreation:y.cache_creation_input_tokens||0,cacheRead:y.cache_read_input_tokens||0,cumulativeInput:e.cumulativeInputTokens,cumulativeOutput:e.cumulativeOutputTokens}));let v=e.cumulativeInputTokens+e.cumulativeOutputTokens-m,b=e.earliestPendingTimestamp;if(g>0){let S=g>100?_.substring(0,100)+"...":_;E.dataOut("SDK",`Response received (${g} chars)`,{sessionId:e.sessionDbId,promptNumber:e.lastPromptNumber},S)}if(typeof _=="string"&&_.includes("Prompt is too long"))throw new Error("Claude session context overflow: prompt is too long");await Yn(_,e,this.dbManager,this.sessionManager,r,v,b,"SDK",n.lastCwd)}f.type==="result"&&f.subtype}let p=Date.now()-e.startTime;E.success("SDK","Agent completed",{sessionId:e.sessionDbId,duration:`${(p/1e3).toFixed(1)}s`})}async*createMessageGenerator(e,r){let n=Ve.getInstance().getActiveMode(),i=e.lastPromptNumber===1;E.info("SDK","Creating message generator",{sessionDbId:e.sessionDbId,contentSessionId:e.contentSessionId,lastPromptNumber:e.lastPromptNumber,isInitPrompt:i,promptType:i?"INIT":"CONTINUATION"});let a=i?ac(e.project,e.contentSessionId,e.userPrompt,n):cc(e.userPrompt,e.lastPromptNumber,e.contentSessionId,n);e.conversationHistory.push({role:"user",content:a}),yield{type:"user",message:{role:"user",content:a},session_id:e.contentSessionId,parent_tool_use_id:null,isSynthetic:!0};for await(let o of this.sessionManager.getMessageIterator(e.sessionDbId))if(o.cwd&&(r.lastCwd=o.cwd),o.type==="observation"){o.prompt_number!==void 0&&(e.lastPromptNumber=o.prompt_number);let s=oc({id:0,tool_name:o.tool_name,tool_input:JSON.stringify(o.tool_input),tool_output:JSON.stringify(o.tool_response),created_at_epoch:Date.now(),cwd:o.cwd});e.conversationHistory.push({role:"user",content:s}),yield{type:"user",message:{role:"user",content:s},session_id:e.contentSessionId,parent_tool_use_id:null,isSynthetic:!0}}else if(o.type==="summarize"){let s=sc({id:e.sessionDbId,memory_session_id:e.memorySessionId,project:e.project,user_prompt:e.userPrompt,last_assistant_message:o.last_assistant_message||""},n);e.conversationHistory.push({role:"user",content:s}),yield{type:"user",message:{role:"user",content:s},session_id:e.contentSessionId,parent_tool_use_id:null,isSynthetic:!0}}}findClaudeExecutable(){let e=Fe.loadFromFile(Yr);if(e.CLAUDE_CODE_PATH){let{existsSync:r}=require("fs");if(!r(e.CLAUDE_CODE_PATH))throw new Error(`CLAUDE_CODE_PATH is set to "${e.CLAUDE_CODE_PATH}" but the file does not exist.`);return e.CLAUDE_CODE_PATH}try{let r=(0,W6.execSync)(process.platform==="win32"?"where claude":"which claude",{encoding:"utf8",windowsHide:!0,stdio:["ignore","pipe","ignore"]}).trim().split(` `)[0].trim();if(r)return r}catch(r){E.debug("SDK","Claude executable auto-detection failed",{},r)}throw new Error(`Claude executable not found. Please either: 1. Add "claude" to your system PATH, or -2. Set CLAUDE_CODE_PATH in ~/.claude-mem/settings.json`)}getModelId(){let e=J6.default.join((0,K6.homedir)(),".claude-mem","settings.json");return Fe.loadFromFile(e).CLAUDE_MEM_MODEL}};var Og=ut(require("path"),1),Pg=require("os");Se();Mr();Ur();var ehe="https://generativelanguage.googleapis.com/v1beta/models",the={"gemini-2.5-flash-lite":10,"gemini-2.5-flash":10,"gemini-2.5-pro":5,"gemini-2.0-flash":15,"gemini-2.0-flash-lite":30,"gemini-3-flash":5},X6=0;async function rhe(t,e){if(!e)return;let r=the[t]||5,n=Math.ceil(6e4/r)+100,a=Date.now()-X6;if(asetTimeout(s,o))}X6=Date.now()}var Ig=class{dbManager;sessionManager;fallbackAgent=null;constructor(e,r){this.dbManager=e,this.sessionManager=r}setFallbackAgent(e){this.fallbackAgent=e}async startSession(e,r){try{let{apiKey:n,model:i,rateLimitingEnabled:a}=this.getGeminiConfig();if(!n)throw new Error("Gemini API key not configured. Set CLAUDE_MEM_GEMINI_API_KEY in settings or GEMINI_API_KEY environment variable.");let o=Ve.getInstance().getActiveMode(),s=e.lastPromptNumber===1?ac(e.project,e.contentSessionId,e.userPrompt,o):cc(e.userPrompt,e.lastPromptNumber,e.contentSessionId,o);e.conversationHistory.push({role:"user",content:s});let c=await this.queryGeminiMultiTurn(e.conversationHistory,n,i,a);if(c.content){e.conversationHistory.push({role:"assistant",content:c.content});let d=c.tokensUsed||0;e.cumulativeInputTokens+=Math.floor(d*.7),e.cumulativeOutputTokens+=Math.floor(d*.3),await Yn(c.content,e,this.dbManager,this.sessionManager,r,d,null,"Gemini")}else E.error("SDK","Empty Gemini init response - session may lack context",{sessionId:e.sessionDbId,model:i});let u;for await(let d of this.sessionManager.getMessageIterator(e.sessionDbId)){d.cwd&&(u=d.cwd);let p=e.earliestPendingTimestamp;if(d.type==="observation"){if(d.prompt_number!==void 0&&(e.lastPromptNumber=d.prompt_number),!e.memorySessionId)throw new Error("Cannot process observations: memorySessionId not yet captured. This session may need to be reinitialized.");let f=oc({id:0,tool_name:d.tool_name,tool_input:JSON.stringify(d.tool_input),tool_output:JSON.stringify(d.tool_response),created_at_epoch:p??Date.now(),cwd:d.cwd});e.conversationHistory.push({role:"user",content:f});let h=await this.queryGeminiMultiTurn(e.conversationHistory,n,i,a),_=0;h.content&&(e.conversationHistory.push({role:"assistant",content:h.content}),_=h.tokensUsed||0,e.cumulativeInputTokens+=Math.floor(_*.7),e.cumulativeOutputTokens+=Math.floor(_*.3)),await Yn(h.content||"",e,this.dbManager,this.sessionManager,r,_,p,"Gemini",u)}else if(d.type==="summarize"){if(!e.memorySessionId)throw new Error("Cannot process summary: memorySessionId not yet captured. This session may need to be reinitialized.");let f=sc({id:e.sessionDbId,memory_session_id:e.memorySessionId,project:e.project,user_prompt:e.userPrompt,last_assistant_message:d.last_assistant_message||""},o);e.conversationHistory.push({role:"user",content:f});let h=await this.queryGeminiMultiTurn(e.conversationHistory,n,i,a),_=0;h.content&&(e.conversationHistory.push({role:"assistant",content:h.content}),_=h.tokensUsed||0,e.cumulativeInputTokens+=Math.floor(_*.7),e.cumulativeOutputTokens+=Math.floor(_*.3)),await Yn(h.content||"",e,this.dbManager,this.sessionManager,r,_,p,"Gemini",u)}}let l=Date.now()-e.startTime;E.success("SDK","Gemini agent completed",{sessionId:e.sessionDbId,duration:`${(l/1e3).toFixed(1)}s`,historyLength:e.conversationHistory.length})}catch(n){if(bd(n))throw E.warn("SDK","Gemini agent aborted",{sessionId:e.sessionDbId}),n;if(_d(n)&&this.fallbackAgent)return E.warn("SDK","Gemini API failed, falling back to Claude SDK",{sessionDbId:e.sessionDbId,error:n instanceof Error?n.message:String(n),historyLength:e.conversationHistory.length}),this.fallbackAgent.startSession(e,r);throw E.failure("SDK","Gemini agent error",{sessionDbId:e.sessionDbId},n),n}}conversationToGeminiContents(e){return e.map(r=>({role:r.role==="assistant"?"model":"user",parts:[{text:r.content}]}))}async queryGeminiMultiTurn(e,r,n,i){let a=this.conversationToGeminiContents(e),o=e.reduce((p,f)=>p+f.content.length,0);E.debug("SDK",`Querying Gemini multi-turn (${n})`,{turns:e.length,totalChars:o});let s=`${ehe}/${n}:generateContent?key=${r}`;await rhe(n,i);let c=await fetch(s,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({contents:a,generationConfig:{temperature:.3,maxOutputTokens:4096}})});if(!c.ok){let p=await c.text();throw new Error(`Gemini API error: ${c.status} - ${p}`)}let u=await c.json();if(!u.candidates?.[0]?.content?.parts?.[0]?.text)return E.error("SDK","Empty response from Gemini"),{content:""};let l=u.candidates[0].content.parts[0].text,d=u.usageMetadata?.totalTokenCount;return{content:l,tokensUsed:d}}getGeminiConfig(){let e=Og.default.join((0,Pg.homedir)(),".claude-mem","settings.json"),r=Fe.loadFromFile(e),n=r.CLAUDE_MEM_GEMINI_API_KEY||lc("GEMINI_API_KEY")||"",i="gemini-2.5-flash",a=r.CLAUDE_MEM_GEMINI_MODEL||i,o=["gemini-2.5-flash-lite","gemini-2.5-flash","gemini-2.5-pro","gemini-2.0-flash","gemini-2.0-flash-lite","gemini-3-flash"],s;o.includes(a)?s=a:(E.warn("SDK",`Invalid Gemini model "${a}", falling back to ${i}`,{configured:a,validModels:o}),s=i);let c=r.CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED!=="false";return{apiKey:n,model:s,rateLimitingEnabled:c}}};function Mc(){let t=Og.default.join((0,Pg.homedir)(),".claude-mem","settings.json");return!!(Fe.loadFromFile(t).CLAUDE_MEM_GEMINI_API_KEY||lc("GEMINI_API_KEY"))}function Hd(){let t=Og.default.join((0,Pg.homedir)(),".claude-mem","settings.json");return Fe.loadFromFile(t).CLAUDE_MEM_PROVIDER==="gemini"}Se();Mr();en();Ur();var nhe="https://openrouter.ai/api/v1/chat/completions",ihe=20,ahe=1e5,ohe=4,Rg=class{dbManager;sessionManager;fallbackAgent=null;constructor(e,r){this.dbManager=e,this.sessionManager=r}setFallbackAgent(e){this.fallbackAgent=e}async startSession(e,r){try{let{apiKey:n,model:i,siteUrl:a,appName:o}=this.getOpenRouterConfig();if(!n)throw new Error("OpenRouter API key not configured. Set CLAUDE_MEM_OPENROUTER_API_KEY in settings or OPENROUTER_API_KEY environment variable.");let s=Ve.getInstance().getActiveMode(),c=e.lastPromptNumber===1?ac(e.project,e.contentSessionId,e.userPrompt,s):cc(e.userPrompt,e.lastPromptNumber,e.contentSessionId,s);e.conversationHistory.push({role:"user",content:c});let u=await this.queryOpenRouterMultiTurn(e.conversationHistory,n,i,a,o);if(u.content){e.conversationHistory.push({role:"assistant",content:u.content});let p=u.tokensUsed||0;e.cumulativeInputTokens+=Math.floor(p*.7),e.cumulativeOutputTokens+=Math.floor(p*.3),await Yn(u.content,e,this.dbManager,this.sessionManager,r,p,null,"OpenRouter",void 0)}else E.error("SDK","Empty OpenRouter init response - session may lack context",{sessionId:e.sessionDbId,model:i});let l;for await(let p of this.sessionManager.getMessageIterator(e.sessionDbId)){p.cwd&&(l=p.cwd);let f=e.earliestPendingTimestamp;if(p.type==="observation"){if(p.prompt_number!==void 0&&(e.lastPromptNumber=p.prompt_number),!e.memorySessionId)throw new Error("Cannot process observations: memorySessionId not yet captured. This session may need to be reinitialized.");let h=oc({id:0,tool_name:p.tool_name,tool_input:JSON.stringify(p.tool_input),tool_output:JSON.stringify(p.tool_response),created_at_epoch:f??Date.now(),cwd:p.cwd});e.conversationHistory.push({role:"user",content:h});let _=await this.queryOpenRouterMultiTurn(e.conversationHistory,n,i,a,o),g=0;_.content&&(e.conversationHistory.push({role:"assistant",content:_.content}),g=_.tokensUsed||0,e.cumulativeInputTokens+=Math.floor(g*.7),e.cumulativeOutputTokens+=Math.floor(g*.3)),await Yn(_.content||"",e,this.dbManager,this.sessionManager,r,g,f,"OpenRouter",l)}else if(p.type==="summarize"){if(!e.memorySessionId)throw new Error("Cannot process summary: memorySessionId not yet captured. This session may need to be reinitialized.");let h=sc({id:e.sessionDbId,memory_session_id:e.memorySessionId,project:e.project,user_prompt:e.userPrompt,last_assistant_message:p.last_assistant_message||""},s);e.conversationHistory.push({role:"user",content:h});let _=await this.queryOpenRouterMultiTurn(e.conversationHistory,n,i,a,o),g=0;_.content&&(e.conversationHistory.push({role:"assistant",content:_.content}),g=_.tokensUsed||0,e.cumulativeInputTokens+=Math.floor(g*.7),e.cumulativeOutputTokens+=Math.floor(g*.3)),await Yn(_.content||"",e,this.dbManager,this.sessionManager,r,g,f,"OpenRouter",l)}}let d=Date.now()-e.startTime;E.success("SDK","OpenRouter agent completed",{sessionId:e.sessionDbId,duration:`${(d/1e3).toFixed(1)}s`,historyLength:e.conversationHistory.length,model:i})}catch(n){if(bd(n))throw E.warn("SDK","OpenRouter agent aborted",{sessionId:e.sessionDbId}),n;if(_d(n)&&this.fallbackAgent)return E.warn("SDK","OpenRouter API failed, falling back to Claude SDK",{sessionDbId:e.sessionDbId,error:n instanceof Error?n.message:String(n),historyLength:e.conversationHistory.length}),this.fallbackAgent.startSession(e,r);throw E.failure("SDK","OpenRouter agent error",{sessionDbId:e.sessionDbId},n),n}}estimateTokens(e){return Math.ceil(e.length/ohe)}truncateHistory(e){let r=Fe.loadFromFile(Yr),n=parseInt(r.CLAUDE_MEM_OPENROUTER_MAX_CONTEXT_MESSAGES)||ihe,i=parseInt(r.CLAUDE_MEM_OPENROUTER_MAX_TOKENS)||ahe;if(e.length<=n&&e.reduce((c,u)=>c+this.estimateTokens(u.content),0)<=i)return e;let a=[],o=0;for(let s=e.length-1;s>=0;s--){let c=e[s],u=this.estimateTokens(c.content);if(a.length>=n||o+u>i){E.warn("SDK","Context window truncated to prevent runaway costs",{originalMessages:e.length,keptMessages:a.length,droppedMessages:s+1,estimatedTokens:o,tokenLimit:i});break}a.unshift(c),o+=u}return a}conversationToOpenAIMessages(e){return e.map(r=>({role:r.role==="assistant"?"assistant":"user",content:r.content}))}async queryOpenRouterMultiTurn(e,r,n,i,a){let o=this.truncateHistory(e),s=this.conversationToOpenAIMessages(o),c=o.reduce((h,_)=>h+_.content.length,0),u=this.estimateTokens(o.map(h=>h.content).join(""));E.debug("SDK",`Querying OpenRouter multi-turn (${n})`,{turns:o.length,totalChars:c,estimatedTokens:u});let l=await fetch(nhe,{method:"POST",headers:{Authorization:`Bearer ${r}`,"HTTP-Referer":i||"https://github.com/thedotmack/claude-mem","X-Title":a||"claude-mem","Content-Type":"application/json"},body:JSON.stringify({model:n,messages:s,temperature:.3,max_tokens:4096})});if(!l.ok){let h=await l.text();throw new Error(`OpenRouter API error: ${l.status} - ${h}`)}let d=await l.json();if(d.error)throw new Error(`OpenRouter API error: ${d.error.code} - ${d.error.message}`);if(!d.choices?.[0]?.message?.content)return E.error("SDK","Empty response from OpenRouter"),{content:""};let p=d.choices[0].message.content,f=d.usage?.total_tokens;if(f){let h=d.usage?.prompt_tokens||0,_=d.usage?.completion_tokens||0,g=h/1e6*3+_/1e6*15;E.info("SDK","OpenRouter API usage",{model:n,inputTokens:h,outputTokens:_,totalTokens:f,estimatedCostUSD:g.toFixed(4),messagesInContext:o.length}),f>5e4&&E.warn("SDK","High token usage detected - consider reducing context",{totalTokens:f,estimatedCost:g.toFixed(4)})}return{content:p,tokensUsed:f}}getOpenRouterConfig(){let e=Yr,r=Fe.loadFromFile(e),n=r.CLAUDE_MEM_OPENROUTER_API_KEY||lc("OPENROUTER_API_KEY")||"",i=r.CLAUDE_MEM_OPENROUTER_MODEL||"xiaomi/mimo-v2-flash:free",a=r.CLAUDE_MEM_OPENROUTER_SITE_URL||"",o=r.CLAUDE_MEM_OPENROUTER_APP_NAME||"claude-mem";return{apiKey:n,model:i,siteUrl:a,appName:o}}};function Dc(){let t=Yr;return!!(Fe.loadFromFile(t).CLAUDE_MEM_OPENROUTER_API_KEY||lc("OPENROUTER_API_KEY"))}function Bd(){let t=Yr;return Fe.loadFromFile(t).CLAUDE_MEM_PROVIDER==="openrouter"}Se();var Cg=class{dbManager;constructor(e){this.dbManager=e}stripProjectPath(e,r){let n=`/${r}/`,i=e.indexOf(n);return i!==-1?e.substring(i+n.length):e}stripProjectPaths(e,r){if(!e)return e;try{let i=JSON.parse(e).map(a=>this.stripProjectPath(a,r));return JSON.stringify(i)}catch(n){return E.debug("WORKER","File paths is plain string, using as-is",{},n),e}}sanitizeObservation(e){return{...e,files_read:this.stripProjectPaths(e.files_read,e.project),files_modified:this.stripProjectPaths(e.files_modified,e.project)}}getObservations(e,r,n){let i=this.paginate("observations","id, memory_session_id, project, type, title, subtitle, narrative, text, facts, concepts, files_read, files_modified, prompt_number, created_at, created_at_epoch",e,r,n);return{...i,items:i.items.map(a=>this.sanitizeObservation(a))}}getSummaries(e,r,n){let i=this.dbManager.getSessionStore().db,a=` +2. Set CLAUDE_CODE_PATH in ~/.claude-mem/settings.json`)}getModelId(){let e=J6.default.join((0,K6.homedir)(),".claude-mem","settings.json");return Fe.loadFromFile(e).CLAUDE_MEM_MODEL}};var Og=ut(require("path"),1),Pg=require("os");Se();Mr();Ur();var ehe="https://generativelanguage.googleapis.com/v1beta/models",the={"gemini-2.5-flash-lite":10,"gemini-2.5-flash":10,"gemini-2.5-pro":5,"gemini-2.0-flash":15,"gemini-2.0-flash-lite":30,"gemini-3-flash":5},X6=0;async function rhe(t,e){if(!e)return;let r=the[t]||5,n=Math.ceil(6e4/r)+100,a=Date.now()-X6;if(asetTimeout(s,o))}X6=Date.now()}var Ig=class{dbManager;sessionManager;fallbackAgent=null;constructor(e,r){this.dbManager=e,this.sessionManager=r}setFallbackAgent(e){this.fallbackAgent=e}async startSession(e,r){try{let{apiKey:n,model:i,rateLimitingEnabled:a}=this.getGeminiConfig();if(!n)throw new Error("Gemini API key not configured. Set CLAUDE_MEM_GEMINI_API_KEY in settings or GEMINI_API_KEY environment variable.");if(!e.memorySessionId){let d=`gemini-${e.contentSessionId}-${Date.now()}`;e.memorySessionId=d,this.dbManager.getSessionStore().updateMemorySessionId(e.sessionDbId,d),E.info("SESSION",`MEMORY_ID_GENERATED | sessionDbId=${e.sessionDbId} | provider=Gemini`)}let o=Ve.getInstance().getActiveMode(),s=e.lastPromptNumber===1?ac(e.project,e.contentSessionId,e.userPrompt,o):cc(e.userPrompt,e.lastPromptNumber,e.contentSessionId,o);e.conversationHistory.push({role:"user",content:s});let c=await this.queryGeminiMultiTurn(e.conversationHistory,n,i,a);if(c.content){e.conversationHistory.push({role:"assistant",content:c.content});let d=c.tokensUsed||0;e.cumulativeInputTokens+=Math.floor(d*.7),e.cumulativeOutputTokens+=Math.floor(d*.3),await Yn(c.content,e,this.dbManager,this.sessionManager,r,d,null,"Gemini")}else E.error("SDK","Empty Gemini init response - session may lack context",{sessionId:e.sessionDbId,model:i});let u;for await(let d of this.sessionManager.getMessageIterator(e.sessionDbId)){d.cwd&&(u=d.cwd);let p=e.earliestPendingTimestamp;if(d.type==="observation"){if(d.prompt_number!==void 0&&(e.lastPromptNumber=d.prompt_number),!e.memorySessionId)throw new Error("Cannot process observations: memorySessionId not yet captured. This session may need to be reinitialized.");let f=oc({id:0,tool_name:d.tool_name,tool_input:JSON.stringify(d.tool_input),tool_output:JSON.stringify(d.tool_response),created_at_epoch:p??Date.now(),cwd:d.cwd});e.conversationHistory.push({role:"user",content:f});let h=await this.queryGeminiMultiTurn(e.conversationHistory,n,i,a),_=0;h.content&&(e.conversationHistory.push({role:"assistant",content:h.content}),_=h.tokensUsed||0,e.cumulativeInputTokens+=Math.floor(_*.7),e.cumulativeOutputTokens+=Math.floor(_*.3)),await Yn(h.content||"",e,this.dbManager,this.sessionManager,r,_,p,"Gemini",u)}else if(d.type==="summarize"){if(!e.memorySessionId)throw new Error("Cannot process summary: memorySessionId not yet captured. This session may need to be reinitialized.");let f=sc({id:e.sessionDbId,memory_session_id:e.memorySessionId,project:e.project,user_prompt:e.userPrompt,last_assistant_message:d.last_assistant_message||""},o);e.conversationHistory.push({role:"user",content:f});let h=await this.queryGeminiMultiTurn(e.conversationHistory,n,i,a),_=0;h.content&&(e.conversationHistory.push({role:"assistant",content:h.content}),_=h.tokensUsed||0,e.cumulativeInputTokens+=Math.floor(_*.7),e.cumulativeOutputTokens+=Math.floor(_*.3)),await Yn(h.content||"",e,this.dbManager,this.sessionManager,r,_,p,"Gemini",u)}}let l=Date.now()-e.startTime;E.success("SDK","Gemini agent completed",{sessionId:e.sessionDbId,duration:`${(l/1e3).toFixed(1)}s`,historyLength:e.conversationHistory.length})}catch(n){if(bd(n))throw E.warn("SDK","Gemini agent aborted",{sessionId:e.sessionDbId}),n;if(_d(n)&&this.fallbackAgent)return E.warn("SDK","Gemini API failed, falling back to Claude SDK",{sessionDbId:e.sessionDbId,error:n instanceof Error?n.message:String(n),historyLength:e.conversationHistory.length}),this.fallbackAgent.startSession(e,r);throw E.failure("SDK","Gemini agent error",{sessionDbId:e.sessionDbId},n),n}}conversationToGeminiContents(e){return e.map(r=>({role:r.role==="assistant"?"model":"user",parts:[{text:r.content}]}))}async queryGeminiMultiTurn(e,r,n,i){let a=this.conversationToGeminiContents(e),o=e.reduce((p,f)=>p+f.content.length,0);E.debug("SDK",`Querying Gemini multi-turn (${n})`,{turns:e.length,totalChars:o});let s=`${ehe}/${n}:generateContent?key=${r}`;await rhe(n,i);let c=await fetch(s,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({contents:a,generationConfig:{temperature:.3,maxOutputTokens:4096}})});if(!c.ok){let p=await c.text();throw new Error(`Gemini API error: ${c.status} - ${p}`)}let u=await c.json();if(!u.candidates?.[0]?.content?.parts?.[0]?.text)return E.error("SDK","Empty response from Gemini"),{content:""};let l=u.candidates[0].content.parts[0].text,d=u.usageMetadata?.totalTokenCount;return{content:l,tokensUsed:d}}getGeminiConfig(){let e=Og.default.join((0,Pg.homedir)(),".claude-mem","settings.json"),r=Fe.loadFromFile(e),n=r.CLAUDE_MEM_GEMINI_API_KEY||lc("GEMINI_API_KEY")||"",i="gemini-2.5-flash",a=r.CLAUDE_MEM_GEMINI_MODEL||i,o=["gemini-2.5-flash-lite","gemini-2.5-flash","gemini-2.5-pro","gemini-2.0-flash","gemini-2.0-flash-lite","gemini-3-flash"],s;o.includes(a)?s=a:(E.warn("SDK",`Invalid Gemini model "${a}", falling back to ${i}`,{configured:a,validModels:o}),s=i);let c=r.CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED!=="false";return{apiKey:n,model:s,rateLimitingEnabled:c}}};function Mc(){let t=Og.default.join((0,Pg.homedir)(),".claude-mem","settings.json");return!!(Fe.loadFromFile(t).CLAUDE_MEM_GEMINI_API_KEY||lc("GEMINI_API_KEY"))}function Hd(){let t=Og.default.join((0,Pg.homedir)(),".claude-mem","settings.json");return Fe.loadFromFile(t).CLAUDE_MEM_PROVIDER==="gemini"}Se();Mr();en();Ur();var nhe="https://openrouter.ai/api/v1/chat/completions",ihe=20,ahe=1e5,ohe=4,Rg=class{dbManager;sessionManager;fallbackAgent=null;constructor(e,r){this.dbManager=e,this.sessionManager=r}setFallbackAgent(e){this.fallbackAgent=e}async startSession(e,r){try{let{apiKey:n,model:i,siteUrl:a,appName:o}=this.getOpenRouterConfig();if(!n)throw new Error("OpenRouter API key not configured. Set CLAUDE_MEM_OPENROUTER_API_KEY in settings or OPENROUTER_API_KEY environment variable.");if(!e.memorySessionId){let p=`openrouter-${e.contentSessionId}-${Date.now()}`;e.memorySessionId=p,this.dbManager.getSessionStore().updateMemorySessionId(e.sessionDbId,p),E.info("SESSION",`MEMORY_ID_GENERATED | sessionDbId=${e.sessionDbId} | provider=OpenRouter`)}let s=Ve.getInstance().getActiveMode(),c=e.lastPromptNumber===1?ac(e.project,e.contentSessionId,e.userPrompt,s):cc(e.userPrompt,e.lastPromptNumber,e.contentSessionId,s);e.conversationHistory.push({role:"user",content:c});let u=await this.queryOpenRouterMultiTurn(e.conversationHistory,n,i,a,o);if(u.content){e.conversationHistory.push({role:"assistant",content:u.content});let p=u.tokensUsed||0;e.cumulativeInputTokens+=Math.floor(p*.7),e.cumulativeOutputTokens+=Math.floor(p*.3),await Yn(u.content,e,this.dbManager,this.sessionManager,r,p,null,"OpenRouter",void 0)}else E.error("SDK","Empty OpenRouter init response - session may lack context",{sessionId:e.sessionDbId,model:i});let l;for await(let p of this.sessionManager.getMessageIterator(e.sessionDbId)){p.cwd&&(l=p.cwd);let f=e.earliestPendingTimestamp;if(p.type==="observation"){if(p.prompt_number!==void 0&&(e.lastPromptNumber=p.prompt_number),!e.memorySessionId)throw new Error("Cannot process observations: memorySessionId not yet captured. This session may need to be reinitialized.");let h=oc({id:0,tool_name:p.tool_name,tool_input:JSON.stringify(p.tool_input),tool_output:JSON.stringify(p.tool_response),created_at_epoch:f??Date.now(),cwd:p.cwd});e.conversationHistory.push({role:"user",content:h});let _=await this.queryOpenRouterMultiTurn(e.conversationHistory,n,i,a,o),g=0;_.content&&(e.conversationHistory.push({role:"assistant",content:_.content}),g=_.tokensUsed||0,e.cumulativeInputTokens+=Math.floor(g*.7),e.cumulativeOutputTokens+=Math.floor(g*.3)),await Yn(_.content||"",e,this.dbManager,this.sessionManager,r,g,f,"OpenRouter",l)}else if(p.type==="summarize"){if(!e.memorySessionId)throw new Error("Cannot process summary: memorySessionId not yet captured. This session may need to be reinitialized.");let h=sc({id:e.sessionDbId,memory_session_id:e.memorySessionId,project:e.project,user_prompt:e.userPrompt,last_assistant_message:p.last_assistant_message||""},s);e.conversationHistory.push({role:"user",content:h});let _=await this.queryOpenRouterMultiTurn(e.conversationHistory,n,i,a,o),g=0;_.content&&(e.conversationHistory.push({role:"assistant",content:_.content}),g=_.tokensUsed||0,e.cumulativeInputTokens+=Math.floor(g*.7),e.cumulativeOutputTokens+=Math.floor(g*.3)),await Yn(_.content||"",e,this.dbManager,this.sessionManager,r,g,f,"OpenRouter",l)}}let d=Date.now()-e.startTime;E.success("SDK","OpenRouter agent completed",{sessionId:e.sessionDbId,duration:`${(d/1e3).toFixed(1)}s`,historyLength:e.conversationHistory.length,model:i})}catch(n){if(bd(n))throw E.warn("SDK","OpenRouter agent aborted",{sessionId:e.sessionDbId}),n;if(_d(n)&&this.fallbackAgent)return E.warn("SDK","OpenRouter API failed, falling back to Claude SDK",{sessionDbId:e.sessionDbId,error:n instanceof Error?n.message:String(n),historyLength:e.conversationHistory.length}),this.fallbackAgent.startSession(e,r);throw E.failure("SDK","OpenRouter agent error",{sessionDbId:e.sessionDbId},n),n}}estimateTokens(e){return Math.ceil(e.length/ohe)}truncateHistory(e){let r=Fe.loadFromFile(Yr),n=parseInt(r.CLAUDE_MEM_OPENROUTER_MAX_CONTEXT_MESSAGES)||ihe,i=parseInt(r.CLAUDE_MEM_OPENROUTER_MAX_TOKENS)||ahe;if(e.length<=n&&e.reduce((c,u)=>c+this.estimateTokens(u.content),0)<=i)return e;let a=[],o=0;for(let s=e.length-1;s>=0;s--){let c=e[s],u=this.estimateTokens(c.content);if(a.length>=n||o+u>i){E.warn("SDK","Context window truncated to prevent runaway costs",{originalMessages:e.length,keptMessages:a.length,droppedMessages:s+1,estimatedTokens:o,tokenLimit:i});break}a.unshift(c),o+=u}return a}conversationToOpenAIMessages(e){return e.map(r=>({role:r.role==="assistant"?"assistant":"user",content:r.content}))}async queryOpenRouterMultiTurn(e,r,n,i,a){let o=this.truncateHistory(e),s=this.conversationToOpenAIMessages(o),c=o.reduce((h,_)=>h+_.content.length,0),u=this.estimateTokens(o.map(h=>h.content).join(""));E.debug("SDK",`Querying OpenRouter multi-turn (${n})`,{turns:o.length,totalChars:c,estimatedTokens:u});let l=await fetch(nhe,{method:"POST",headers:{Authorization:`Bearer ${r}`,"HTTP-Referer":i||"https://github.com/thedotmack/claude-mem","X-Title":a||"claude-mem","Content-Type":"application/json"},body:JSON.stringify({model:n,messages:s,temperature:.3,max_tokens:4096})});if(!l.ok){let h=await l.text();throw new Error(`OpenRouter API error: ${l.status} - ${h}`)}let d=await l.json();if(d.error)throw new Error(`OpenRouter API error: ${d.error.code} - ${d.error.message}`);if(!d.choices?.[0]?.message?.content)return E.error("SDK","Empty response from OpenRouter"),{content:""};let p=d.choices[0].message.content,f=d.usage?.total_tokens;if(f){let h=d.usage?.prompt_tokens||0,_=d.usage?.completion_tokens||0,g=h/1e6*3+_/1e6*15;E.info("SDK","OpenRouter API usage",{model:n,inputTokens:h,outputTokens:_,totalTokens:f,estimatedCostUSD:g.toFixed(4),messagesInContext:o.length}),f>5e4&&E.warn("SDK","High token usage detected - consider reducing context",{totalTokens:f,estimatedCost:g.toFixed(4)})}return{content:p,tokensUsed:f}}getOpenRouterConfig(){let e=Yr,r=Fe.loadFromFile(e),n=r.CLAUDE_MEM_OPENROUTER_API_KEY||lc("OPENROUTER_API_KEY")||"",i=r.CLAUDE_MEM_OPENROUTER_MODEL||"xiaomi/mimo-v2-flash:free",a=r.CLAUDE_MEM_OPENROUTER_SITE_URL||"",o=r.CLAUDE_MEM_OPENROUTER_APP_NAME||"claude-mem";return{apiKey:n,model:i,siteUrl:a,appName:o}}};function Dc(){let t=Yr;return!!(Fe.loadFromFile(t).CLAUDE_MEM_OPENROUTER_API_KEY||lc("OPENROUTER_API_KEY"))}function Bd(){let t=Yr;return Fe.loadFromFile(t).CLAUDE_MEM_PROVIDER==="openrouter"}Se();var Cg=class{dbManager;constructor(e){this.dbManager=e}stripProjectPath(e,r){let n=`/${r}/`,i=e.indexOf(n);return i!==-1?e.substring(i+n.length):e}stripProjectPaths(e,r){if(!e)return e;try{let i=JSON.parse(e).map(a=>this.stripProjectPath(a,r));return JSON.stringify(i)}catch(n){return E.debug("WORKER","File paths is plain string, using as-is",{},n),e}}sanitizeObservation(e){return{...e,files_read:this.stripProjectPaths(e.files_read,e.project),files_modified:this.stripProjectPaths(e.files_modified,e.project)}}getObservations(e,r,n){let i=this.paginate("observations","id, memory_session_id, project, type, title, subtitle, narrative, text, facts, concepts, files_read, files_modified, prompt_number, created_at, created_at_epoch",e,r,n);return{...i,items:i.items.map(a=>this.sanitizeObservation(a))}}getSummaries(e,r,n){let i=this.dbManager.getSessionStore().db,a=` SELECT ss.id, s.content_session_id as session_id, diff --git a/src/services/worker/GeminiAgent.ts b/src/services/worker/GeminiAgent.ts index 8e0f9b1f..649d7484 100644 --- a/src/services/worker/GeminiAgent.ts +++ b/src/services/worker/GeminiAgent.ts @@ -134,6 +134,14 @@ export class GeminiAgent { throw new Error('Gemini API key not configured. Set CLAUDE_MEM_GEMINI_API_KEY in settings or GEMINI_API_KEY environment variable.'); } + // Generate synthetic memorySessionId (Gemini is stateless, doesn't return session IDs) + if (!session.memorySessionId) { + const syntheticMemorySessionId = `gemini-${session.contentSessionId}-${Date.now()}`; + session.memorySessionId = syntheticMemorySessionId; + this.dbManager.getSessionStore().updateMemorySessionId(session.sessionDbId, syntheticMemorySessionId); + logger.info('SESSION', `MEMORY_ID_GENERATED | sessionDbId=${session.sessionDbId} | provider=Gemini`); + } + // Load active mode const mode = ModeManager.getInstance().getActiveMode(); diff --git a/src/services/worker/OpenRouterAgent.ts b/src/services/worker/OpenRouterAgent.ts index 5263e22c..0cbf4987 100644 --- a/src/services/worker/OpenRouterAgent.ts +++ b/src/services/worker/OpenRouterAgent.ts @@ -92,6 +92,14 @@ export class OpenRouterAgent { throw new Error('OpenRouter API key not configured. Set CLAUDE_MEM_OPENROUTER_API_KEY in settings or OPENROUTER_API_KEY environment variable.'); } + // Generate synthetic memorySessionId (OpenRouter is stateless, doesn't return session IDs) + if (!session.memorySessionId) { + const syntheticMemorySessionId = `openrouter-${session.contentSessionId}-${Date.now()}`; + session.memorySessionId = syntheticMemorySessionId; + this.dbManager.getSessionStore().updateMemorySessionId(session.sessionDbId, syntheticMemorySessionId); + logger.info('SESSION', `MEMORY_ID_GENERATED | sessionDbId=${session.sessionDbId} | provider=OpenRouter`); + } + // Load active mode const mode = ModeManager.getInstance().getActiveMode();