/** * Summarize Hook Handler (Stop) * * Generates a session summary when Claude Code session ends. * Uses template-based summarization (no LLM required). * * @module @agentkits/memory/hooks/summarize */ import { spawn } from 'node:child_process'; import * as path from 'node:path'; import { NormalizedHookInput, HookResult, EventHandler, } from './types.js'; import { MemoryHookService } from './service.js'; import { isAIEnrichmentEnabled } from './ai-enrichment.js'; /** * Summarize Hook - Stop Event * * Called when a Claude Code session ends. * Generates a summary and marks the session as completed. */ export class SummarizeHook implements EventHandler { private service: MemoryHookService; private ownsService: boolean; constructor(service: MemoryHookService, ownsService = false) { this.service = service; this.ownsService = ownsService; } /** * Shutdown the hook (closes database if owned) */ async shutdown(): Promise { if (this.ownsService) { await this.service.shutdown(); } } /** * Execute the summarize hook */ async execute(input: NormalizedHookInput): Promise { try { // Initialize service await this.service.initialize(); // Check if session exists const session = this.service.getSession(input.sessionId); if (!session) { // No session to summarize return { continue: true, suppressOutput: true, }; } // Generate structured summary from observations + prompts const structured = await this.service.generateStructuredSummary(input.sessionId); // Save structured summary to session_summaries table (same DB as memories) await this.service.saveSessionSummary(structured); // Complete the session with text summary (legacy field) const textSummary = await this.service.generateSummary(input.sessionId); await this.service.completeSession(input.sessionId, textSummary); // Spawn background workers to process queued tasks (one per type, gated by lock file) this.service.ensureWorkerRunning(input.cwd, 'embed-session', 'embed-worker.lock'); if (isAIEnrichmentEnabled()) { this.service.ensureWorkerRunning(input.cwd, 'enrich-session', 'enrich-worker.lock'); // Queue compression for this session's observations (runs after enrichment) this.service.queueTask('compress', 'sessions', input.sessionId); this.service.ensureWorkerRunning(input.cwd, 'compress-session', 'compress-worker.lock'); } // Summary enrichment needs transcript path — handled separately (not via queue) if (isAIEnrichmentEnabled() && input.transcriptPath) { try { const cliPath = path.resolve(path.dirname(new URL(import.meta.url).pathname), 'cli.js'); const child = spawn('node', [ cliPath, 'enrich-summary', input.sessionId, input.cwd, input.transcriptPath, ], { detached: true, stdio: 'ignore', env: { ...process.env }, }); child.on('error', () => { /* spawn failure — silently ignore */ }); child.unref(); } catch { // Silently ignore } } // Shutdown service await this.service.shutdown(); return { continue: true, suppressOutput: true, }; } catch (error) { // Log error but don't block session end console.error('[AgentKits Memory] Summarize hook error:', error); // Try to shutdown anyway try { await this.service.shutdown(); } catch { // Ignore shutdown errors } return { continue: true, suppressOutput: true, error: error instanceof Error ? error.message : 'Unknown error', }; } } } /** * Create summarize hook handler */ export function createSummarizeHook(cwd: string): SummarizeHook { const service = new MemoryHookService(cwd); return new SummarizeHook(service, true); } export default SummarizeHook;