diff --git a/docs/reports/2026-01-02--stuck-observations.md b/docs/reports/2026-01-02--stuck-observations.md new file mode 100644 index 00000000..eabcc02a --- /dev/null +++ b/docs/reports/2026-01-02--stuck-observations.md @@ -0,0 +1,384 @@ +# Investigation Report: Stuck Observations in Processing State + +**Date:** January 2, 2026 +**Investigator:** Claude +**Status:** Complete +**Severity:** High - Observations can get permanently stuck until worker restart + +--- + +## Executive Summary + +Observations get stuck in "processing" state due to **six critical gaps** in the message lifecycle: + +1. **In-memory tracking set not cleared on error** - `pendingProcessingIds` retains stale IDs after crashes +2. **No try-catch around database updates** - Partial updates leave system in inconsistent state +3. **Hook exit code inconsistency** - Some hooks exit explicitly, others rely on implicit Node.js behavior +4. **5-minute recovery threshold only on startup** - No continuous monitoring during runtime +5. **Iterator doesn't resume after yield errors** - Messages left in "processing" forever +6. **No global error handlers in hooks** - Unhandled promise rejections crash without cleanup + +--- + +## Message Lifecycle Architecture + +### Status States + +The `pending_messages` table uses 4 states: + +| Status | Description | Transition From | Transition To | +|--------|-------------|-----------------|---------------| +| `pending` | Queued, awaiting processing | (created) | `processing` | +| `processing` | Actively being processed by SDK | `pending` | `processed`, `failed`, or stuck | +| `processed` | Successfully completed | `processing` | (deleted after retention) | +| `failed` | Max retries exceeded | `processing` | (permanent) | + +### Normal Flow + +``` +HTTP Request → enqueue() → pending + ↓ + claimNextMessage() → processing + ↓ + SDK processes → markProcessed() → processed + ↓ + cleanup → deleted +``` + +### Key Files + +| Component | File | Lines | +|-----------|------|-------| +| Status enum | `src/services/sqlite/PendingMessageStore.ts` | 19 | +| Claim message | `src/services/sqlite/PendingMessageStore.ts` | 87-118 | +| Mark processed | `src/services/sqlite/PendingMessageStore.ts` | 252-264 | +| Mark failed | `src/services/sqlite/PendingMessageStore.ts` | 271-296 | +| In-memory tracking | `src/services/worker/SessionManager.ts` | 386 | +| Clear tracking | `src/services/worker/SDKAgent.ts` | 497 | +| Error handler | `src/services/worker/http/routes/SessionRoutes.ts` | 137-168 | + +--- + +## Critical Stuck Points + +### Stuck Point #1: In-Memory Set Not Cleared on Error + +**Location:** `src/services/worker/http/routes/SessionRoutes.ts:137-168` + +**Problem:** When a generator crashes, the error handler marks database messages as failed but **never resets `session.pendingProcessingIds`**. + +**Code Path:** +```typescript +session.generatorPromise = agent.startSession(session, this.workerService) + .catch(error => { + // Mark all processing messages as failed in DB + for (const msg of processingMessages) { + pendingStore.markFailed(msg.id); // ✓ DB updated + } + // ✗ session.pendingProcessingIds.clear() - MISSING! + }); +``` + +**Result:** +- Database shows messages as `failed` +- In-memory set still contains stale message IDs +- On generator restart, same IDs added again (duplicates possible) +- Memory-database state divergence + +**Fix Required:** Add `session.pendingProcessingIds.clear()` in catch block. + +--- + +### Stuck Point #2: No Try-Catch Around markProcessed() + +**Location:** `src/services/worker/SDKAgent.ts:487-516` + +**Problem:** The `markMessagesProcessed()` function loops through all pending IDs but has no error handling around individual `markProcessed()` calls. + +**Code Path:** +```typescript +private async markMessagesProcessed(session, worker): Promise { + for (const messageId of session.pendingProcessingIds) { + pendingMessageStore.markProcessed(messageId); // ✗ No try-catch + } + session.pendingProcessingIds.clear(); // Never reached if above throws +} +``` + +**Result:** +- If DB error occurs on message N, messages N+1...M never marked +- `pendingProcessingIds.clear()` never called +- Partial database update + stale in-memory set + +**Fix Required:** Wrap individual `markProcessed()` calls in try-catch, continue on error, log failures. + +--- + +### Stuck Point #3: Hook Exit Code Inconsistency + +**Location:** All hooks in `src/hooks/` + +**Problem:** Hooks have inconsistent exit patterns: + +| Hook | Explicit Exit? | Method | Timeout | +|------|----------------|--------|---------| +| context-hook | YES | `process.exit(0)` | 15s | +| user-message-hook | YES | `process.exit(3)` | 15s | +| new-hook | NO | Implicit | 15s | +| save-hook | NO | Implicit | 300s | +| summary-hook | NO | Implicit | 300s | + +**Critical Issues:** + +1. **No global error handlers** - No `process.on('unhandledRejection', ...)` in any hook +2. **Async errors bubble to Node.js** - Causes exit(1) with stack trace to stderr +3. **save-hook fire-and-forget pattern** - Errors may not surface + +**save-hook.ts Entry Point (lines 75-85):** +```typescript +stdin.on('end', async () => { + // No try-catch wrapper! + try { + parsed = input.trim() ? JSON.parse(input) : undefined; + } catch (error) { + throw new Error(`Failed to parse...`); // Unhandled! + } + await saveHook(parsed); // Also can throw, unhandled! +}); +``` + +**summary-hook.ts Bug (line 68):** +```typescript +if (!response.ok) { + console.log(STANDARD_HOOK_RESPONSE); // Outputs success BEFORE throwing! + throw new Error(`Summary generation failed: ${response.status}`); +} +``` + +This sends success response to Claude Code, then crashes. + +--- + +### Stuck Point #4: Iterator Doesn't Resume After Yield Error + +**Location:** `src/services/queue/SessionQueueProcessor.ts:17-38` + +**Problem:** The async iterator stops completely if the consuming agent throws while processing a yielded message. + +**Code Path:** +```typescript +async *createIterator(sessionDbId, signal) { + while (!signal.aborted) { + const message = this.store.claimNextMessage(sessionDbId); // → processing + if (message) { + yield message; // Agent throws here = iterator stops + } else { + await this.waitForMessage(signal); + } + } +} +``` + +**Result:** +- Message claimed → status = `processing` +- Message yielded → agent throws during processing +- Iterator stops, never resumes +- Message stuck until 5-minute timeout + +**Fix Required:** Wrap yield in try-catch, mark failed on error, continue loop. + +--- + +### Stuck Point #5: 5-Minute Recovery Only on Startup + +**Location:** `src/services/worker-service.ts:686-690` + +**Problem:** Stuck message recovery only runs when worker initializes. + +**Code Path:** +```typescript +// In initializeWorker() +const STUCK_THRESHOLD_MS = 5 * 60 * 1000; +const resetCount = pendingStore.resetStuckMessages(STUCK_THRESHOLD_MS); +``` + +**Result:** +- During normal operation, no continuous monitoring +- Messages can stay stuck for hours if worker doesn't restart +- User must manually restart worker or wait + +**Fix Required:** Add periodic stuck message check (every 60 seconds) during runtime. + +--- + +### Stuck Point #6: markFailed() Not Transactional + +**Location:** `src/services/sqlite/PendingMessageStore.ts:271-296` + +**Problem:** The `markFailed()` method does SELECT then UPDATE without a transaction wrapper. + +**Code Path:** +```typescript +markFailed(messageId: number): void { + const msg = this.db.prepare(`SELECT retry_count FROM pending_messages WHERE id = ?`).get(messageId); + + // Race condition window here! + + if (msg.retry_count < this.maxRetries) { + this.db.prepare(`UPDATE pending_messages SET status = 'pending', retry_count = retry_count + 1...`).run(messageId); + } else { + this.db.prepare(`UPDATE pending_messages SET status = 'failed'...`).run(messageId); + } +} +``` + +**Result:** +- If process crashes between SELECT and UPDATE, retry_count may be stale +- Could lead to wrong retry decision + +**Fix Required:** Wrap in `this.db.transaction(() => { ... })()`. + +--- + +## Stuck Scenarios + +### Scenario A: SDK Hangs During Processing + +1. Message claimed → `status = 'processing'` +2. Added to `pendingProcessingIds` +3. Yielded to SDK agent +4. SDK hangs (e.g., network timeout, infinite loop) +5. **Result:** Stuck forever until 5-minute timeout on worker restart + +### Scenario B: Generator Crash After Yielding + +1. Message claimed and yielded +2. Agent throws error before `markProcessed()` +3. Error handler marks DB messages as `failed` +4. `pendingProcessingIds` NOT cleared +5. Generator restarts +6. Same message IDs added to set again +7. **Result:** Duplicate tracking, potential double-processing + +### Scenario C: Partial Database Update + +1. 5 messages being marked processed +2. Messages 1-3 succeed +3. Database connection drops +4. Message 4 throws error +5. Loop breaks, messages 4-5 never marked +6. `pendingProcessingIds.clear()` never called +7. **Result:** Mixed state - some processed, some stuck + +### Scenario D: Hook Throws Without Cleanup + +1. `save-hook.ts` receives observation +2. HTTP request to worker succeeds +3. Output `STANDARD_HOOK_RESPONSE` sent +4. Later code throws (e.g., Chroma sync fails) +5. Node.js exits with code 1 +6. **Result:** Claude Code sees success, but observation may be partial + +--- + +## Recovery Mechanisms + +### Current Mechanisms + +| Mechanism | Location | Trigger | Limitation | +|-----------|----------|---------|------------| +| Startup stuck reset | worker-service.ts:687 | Worker restart | Only on restart | +| Generator crash recovery | SessionRoutes.ts:183-216 | Generator exit | Requires full exit | +| Manual retry | (needs verification) | User action | Requires UI intervention | +| Old message cleanup | SDKAgent.ts:504 | After processing | Only cleans processed | + +### Missing Mechanisms + +1. **Continuous stuck monitoring** - No runtime detection +2. **Per-message timeout** - No kill switch for hung SDK +3. **UI stuck count display** - User can't see stuck messages +4. **Manual recovery API** - No endpoint to retry individual messages + +--- + +## Recommendations + +### Priority 1: Critical Fixes + +1. **Clear pendingProcessingIds in error handler** + - File: `SessionRoutes.ts:168` + - Add: `session.pendingProcessingIds.clear()` + +2. **Add try-catch around markProcessed loop** + - File: `SDKAgent.ts:489` + - Wrap individual calls, continue on error + +3. **Add global error handler to hooks** + - All hooks in `src/hooks/` + - Add `process.on('unhandledRejection', ...)` at entry + +### Priority 2: Robustness Improvements + +4. **Add continuous stuck message monitor** + - Check every 60 seconds during runtime + - Reset messages stuck > 5 minutes + +5. **Make markFailed transactional** + - Wrap SELECT + UPDATE in transaction + +6. **Fix summary-hook output-before-throw bug** + - Move `console.log(STANDARD_HOOK_RESPONSE)` after error check + +### Priority 3: Observability + +7. **Add stuck message count to viewer UI** + - Show processing messages > 2 minutes old + +8. **Add manual retry API endpoint** + - Allow user to retry stuck messages without restart + +9. **Add explicit exit to all hooks** + - Consistent `process.exit(0)` on success path + +--- + +## Appendix: File Reference + +### Database Layer +- `src/services/sqlite/PendingMessageStore.ts` - Message queue persistence +- `src/services/sqlite/SessionStore.ts` - Session management, table schemas + +### Processing Layer +- `src/services/queue/SessionQueueProcessor.ts` - Async iterator for claiming +- `src/services/worker/SessionManager.ts` - Session state, message iterator +- `src/services/worker/SDKAgent.ts` - SDK interaction, response processing + +### HTTP Layer +- `src/services/worker/http/routes/SessionRoutes.ts` - Generator lifecycle, error handling + +### Worker Layer +- `src/services/worker-service.ts` - Startup recovery, health checks + +### Hooks +- `src/hooks/context-hook.ts` - SessionStart (explicit exit) +- `src/hooks/user-message-hook.ts` - SessionStart parallel (explicit exit) +- `src/hooks/new-hook.ts` - UserPromptSubmit (implicit exit) +- `src/hooks/save-hook.ts` - PostToolUse (implicit exit, fire-and-forget) +- `src/hooks/summary-hook.ts` - Stop (implicit exit, output bug) + +### Constants +- `src/shared/hook-constants.ts` - Exit codes, timeouts + +--- + +## Conclusion + +The primary cause of stuck observations is the **disconnect between in-memory tracking (`pendingProcessingIds`) and database state**. When errors occur, the database may be updated but the in-memory set is not cleared, leading to: + +1. Duplicate tracking on restart +2. Memory-database state divergence +3. Messages appearing stuck in UI + +Secondary causes include inconsistent hook exit patterns and the lack of runtime stuck message monitoring. + +The 5-minute startup recovery is a safety net, but it only works when the worker restarts. For a production system, continuous monitoring and proper error handling at all state transition points are essential. diff --git a/plugin/package.json b/plugin/package.json index 58cc0331..d0ad9a03 100644 --- a/plugin/package.json +++ b/plugin/package.json @@ -1,6 +1,6 @@ { "name": "claude-mem-plugin", - "version": "8.5.3", + "version": "8.5.4", "private": true, "description": "Runtime dependencies for claude-mem bundled hooks", "type": "module", diff --git a/plugin/scripts/worker-service.cjs b/plugin/scripts/worker-service.cjs index 84436d15..d2edfa0a 100755 --- a/plugin/scripts/worker-service.cjs +++ b/plugin/scripts/worker-service.cjs @@ -1146,7 +1146,7 @@ Tips: ORDER BY MAX(created_at_epoch) DESC `).all().map(s=>s.project);n.json({projects:o})});handleGetProcessingStatus=this.wrapHandler((r,n)=>{let i=this.sessionManager.isAnySessionProcessing(),a=this.sessionManager.getTotalActiveWork();n.json({isProcessing:i,queueDepth:a})});handleSetProcessing=this.wrapHandler((r,n)=>{this.workerService.broadcastProcessingStatus();let i=this.sessionManager.isAnySessionProcessing(),a=this.sessionManager.getTotalQueueDepth(),o=this.sessionManager.getActiveSessionCount();n.json({status:"ok",isProcessing:i,queueDepth:a,activeSessions:o})});parsePaginationParams(r){let n=parseInt(r.query.offset,10)||0,i=Math.min(parseInt(r.query.limit,10)||20,100),a=r.query.project;return{offset:n,limit:i,project:a}}handleImport=this.wrapHandler((r,n)=>{let{sessions:i,summaries:a,observations:o,prompts:s}=r.body,c={sessionsImported:0,sessionsSkipped:0,summariesImported:0,summariesSkipped:0,observationsImported:0,observationsSkipped:0,promptsImported:0,promptsSkipped:0},u=this.dbManager.getSessionStore();if(Array.isArray(i))for(let l of i)u.importSdkSession(l).imported?c.sessionsImported++:c.sessionsSkipped++;if(Array.isArray(a))for(let l of a)u.importSessionSummary(l).imported?c.summariesImported++:c.summariesSkipped++;if(Array.isArray(o))for(let l of o)u.importObservation(l).imported?c.observationsImported++:c.observationsSkipped++;if(Array.isArray(s))for(let l of s)u.importUserPrompt(l).imported?c.promptsImported++:c.promptsSkipped++;n.json({success:!0,stats:c})});handleGetPendingQueue=this.wrapHandler((r,n)=>{let{PendingMessageStore:i}=(ao(),Sd(Fs)),a=new i(this.dbManager.getSessionStore().db,3),o=a.getQueueMessages(),s=a.getRecentlyProcessed(20,30),c=a.getStuckCount(300*1e3),u=a.getSessionsWithPendingMessages();n.json({queue:{messages:o,totalPending:o.filter(l=>l.status==="pending").length,totalProcessing:o.filter(l=>l.status==="processing").length,totalFailed:o.filter(l=>l.status==="failed").length,stuckCount:c},recentlyProcessed:s,sessionsWithPendingWork:u})});handleProcessPendingQueue=this.wrapHandler(async(r,n)=>{let i=Math.min(Math.max(parseInt(r.body.sessionLimit,10)||10,1),100),a=await this.workerService.processPendingQueues(i);n.json({success:!0,...a})});handleClearFailedQueue=this.wrapHandler((r,n)=>{let{PendingMessageStore:i}=(ao(),Sd(Fs)),o=new i(this.dbManager.getSessionStore().db,3).clearFailed();T.info("QUEUE","Cleared failed queue messages",{clearedCount:o}),n.json({success:!0,clearedCount:o})});handleClearAllQueue=this.wrapHandler((r,n)=>{let{PendingMessageStore:i}=(ao(),Sd(Fs)),o=new i(this.dbManager.getSessionStore().db,3).clearAll();T.warn("QUEUE","Cleared ALL queue messages (pending, processing, failed)",{clearedCount:o}),n.json({success:!0,clearedCount:o})})};var Yh=class extends Kr{constructor(r){super();this.searchManager=r}setupRoutes(r){r.get("/api/search",this.handleUnifiedSearch.bind(this)),r.get("/api/timeline",this.handleUnifiedTimeline.bind(this)),r.get("/api/decisions",this.handleDecisions.bind(this)),r.get("/api/changes",this.handleChanges.bind(this)),r.get("/api/how-it-works",this.handleHowItWorks.bind(this)),r.get("/api/search/observations",this.handleSearchObservations.bind(this)),r.get("/api/search/sessions",this.handleSearchSessions.bind(this)),r.get("/api/search/prompts",this.handleSearchPrompts.bind(this)),r.get("/api/search/by-concept",this.handleSearchByConcept.bind(this)),r.get("/api/search/by-file",this.handleSearchByFile.bind(this)),r.get("/api/search/by-type",this.handleSearchByType.bind(this)),r.get("/api/context/recent",this.handleGetRecentContext.bind(this)),r.get("/api/context/timeline",this.handleGetContextTimeline.bind(this)),r.get("/api/context/preview",this.handleContextPreview.bind(this)),r.get("/api/context/inject",this.handleContextInject.bind(this)),r.get("/api/timeline/by-query",this.handleGetTimelineByQuery.bind(this)),r.get("/api/search/help",this.handleSearchHelp.bind(this))}handleUnifiedSearch=this.wrapHandler(async(r,n)=>{let i=await this.searchManager.search(r.query);n.json(i)});handleUnifiedTimeline=this.wrapHandler(async(r,n)=>{let i=await this.searchManager.timeline(r.query);n.json(i)});handleDecisions=this.wrapHandler(async(r,n)=>{let i=await this.searchManager.decisions(r.query);n.json(i)});handleChanges=this.wrapHandler(async(r,n)=>{let i=await this.searchManager.changes(r.query);n.json(i)});handleHowItWorks=this.wrapHandler(async(r,n)=>{let i=await this.searchManager.howItWorks(r.query);n.json(i)});handleSearchObservations=this.wrapHandler(async(r,n)=>{let i=await this.searchManager.searchObservations(r.query);n.json(i)});handleSearchSessions=this.wrapHandler(async(r,n)=>{let i=await this.searchManager.searchSessions(r.query);n.json(i)});handleSearchPrompts=this.wrapHandler(async(r,n)=>{let i=await this.searchManager.searchUserPrompts(r.query);n.json(i)});handleSearchByConcept=this.wrapHandler(async(r,n)=>{let i=await this.searchManager.findByConcept(r.query);n.json(i)});handleSearchByFile=this.wrapHandler(async(r,n)=>{let i=await this.searchManager.findByFile(r.query);n.json(i)});handleSearchByType=this.wrapHandler(async(r,n)=>{let i=await this.searchManager.findByType(r.query);n.json(i)});handleGetRecentContext=this.wrapHandler(async(r,n)=>{let i=await this.searchManager.getRecentContext(r.query);n.json(i)});handleGetContextTimeline=this.wrapHandler(async(r,n)=>{let i=await this.searchManager.getContextTimeline(r.query);n.json(i)});handleContextPreview=this.wrapHandler(async(r,n)=>{let i=r.query.project;if(!i){this.badRequest(n,"Project parameter is required");return}let{generateContext:a}=await Promise.resolve().then(()=>(y$(),v$)),o=`/preview/${i}`,s=await a({session_id:"preview-"+Date.now(),cwd:o},!0);n.setHeader("Content-Type","text/plain; charset=utf-8"),n.send(s)});handleContextInject=this.wrapHandler(async(r,n)=>{let i=r.query.project,a=r.query.colors==="true";if(!i){this.badRequest(n,"Project parameter is required");return}let{generateContext:o}=await Promise.resolve().then(()=>(y$(),v$)),s=`/context/${i}`,c=await o({session_id:"context-inject-"+Date.now(),cwd:s},a);n.setHeader("Content-Type","text/plain; charset=utf-8"),n.send(c)});handleGetTimelineByQuery=this.wrapHandler(async(r,n)=>{let i=await this.searchManager.getTimelineByQuery(r.query);n.json(i)});handleSearchHelp=this.wrapHandler((r,n)=>{n.json({title:"Claude-Mem Search API",description:"HTTP API for searching persistent memory",endpoints:[{path:"/api/search/observations",method:"GET",description:"Search observations using full-text search",parameters:{query:"Search query (required)",limit:"Number of results (default: 20)",project:"Filter by project name (optional)"}},{path:"/api/search/sessions",method:"GET",description:"Search session summaries using full-text search",parameters:{query:"Search query (required)",limit:"Number of results (default: 20)"}},{path:"/api/search/prompts",method:"GET",description:"Search user prompts using full-text search",parameters:{query:"Search query (required)",limit:"Number of results (default: 20)",project:"Filter by project name (optional)"}},{path:"/api/search/by-concept",method:"GET",description:"Find observations by concept tag",parameters:{concept:"Concept tag (required): discovery, decision, bugfix, feature, refactor",limit:"Number of results (default: 10)",project:"Filter by project name (optional)"}},{path:"/api/search/by-file",method:"GET",description:"Find observations and sessions by file path",parameters:{filePath:"File path or partial path (required)",limit:"Number of results per type (default: 10)",project:"Filter by project name (optional)"}},{path:"/api/search/by-type",method:"GET",description:"Find observations by type",parameters:{type:"Observation type (required): discovery, decision, bugfix, feature, refactor",limit:"Number of results (default: 10)",project:"Filter by project name (optional)"}},{path:"/api/context/recent",method:"GET",description:"Get recent session context including summaries and observations",parameters:{project:"Project name (default: current directory)",limit:"Number of recent sessions (default: 3)"}},{path:"/api/context/timeline",method:"GET",description:"Get unified timeline around a specific point in time",parameters:{anchor:'Anchor point: observation ID, session ID (e.g., "S123"), or ISO timestamp (required)',depth_before:"Number of records before anchor (default: 10)",depth_after:"Number of records after anchor (default: 10)",project:"Filter by project name (optional)"}},{path:"/api/timeline/by-query",method:"GET",description:"Search for best match, then get timeline around it",parameters:{query:"Search query (required)",mode:'Search mode: "auto", "observations", or "sessions" (default: "auto")',depth_before:"Number of records before match (default: 10)",depth_after:"Number of records after match (default: 10)",project:"Filter by project name (optional)"}},{path:"/api/search/help",method:"GET",description:"Get this help documentation"}],examples:['curl "http://localhost:37777/api/search/observations?query=authentication&limit=5"','curl "http://localhost:37777/api/search/by-type?type=bugfix&limit=10"','curl "http://localhost:37777/api/context/recent?project=claude-mem&limit=3"','curl "http://localhost:37777/api/context/timeline?anchor=123&depth_before=5&depth_after=5"']})})};var vo=qt(require("path"),1),pr=require("fs"),w$=require("os");an();at();var b$=require("child_process"),go=require("fs"),e6=require("os"),_d=require("path");at();var bd=(0,_d.join)((0,e6.homedir)(),".claude","plugins","marketplaces","thedotmack");function _$(t){return!t||typeof t!="string"?!1:/^[a-zA-Z0-9][a-zA-Z0-9._/-]*$/.test(t)&&!t.includes("..")}var dde=3e5,x$=6e5;function En(t){let e=(0,b$.spawnSync)("git",t,{cwd:bd,encoding:"utf-8",timeout:dde,windowsHide:!0,shell:!1});if(e.error)throw e.error;if(e.status!==0)throw new Error(e.stderr||e.stdout||"Git command failed");return e.stdout.trim()}function t6(t,e=x$){let n=process.platform==="win32"?"npm.cmd":"npm",i=(0,b$.spawnSync)(n,t,{cwd:bd,encoding:"utf-8",timeout:e,windowsHide:!0,shell:!1});if(i.error)throw i.error;if(i.status!==0)throw new Error(i.stderr||i.stdout||"npm command failed");return i.stdout.trim()}function Qh(){let t=(0,_d.join)(bd,".git");if(!(0,go.existsSync)(t))return{branch:null,isBeta:!1,isGitRepo:!1,isDirty:!1,canSwitch:!1,error:"Installed plugin is not a git repository"};try{let e=En(["rev-parse","--abbrev-ref","HEAD"]),n=En(["status","--porcelain"]).length>0,i=e.startsWith("beta");return{branch:e,isBeta:i,isGitRepo:!0,isDirty:n,canSwitch:!0}}catch(e){return T.error("BRANCH","Failed to get branch info",{},e),{branch:null,isBeta:!1,isGitRepo:!0,isDirty:!1,canSwitch:!1,error:e.message}}}async function r6(t){if(!_$(t))return{success:!1,error:`Invalid branch name: ${t}. Branch names must be alphanumeric with hyphens, underscores, slashes, or dots.`};let e=Qh();if(!e.isGitRepo)return{success:!1,error:"Installed plugin is not a git repository. Please reinstall."};if(e.branch===t)return{success:!0,branch:t,message:`Already on branch ${t}`};try{T.info("BRANCH","Starting branch switch",{from:e.branch,to:t}),T.debug("BRANCH","Discarding local changes"),En(["checkout","--","."]),En(["clean","-fd"]),T.debug("BRANCH","Fetching from origin"),En(["fetch","origin"]),T.debug("BRANCH","Checking out branch",{branch:t});try{En(["checkout",t])}catch(n){T.debug("BRANCH","Branch not local, tracking remote",{branch:t,error:n instanceof Error?n.message:String(n)}),En(["checkout","-b",t,`origin/${t}`])}T.debug("BRANCH","Pulling latest"),En(["pull","origin",t]);let r=(0,_d.join)(bd,".install-version");return(0,go.existsSync)(r)&&(0,go.unlinkSync)(r),T.debug("BRANCH","Running npm install"),t6(["install"],x$),T.success("BRANCH","Branch switch complete",{branch:t}),{success:!0,branch:t,message:`Switched to ${t}. Worker will restart automatically.`}}catch(r){T.error("BRANCH","Branch switch failed",{targetBranch:t},r);try{e.branch&&_$(e.branch)&&En(["checkout",e.branch])}catch(n){T.warn("BRANCH","Recovery checkout also failed",{originalBranch:e.branch},n)}return{success:!1,error:`Branch switch failed: ${r.message}`}}}async function n6(){let t=Qh();if(!t.isGitRepo||!t.branch)return{success:!1,error:"Cannot pull updates: not a git repository"};try{if(!_$(t.branch))return{success:!1,error:`Invalid current branch name: ${t.branch}`};T.info("BRANCH","Pulling updates",{branch:t.branch}),En(["checkout","--","."]),En(["fetch","origin"]),En(["pull","origin",t.branch]);let e=(0,_d.join)(bd,".install-version");return(0,go.existsSync)(e)&&(0,go.unlinkSync)(e),t6(["install"],x$),T.success("BRANCH","Updates pulled",{branch:t.branch}),{success:!0,branch:t.branch,message:`Updated ${t.branch}. Worker will restart automatically.`}}catch(e){return T.error("BRANCH","Pull failed",{},e),{success:!1,error:`Pull failed: ${e.message}`}}}nn();var eg=class extends Kr{constructor(r){super();this.settingsManager=r}setupRoutes(r){r.get("/api/settings",this.handleGetSettings.bind(this)),r.post("/api/settings",this.handleUpdateSettings.bind(this)),r.get("/api/mcp/status",this.handleGetMcpStatus.bind(this)),r.post("/api/mcp/toggle",this.handleToggleMcp.bind(this)),r.get("/api/branch/status",this.handleGetBranchStatus.bind(this)),r.post("/api/branch/switch",this.handleSwitchBranch.bind(this)),r.post("/api/branch/update",this.handleUpdateBranch.bind(this))}handleGetSettings=this.wrapHandler((r,n)=>{let i=vo.default.join((0,w$.homedir)(),".claude-mem","settings.json");this.ensureSettingsFile(i);let a=Xe.loadFromFile(i);n.json(a)});handleUpdateSettings=this.wrapHandler((r,n)=>{let i=this.validateSettings(r.body);if(!i.valid){n.status(400).json({success:!1,error:i.error});return}let a=vo.default.join((0,w$.homedir)(),".claude-mem","settings.json");this.ensureSettingsFile(a);let o={};if((0,pr.existsSync)(a)){let c=(0,pr.readFileSync)(a,"utf-8");try{o=JSON.parse(c)}catch(u){T.error("SETTINGS","Failed to parse settings file",{settingsPath:a},u),n.status(500).json({success:!1,error:"Settings file is corrupted. Delete ~/.claude-mem/settings.json to reset."});return}}let s=["CLAUDE_MEM_MODEL","CLAUDE_MEM_CONTEXT_OBSERVATIONS","CLAUDE_MEM_WORKER_PORT","CLAUDE_MEM_WORKER_HOST","CLAUDE_MEM_PROVIDER","CLAUDE_MEM_GEMINI_API_KEY","CLAUDE_MEM_GEMINI_MODEL","CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED","CLAUDE_MEM_OPENROUTER_API_KEY","CLAUDE_MEM_OPENROUTER_MODEL","CLAUDE_MEM_OPENROUTER_SITE_URL","CLAUDE_MEM_OPENROUTER_APP_NAME","CLAUDE_MEM_OPENROUTER_MAX_CONTEXT_MESSAGES","CLAUDE_MEM_OPENROUTER_MAX_TOKENS","CLAUDE_MEM_DATA_DIR","CLAUDE_MEM_LOG_LEVEL","CLAUDE_MEM_PYTHON_VERSION","CLAUDE_CODE_PATH","CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS","CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS","CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT","CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT","CLAUDE_MEM_CONTEXT_OBSERVATION_TYPES","CLAUDE_MEM_CONTEXT_OBSERVATION_CONCEPTS","CLAUDE_MEM_CONTEXT_FULL_COUNT","CLAUDE_MEM_CONTEXT_FULL_FIELD","CLAUDE_MEM_CONTEXT_SESSION_COUNT","CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY","CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE"];for(let c of s)r.body[c]!==void 0&&(o[c]=r.body[c]);(0,pr.writeFileSync)(a,JSON.stringify(o,null,2),"utf-8"),zz(),T.info("WORKER","Settings updated"),n.json({success:!0,message:"Settings updated successfully"})});handleGetMcpStatus=this.wrapHandler((r,n)=>{let i=this.isMcpEnabled();n.json({enabled:i})});handleToggleMcp=this.wrapHandler((r,n)=>{let{enabled:i}=r.body;if(typeof i!="boolean"){this.badRequest(n,"enabled must be a boolean");return}this.toggleMcp(i),n.json({success:!0,enabled:this.isMcpEnabled()})});handleGetBranchStatus=this.wrapHandler((r,n)=>{let i=Qh();n.json(i)});handleSwitchBranch=this.wrapHandler(async(r,n)=>{let{branch:i}=r.body;if(!i){n.status(400).json({success:!1,error:"Missing branch parameter"});return}let a=["main","beta/7.0","feature/bun-executable"];if(!a.includes(i)){n.status(400).json({success:!1,error:`Invalid branch. Allowed: ${a.join(", ")}`});return}T.info("WORKER","Branch switch requested",{branch:i});let o=await r6(i);o.success&&setTimeout(()=>{T.info("WORKER","Restarting worker after branch switch"),process.exit(0)},1e3),n.json(o)});handleUpdateBranch=this.wrapHandler(async(r,n)=>{T.info("WORKER","Branch update requested");let i=await n6();i.success&&setTimeout(()=>{T.info("WORKER","Restarting worker after branch update"),process.exit(0)},1e3),n.json(i)});validateSettings(r){if(r.CLAUDE_MEM_PROVIDER&&!["claude","gemini","openrouter"].includes(r.CLAUDE_MEM_PROVIDER))return{valid:!1,error:'CLAUDE_MEM_PROVIDER must be "claude", "gemini", or "openrouter"'};if(r.CLAUDE_MEM_GEMINI_MODEL&&!["gemini-2.5-flash-lite","gemini-2.5-flash","gemini-3-flash"].includes(r.CLAUDE_MEM_GEMINI_MODEL))return{valid:!1,error:"CLAUDE_MEM_GEMINI_MODEL must be one of: gemini-2.5-flash-lite, gemini-2.5-flash, gemini-3-flash"};if(r.CLAUDE_MEM_CONTEXT_OBSERVATIONS){let i=parseInt(r.CLAUDE_MEM_CONTEXT_OBSERVATIONS,10);if(isNaN(i)||i<1||i>200)return{valid:!1,error:"CLAUDE_MEM_CONTEXT_OBSERVATIONS must be between 1 and 200"}}if(r.CLAUDE_MEM_WORKER_PORT){let i=parseInt(r.CLAUDE_MEM_WORKER_PORT,10);if(isNaN(i)||i<1024||i>65535)return{valid:!1,error:"CLAUDE_MEM_WORKER_PORT must be between 1024 and 65535"}}if(r.CLAUDE_MEM_WORKER_HOST){let i=r.CLAUDE_MEM_WORKER_HOST;if(!/^(127\.0\.0\.1|0\.0\.0\.0|localhost|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/.test(i))return{valid:!1,error:"CLAUDE_MEM_WORKER_HOST must be a valid IP address (e.g., 127.0.0.1, 0.0.0.0)"}}if(r.CLAUDE_MEM_LOG_LEVEL&&!["DEBUG","INFO","WARN","ERROR","SILENT"].includes(r.CLAUDE_MEM_LOG_LEVEL.toUpperCase()))return{valid:!1,error:"CLAUDE_MEM_LOG_LEVEL must be one of: DEBUG, INFO, WARN, ERROR, SILENT"};if(r.CLAUDE_MEM_PYTHON_VERSION&&!/^3\.\d{1,2}$/.test(r.CLAUDE_MEM_PYTHON_VERSION))return{valid:!1,error:'CLAUDE_MEM_PYTHON_VERSION must be in format "3.X" or "3.XX" (e.g., "3.13")'};let n=["CLAUDE_MEM_CONTEXT_SHOW_READ_TOKENS","CLAUDE_MEM_CONTEXT_SHOW_WORK_TOKENS","CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_AMOUNT","CLAUDE_MEM_CONTEXT_SHOW_SAVINGS_PERCENT","CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY","CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE"];for(let i of n)if(r[i]&&!["true","false"].includes(r[i]))return{valid:!1,error:`${i} must be "true" or "false"`};if(r.CLAUDE_MEM_CONTEXT_FULL_COUNT){let i=parseInt(r.CLAUDE_MEM_CONTEXT_FULL_COUNT,10);if(isNaN(i)||i<0||i>20)return{valid:!1,error:"CLAUDE_MEM_CONTEXT_FULL_COUNT must be between 0 and 20"}}if(r.CLAUDE_MEM_CONTEXT_SESSION_COUNT){let i=parseInt(r.CLAUDE_MEM_CONTEXT_SESSION_COUNT,10);if(isNaN(i)||i<1||i>50)return{valid:!1,error:"CLAUDE_MEM_CONTEXT_SESSION_COUNT must be between 1 and 50"}}if(r.CLAUDE_MEM_CONTEXT_FULL_FIELD&&!["narrative","facts"].includes(r.CLAUDE_MEM_CONTEXT_FULL_FIELD))return{valid:!1,error:'CLAUDE_MEM_CONTEXT_FULL_FIELD must be "narrative" or "facts"'};if(r.CLAUDE_MEM_OPENROUTER_MAX_CONTEXT_MESSAGES){let i=parseInt(r.CLAUDE_MEM_OPENROUTER_MAX_CONTEXT_MESSAGES,10);if(isNaN(i)||i<1||i>100)return{valid:!1,error:"CLAUDE_MEM_OPENROUTER_MAX_CONTEXT_MESSAGES must be between 1 and 100"}}if(r.CLAUDE_MEM_OPENROUTER_MAX_TOKENS){let i=parseInt(r.CLAUDE_MEM_OPENROUTER_MAX_TOKENS,10);if(isNaN(i)||i<1e3||i>1e6)return{valid:!1,error:"CLAUDE_MEM_OPENROUTER_MAX_TOKENS must be between 1000 and 1000000"}}if(r.CLAUDE_MEM_OPENROUTER_SITE_URL)try{new URL(r.CLAUDE_MEM_OPENROUTER_SITE_URL)}catch(i){return T.debug("SETTINGS","Invalid URL format",{url:r.CLAUDE_MEM_OPENROUTER_SITE_URL,error:i instanceof Error?i.message:String(i)}),{valid:!1,error:"CLAUDE_MEM_OPENROUTER_SITE_URL must be a valid URL"}}return{valid:!0}}isMcpEnabled(){let r=Hr(),n=vo.default.join(r,"plugin",".mcp.json");return(0,pr.existsSync)(n)}toggleMcp(r){let n=Hr(),i=vo.default.join(n,"plugin",".mcp.json"),a=vo.default.join(n,"plugin",".mcp.json.disabled");r&&(0,pr.existsSync)(a)?((0,pr.renameSync)(a,i),T.info("WORKER","MCP search server enabled")):!r&&(0,pr.existsSync)(i)?((0,pr.renameSync)(i,a),T.info("WORKER","MCP search server disabled")):T.debug("WORKER","MCP toggle no-op (already in desired state)",{enabled:r})}ensureSettingsFile(r){if(!(0,pr.existsSync)(r)){let n=Xe.getAllDefaults(),i=vo.default.dirname(r);(0,pr.existsSync)(i)||(0,pr.mkdirSync)(i,{recursive:!0}),(0,pr.writeFileSync)(r,JSON.stringify(n,null,2),"utf-8"),T.info("SETTINGS","Created settings file with defaults",{settingsPath:r})}}};var yo=require("fs"),tg=require("path");at();nn();var rg=class extends Kr{getLogFilePath(){let e=Xe.get("CLAUDE_MEM_DATA_DIR"),r=(0,tg.join)(e,"logs"),n=new Date().toISOString().split("T")[0];return(0,tg.join)(r,`claude-mem-${n}.log`)}getLogsDir(){let e=Xe.get("CLAUDE_MEM_DATA_DIR");return(0,tg.join)(e,"logs")}setupRoutes(e){e.get("/api/logs",this.handleGetLogs.bind(this)),e.post("/api/logs/clear",this.handleClearLogs.bind(this))}handleGetLogs=this.wrapHandler((e,r)=>{let n=this.getLogFilePath();if(!(0,yo.existsSync)(n)){r.json({logs:"",path:n,exists:!1});return}let i=parseInt(e.query.lines||"1000",10),a=Math.min(i,1e4),s=(0,yo.readFileSync)(n,"utf-8").split(` `),c=Math.max(0,s.length-a),u=s.slice(c).join(` -`);r.json({logs:u,path:n,exists:!0,totalLines:s.length,returnedLines:s.length-c})});handleClearLogs=this.wrapHandler((e,r)=>{let n=this.getLogFilePath();if(!(0,yo.existsSync)(n)){r.json({success:!0,message:"Log file does not exist",path:n});return}(0,yo.writeFileSync)(n,"","utf-8"),T.info("SYSTEM","Log file cleared via UI",{path:n}),r.json({success:!0,message:"Log file cleared",path:n})})};var xd=(0,o6.promisify)(ba.exec),pde="8.5.3",I$=be.default.join((0,Vn.homedir)(),".claude-mem"),bo=be.default.join(I$,"worker.pid"),s6=be.default.join(I$,"cursor-projects.json");function k$(t){(0,je.mkdirSync)(I$,{recursive:!0}),(0,je.writeFileSync)(bo,JSON.stringify(t,null,2))}function fde(){try{return(0,je.existsSync)(bo)?JSON.parse((0,je.readFileSync)(bo,"utf-8")):null}catch(t){return T.warn("SYSTEM","Failed to read PID file",{path:bo,error:t.message}),null}}function _a(){try{(0,je.existsSync)(bo)&&(0,je.unlinkSync)(bo)}catch(t){T.warn("SYSTEM","Failed to remove PID file",{path:bo},t);return}}function P$(){return Dz(s6)}function c6(t){Uz(s6,t)}function mde(t,e){let r=P$();r[t]={workspacePath:e,installedAt:new Date().toISOString()},c6(r),T.info("CURSOR","Registered project for auto-context updates",{projectName:t,workspacePath:e})}function hde(t){let e=P$();e[t]&&(delete e[t],c6(e),T.info("CURSOR","Unregistered project",{projectName:t}))}async function fo(t,e){let n=P$()[t];if(n)try{let i=await fetch(`http://127.0.0.1:${e}/api/context/inject?project=${encodeURIComponent(t)}`);if(!i.ok)return;let a=await i.text();if(!a||!a.trim())return;qz(n.workspacePath,a),T.debug("CURSOR","Updated context file",{projectName:t,workspacePath:n.workspacePath})}catch(i){T.warn("CURSOR","Failed to update context file",{projectName:t},i);return}}function _o(t){return process.platform==="win32"?Math.round(t*2):t}async function T$(t){try{return(await fetch(`http://127.0.0.1:${t}/api/health`)).ok}catch{return!1}}async function wc(t,e=3e4){let r=Date.now();for(;Date.now()-rsetTimeout(n,500))}return!1}async function S$(t){try{let e=await fetch(`http://127.0.0.1:${t}/api/admin/shutdown`,{method:"POST"});return e.ok?!0:(T.warn("SYSTEM","Shutdown request returned error",{port:t,status:e.status}),!1)}catch(e){return e.message?.includes("ECONNREFUSED")||T.warn("SYSTEM","Shutdown request failed",{port:t,error:e.message}),!1}}async function $$(t,e=1e4){let r=Date.now();for(;Date.now()-rsetTimeout(n,500))}return!1}function gde(){let t=be.default.join((0,Vn.homedir)(),".claude","plugins","marketplaces","thedotmack"),e=be.default.join(t,"package.json");return JSON.parse((0,je.readFileSync)(e,"utf-8")).version}async function vde(t){try{let e=await fetch(`http://127.0.0.1:${t}/api/version`);return e.ok?(await e.json()).version:null}catch{return T.debug("SYSTEM","Could not fetch worker version",{port:t}),null}}async function yde(t){let e=gde(),r=await vde(t);return r?{matches:e===r,pluginVersion:e,workerVersion:r}:{matches:!0,pluginVersion:e,workerVersion:r}}var ng=class{app;server=null;startTime=Date.now();mcpClient;mcpReady=!1;initializationCompleteFlag=!1;isShuttingDown=!1;dbManager;sessionManager;sseBroadcaster;sdkAgent;geminiAgent;openRouterAgent;paginationHelper;settingsManager;sessionEventBroadcaster;viewerRoutes;sessionRoutes;dataRoutes;searchRoutes;settingsRoutes;logsRoutes;initializationComplete;resolveInitialization;constructor(){this.app=(0,i6.default)(),this.initializationComplete=new Promise(e=>{this.resolveInitialization=e}),this.dbManager=new Vm,this.sessionManager=new Gm(this.dbManager),this.sseBroadcaster=new Km,this.sdkAgent=new Rh(this.dbManager,this.sessionManager),this.geminiAgent=new Ch(this.dbManager,this.sessionManager),this.geminiAgent.setFallbackAgent(this.sdkAgent),this.openRouterAgent=new Ah(this.dbManager,this.sessionManager),this.openRouterAgent.setFallbackAgent(this.sdkAgent),this.paginationHelper=new Mh(this.dbManager),this.settingsManager=new zh(this.dbManager),this.sessionEventBroadcaster=new Fh(this.sseBroadcaster,this),this.sessionManager.setOnSessionDeleted(()=>{this.broadcastProcessingStatus()}),this.mcpClient=new Ms({name:"worker-search-proxy",version:"1.0.0"},{capabilities:{}}),this.viewerRoutes=new Hh(this.sseBroadcaster,this.dbManager,this.sessionManager),this.sessionRoutes=new Gh(this.sessionManager,this.dbManager,this.sdkAgent,this.geminiAgent,this.openRouterAgent,this.sessionEventBroadcaster,this),this.dataRoutes=new Kh(this.paginationHelper,this.dbManager,this.sessionManager,this.sseBroadcaster,this,this.startTime),this.searchRoutes=null,this.settingsRoutes=new eg(this.settingsManager),this.logsRoutes=new rg,this.setupMiddleware(),this.setupRoutes(),this.registerSignalHandlers()}registerSignalHandlers(){let e=async r=>{if(this.isShuttingDown){T.warn("SYSTEM",`Received ${r} but shutdown already in progress`);return}this.isShuttingDown=!0,T.info("SYSTEM",`Received ${r}, shutting down...`);try{await this.shutdown(),process.exit(0)}catch(n){T.error("SYSTEM","Error during shutdown",{},n),process.exit(1)}};process.on("SIGTERM",()=>e("SIGTERM")),process.on("SIGINT",()=>e("SIGINT"))}setupMiddleware(){Z2(this.summarizeRequestBody.bind(this)).forEach(r=>this.app.use(r))}setupRoutes(){let e="TEST-008-wrapper-ipc";this.app.get("/api/health",(r,n)=>{n.status(200).json({status:"ok",build:e,managed:process.env.CLAUDE_MEM_MANAGED==="true",hasIpc:typeof process.send=="function",platform:process.platform,pid:process.pid,initialized:this.initializationCompleteFlag,mcpReady:this.mcpReady})}),this.app.get("/api/readiness",(r,n)=>{this.initializationCompleteFlag?n.status(200).json({status:"ready",mcpReady:this.mcpReady}):n.status(503).json({status:"initializing",message:"Worker is still initializing, please retry"})}),this.app.get("/api/version",(r,n)=>{n.status(200).json({version:pde})}),this.app.get("/api/instructions",async(r,n)=>{let i=r.query.topic||"all",a=r.query.operation;try{let o;if(a){let s=be.default.join(__dirname,"../skills/mem-search/operations",`${a}.md`);o=await E$.promises.readFile(s,"utf-8")}else{let s=be.default.join(__dirname,"../skills/mem-search/SKILL.md"),c=await E$.promises.readFile(s,"utf-8");o=this.extractInstructionSection(c,i)}n.json({content:[{type:"text",text:o}]})}catch(o){T.error("WORKER","Failed to load instructions",{topic:i,operation:a},o),n.status(500).json({content:[{type:"text",text:`Error loading instructions: ${o instanceof Error?o.message:"Unknown error"}`}],isError:!0})}}),this.app.post("/api/admin/restart",m$,async(r,n)=>{n.json({status:"restarting"}),process.platform==="win32"&&process.env.CLAUDE_MEM_MANAGED==="true"&&process.send?(T.info("SYSTEM","Sending restart request to wrapper"),process.send({type:"restart"})):setTimeout(async()=>{await this.shutdown(),process.exit(0)},100)}),this.app.post("/api/admin/shutdown",m$,async(r,n)=>{n.json({status:"shutting_down"}),process.platform==="win32"&&process.env.CLAUDE_MEM_MANAGED==="true"&&process.send?(T.info("SYSTEM","Sending shutdown request to wrapper"),process.send({type:"shutdown"})):setTimeout(async()=>{await this.shutdown(),process.exit(0)},100)}),this.viewerRoutes.setupRoutes(this.app),this.sessionRoutes.setupRoutes(this.app),this.dataRoutes.setupRoutes(this.app),this.settingsRoutes.setupRoutes(this.app),this.logsRoutes.setupRoutes(this.app),this.app.get("/api/context/inject",async(r,n,i)=>{try{let o=new Promise((s,c)=>setTimeout(()=>c(new Error("Initialization timeout")),3e5));if(await Promise.race([this.initializationComplete,o]),!this.searchRoutes){n.status(503).json({error:"Search routes not initialized"});return}i()}catch(a){T.error("WORKER","Context inject handler failed",{},a),n.headersSent||n.status(500).json({error:a instanceof Error?a.message:"Internal server error"})}})}async cleanupOrphanedProcesses(){let e=process.platform==="win32",r=[];if(e){let n=`powershell -Command "Get-CimInstance Win32_Process | Where-Object { $_.Name -like '*python*' -and $_.CommandLine -like '*chroma-mcp*' } | Select-Object -ExpandProperty ProcessId"`,{stdout:i}=await xd(n,{timeout:6e4});if(!i.trim()){T.debug("SYSTEM","No orphaned chroma-mcp processes found (Windows)");return}let a=i.trim().split(` +`);r.json({logs:u,path:n,exists:!0,totalLines:s.length,returnedLines:s.length-c})});handleClearLogs=this.wrapHandler((e,r)=>{let n=this.getLogFilePath();if(!(0,yo.existsSync)(n)){r.json({success:!0,message:"Log file does not exist",path:n});return}(0,yo.writeFileSync)(n,"","utf-8"),T.info("SYSTEM","Log file cleared via UI",{path:n}),r.json({success:!0,message:"Log file cleared",path:n})})};var xd=(0,o6.promisify)(ba.exec),pde="8.5.4",I$=be.default.join((0,Vn.homedir)(),".claude-mem"),bo=be.default.join(I$,"worker.pid"),s6=be.default.join(I$,"cursor-projects.json");function k$(t){(0,je.mkdirSync)(I$,{recursive:!0}),(0,je.writeFileSync)(bo,JSON.stringify(t,null,2))}function fde(){try{return(0,je.existsSync)(bo)?JSON.parse((0,je.readFileSync)(bo,"utf-8")):null}catch(t){return T.warn("SYSTEM","Failed to read PID file",{path:bo,error:t.message}),null}}function _a(){try{(0,je.existsSync)(bo)&&(0,je.unlinkSync)(bo)}catch(t){T.warn("SYSTEM","Failed to remove PID file",{path:bo},t);return}}function P$(){return Dz(s6)}function c6(t){Uz(s6,t)}function mde(t,e){let r=P$();r[t]={workspacePath:e,installedAt:new Date().toISOString()},c6(r),T.info("CURSOR","Registered project for auto-context updates",{projectName:t,workspacePath:e})}function hde(t){let e=P$();e[t]&&(delete e[t],c6(e),T.info("CURSOR","Unregistered project",{projectName:t}))}async function fo(t,e){let n=P$()[t];if(n)try{let i=await fetch(`http://127.0.0.1:${e}/api/context/inject?project=${encodeURIComponent(t)}`);if(!i.ok)return;let a=await i.text();if(!a||!a.trim())return;qz(n.workspacePath,a),T.debug("CURSOR","Updated context file",{projectName:t,workspacePath:n.workspacePath})}catch(i){T.warn("CURSOR","Failed to update context file",{projectName:t},i);return}}function _o(t){return process.platform==="win32"?Math.round(t*2):t}async function T$(t){try{return(await fetch(`http://127.0.0.1:${t}/api/health`)).ok}catch{return!1}}async function wc(t,e=3e4){let r=Date.now();for(;Date.now()-rsetTimeout(n,500))}return!1}async function S$(t){try{let e=await fetch(`http://127.0.0.1:${t}/api/admin/shutdown`,{method:"POST"});return e.ok?!0:(T.warn("SYSTEM","Shutdown request returned error",{port:t,status:e.status}),!1)}catch(e){return e.message?.includes("ECONNREFUSED")||T.warn("SYSTEM","Shutdown request failed",{port:t,error:e.message}),!1}}async function $$(t,e=1e4){let r=Date.now();for(;Date.now()-rsetTimeout(n,500))}return!1}function gde(){let t=be.default.join((0,Vn.homedir)(),".claude","plugins","marketplaces","thedotmack"),e=be.default.join(t,"package.json");return JSON.parse((0,je.readFileSync)(e,"utf-8")).version}async function vde(t){try{let e=await fetch(`http://127.0.0.1:${t}/api/version`);return e.ok?(await e.json()).version:null}catch{return T.debug("SYSTEM","Could not fetch worker version",{port:t}),null}}async function yde(t){let e=gde(),r=await vde(t);return r?{matches:e===r,pluginVersion:e,workerVersion:r}:{matches:!0,pluginVersion:e,workerVersion:r}}var ng=class{app;server=null;startTime=Date.now();mcpClient;mcpReady=!1;initializationCompleteFlag=!1;isShuttingDown=!1;dbManager;sessionManager;sseBroadcaster;sdkAgent;geminiAgent;openRouterAgent;paginationHelper;settingsManager;sessionEventBroadcaster;viewerRoutes;sessionRoutes;dataRoutes;searchRoutes;settingsRoutes;logsRoutes;initializationComplete;resolveInitialization;constructor(){this.app=(0,i6.default)(),this.initializationComplete=new Promise(e=>{this.resolveInitialization=e}),this.dbManager=new Vm,this.sessionManager=new Gm(this.dbManager),this.sseBroadcaster=new Km,this.sdkAgent=new Rh(this.dbManager,this.sessionManager),this.geminiAgent=new Ch(this.dbManager,this.sessionManager),this.geminiAgent.setFallbackAgent(this.sdkAgent),this.openRouterAgent=new Ah(this.dbManager,this.sessionManager),this.openRouterAgent.setFallbackAgent(this.sdkAgent),this.paginationHelper=new Mh(this.dbManager),this.settingsManager=new zh(this.dbManager),this.sessionEventBroadcaster=new Fh(this.sseBroadcaster,this),this.sessionManager.setOnSessionDeleted(()=>{this.broadcastProcessingStatus()}),this.mcpClient=new Ms({name:"worker-search-proxy",version:"1.0.0"},{capabilities:{}}),this.viewerRoutes=new Hh(this.sseBroadcaster,this.dbManager,this.sessionManager),this.sessionRoutes=new Gh(this.sessionManager,this.dbManager,this.sdkAgent,this.geminiAgent,this.openRouterAgent,this.sessionEventBroadcaster,this),this.dataRoutes=new Kh(this.paginationHelper,this.dbManager,this.sessionManager,this.sseBroadcaster,this,this.startTime),this.searchRoutes=null,this.settingsRoutes=new eg(this.settingsManager),this.logsRoutes=new rg,this.setupMiddleware(),this.setupRoutes(),this.registerSignalHandlers()}registerSignalHandlers(){let e=async r=>{if(this.isShuttingDown){T.warn("SYSTEM",`Received ${r} but shutdown already in progress`);return}this.isShuttingDown=!0,T.info("SYSTEM",`Received ${r}, shutting down...`);try{await this.shutdown(),process.exit(0)}catch(n){T.error("SYSTEM","Error during shutdown",{},n),process.exit(1)}};process.on("SIGTERM",()=>e("SIGTERM")),process.on("SIGINT",()=>e("SIGINT"))}setupMiddleware(){Z2(this.summarizeRequestBody.bind(this)).forEach(r=>this.app.use(r))}setupRoutes(){let e="TEST-008-wrapper-ipc";this.app.get("/api/health",(r,n)=>{n.status(200).json({status:"ok",build:e,managed:process.env.CLAUDE_MEM_MANAGED==="true",hasIpc:typeof process.send=="function",platform:process.platform,pid:process.pid,initialized:this.initializationCompleteFlag,mcpReady:this.mcpReady})}),this.app.get("/api/readiness",(r,n)=>{this.initializationCompleteFlag?n.status(200).json({status:"ready",mcpReady:this.mcpReady}):n.status(503).json({status:"initializing",message:"Worker is still initializing, please retry"})}),this.app.get("/api/version",(r,n)=>{n.status(200).json({version:pde})}),this.app.get("/api/instructions",async(r,n)=>{let i=r.query.topic||"all",a=r.query.operation;try{let o;if(a){let s=be.default.join(__dirname,"../skills/mem-search/operations",`${a}.md`);o=await E$.promises.readFile(s,"utf-8")}else{let s=be.default.join(__dirname,"../skills/mem-search/SKILL.md"),c=await E$.promises.readFile(s,"utf-8");o=this.extractInstructionSection(c,i)}n.json({content:[{type:"text",text:o}]})}catch(o){T.error("WORKER","Failed to load instructions",{topic:i,operation:a},o),n.status(500).json({content:[{type:"text",text:`Error loading instructions: ${o instanceof Error?o.message:"Unknown error"}`}],isError:!0})}}),this.app.post("/api/admin/restart",m$,async(r,n)=>{n.json({status:"restarting"}),process.platform==="win32"&&process.env.CLAUDE_MEM_MANAGED==="true"&&process.send?(T.info("SYSTEM","Sending restart request to wrapper"),process.send({type:"restart"})):setTimeout(async()=>{await this.shutdown(),process.exit(0)},100)}),this.app.post("/api/admin/shutdown",m$,async(r,n)=>{n.json({status:"shutting_down"}),process.platform==="win32"&&process.env.CLAUDE_MEM_MANAGED==="true"&&process.send?(T.info("SYSTEM","Sending shutdown request to wrapper"),process.send({type:"shutdown"})):setTimeout(async()=>{await this.shutdown(),process.exit(0)},100)}),this.viewerRoutes.setupRoutes(this.app),this.sessionRoutes.setupRoutes(this.app),this.dataRoutes.setupRoutes(this.app),this.settingsRoutes.setupRoutes(this.app),this.logsRoutes.setupRoutes(this.app),this.app.get("/api/context/inject",async(r,n,i)=>{try{let o=new Promise((s,c)=>setTimeout(()=>c(new Error("Initialization timeout")),3e5));if(await Promise.race([this.initializationComplete,o]),!this.searchRoutes){n.status(503).json({error:"Search routes not initialized"});return}i()}catch(a){T.error("WORKER","Context inject handler failed",{},a),n.headersSent||n.status(500).json({error:a instanceof Error?a.message:"Internal server error"})}})}async cleanupOrphanedProcesses(){let e=process.platform==="win32",r=[];if(e){let n=`powershell -Command "Get-CimInstance Win32_Process | Where-Object { $_.Name -like '*python*' -and $_.CommandLine -like '*chroma-mcp*' } | Select-Object -ExpandProperty ProcessId"`,{stdout:i}=await xd(n,{timeout:6e4});if(!i.trim()){T.debug("SYSTEM","No orphaned chroma-mcp processes found (Windows)");return}let a=i.trim().split(` `);for(let o of a){let s=parseInt(o.trim(),10);!isNaN(s)&&Number.isInteger(s)&&s>0&&r.push(s)}}else{let{stdout:n}=await xd('ps aux | grep "chroma-mcp" | grep -v grep || true');if(!n.trim()){T.debug("SYSTEM","No orphaned chroma-mcp processes found (Unix)");return}let i=n.trim().split(` `);for(let a of i){let o=a.trim().split(/\s+/);if(o.length>1){let s=parseInt(o[1],10);!isNaN(s)&&Number.isInteger(s)&&s>0&&r.push(s)}}}if(r.length!==0){if(T.info("SYSTEM","Cleaning up orphaned chroma-mcp processes",{platform:e?"Windows":"Unix",count:r.length,pids:r}),e)for(let n of r){if(!Number.isInteger(n)||n<=0){T.warn("SYSTEM","Skipping invalid PID",{pid:n});continue}try{(0,ba.execSync)(`taskkill /PID ${n} /T /F`,{timeout:6e4,stdio:"ignore"})}catch(i){T.debug("SYSTEM","Failed to kill process, may have already exited",{pid:n},i)}}else for(let n of r)try{process.kill(n,"SIGKILL")}catch(i){T.debug("SYSTEM","Process already exited",{pid:n},i)}T.info("SYSTEM","Orphaned processes cleaned up",{count:r.length})}}async start(){let e=Rr(),r=Mz();this.server=await new Promise((n,i)=>{let a=this.app.listen(e,r,()=>n(a));a.on("error",i)}),T.info("SYSTEM","Worker started",{host:r,port:e,pid:process.pid}),this.initializeBackground().catch(n=>{T.error("SYSTEM","Background initialization failed",{},n)})}async initializeBackground(){try{await this.cleanupOrphanedProcesses();let{ModeManager:e}=await Promise.resolve().then(()=>(ui(),t4)),{SettingsDefaultsManager:r}=await Promise.resolve().then(()=>(nn(),Nz)),{USER_SETTINGS_PATH:n}=await Promise.resolve().then(()=>(an(),Wz)),a=r.loadFromFile(n).CLAUDE_MEM_MODE;e.getInstance().loadMode(a),T.info("SYSTEM",`Mode loaded: ${a}`),await this.dbManager.initialize();let{PendingMessageStore:o}=await Promise.resolve().then(()=>(ao(),Fs)),s=new o(this.dbManager.getSessionStore().db,3),c=300*1e3,u=s.resetStuckMessages(c);u>0&&T.info("SYSTEM",`Recovered ${u} stuck messages from previous session`,{thresholdMinutes:5});let l=new qh,d=new Lh,p=new Uh(this.dbManager.getSessionSearch(),this.dbManager.getSessionStore(),this.dbManager.getChromaSync(),l,d);this.searchRoutes=new Yh(p),this.searchRoutes.setupRoutes(this.app),T.info("WORKER","SearchManager initialized and search routes registered");let m=be.default.join(__dirname,"mcp-server.cjs"),v=new Us({command:"node",args:[m],env:process.env}),_=3e5,h=this.mcpClient.connect(v),f=new Promise((y,g)=>setTimeout(()=>g(new Error("MCP connection timeout after 5 minutes")),_));await Promise.race([h,f]),this.mcpReady=!0,T.success("WORKER","Connected to MCP server"),this.initializationCompleteFlag=!0,this.resolveInitialization(),T.info("SYSTEM","Background initialization complete"),this.processPendingQueues(50).then(y=>{y.sessionsStarted>0&&T.info("SYSTEM",`Auto-recovered ${y.sessionsStarted} sessions with pending work`,{totalPending:y.totalPendingSessions,started:y.sessionsStarted,sessionIds:y.startedSessionIds})}).catch(y=>{T.warn("SYSTEM","Auto-recovery of pending queues failed",{},y)})}catch(e){throw T.error("SYSTEM","Background initialization failed",{},e),e}}startSessionProcessor(e,r){if(!e)return;let n=e.sessionDbId;T.info("SYSTEM",`Starting generator (${r})`,{sessionId:n}),e.generatorPromise=this.sdkAgent.startSession(e,this).catch(i=>{T.error("SDK","Session generator failed",{sessionId:e.sessionDbId,project:e.project},i)}).finally(()=>{e.generatorPromise=null,this.broadcastProcessingStatus()})}async processPendingQueues(e=10){let{PendingMessageStore:r}=await Promise.resolve().then(()=>(ao(),Fs)),n=new r(this.dbManager.getSessionStore().db,3),i=n.getSessionsWithPendingMessages(),a={totalPendingSessions:i.length,sessionsStarted:0,sessionsSkipped:0,startedSessionIds:[]};if(i.length===0)return a;T.info("SYSTEM",`Processing up to ${e} of ${i.length} pending session queues`);for(let o of i){if(a.sessionsStarted>=e)break;try{if(this.sessionManager.getSession(o)?.generatorPromise){a.sessionsSkipped++;continue}let c=this.sessionManager.initializeSession(o);T.info("SYSTEM",`Starting processor for session ${o}`,{project:c.project,pendingCount:n.getPendingCount(o)}),this.startSessionProcessor(c,"startup-recovery"),a.sessionsStarted++,a.startedSessionIds.push(o),await new Promise(u=>setTimeout(u,100))}catch(s){T.warn("SYSTEM",`Failed to process session ${o}`,{},s),a.sessionsSkipped++}}return a}extractInstructionSection(e,r){let n={workflow:this.extractBetween(e,"## The Workflow","## Search Parameters"),search_params:this.extractBetween(e,"## Search Parameters","## Examples"),examples:this.extractBetween(e,"## Examples","## Why This Workflow"),all:e};return n[r]||n.all}extractBetween(e,r,n){let i=e.indexOf(r),a=e.indexOf(n);return i===-1?e:a===-1?e.substring(i):e.substring(i,a).trim()}async shutdown(){T.info("SYSTEM","Shutdown initiated"),_a();let e=await this.getChildProcesses(process.pid);if(T.info("SYSTEM","Found child processes",{count:e.length,pids:e}),this.server&&(this.server.closeAllConnections(),process.platform==="win32"&&await new Promise(r=>setTimeout(r,500)),await new Promise((r,n)=>{this.server.close(i=>i?n(i):r())}),this.server=null,T.info("SYSTEM","HTTP server closed"),process.platform==="win32"&&(await new Promise(r=>setTimeout(r,500)),T.info("SYSTEM","Waited for Windows port cleanup"))),await this.sessionManager.shutdownAll(),this.mcpClient&&(await this.mcpClient.close(),T.info("SYSTEM","MCP client closed")),await this.dbManager.close(),e.length>0){T.info("SYSTEM","Force killing remaining children");for(let r of e)await this.forceKillProcess(r);await this.waitForProcessesExit(e,5e3)}T.info("SYSTEM","Worker shutdown complete")}async getChildProcesses(e){if(process.platform!=="win32")return[];if(!Number.isInteger(e)||e<=0)return T.warn("SYSTEM","Invalid parent PID for child process enumeration",{parentPid:e}),[];try{let r=`powershell -Command "Get-CimInstance Win32_Process | Where-Object { $_.ParentProcessId -eq ${e} } | Select-Object -ExpandProperty ProcessId"`,{stdout:n}=await xd(r,{timeout:6e4});return n.trim().split(` `).map(i=>parseInt(i.trim(),10)).filter(i=>!isNaN(i)&&Number.isInteger(i)&&i>0)}catch(r){return T.warn("SYSTEM","Failed to enumerate child processes",{parentPid:e,error:r.message}),[]}}async forceKillProcess(e){if(!Number.isInteger(e)||e<=0){T.warn("SYSTEM","Invalid PID for force kill",{pid:e});return}try{process.platform==="win32"?await xd(`taskkill /PID ${e} /T /F`,{timeout:6e4}):process.kill(e,"SIGKILL"),T.info("SYSTEM","Killed process",{pid:e})}catch(r){T.debug("SYSTEM","Process already exited during force kill",{pid:e},r)}}async waitForProcessesExit(e,r){let n=Date.now();for(;Date.now()-n{try{return process.kill(a,0),!0}catch{return!1}});if(i.length===0){T.info("SYSTEM","All child processes exited");return}T.debug("SYSTEM","Waiting for processes to exit",{stillAlive:i}),await new Promise(a=>setTimeout(a,100))}T.warn("SYSTEM","Timeout waiting for child processes to exit")}summarizeRequestBody(e,r,n){return H2(e,r,n)}broadcastProcessingStatus(){let e=this.sessionManager.isAnySessionProcessing(),r=this.sessionManager.getTotalActiveWork(),n=this.sessionManager.getActiveSessionCount();T.info("WORKER","Broadcasting processing status",{isProcessing:e,queueDepth:r,activeSessions:n}),this.sseBroadcaster.broadcast({type:"processing_status",isProcessing:e,queueDepth:r})}};async function _de(){let t=a6.createInterface({input:process.stdin,output:process.stdout}),e=r=>new Promise(n=>t.question(r,n));console.log(`