Skip to content

Commit 6c427a5

Browse files
udhaya10claude
andcommitted
fix: port ensureMemoryDaemon to TypeScript session-start hook
The memory daemon was never auto-started because ensure_memory_daemon() only existed in the Python hook (never registered) while the registered MJS hook had zero memory/daemon logic. Port the function to session-start-continuity.ts with improvements: - Logs to ~/.claude/memory-daemon.log instead of /dev/null - Warns on stderr if daemon script not found - Uses spawn() with detached + unref for proper daemonization Fixes #156 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d07ff4b commit 6c427a5

2 files changed

Lines changed: 148 additions & 4 deletions

File tree

.claude/hooks/dist/session-start-continuity.mjs

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
// src/session-start-continuity.ts
22
import * as fs from "fs";
3+
import * as os from "os";
34
import * as path from "path";
4-
import { execSync } from "child_process";
5+
import { execSync, spawn } from "child_process";
56
function buildHandoffDirName(sessionName, sessionId) {
67
const uuidShort = sessionId.replace(/-/g, "").slice(0, 8);
78
return `${sessionName}-${uuidShort}`;
@@ -159,6 +160,62 @@ function getUnmarkedHandoffs() {
159160
return [];
160161
}
161162
}
163+
function ensureMemoryDaemon() {
164+
const pidFile = path.join(os.homedir(), ".claude", "memory-daemon.pid");
165+
if (fs.existsSync(pidFile)) {
166+
try {
167+
const pid = parseInt(fs.readFileSync(pidFile, "utf-8").trim(), 10);
168+
if (!isNaN(pid)) {
169+
try {
170+
process.kill(pid, 0);
171+
return null;
172+
} catch {
173+
}
174+
}
175+
} catch {
176+
}
177+
try {
178+
fs.unlinkSync(pidFile);
179+
} catch {
180+
}
181+
}
182+
const hookDir = __dirname;
183+
const possibleLocations = [
184+
// 1. Relative to compiled hook (development: .claude/hooks/dist/ → opc/scripts/core/)
185+
path.resolve(hookDir, "..", "..", "..", "opc", "scripts", "core", "memory_daemon.py"),
186+
// 2. In .claude/scripts/core/ (wizard-installed)
187+
path.resolve(hookDir, "..", "scripts", "core", "memory_daemon.py"),
188+
// 3. Global ~/.claude/scripts/core/
189+
path.join(os.homedir(), ".claude", "scripts", "core", "memory_daemon.py")
190+
];
191+
let daemonScript = null;
192+
for (const loc of possibleLocations) {
193+
if (fs.existsSync(loc)) {
194+
daemonScript = loc;
195+
break;
196+
}
197+
}
198+
if (!daemonScript) {
199+
console.error("Warning: memory_daemon.py not found, cannot auto-start memory daemon");
200+
return null;
201+
}
202+
try {
203+
const cwd = path.resolve(daemonScript, "..", "..", "..");
204+
const logFile = path.join(os.homedir(), ".claude", "memory-daemon.log");
205+
const logFd = fs.openSync(logFile, "a");
206+
const child = spawn("uv", ["run", daemonScript, "start"], {
207+
cwd,
208+
stdio: ["ignore", logFd, logFd],
209+
detached: true
210+
});
211+
child.unref();
212+
fs.closeSync(logFd);
213+
return "Memory daemon: Started";
214+
} catch (e) {
215+
console.error(`Warning: Failed to start memory daemon: ${e}`);
216+
return null;
217+
}
218+
}
162219
async function main() {
163220
const input = JSON.parse(await readStdin());
164221
const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
@@ -374,6 +431,10 @@ All handoffs in ${handoffDir}:
374431
}
375432
}
376433
}
434+
const daemonStatus = ensureMemoryDaemon();
435+
if (daemonStatus) {
436+
console.error(`\u2713 ${daemonStatus}`);
437+
}
377438
const output = { result: "continue" };
378439
if (message) {
379440
output.message = message;
@@ -388,10 +449,10 @@ All handoffs in ${handoffDir}:
388449
console.log(JSON.stringify(output));
389450
}
390451
async function readStdin() {
391-
return new Promise((resolve) => {
452+
return new Promise((resolve2) => {
392453
let data = "";
393454
process.stdin.on("data", (chunk) => data += chunk);
394-
process.stdin.on("end", () => resolve(data));
455+
process.stdin.on("end", () => resolve2(data));
395456
});
396457
}
397458
main().catch(console.error);

.claude/hooks/src/session-start-continuity.ts

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as fs from 'fs';
2+
import * as os from 'os';
23
import * as path from 'path';
3-
import { execSync } from 'child_process';
4+
import { execSync, spawn } from 'child_process';
45

56
interface SessionStartInput {
67
type?: 'startup' | 'resume' | 'clear' | 'compact'; // Legacy field
@@ -308,6 +309,82 @@ function getUnmarkedHandoffs(): UnmarkedHandoff[] {
308309
}
309310
}
310311

312+
/**
313+
* Start global memory extraction daemon if not running.
314+
*
315+
* The memory daemon monitors for stale sessions (heartbeat > 5 min)
316+
* and automatically extracts learnings when sessions end.
317+
*
318+
* Returns status message or null if daemon was already running.
319+
*/
320+
function ensureMemoryDaemon(): string | null {
321+
const pidFile = path.join(os.homedir(), '.claude', 'memory-daemon.pid');
322+
323+
// Check if already running
324+
if (fs.existsSync(pidFile)) {
325+
try {
326+
const pid = parseInt(fs.readFileSync(pidFile, 'utf-8').trim(), 10);
327+
if (!isNaN(pid)) {
328+
// Check if process exists (kill -0)
329+
try {
330+
process.kill(pid, 0);
331+
return null; // Already running
332+
} catch {
333+
// Process not found — stale PID file
334+
}
335+
}
336+
} catch {
337+
// Can't read PID file
338+
}
339+
// Remove stale PID file
340+
try { fs.unlinkSync(pidFile); } catch { /* ignore */ }
341+
}
342+
343+
// Find daemon script
344+
const hookDir = __dirname;
345+
const possibleLocations = [
346+
// 1. Relative to compiled hook (development: .claude/hooks/dist/ → opc/scripts/core/)
347+
path.resolve(hookDir, '..', '..', '..', 'opc', 'scripts', 'core', 'memory_daemon.py'),
348+
// 2. In .claude/scripts/core/ (wizard-installed)
349+
path.resolve(hookDir, '..', 'scripts', 'core', 'memory_daemon.py'),
350+
// 3. Global ~/.claude/scripts/core/
351+
path.join(os.homedir(), '.claude', 'scripts', 'core', 'memory_daemon.py'),
352+
];
353+
354+
let daemonScript: string | null = null;
355+
for (const loc of possibleLocations) {
356+
if (fs.existsSync(loc)) {
357+
daemonScript = loc;
358+
break;
359+
}
360+
}
361+
362+
if (!daemonScript) {
363+
console.error('Warning: memory_daemon.py not found, cannot auto-start memory daemon');
364+
return null;
365+
}
366+
367+
try {
368+
// cwd = opc/ directory (3 levels up from the script)
369+
const cwd = path.resolve(daemonScript, '..', '..', '..');
370+
const logFile = path.join(os.homedir(), '.claude', 'memory-daemon.log');
371+
372+
const logFd = fs.openSync(logFile, 'a');
373+
const child = spawn('uv', ['run', daemonScript, 'start'], {
374+
cwd,
375+
stdio: ['ignore', logFd, logFd],
376+
detached: true,
377+
});
378+
child.unref();
379+
fs.closeSync(logFd);
380+
381+
return 'Memory daemon: Started';
382+
} catch (e) {
383+
console.error(`Warning: Failed to start memory daemon: ${e}`);
384+
return null;
385+
}
386+
}
387+
311388
async function main() {
312389
const input: SessionStartInput = JSON.parse(await readStdin());
313390
const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
@@ -557,6 +634,12 @@ async function main() {
557634
}
558635
}
559636

637+
// Ensure memory daemon is running (auto-extracts learnings from ended sessions)
638+
const daemonStatus = ensureMemoryDaemon();
639+
if (daemonStatus) {
640+
console.error(`✓ ${daemonStatus}`);
641+
}
642+
560643
// Output with proper format per Claude Code docs
561644
const output: Record<string, unknown> = { result: 'continue' };
562645

0 commit comments

Comments
 (0)