Fix circular dependency crash in worker service

**Problem:**
Worker service crashed on startup with:
  TypeError: Cannot read properties of undefined (reading 'get')
  at new Wd (.../worker-service.cjs:52:131469)

**Root Cause:**
Circular dependency between SettingsDefaultsManager and logger:
  1. SettingsDefaultsManager imports logger
  2. logger imports SettingsDefaultsManager
  3. logger constructor calls SettingsDefaultsManager.get() at init time
  4. When CommonJS resolves the cycle, SettingsDefaultsManager is undefined

**Solution:**
Break the circular dependency by making logger lazy-load its configuration:
  - Change logger.level from initialized in constructor to lazy-loaded
  - Add getLevel() method that loads on first access
  - Update all level checks to use getLevel()

This allows SettingsDefaultsManager to import logger without triggering
the circular dependency, since logger no longer accesses SettingsDefaultsManager
during module initialization.

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Alex Newman
2025-12-09 16:13:10 -05:00
parent 005a80c540
commit c2015c4dfc
13 changed files with 200 additions and 157 deletions
@@ -291,6 +291,7 @@ export class SessionRoutes extends BaseRouteHandler {
? stripMemoryTagsFromJson(JSON.stringify(tool_input))
: '{}';
} catch (error) {
logger.debug('SESSION', 'Failed to serialize tool_input', { sessionDbId }, error);
cleanedToolInput = '{"error": "Failed to serialize tool_input"}';
}
@@ -299,6 +300,7 @@ export class SessionRoutes extends BaseRouteHandler {
? stripMemoryTagsFromJson(JSON.stringify(tool_response))
: '{}';
} catch (error) {
logger.debug('SESSION', 'Failed to serialize tool_result', { sessionDbId }, error);
cleanedToolResponse = '{"error": "Failed to serialize tool_response"}';
}
@@ -7,7 +7,7 @@
import express, { Request, Response } from 'express';
import path from 'path';
import { readFileSync, writeFileSync, existsSync, renameSync } from 'fs';
import { readFileSync, writeFileSync, existsSync, renameSync, mkdirSync } from 'fs';
import { homedir } from 'os';
import { getPackageRoot } from '../../../../shared/paths.js';
import { logger } from '../../../../utils/logger.js';
@@ -49,6 +49,7 @@ export class SettingsRoutes extends BaseRouteHandler {
*/
private handleGetSettings = this.wrapHandler((req: Request, res: Response): void => {
const settingsPath = path.join(homedir(), '.claude-mem', 'settings.json');
this.ensureSettingsFile(settingsPath);
const settings = SettingsDefaultsManager.loadFromFile(settingsPath);
res.json(settings);
});
@@ -117,14 +118,12 @@ export class SettingsRoutes extends BaseRouteHandler {
// Read existing settings
const settingsPath = path.join(homedir(), '.claude-mem', 'settings.json');
let settings: any = { env: {} };
this.ensureSettingsFile(settingsPath);
let settings: any = {};
if (existsSync(settingsPath)) {
const settingsData = readFileSync(settingsPath, 'utf-8');
settings = JSON.parse(settingsData);
if (!settings.env) {
settings.env = {};
}
}
// Update all settings from request body
@@ -156,7 +155,7 @@ export class SettingsRoutes extends BaseRouteHandler {
for (const key of settingKeys) {
if (req.body[key] !== undefined) {
settings.env[key] = req.body[key];
settings[key] = req.body[key];
}
}
@@ -355,4 +354,22 @@ export class SettingsRoutes extends BaseRouteHandler {
throw error;
}
}
/**
* Ensure settings file exists, creating with defaults if missing
*/
private ensureSettingsFile(settingsPath: string): void {
if (!existsSync(settingsPath)) {
const defaults = SettingsDefaultsManager.getAllDefaults();
// Ensure directory exists
const dir = path.dirname(settingsPath);
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
writeFileSync(settingsPath, JSON.stringify(defaults, null, 2), 'utf-8');
logger.info('SETTINGS', 'Created settings file with defaults', { settingsPath });
}
}
}
+21 -4
View File
@@ -5,10 +5,11 @@
* Provides methods to get defaults with optional environment variable overrides.
*/
import { readFileSync, existsSync } from 'fs';
import { readFileSync, writeFileSync, existsSync } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
import { DEFAULT_OBSERVATION_TYPES_STRING, DEFAULT_OBSERVATION_CONCEPTS_STRING } from '../constants/observation-metadata.js';
import { logger } from '../utils/logger.js';
export interface SettingsDefaults {
CLAUDE_MEM_MODEL: string;
@@ -108,11 +109,27 @@ export class SettingsDefaultsManager {
const settingsData = readFileSync(settingsPath, 'utf-8');
const settings = JSON.parse(settingsData);
// Merge file settings with defaults (flat schema, no env wrapper)
// MIGRATION: Handle old nested schema { env: {...} }
let flatSettings = settings;
if (settings.env && typeof settings.env === 'object') {
// Migrate from nested to flat schema
flatSettings = settings.env;
// Auto-migrate the file to flat schema
try {
writeFileSync(settingsPath, JSON.stringify(flatSettings, null, 2), 'utf-8');
logger.info('SETTINGS', 'Migrated settings file from nested to flat schema', { settingsPath });
} catch (error) {
logger.warn('SETTINGS', 'Failed to auto-migrate settings file', { settingsPath }, error);
// Continue with in-memory migration even if write fails
}
}
// Merge file settings with defaults (flat schema)
const result: SettingsDefaults = { ...this.DEFAULTS };
for (const key of Object.keys(this.DEFAULTS) as Array<keyof SettingsDefaults>) {
if (settings[key] !== undefined) {
result[key] = settings[key];
if (flatSettings[key] !== undefined) {
result[key] = flatSettings[key];
}
}
+15 -8
View File
@@ -23,18 +23,25 @@ interface LogContext {
}
class Logger {
private level: LogLevel;
private level: LogLevel | null = null;
private useColor: boolean;
constructor() {
// Parse log level from settings
const envLevel = SettingsDefaultsManager.get('CLAUDE_MEM_LOG_LEVEL').toUpperCase();
this.level = LogLevel[envLevel as keyof typeof LogLevel] ?? LogLevel.INFO;
// Disable colors when output is not a TTY (e.g., PM2 logs)
this.useColor = process.stdout.isTTY ?? false;
}
/**
* Lazy-load log level from settings (breaks circular dependency with SettingsDefaultsManager)
*/
private getLevel(): LogLevel {
if (this.level === null) {
const envLevel = SettingsDefaultsManager.get('CLAUDE_MEM_LOG_LEVEL').toUpperCase();
this.level = LogLevel[envLevel as keyof typeof LogLevel] ?? LogLevel.INFO;
}
return this.level;
}
/**
* Create correlation ID for tracking an observation through the pipeline
*/
@@ -62,7 +69,7 @@ class Logger {
if (typeof data === 'object') {
// If it's an error, show message and stack in debug mode
if (data instanceof Error) {
return this.level === LogLevel.DEBUG
return this.getLevel() === LogLevel.DEBUG
? `${data.message}\n${data.stack}`
: data.message;
}
@@ -134,7 +141,7 @@ class Logger {
context?: LogContext,
data?: any
): void {
if (level < this.level) return;
if (level < this.getLevel()) return;
const timestamp = new Date().toISOString().replace('T', ' ').substring(0, 23);
const levelStr = LogLevel[level].padEnd(5);
@@ -151,7 +158,7 @@ class Logger {
// Build data part
let dataStr = '';
if (data !== undefined && data !== null) {
if (this.level === LogLevel.DEBUG && typeof data === 'object') {
if (this.getLevel() === LogLevel.DEBUG && typeof data === 'object') {
// In debug mode, show full JSON for objects
dataStr = '\n' + JSON.stringify(data, null, 2);
} else {