refactor: Convert all hooks to HTTP clients (remove all SQL)
Architecture transformation: Hooks → HTTP → Worker → Database **context-hook.ts** (843 → 104 lines, 88% reduction) - Remove all database imports and raw SQL queries - HTTP GET to /api/context/inject - Returns both formatted (stderr) and unformatted (stdout) context - Dual output: colored display for users, plain text for model **user-message-hook.ts** (updated, 113 lines) - HTTP GET to /api/context/inject with colors=true - Displays formatted context to users via stderr - No database dependencies **save-hook.ts** (418 → 99 lines, 76% reduction) - Remove all SessionStore database methods - HTTP POST to /api/sessions/observations - Worker handles privacy checks and observation creation - Fire-and-forget pattern with 2s timeout **summary-hook.ts** (435 → 200 lines, 54% reduction) - Remove all SessionStore database methods - Keep local transcript parsing (hook has file access) - HTTP POST to /api/sessions/summarize - Worker handles privacy checks and summary generation **cleanup-hook.ts** (414 → 90 lines, 78% reduction) - Remove all SessionStore database methods - HTTP POST to /api/sessions/complete - Worker handles session completion and DB cleanup - Non-fatal if worker unavailable **Benefits:** - Zero native module dependencies in hooks (Node.js or Bun compatible) - Hooks can run in any runtime without recompilation - All database operations centralized in worker service - Simpler, more maintainable hook code - Complete separation of concerns: I/O vs business logic
This commit is contained in:
+15
-64
@@ -1,15 +1,19 @@
|
||||
/**
|
||||
* Summary Hook - Stop
|
||||
* Consolidated entry point + logic
|
||||
*
|
||||
* Pure HTTP client - sends data to worker, worker handles all database operations
|
||||
* including privacy checks. This allows the hook to run under any runtime
|
||||
* (Node.js or Bun) since it has no native module dependencies.
|
||||
*
|
||||
* Transcript parsing stays in the hook because only the hook has access to
|
||||
* the transcript file path.
|
||||
*/
|
||||
|
||||
import { stdin } from 'process';
|
||||
import { readFileSync, existsSync } from 'fs';
|
||||
import { SessionStore } from '../services/sqlite/SessionStore.js';
|
||||
import { createHookResponse } from './hook-response.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
import { ensureWorkerRunning, getWorkerPort } from '../shared/worker-utils.js';
|
||||
import { silentDebug } from '../utils/silent-debug.js';
|
||||
|
||||
export interface StopInput {
|
||||
session_id: string;
|
||||
@@ -123,7 +127,7 @@ function extractLastAssistantMessage(transcriptPath: string): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Summary Hook Main Logic
|
||||
* Summary Hook Main Logic - Fire-and-forget HTTP client
|
||||
*/
|
||||
async function summaryHook(input?: StopInput): Promise<void> {
|
||||
if (!input) {
|
||||
@@ -135,77 +139,25 @@ async function summaryHook(input?: StopInput): Promise<void> {
|
||||
// Ensure worker is running
|
||||
await ensureWorkerRunning();
|
||||
|
||||
const db = new SessionStore();
|
||||
|
||||
// Get or create session
|
||||
const sessionDbId = db.createSDKSession(session_id, '', '');
|
||||
const promptNumber = db.getPromptCounter(sessionDbId);
|
||||
|
||||
// Skip summary if user prompt was entirely private
|
||||
// This respects the user's intent: if they marked the entire prompt as <private>,
|
||||
// they don't want ANY memory operations including summaries
|
||||
const userPrompt = db.getUserPrompt(session_id, promptNumber);
|
||||
if (!userPrompt || userPrompt.trim() === '') {
|
||||
silentDebug('[summary-hook] Skipping summary - user prompt was entirely private', {
|
||||
session_id,
|
||||
promptNumber
|
||||
});
|
||||
db.close();
|
||||
console.log(createHookResponse('Stop', true));
|
||||
return;
|
||||
}
|
||||
|
||||
// DIAGNOSTIC: Check session and observations
|
||||
const sessionInfo = db.db.prepare(`
|
||||
SELECT id, claude_session_id, sdk_session_id, project
|
||||
FROM sdk_sessions WHERE id = ?
|
||||
`).get(sessionDbId) as any;
|
||||
|
||||
const obsCount = db.db.prepare(`
|
||||
SELECT COUNT(*) as count
|
||||
FROM observations
|
||||
WHERE sdk_session_id = ?
|
||||
`).get(sessionInfo?.sdk_session_id) as { count: number };
|
||||
|
||||
silentDebug('[summary-hook] Session diagnostics', {
|
||||
claudeSessionId: session_id,
|
||||
sessionDbId,
|
||||
sdkSessionId: sessionInfo?.sdk_session_id,
|
||||
project: sessionInfo?.project,
|
||||
promptNumber,
|
||||
observationCount: obsCount?.count || 0,
|
||||
transcriptPath: input.transcript_path
|
||||
});
|
||||
|
||||
db.close();
|
||||
|
||||
const port = getWorkerPort();
|
||||
|
||||
// Extract last user AND assistant messages from transcript
|
||||
const lastUserMessage = extractLastUserMessage(input.transcript_path || '');
|
||||
const lastAssistantMessage = extractLastAssistantMessage(input.transcript_path || '');
|
||||
|
||||
silentDebug('[summary-hook] Extracted messages', {
|
||||
hasLastUserMessage: !!lastUserMessage,
|
||||
hasLastAssistantMessage: !!lastAssistantMessage,
|
||||
lastAssistantPreview: lastAssistantMessage.substring(0, 200),
|
||||
lastAssistantLength: lastAssistantMessage.length
|
||||
});
|
||||
|
||||
logger.dataIn('HOOK', 'Stop: Requesting summary', {
|
||||
sessionId: sessionDbId,
|
||||
workerPort: port,
|
||||
promptNumber,
|
||||
hasLastUserMessage: !!lastUserMessage,
|
||||
hasLastAssistantMessage: !!lastAssistantMessage
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await fetch(`http://127.0.0.1:${port}/sessions/${sessionDbId}/summarize`, {
|
||||
// Send to worker - worker handles privacy check and database operations
|
||||
const response = await fetch(`http://127.0.0.1:${port}/api/sessions/summarize`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
prompt_number: promptNumber,
|
||||
claudeSessionId: session_id,
|
||||
last_user_message: lastUserMessage,
|
||||
last_assistant_message: lastAssistantMessage
|
||||
}),
|
||||
@@ -215,26 +167,25 @@ async function summaryHook(input?: StopInput): Promise<void> {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
logger.failure('HOOK', 'Failed to generate summary', {
|
||||
sessionId: sessionDbId,
|
||||
status: response.status
|
||||
}, errorText);
|
||||
throw new Error(`Failed to request summary from worker: ${response.status} ${errorText}`);
|
||||
}
|
||||
|
||||
logger.debug('HOOK', 'Summary request sent successfully', { sessionId: sessionDbId });
|
||||
logger.debug('HOOK', 'Summary request sent successfully');
|
||||
} catch (error: any) {
|
||||
// Only show restart message for connection errors, not HTTP errors
|
||||
if (error.cause?.code === 'ECONNREFUSED' || error.name === 'TimeoutError' || error.message.includes('fetch failed')) {
|
||||
throw new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue");
|
||||
}
|
||||
// Re-throw HTTP errors and other errors as-is
|
||||
throw error;
|
||||
} finally {
|
||||
await fetch(`http://127.0.0.1:${port}/api/processing`, {
|
||||
// Notify worker to stop spinner (fire-and-forget)
|
||||
fetch(`http://127.0.0.1:${port}/api/processing`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ isProcessing: false })
|
||||
});
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
console.log(createHookResponse('Stop', true));
|
||||
|
||||
Reference in New Issue
Block a user