Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 65fb8d1ed2 | |||
| e7380adb2f | |||
| 245c85a580 |
@@ -10,7 +10,7 @@
|
||||
"plugins": [
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "7.1.14",
|
||||
"version": "7.1.15",
|
||||
"source": "./plugin",
|
||||
"description": "Persistent memory system for Claude Code - context compression across sessions"
|
||||
}
|
||||
|
||||
@@ -4,6 +4,63 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
## [7.1.14] - 2025-12-14
|
||||
|
||||
## Enhanced Error Handling & Logging
|
||||
|
||||
This patch release improves error message quality and logging across the claude-mem system.
|
||||
|
||||
### Error Message Improvements
|
||||
|
||||
**Standardized Hook Error Handling**
|
||||
- Created shared error handlers (`handleFetchError`, `handleWorkerError`) for consistent error messages
|
||||
- Platform-aware restart instructions (macOS, Linux, Windows) with correct commands
|
||||
- Migrated all hooks (context, new, save, summary) to use standardized handlers
|
||||
- Enhanced error logging with actionable context before throwing restart instructions
|
||||
|
||||
**ChromaSync Error Standardization**
|
||||
- Consistent client initialization checks across all methods
|
||||
- Enhanced error messages with troubleshooting steps and restart instructions
|
||||
- Better context about which operation failed
|
||||
|
||||
**Worker Service Improvements**
|
||||
- Enhanced version endpoint error logging with status codes and response text
|
||||
- Improved worker restart error messages with PM2 commands
|
||||
- Better context in all worker-related error scenarios
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **Issue #260**: Fixed `happy_path_error__with_fallback` misuse in save-hook causing false "Missing cwd" errors
|
||||
- Removed unnecessary `happy_path_error` calls from SDKAgent that were masking real error messages
|
||||
- Cleaned up migration logging to use `console.log` instead of `console.error` for non-error events
|
||||
|
||||
### Logging Improvements
|
||||
|
||||
**Timezone-Aware Timestamps**
|
||||
- Worker logs now use local machine timezone instead of UTC
|
||||
- Maintains same format (`YYYY-MM-DD HH:MM:SS.mmm`) but reflects local time
|
||||
- Easier debugging and log correlation with system events
|
||||
- Enhanced worker-cli logging output format
|
||||
|
||||
### Test Coverage
|
||||
|
||||
Added comprehensive test suites:
|
||||
- `tests/error-handling/hook-error-logging.test.ts` - 12 tests for hook error handler behavior
|
||||
- `tests/services/chroma-sync-errors.test.ts` - ChromaSync error message consistency
|
||||
- `tests/integration/hook-execution-environments.test.ts` - Bun PATH resolution across shells
|
||||
- `docs/context/TEST_AUDIT_2025-12-13.md` - Comprehensive audit report
|
||||
|
||||
### Files Changed
|
||||
|
||||
27 files changed: 1,435 additions, 200 deletions
|
||||
|
||||
**What's Changed**
|
||||
* Standardize and enhance error handling across hooks and worker service by @thedotmack in #295
|
||||
* Timezone-aware logging for worker service and CLI
|
||||
* Complete build with all plugin files included
|
||||
|
||||
**Full Changelog**: https://github.com/thedotmack/claude-mem/compare/v7.1.12...v7.1.14
|
||||
|
||||
## [7.1.13] - 2025-12-14
|
||||
|
||||
## Enhanced Error Handling & Logging
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
Claude-mem is a Claude Code plugin providing persistent memory across sessions. It captures tool usage, compresses observations using the Claude Agent SDK, and injects relevant context into future sessions.
|
||||
|
||||
**Current Version**: 7.1.14
|
||||
**Current Version**: 7.1.15
|
||||
|
||||
## Architecture
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "7.1.14",
|
||||
"version": "7.1.15",
|
||||
"description": "Memory compression system for Claude Code - persist context across sessions",
|
||||
"keywords": [
|
||||
"claude",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "7.1.14",
|
||||
"version": "7.1.15",
|
||||
"description": "Persistent memory system for Claude Code - seamlessly preserve context across sessions",
|
||||
"author": {
|
||||
"name": "Alex Newman"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claude-mem-plugin",
|
||||
"version": "7.1.14",
|
||||
"version": "7.1.15",
|
||||
"private": true,
|
||||
"description": "Runtime dependencies for claude-mem bundled hooks",
|
||||
"type": "module",
|
||||
|
||||
File diff suppressed because one or more lines are too long
+100
-26
@@ -60,9 +60,18 @@ export class WorkerService {
|
||||
private searchRoutes: SearchRoutes | null;
|
||||
private settingsRoutes: SettingsRoutes;
|
||||
|
||||
// Initialization tracking
|
||||
private initializationComplete: Promise<void>;
|
||||
private resolveInitialization!: () => void;
|
||||
|
||||
constructor() {
|
||||
this.app = express();
|
||||
|
||||
// Initialize the promise that will resolve when background initialization completes
|
||||
this.initializationComplete = new Promise((resolve) => {
|
||||
this.resolveInitialization = resolve;
|
||||
});
|
||||
|
||||
// Initialize domain services
|
||||
this.dbManager = new DatabaseManager();
|
||||
this.sessionManager = new SessionManager(this.dbManager);
|
||||
@@ -155,6 +164,60 @@ export class WorkerService {
|
||||
this.dataRoutes.setupRoutes(this.app);
|
||||
// searchRoutes is set up after database initialization in initializeBackground()
|
||||
this.settingsRoutes.setupRoutes(this.app);
|
||||
|
||||
// Register early handler for /api/context/inject to avoid 404 during startup
|
||||
// This handler waits for initialization to complete before delegating to SearchRoutes
|
||||
// NOTE: This duplicates logic from SearchRoutes.handleContextInject by design,
|
||||
// as we need the route available immediately before SearchRoutes is initialized
|
||||
this.app.get('/api/context/inject', async (req, res, next) => {
|
||||
try {
|
||||
// Wait for initialization to complete (with timeout)
|
||||
const timeoutMs = 30000; // 30 second timeout
|
||||
const timeoutPromise = new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Initialization timeout')), timeoutMs)
|
||||
);
|
||||
|
||||
await Promise.race([this.initializationComplete, timeoutPromise]);
|
||||
|
||||
// If searchRoutes is still null after initialization, something went wrong
|
||||
if (!this.searchRoutes) {
|
||||
res.status(503).json({ error: 'Search routes not initialized' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Delegate to the proper handler by re-processing the request
|
||||
// Since we're already in the middleware chain, we need to call the handler directly
|
||||
const projectName = req.query.project as string;
|
||||
const useColors = req.query.colors === 'true';
|
||||
|
||||
if (!projectName) {
|
||||
res.status(400).json({ error: 'Project parameter is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Import context generator (runs in worker, has access to database)
|
||||
const { generateContext } = await import('./context-generator.js');
|
||||
|
||||
// Use project name as CWD (generateContext uses path.basename to get project)
|
||||
const cwd = `/context/${projectName}`;
|
||||
|
||||
// Generate context
|
||||
const contextText = await generateContext(
|
||||
{
|
||||
session_id: 'context-inject-' + Date.now(),
|
||||
cwd: cwd
|
||||
},
|
||||
useColors
|
||||
);
|
||||
|
||||
// Return as plain text
|
||||
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
||||
res.send(contextText);
|
||||
} catch (error) {
|
||||
logger.error('WORKER', 'Context inject handler failed', {}, error as Error);
|
||||
res.status(500).json({ error: error instanceof Error ? error.message : 'Internal server error' });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -228,36 +291,47 @@ export class WorkerService {
|
||||
* Background initialization - runs after HTTP server is listening
|
||||
*/
|
||||
private async initializeBackground(): Promise<void> {
|
||||
// Clean up any orphaned chroma-mcp processes BEFORE starting our own
|
||||
await this.cleanupOrphanedProcesses();
|
||||
try {
|
||||
// Clean up any orphaned chroma-mcp processes BEFORE starting our own
|
||||
await this.cleanupOrphanedProcesses();
|
||||
|
||||
// Initialize database (once, stays open)
|
||||
await this.dbManager.initialize();
|
||||
// Initialize database (once, stays open)
|
||||
await this.dbManager.initialize();
|
||||
|
||||
// Initialize search services (requires initialized database)
|
||||
const formattingService = new FormattingService();
|
||||
const timelineService = new TimelineService();
|
||||
const searchManager = new SearchManager(
|
||||
this.dbManager.getSessionSearch(),
|
||||
this.dbManager.getSessionStore(),
|
||||
this.dbManager.getChromaSync(),
|
||||
formattingService,
|
||||
timelineService
|
||||
);
|
||||
this.searchRoutes = new SearchRoutes(searchManager);
|
||||
this.searchRoutes.setupRoutes(this.app); // Setup search routes now that SearchManager is ready
|
||||
logger.info('WORKER', 'SearchManager initialized and search routes registered');
|
||||
// Initialize search services (requires initialized database)
|
||||
const formattingService = new FormattingService();
|
||||
const timelineService = new TimelineService();
|
||||
const searchManager = new SearchManager(
|
||||
this.dbManager.getSessionSearch(),
|
||||
this.dbManager.getSessionStore(),
|
||||
this.dbManager.getChromaSync(),
|
||||
formattingService,
|
||||
timelineService
|
||||
);
|
||||
this.searchRoutes = new SearchRoutes(searchManager);
|
||||
this.searchRoutes.setupRoutes(this.app); // Setup search routes now that SearchManager is ready
|
||||
logger.info('WORKER', 'SearchManager initialized and search routes registered');
|
||||
|
||||
// Connect to MCP server
|
||||
const mcpServerPath = path.join(__dirname, 'mcp-server.cjs');
|
||||
const transport = new StdioClientTransport({
|
||||
command: 'node',
|
||||
args: [mcpServerPath],
|
||||
env: process.env
|
||||
});
|
||||
// Connect to MCP server
|
||||
const mcpServerPath = path.join(__dirname, 'mcp-server.cjs');
|
||||
const transport = new StdioClientTransport({
|
||||
command: 'node',
|
||||
args: [mcpServerPath],
|
||||
env: process.env
|
||||
});
|
||||
|
||||
await this.mcpClient.connect(transport);
|
||||
logger.success('WORKER', 'Connected to MCP server');
|
||||
await this.mcpClient.connect(transport);
|
||||
logger.success('WORKER', 'Connected to MCP server');
|
||||
|
||||
// Signal that initialization is complete
|
||||
this.resolveInitialization();
|
||||
logger.info('SYSTEM', 'Background initialization complete');
|
||||
} catch (error) {
|
||||
logger.error('SYSTEM', 'Background initialization failed', {}, error as Error);
|
||||
// Still resolve to prevent hanging requests, but they'll see searchRoutes is null
|
||||
this.resolveInitialization();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Integration Test: Context Inject Early Access
|
||||
*
|
||||
* Tests that /api/context/inject endpoint is available immediately
|
||||
* when worker starts, even before background initialization completes.
|
||||
*
|
||||
* This prevents the 404 error described in the issue where the hook
|
||||
* tries to access the endpoint before SearchRoutes are registered.
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
describe('Context Inject Early Access', () => {
|
||||
const workerPath = path.join(__dirname, '../../plugin/scripts/worker-service.cjs');
|
||||
|
||||
it('should have /api/context/inject route available immediately on startup', async () => {
|
||||
// This test verifies the fix by checking that:
|
||||
// 1. The route exists immediately (no 404)
|
||||
// 2. The route waits for initialization before processing
|
||||
// 3. Requests don't fail with "Cannot GET /api/context/inject"
|
||||
|
||||
// The fix adds an early handler that:
|
||||
// - Registers the route in setupRoutes() (called during construction)
|
||||
// - Waits for initializationComplete promise
|
||||
// - Processes the request after initialization
|
||||
|
||||
// Since we can't easily spin up a full worker in tests,
|
||||
// we verify the code structure is correct by checking
|
||||
// the compiled output contains the necessary pieces
|
||||
|
||||
const workerCode = fs.readFileSync(workerPath, 'utf-8');
|
||||
|
||||
// Verify initialization promise exists
|
||||
expect(workerCode).toContain('initializationComplete');
|
||||
expect(workerCode).toContain('resolveInitialization');
|
||||
|
||||
// Verify early route handler is registered in setupRoutes
|
||||
expect(workerCode).toContain('/api/context/inject');
|
||||
expect(workerCode).toContain('Promise.race');
|
||||
|
||||
// Verify the promise is resolved after initialization
|
||||
expect(workerCode).toContain('this.resolveInitialization()');
|
||||
});
|
||||
|
||||
it('should handle timeout if initialization takes too long', () => {
|
||||
const workerCode = fs.readFileSync(workerPath, 'utf-8');
|
||||
|
||||
// Verify timeout protection (30 seconds)
|
||||
expect(workerCode).toContain('3e4'); // 30000 in scientific notation
|
||||
expect(workerCode).toContain('Initialization timeout');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user