Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Dec 14, 2025

The context hook fails with Cannot GET /api/context/inject on fresh installs and restarts because the route is registered after database initialization completes, creating a race condition.

Changes

Worker service initialization

  • Added initializationComplete Promise to track async background initialization
  • Register /api/context/inject route immediately in setupRoutes() instead of after DB init
  • Early handler blocks requests until initialization resolves (30s timeout)
  • Resolve promise in initializeBackground() when SearchRoutes are ready

Implementation

// In constructor
this.initializationComplete = new Promise((resolve) => {
  this.resolveInitialization = resolve;
});

// In setupRoutes() - route exists immediately
this.app.get('/api/context/inject', async (req, res) => {
  await Promise.race([this.initializationComplete, timeoutPromise]);
  // Process request after initialization...
});

// In initializeBackground() - unblock waiting requests
this.searchRoutes = new SearchRoutes(searchManager);
this.searchRoutes.setupRoutes(this.app);
this.resolveInitialization();

The route handler duplicates logic from SearchRoutes.handleContextInject by design—the route must exist before SearchRoutes initializes to prevent 404s.

Test coverage

  • Added integration test verifying route registration and timeout handling in compiled output
Original prompt

This section details on the original issue you should resolve

<issue_title>Error: Context generation failed: Worker service connection failed.</issue_title>
<issue_description>Describe the bug
Plugin always starts with error about worker service connection failed.

To Reproduce
Steps to reproduce the behavior:

  1. On MacOS 26.1
  2. Install Claude Code v2.0.69
  3. Start Claude
  4. Run /plugin marketplace add thedotmack/claude-mem then plugin install claude-mem
  5. Install for user scope
  6. Exit and restart Claude Code
  7. See error
  8. Exit Claude Code and follow instructions to restart worker
  9. Open Claude Code
  10. See error again

Expected behavior
I am expecting that the plugin works after installing and reopening Claude Code. I am also expecting that following the instructions from the error fixes the issue.

Instructions from the error:

To restart the worker:
    1. Exit Claude Code completely
    2. Open Terminal
    3. Navigate to: ~/.claude/plugins/marketplaces/thedotmack
    4. Run: npm run worker:restart

Desktop (please complete the following information):

  • OS: MacOS 26.1
  • CC Version 2.0.69
  • Plugin version: 7.1.14

Full Claude output:

claude

 * ▐▛███▜▌ *   Claude Code v2.0.69
