feat: Implement Worker Service with session management and SDK integration

- Added WorkerService to handle long-running HTTP service with session management.
- Implemented endpoints for initializing, observing, finalizing, checking status, and deleting sessions.
- Integrated with Claude SDK for processing observations and generating responses.
- Added port allocator utility to dynamically find available ports for the service.
- Configured TypeScript settings for the project.
This commit is contained in:
Alex Newman
2025-10-17 15:59:36 -04:00
parent d6462919cb
commit 372854948c
57 changed files with 7055 additions and 6649 deletions
+136 -27
View File
@@ -1,4 +1,4 @@
import { Database } from 'bun:sqlite';
import Database from 'better-sqlite3';
import { DATA_DIR, DB_PATH, ensureDir } from '../../shared/paths.js';
/**
@@ -11,12 +11,33 @@ export class HooksDatabase {
constructor() {
ensureDir(DATA_DIR);
this.db = new Database(DB_PATH, { create: true, readwrite: true });
this.db = new Database(DB_PATH);
// Ensure optimized settings
this.db.run('PRAGMA journal_mode = WAL');
this.db.run('PRAGMA synchronous = NORMAL');
this.db.run('PRAGMA foreign_keys = ON');
this.db.pragma('journal_mode = WAL');
this.db.pragma('synchronous = NORMAL');
this.db.pragma('foreign_keys = ON');
// Run migration to add worker_port column if it doesn't exist
this.ensureWorkerPortColumn();
}
/**
* Ensure worker_port column exists (migration)
*/
private ensureWorkerPortColumn(): void {
try {
// Check if column exists
const tableInfo = this.db.pragma('table_info(sdk_sessions)');
const hasWorkerPort = (tableInfo as any[]).some((col: any) => col.name === 'worker_port');
if (!hasWorkerPort) {
this.db.exec('ALTER TABLE sdk_sessions ADD COLUMN worker_port INTEGER');
console.error('[HooksDatabase] Added worker_port column to sdk_sessions table');
}
} catch (error: any) {
console.error('[HooksDatabase] Migration error:', error.message);
}
}
/**
@@ -33,7 +54,7 @@ export class HooksDatabase {
notes: string | null;
created_at: string;
}> {
const query = this.db.query(`
const stmt = this.db.prepare(`
SELECT
request, investigated, learned, completed, next_steps,
files_read, files_edited, notes, created_at
@@ -43,7 +64,26 @@ export class HooksDatabase {
LIMIT ?
`);
return query.all(project, limit) as any[];
return stmt.all(project, limit) as any[];
}
/**
* Get recent observations for a project
*/
getRecentObservations(project: string, limit: number = 20): Array<{
type: string;
text: string;
created_at: string;
}> {
const stmt = this.db.prepare(`
SELECT type, text, created_at
FROM observations
WHERE project = ?
ORDER BY created_at_epoch DESC
LIMIT ?
`);
return stmt.all(project, limit) as any[];
}
/**
@@ -53,15 +93,43 @@ export class HooksDatabase {
id: number;
sdk_session_id: string | null;
project: string;
worker_port: number | null;
} | null {
const query = this.db.query(`
SELECT id, sdk_session_id, project
const stmt = this.db.prepare(`
SELECT id, sdk_session_id, project, worker_port
FROM sdk_sessions
WHERE claude_session_id = ? AND status = 'active'
LIMIT 1
`);
return query.get(claudeSessionId) as any || null;
return stmt.get(claudeSessionId) as any || null;
}
/**
* Find any SDK session for a Claude session (active, failed, or completed)
*/
findAnySDKSession(claudeSessionId: string): { id: number } | null {
const stmt = this.db.prepare(`
SELECT id
FROM sdk_sessions
WHERE claude_session_id = ?
LIMIT 1
`);
return stmt.get(claudeSessionId) as any || null;
}
/**
* Reactivate an existing session
*/
reactivateSession(id: number, userPrompt: string): void {
const stmt = this.db.prepare(`
UPDATE sdk_sessions
SET status = 'active', user_prompt = ?, worker_port = NULL
WHERE id = ?
`);
stmt.run(userPrompt, id);
}
/**
@@ -71,31 +139,55 @@ export class HooksDatabase {
const now = new Date();
const nowEpoch = now.getTime();
const query = this.db.query(`
const stmt = this.db.prepare(`
INSERT INTO sdk_sessions
(claude_session_id, project, user_prompt, started_at, started_at_epoch, status)
VALUES (?, ?, ?, ?, ?, 'active')
`);
query.run(claudeSessionId, project, userPrompt, now.toISOString(), nowEpoch);
// Get the last inserted ID
const lastIdQuery = this.db.query('SELECT last_insert_rowid() as id');
const result = lastIdQuery.get() as { id: number };
return result.id;
const result = stmt.run(claudeSessionId, project, userPrompt, now.toISOString(), nowEpoch);
return result.lastInsertRowid as number;
}
/**
* Update SDK session ID (captured from init message)
*/
updateSDKSessionId(id: number, sdkSessionId: string): void {
const query = this.db.query(`
const stmt = this.db.prepare(`
UPDATE sdk_sessions
SET sdk_session_id = ?
WHERE id = ?
`);
query.run(sdkSessionId, id);
stmt.run(sdkSessionId, id);
}
/**
* Set worker port for a session
*/
setWorkerPort(id: number, port: number): void {
const stmt = this.db.prepare(`
UPDATE sdk_sessions
SET worker_port = ?
WHERE id = ?
`);
stmt.run(port, id);
}
/**
* Get worker port for a session
*/
getWorkerPort(id: number): number | null {
const stmt = this.db.prepare(`
SELECT worker_port
FROM sdk_sessions
WHERE id = ?
LIMIT 1
`);
const result = stmt.get(id) as { worker_port: number | null } | undefined;
return result?.worker_port || null;
}
/**
@@ -110,13 +202,13 @@ export class HooksDatabase {
const now = new Date();
const nowEpoch = now.getTime();
const query = this.db.query(`
const stmt = this.db.prepare(`
INSERT INTO observations
(sdk_session_id, project, text, type, created_at, created_at_epoch)
VALUES (?, ?, ?, ?, ?, ?)
`);
query.run(sdkSessionId, project, text, type, now.toISOString(), nowEpoch);
stmt.run(sdkSessionId, project, text, type, now.toISOString(), nowEpoch);
}
/**
@@ -139,14 +231,14 @@ export class HooksDatabase {
const now = new Date();
const nowEpoch = now.getTime();
const query = this.db.query(`
const stmt = this.db.prepare(`
INSERT INTO session_summaries
(sdk_session_id, project, request, investigated, learned, completed,
next_steps, files_read, files_edited, notes, created_at, created_at_epoch)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
query.run(
stmt.run(
sdkSessionId,
project,
summary.request || null,
@@ -169,13 +261,13 @@ export class HooksDatabase {
const now = new Date();
const nowEpoch = now.getTime();
const query = this.db.query(`
const stmt = this.db.prepare(`
UPDATE sdk_sessions
SET status = 'completed', completed_at = ?, completed_at_epoch = ?
WHERE id = ?
`);
query.run(now.toISOString(), nowEpoch, id);
stmt.run(now.toISOString(), nowEpoch, id);
}
/**
@@ -185,13 +277,30 @@ export class HooksDatabase {
const now = new Date();
const nowEpoch = now.getTime();
const query = this.db.query(`
const stmt = this.db.prepare(`
UPDATE sdk_sessions
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
WHERE id = ?
`);
query.run(now.toISOString(), nowEpoch, id);
stmt.run(now.toISOString(), nowEpoch, id);
}
/**
* Clean up orphaned active sessions (called on worker startup)
*/
cleanupOrphanedSessions(): number {
const now = new Date();
const nowEpoch = now.getTime();
const stmt = this.db.prepare(`
UPDATE sdk_sessions
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
WHERE status = 'active'
`);
const result = stmt.run(now.toISOString(), nowEpoch);
return result.changes;
}
/**