Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/public/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Settings are managed in `~/.claude-mem/settings.json`. The file is auto-created
| `CLAUDE_MEM_CONTEXT_OBSERVATIONS` | `50` | Number of observations to inject |
| `CLAUDE_MEM_WORKER_PORT` | `37700 + (uid % 100)` | Worker service port (per-user default; override for fixed port) |
| `CLAUDE_MEM_WORKER_HOST` | `127.0.0.1` | Worker service host address |
| `CLAUDE_MEM_WORKER_AUTOSTART` | `true` | When `false`, hooks won't lazy-spawn the worker daemon — an opt-out for server-beta-only or externally-managed deployments. Default `true` preserves existing behavior (you lose worker-only features like the viewer, corpus/skills, and semantic injection when off) |
| `CLAUDE_MEM_DATA_DIR` | `~/.claude-mem` | Data root — every other path (database, chroma, logs, settings.json, worker.pid) derives from this |
| `CLAUDE_MEM_SKIP_TOOLS` | `ListMcpResourcesTool,SlashCommand,Skill,TodoWrite,AskUserQuestion` | Comma-separated tools to exclude from observations |

Expand Down
2 changes: 2 additions & 0 deletions src/shared/SettingsDefaultsManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export interface SettingsDefaults {
CLAUDE_MEM_SERVER_BETA_URL: string;
CLAUDE_MEM_SERVER_BETA_API_KEY: string;
CLAUDE_MEM_SERVER_BETA_PROJECT_ID: string;
CLAUDE_MEM_WORKER_AUTOSTART: string;
}

export class SettingsDefaultsManager {
Expand Down Expand Up @@ -160,6 +161,7 @@ export class SettingsDefaultsManager {
CLAUDE_MEM_SERVER_BETA_URL: `http://127.0.0.1:${process.env.CLAUDE_MEM_SERVER_PORT ?? String(37877 + ((process.getuid?.() ?? 77) % 100))}`, // Default server-beta runtime URL — UID-derived for multi-account isolation
CLAUDE_MEM_SERVER_BETA_API_KEY: '', // Local hook API key, populated by installer when runtime=server-beta
CLAUDE_MEM_SERVER_BETA_PROJECT_ID: '', // Default Postgres project_id used by hooks when runtime=server-beta
CLAUDE_MEM_WORKER_AUTOSTART: 'true', // When 'false', hooks will NOT lazy-spawn the worker daemon. Default 'true' preserves existing behavior; opt-in for deployments that drive the worker out-of-band (e.g. server-beta-only, where worker-only features like the data viewer/corpus/semantic-injection are intentionally not used).
};

static getAllDefaults(): SettingsDefaults {
Expand Down
13 changes: 13 additions & 0 deletions src/shared/worker-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,8 +310,21 @@ export async function ensureWorkerRunning(): Promise<boolean> {

let aliveCache: boolean | null = null;

/** Test-only: reset the ensureWorkerAliveOnce() cache (mirrors clearPortCache). */
export function resetAliveCache(): void {
aliveCache = null;
}

export async function ensureWorkerAliveOnce(): Promise<boolean> {
if (aliveCache !== null) return aliveCache;
// Opt-out: when CLAUDE_MEM_WORKER_AUTOSTART=false, hooks must NOT lazy-spawn
// the worker daemon. Lets server-beta-only or externally-managed deployments
// stop hook activity from resurrecting the worker. Default 'true' preserves
// existing behavior.
if ((loadFromFileOnce().CLAUDE_MEM_WORKER_AUTOSTART ?? 'true').trim().toLowerCase() === 'false') {
aliveCache = false;
return aliveCache;
}
aliveCache = await ensureWorkerRunning();
return aliveCache;
}
Expand Down
76 changes: 76 additions & 0 deletions tests/shared/worker-autostart-flag.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { describe, it, expect, beforeEach, afterEach, mock } from 'bun:test';

// Drives what loadFromFileOnce() returns per test (the settings object).
let settings: Record<string, unknown> = {};

// Record fetch calls so we can assert the worker was never contacted in the
// opt-out path (no health check, no lazy-spawn).
const fetchLog: Array<{ url: string; method: string }> = [];

mock.module('../../src/shared/hook-settings.js', () => ({
loadFromFileOnce: () => settings,
}));

// For the default (autostart on) path, present a healthy/version-matched worker
// so ensureWorkerRunning() resolves true without an actual spawn.
mock.module('../../src/supervisor/index.js', () => ({
validateWorkerPidFile: () => 'alive',
}));
mock.module('../../src/services/infrastructure/index.js', () => ({
checkVersionMatch: () =>
Promise.resolve({ matches: true, pluginVersion: '13.4.1', workerVersion: '13.4.1' }),
}));

function installFetchMock(): void {
fetchLog.length = 0;
global.fetch = mock((url: string | URL | Request, init?: RequestInit) => {
const u = typeof url === 'string' ? url : url.toString();
fetchLog.push({ url: u, method: (init?.method ?? 'GET').toUpperCase() });
return Promise.resolve({
ok: true,
status: 200,
text: () => Promise.resolve(''),
json: () => Promise.resolve({}),
} as unknown as Response);
}) as unknown as typeof fetch;
}

describe('ensureWorkerAliveOnce — CLAUDE_MEM_WORKER_AUTOSTART opt-out', () => {
const originalFetch = global.fetch;

beforeEach(async () => {
installFetchMock();
const { resetAliveCache } = await import('../../src/shared/worker-utils.js');
resetAliveCache();
});

afterEach(() => {
global.fetch = originalFetch;
mock.restore();
});

it('returns false and never contacts the worker when AUTOSTART=false', async () => {
settings = { CLAUDE_MEM_WORKER_AUTOSTART: 'false' };

const { ensureWorkerAliveOnce } = await import('../../src/shared/worker-utils.js');

expect(await ensureWorkerAliveOnce()).toBe(false);
expect(fetchLog).toHaveLength(0); // short-circuited before any spawn/health check
});

it('proceeds normally (true for a live worker) when AUTOSTART is unset (default)', async () => {
settings = {};

const { ensureWorkerAliveOnce } = await import('../../src/shared/worker-utils.js');

expect(await ensureWorkerAliveOnce()).toBe(true);
});

it('treats AUTOSTART=true the same as unset', async () => {
settings = { CLAUDE_MEM_WORKER_AUTOSTART: 'true' };

const { ensureWorkerAliveOnce } = await import('../../src/shared/worker-utils.js');

expect(await ensureWorkerAliveOnce()).toBe(true);
});
});