-
Notifications
You must be signed in to change notification settings - Fork 284
fix: port ensureMemoryDaemon to TypeScript session-start hook #157
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| import * as fs from 'fs'; | ||
| import * as os from 'os'; | ||
| import * as path from 'path'; | ||
| import { execSync } from 'child_process'; | ||
| import { execSync, spawn } from 'child_process'; | ||
|
|
||
| interface SessionStartInput { | ||
| type?: 'startup' | 'resume' | 'clear' | 'compact'; // Legacy field | ||
|
|
@@ -308,6 +309,82 @@ function getUnmarkedHandoffs(): UnmarkedHandoff[] { | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * Start global memory extraction daemon if not running. | ||
| * | ||
| * The memory daemon monitors for stale sessions (heartbeat > 5 min) | ||
| * and automatically extracts learnings when sessions end. | ||
| * | ||
| * Returns status message or null if daemon was already running. | ||
| */ | ||
| function ensureMemoryDaemon(): string | null { | ||
| const pidFile = path.join(os.homedir(), '.claude', 'memory-daemon.pid'); | ||
|
|
||
| // Check if already running | ||
| if (fs.existsSync(pidFile)) { | ||
| try { | ||
| const pid = parseInt(fs.readFileSync(pidFile, 'utf-8').trim(), 10); | ||
| if (!isNaN(pid)) { | ||
| // Check if process exists (kill -0) | ||
| try { | ||
| process.kill(pid, 0); | ||
| return null; // Already running | ||
| } catch { | ||
| // Process not found — stale PID file | ||
| } | ||
| } | ||
| } catch { | ||
| // Can't read PID file | ||
| } | ||
| // Remove stale PID file | ||
| try { fs.unlinkSync(pidFile); } catch { /* ignore */ } | ||
| } | ||
|
|
||
| // Find daemon script | ||
| const hookDir = path.dirname(new URL(import.meta.url).pathname); | ||
| const possibleLocations = [ | ||
| // 1. Relative to compiled hook (development: .claude/hooks/dist/ → opc/scripts/core/) | ||
| path.resolve(hookDir, '..', '..', '..', 'opc', 'scripts', 'core', 'memory_daemon.py'), | ||
| // 2. In .claude/scripts/core/ (wizard-installed) | ||
| path.resolve(hookDir, '..', 'scripts', 'core', 'memory_daemon.py'), | ||
| // 3. Global ~/.claude/scripts/core/ | ||
| path.join(os.homedir(), '.claude', 'scripts', 'core', 'memory_daemon.py'), | ||
| ]; | ||
|
|
||
| let daemonScript: string | null = null; | ||
| for (const loc of possibleLocations) { | ||
| if (fs.existsSync(loc)) { | ||
| daemonScript = loc; | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| if (!daemonScript) { | ||
| console.error('Warning: memory_daemon.py not found, cannot auto-start memory daemon'); | ||
| return null; | ||
| } | ||
|
|
||
| try { | ||
| // cwd = opc/ directory (3 levels up from the script) | ||
| const cwd = path.resolve(daemonScript, '..', '..', '..'); | ||
| const logFile = path.join(os.homedir(), '.claude', 'memory-daemon.log'); | ||
|
|
||
| const logFd = fs.openSync(logFile, 'a'); | ||
| const child = spawn('uv', ['run', daemonScript, 'start'], { | ||
| cwd, | ||
| stdio: ['ignore', logFd, logFd], | ||
| detached: true, | ||
| }); | ||
| child.unref(); | ||
| fs.closeSync(logFd); | ||
|
Comment on lines
+372
to
+379
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Two issues: unhandled 1. Missing 2. File-descriptor leak (major): 🐛 Proposed fix- const logFd = fs.openSync(logFile, 'a');
- const child = spawn('uv', ['run', daemonScript, 'start'], {
- cwd,
- stdio: ['ignore', logFd, logFd],
- detached: true,
- });
- child.unref();
- fs.closeSync(logFd);
+ let logFd: number | null = null;
+ try {
+ logFd = fs.openSync(logFile, 'a');
+ const child = spawn('uv', ['run', daemonScript, 'start'], {
+ cwd,
+ stdio: ['ignore', logFd, logFd],
+ detached: true,
+ });
+ child.on('error', (err) => {
+ console.error(`Warning: memory daemon spawn failed (uv not found?): ${err.message}`);
+ });
+ child.unref();
+ } finally {
+ if (logFd !== null) fs.closeSync(logFd);
+ }🤖 Prompt for AI Agents |
||
|
|
||
| return 'Memory daemon: Started'; | ||
| } catch (e) { | ||
| console.error(`Warning: Failed to start memory daemon: ${e}`); | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| async function main() { | ||
| const input: SessionStartInput = JSON.parse(await readStdin()); | ||
| const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd(); | ||
|
|
@@ -557,6 +634,12 @@ async function main() { | |
| } | ||
| } | ||
|
|
||
| // Ensure memory daemon is running (auto-extracts learnings from ended sessions) | ||
| const daemonStatus = ensureMemoryDaemon(); | ||
| if (daemonStatus) { | ||
| console.error(`✓ ${daemonStatus}`); | ||
| } | ||
|
Comment on lines
+637
to
+641
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Daemon is never started for new projects — early return at line 517 bypasses this call The Move the 🐛 Proposed fix — hoist the daemon call async function main() {
const input: SessionStartInput = JSON.parse(await readStdin());
const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
const sessionType = input.source || input.type;
+ // Ensure memory daemon is running regardless of ledger state
+ const daemonStatus = ensureMemoryDaemon();
+ if (daemonStatus) {
+ console.error(`✓ ${daemonStatus}`);
+ }
+
let message = '';
...And remove the duplicate call at line 637–641. 🤖 Prompt for AI Agents |
||
|
|
||
| // Output with proper format per Claude Code docs | ||
| const output: Record<string, unknown> = { result: 'continue' }; | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cwdderivation is correct only for path option 1;uv runmay fail for wizard-installed/global pathsThe comment "3 levels up from the script" is accurate for path 1 (
opc/scripts/core/memory_daemon.py→opc/), which is whereuv's lockfile likely lives. However for paths 2 and 3 the three-level walk resolves to.claude/and~/.claude/respectively — directories that probably lack apyproject.toml/uv.lock. Runninguv runfrom those locations may silently fall back to a different Python environment or fail.Consider deriving the
cwdbased on which path matched (e.g., always resolving to the directory that containspyproject.toml), or documenting the expected project structure for wizard and global installs.🤖 Prompt for AI Agents