* ▝▜█████▛▘ *  Opus 4.5 · Claude Max
 *  ▘▘ ▝▝  *   ~/Projects/MyProject
  ⎿  SessionStart:startup says: Plugin hook error: ---
     🎉  Note: This appears under Plugin Hook Error, but it's not an error. That's the only option for
        user messages in Claude Code UI until a better method is provided.
     ---

     ⚠️  Claude-Mem: First-Time Setup

     Dependencies are installing in the background. This only happens once.

     💡 TIPS:
        • Memories will start generating while you work
        • Use /init to write or update your CLAUDE.md for better project context
        • Try /clear after one session to see what context looks like

     Thank you for installing Claude-Mem!

     This message was not added to your startup context, so you can continue working as normal.
  ⎿ SessionStart:startup says: Plugin hook error: [2025-12-14 11:41:33.256] [ERROR] [HOOK  ] Context 
    generation failed {status=404, hookName=context, operation=Context generation, 
    project=MyProject, port=37777} <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="utf-8">
    <title>Error</title>
    </head>
    <body>
    <pre>Cannot GET /api/context/inject</pre>
    </body>
    </html>

    file:///Users/clayton/.claude/plugins/cache/thedotmack/claude-mem/7.1.14/scripts/context-hook.js:14
    ${u}`),u}var 
    j=C.join(Ct(),".claude","plugins","marketplaces","thedotmack"),V=U(S.HEALTH_CHECK),_=null;function 
    E(){if(_!==null)return _;try{let 
    n=C.join(l.get("CLAUDE_MEM_DATA_DIR"),"settings.json"),t=l.loadFromFile(n);return 
    _=parseInt(t.CLAUDE_MEM_WORKER_PORT,10),_}catch(n){return c.debug("SYSTEM","Failed to load port 
    from settings, using default",{error:n}),_=parseInt(l.get("CLAUDE_MEM_WORKER_PORT"),10),_}}async 
    function I(){try{let n=E();return(await 
    fetch(`http://127.0.0.1:${n}/health`,{signal:AbortSignal.timeout(V)})).ok}catch(n){return 
    c.debug("SYSTEM","Worker health check failed",{error:n instanceof 
    Error?n.message:String(n),errorType:n?.constructor?.name}),!1}}function Rt(){try{let 
    n=C.join(j,"package.json");return JSON.parse(Dt(n,"utf-8")).version}catch(n){return 
    c.debug("SYSTEM","Failed to read plugin version",{error:n instanceof 
    Error?n.message:String(n)}),null}}async function It(){try{let n=E(),t=await 
    fetch(`http://127.0.0.1:${n}/api/version`,{signal:AbortSignal.timeout(V)});return t.ok?(await 
    t.json()).version:null}catch(n){return c.debug("SYSTEM","Failed to get worker version",{error:n 
    instanceof Error?n.message:String(n)}),null}}async function B(){let n=Rt(),t=await 
    It();!n||!t||n!==t&&(c.info("SYSTEM","Worker version mismatch detected - restarting 
    worker",{pluginVersion:n,workerVersion:t}),await O.restart(E()),await new 
    Promise(e=>setTimeout(e,1e3)),await I()||c.error("SYSTEM","Worker failed to restart after version 
    mismatch",{expectedVersion:n,runningVersion:t,port}))}async function wt(){let 
    n=l.get("CLAUDE_MEM_DATA_DIR"),t=C.join(n,".pm2-migrated");if(Lt(n,{recursive:!0}),!Mt(t))try{At("p
    m2",["delete","claude-mem-worker"],{stdio:"ignore"}),K(t,new 
    Date().toISOString(),"utf-8"),c.debug("SYSTEM","PM2 cleanup completed and marked")}catch{K(t,new 
    Date().toISOString(),"utf-8")}let e=E(),r=await O.start(e);return 
    r.success||c.error("SYSTEM","Failed to start 
    worker",{platform:process.platform,port:e,error:r.error,marketplaceRoot:j}),r.success}async 
    function G(){if(await I()){await B();return}if(!await wt()){let e=E();throw new 
    Error(m({port:e,customPrefix:`Worker service failed to start on port ${e}.`}))}for(let 
    e=0;e<5;e++)if(await new Promise(r=>setTimeout(r,500)),await I()){await B();return}let t=E();throw 
    c.error("SYSTEM","Worker ...

</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

- Fixes thedotmack/claude-mem#305

<!-- START COPILOT CODING AGENT TIPS -->
---

✨ Let Copilot coding agent [set things up for you](https://github.com/thedotmack/claude-mem/issues/new?title=✨+Set+up+Copilot+instructions&body=Configure%20instructions%20for%20this%20repository%20as%20documented%20in%20%5BBest%20practices%20for%20Copilot%20coding%20agent%20in%20your%20repository%5D%28https://gh.io/copilot-coding-agent-tips%29%2E%0A%0A%3COnboard%20this%20repo%3E&assignees=copilot) — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI changed the title [WIP] Fix context generation failure in worker service connection Fix 404 error on /api/context/inject during worker startup Dec 14, 2025
Copilot AI requested a review from thedotmack December 14, 2025 19:14
Compiled worker service and MCP server with the initialization race condition fix.
Validation results: All tests passing, route available immediately on restart.

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

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
@claude
Copy link

claude bot commented Dec 14, 2025

PR Review: Fix 404 error on /api/context/inject during worker startup

Summary

This PR addresses a critical race condition where the /api/context/inject endpoint returns 404 errors during fresh installs and worker restarts. The fix ensures the route is registered immediately while deferring initialization-dependent processing.

Code Quality & Architecture ✅

Strong Points:

  • Clear problem identification: The race condition is well-documented in the PR description
  • Minimal invasive change: The fix adds initialization tracking without restructuring existing code
  • Good separation of concerns: Route registration happens early, initialization runs async

Design Pattern Analysis:
The Promise-based initialization tracking is a solid pattern:

this.initializationComplete = new Promise((resolve) => {
  this.resolveInitialization = resolve;
});

This is idiomatic and clear.

Potential Issues & Concerns

1. Code Duplication (Medium Priority) ⚠️

Location: src/services/worker-service.ts:172-220

The early handler duplicates the entire handleContextInject logic from SearchRoutes.ts:202-229. This creates maintenance burden:

// worker-service.ts lines 172-220 duplicate SearchRoutes.handleContextInject
this.app.get('/api/context/inject', async (req, res, next) => {
  // ... 48 lines of duplicated logic
});

Recommendation: Consider one of these approaches:

  1. Extract shared logic to a separate function that both handlers can call
  2. Use middleware to block requests until initialization, then let express route to SearchRoutes normally
  3. Dynamic route registration - don't register SearchRoutes endpoints in setupRoutes(), instead have SearchRoutes register all its own routes including this one

Example middleware approach:

// In setupRoutes()
this.app.use('/api/context/*', async (req, res, next) => {
  await Promise.race([this.initializationComplete, timeoutPromise]);
  next();
});

// Let SearchRoutes register normally after DB init - no duplication

2. Error Handling Gap ⚠️

Location: src/services/worker-service.ts:331-333

When background initialization fails, resolveInitialization() is still called, but requests will fail when checking if (!this.searchRoutes). However, the error that caused initialization to fail is not accessible to the request handler.

} catch (error) {
  logger.error('SYSTEM', 'Background initialization failed', {}, error as Error);
  this.resolveInitialization(); // ❌ Error is logged but lost
  throw error;
}

Recommendation: Store the initialization error and return it in the response:

private initializationError: Error | null = null;

// In catch block:
this.initializationError = error as Error;
this.resolveInitialization();

// In route handler:
if (this.initializationError) {
  res.status(503).json({ 
    error: 'Initialization failed', 
    details: this.initializationError.message 
  });
  return;
}

3. Type Safety 💡

Location: src/services/worker-service.ts:65

The definite assignment assertion may hide issues:

private resolveInitialization!: () => void;

Recommendation: Initialize in the same statement to avoid the assertion:

private resolveInitialization: () => void;

constructor() {
  this.resolveInitialization = () => {}; // Default no-op
  this.initializationComplete = new Promise((resolve) => {
    this.resolveInitialization = resolve;
  });
}

4. Timeout Value Documentation 📝

The 30-second timeout is hardcoded without justification. On slow systems or with cold starts, is 30s sufficient?

Recommendation:

  • Add comment explaining why 30s was chosen
  • Consider making it configurable via settings
  • Log a warning when initialization takes >10s

Test Coverage 📊

Current Test: tests/integration/context-inject-early.test.ts

Issues:

  1. Not a true integration test - only checks compiled code contains strings, doesn't actually test behavior
  2. Fragile - scientific notation check (expect(workerCode).toContain('3e4')) could break with different minification
  3. No runtime validation - doesn't verify the fix works under race conditions

Recommendations:

// Better integration test
it('should handle concurrent requests during initialization', async () => {
  const worker = new WorkerService();
  const startPromise = worker.start();
  
  // Fire request immediately, before initialization completes
  const response = await fetch('http://localhost:37777/api/context/inject?project=test');
  
  expect(response.status).not.toBe(404);
  await startPromise;
  await worker.shutdown();
});

Performance Considerations ✅

Positive:

  • Non-blocking initialization is excellent
  • HTTP server starts immediately (good for health checks)
  • Promise.race() with timeout prevents indefinite hangs

Concern:

  • Every request to /api/context/inject awaits the initialization promise, even after initialization completes
  • After first resolution, subsequent awaits are instant (microtask), so impact is negligible

Security Concerns ✅

No security issues identified:

  • Input validation present (if (!projectName))
  • No SQL injection vectors (using proper parameter binding in SearchManager)
  • Error messages don't leak sensitive info

Best Practices Alignment

Follows CLAUDE.md conventions:

  • ✅ Uses TypeScript with proper typing
  • ✅ Integrates with existing logger
  • ✅ Maintains Express routing patterns
  • ✅ Non-blocking async operations

Minor Deviation:

  • Code duplication violates DRY principle mentioned in architecture docs

Recommendations Summary

Must Fix (Blocking):

None - PR is functional and solves the critical issue

Should Fix (High Priority):

  1. Eliminate code duplication between worker-service and SearchRoutes
  2. Improve error handling - store and surface initialization errors to requests
  3. Add runtime integration test - verify race condition is actually fixed

Nice to Have (Low Priority):

  1. Remove definite assignment assertion for better type safety
  2. Document timeout value rationale
  3. Add monitoring/logging for slow initialization times

Verdict: ✅ Approve with Recommendations

This PR successfully fixes the critical 404 error on fresh installs. The implementation is sound, but the code duplication creates technical debt that should be addressed in a follow-up PR.

Suggested Merge Strategy:

  1. Merge this PR to fix the immediate issue
  2. Create follow-up issue for refactoring to eliminate duplication
  3. Create follow-up issue for improved integration testing

Great work identifying and fixing this race condition! 🎉

@thedotmack thedotmack marked this pull request as ready for review December 14, 2025 19:31
@thedotmack thedotmack merged commit e7380ad into main Dec 14, 2025
2 checks passed
@thedotmack thedotmack deleted the copilot/fix-worker-service-error branch December 14, 2025 19:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants