feat: Enhance session management with prompt tracking

- Added prompt_number to observations and session summaries for better tracking.
- Implemented prompt counter in SDK sessions to manage user prompts effectively.
- Updated database schema to include prompt tracking columns and removed unique constraints on session summaries.
- Modified hooks to utilize prompt_number in observations and summaries.
- Changed worker service to handle summarize requests instead of finalize, keeping the SDK agent active.
- Improved logging for better debugging and tracking of prompt numbers across sessions.
This commit is contained in:
Alex Newman
2025-10-17 16:23:11 -04:00
parent 372854948c
commit d4a71c994d
13 changed files with 480 additions and 265 deletions
+173 -12
View File
@@ -18,8 +18,10 @@ export class HooksDatabase {
this.db.pragma('synchronous = NORMAL');
this.db.pragma('foreign_keys = ON');
// Run migration to add worker_port column if it doesn't exist
// Run migrations
this.ensureWorkerPortColumn();
this.ensurePromptTrackingColumns();
this.removeSessionSummariesUniqueConstraint();
}
/**
@@ -40,6 +42,125 @@ export class HooksDatabase {
}
}
/**
* Ensure prompt tracking columns exist (migration 006)
*/
private ensurePromptTrackingColumns(): void {
try {
// Check sdk_sessions for prompt_counter
const sessionsInfo = this.db.pragma('table_info(sdk_sessions)');
const hasPromptCounter = (sessionsInfo as any[]).some((col: any) => col.name === 'prompt_counter');
if (!hasPromptCounter) {
this.db.exec('ALTER TABLE sdk_sessions ADD COLUMN prompt_counter INTEGER DEFAULT 0');
console.error('[HooksDatabase] Added prompt_counter column to sdk_sessions table');
}
// Check observations for prompt_number
const observationsInfo = this.db.pragma('table_info(observations)');
const obsHasPromptNumber = (observationsInfo as any[]).some((col: any) => col.name === 'prompt_number');
if (!obsHasPromptNumber) {
this.db.exec('ALTER TABLE observations ADD COLUMN prompt_number INTEGER');
console.error('[HooksDatabase] Added prompt_number column to observations table');
}
// Check session_summaries for prompt_number
const summariesInfo = this.db.pragma('table_info(session_summaries)');
const sumHasPromptNumber = (summariesInfo as any[]).some((col: any) => col.name === 'prompt_number');
if (!sumHasPromptNumber) {
this.db.exec('ALTER TABLE session_summaries ADD COLUMN prompt_number INTEGER');
console.error('[HooksDatabase] Added prompt_number column to session_summaries table');
}
// Remove UNIQUE constraint on session_summaries.sdk_session_id
// SQLite doesn't support dropping constraints, so we need to check if it exists first
const summariesIndexes = this.db.pragma('index_list(session_summaries)');
const hasUniqueConstraint = (summariesIndexes as any[]).some((idx: any) => idx.unique === 1);
} catch (error: any) {
console.error('[HooksDatabase] Prompt tracking migration error:', error.message);
}
}
/**
* Remove UNIQUE constraint from session_summaries.sdk_session_id (migration 007)
*/
private removeSessionSummariesUniqueConstraint(): void {
try {
// Check if UNIQUE constraint exists
const summariesIndexes = this.db.pragma('index_list(session_summaries)');
const hasUniqueConstraint = (summariesIndexes as any[]).some((idx: any) => idx.unique === 1);
if (!hasUniqueConstraint) {
// Already migrated
return;
}
console.error('[HooksDatabase] Removing UNIQUE constraint from session_summaries.sdk_session_id...');
// Begin transaction
this.db.exec('BEGIN TRANSACTION');
try {
// Create new table without UNIQUE constraint
this.db.exec(`
CREATE TABLE session_summaries_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sdk_session_id TEXT NOT NULL,
project TEXT NOT NULL,
request TEXT,
investigated TEXT,
learned TEXT,
completed TEXT,
next_steps TEXT,
files_read TEXT,
files_edited TEXT,
notes TEXT,
prompt_number INTEGER,
created_at TEXT NOT NULL,
created_at_epoch INTEGER NOT NULL,
FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id) ON DELETE CASCADE
)
`);
// Copy data from old table
this.db.exec(`
INSERT INTO session_summaries_new
SELECT id, sdk_session_id, project, request, investigated, learned,
completed, next_steps, files_read, files_edited, notes,
prompt_number, created_at, created_at_epoch
FROM session_summaries
`);
// Drop old table
this.db.exec('DROP TABLE session_summaries');
// Rename new table
this.db.exec('ALTER TABLE session_summaries_new RENAME TO session_summaries');
// Recreate indexes
this.db.exec(`
CREATE INDEX idx_session_summaries_sdk_session ON session_summaries(sdk_session_id);
CREATE INDEX idx_session_summaries_project ON session_summaries(project);
CREATE INDEX idx_session_summaries_created ON session_summaries(created_at_epoch DESC);
`);
// Commit transaction
this.db.exec('COMMIT');
console.error('[HooksDatabase] Successfully removed UNIQUE constraint from session_summaries.sdk_session_id');
} catch (error: any) {
// Rollback on error
this.db.exec('ROLLBACK');
throw error;
}
} catch (error: any) {
console.error('[HooksDatabase] Migration error (remove UNIQUE constraint):', error.message);
}
}
/**
* Get recent session summaries for a project
*/
@@ -52,12 +173,13 @@ export class HooksDatabase {
files_read: string | null;
files_edited: string | null;
notes: string | null;
prompt_number: number | null;
created_at: string;
}> {
const stmt = this.db.prepare(`
SELECT
request, investigated, learned, completed, next_steps,
files_read, files_edited, notes, created_at
files_read, files_edited, notes, prompt_number, created_at
FROM session_summaries
WHERE project = ?
ORDER BY created_at_epoch DESC
@@ -73,10 +195,11 @@ export class HooksDatabase {
getRecentObservations(project: string, limit: number = 20): Array<{
type: string;
text: string;
prompt_number: number | null;
created_at: string;
}> {
const stmt = this.db.prepare(`
SELECT type, text, created_at
SELECT type, text, prompt_number, created_at
FROM observations
WHERE project = ?
ORDER BY created_at_epoch DESC
@@ -132,6 +255,36 @@ export class HooksDatabase {
stmt.run(userPrompt, id);
}
/**
* Increment prompt counter and return new value
*/
incrementPromptCounter(id: number): number {
const stmt = this.db.prepare(`
UPDATE sdk_sessions
SET prompt_counter = COALESCE(prompt_counter, 0) + 1
WHERE id = ?
`);
stmt.run(id);
const result = this.db.prepare(`
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
`).get(id) as { prompt_counter: number } | undefined;
return result?.prompt_counter || 1;
}
/**
* Get current prompt counter for a session
*/
getPromptCounter(id: number): number {
const result = this.db.prepare(`
SELECT prompt_counter FROM sdk_sessions WHERE id = ?
`).get(id) as { prompt_counter: number | null } | undefined;
return result?.prompt_counter || 0;
}
/**
* Create a new SDK session
*/
@@ -151,15 +304,20 @@ export class HooksDatabase {
/**
* Update SDK session ID (captured from init message)
* Only updates if current sdk_session_id is NULL to avoid breaking foreign keys
*/
updateSDKSessionId(id: number, sdkSessionId: string): void {
const stmt = this.db.prepare(`
UPDATE sdk_sessions
SET sdk_session_id = ?
WHERE id = ?
WHERE id = ? AND sdk_session_id IS NULL
`);
stmt.run(sdkSessionId, id);
const result = stmt.run(sdkSessionId, id);
if (result.changes === 0) {
console.error(`[HooksDatabase] Skipped updating sdk_session_id for session ${id} - already set (prevents FOREIGN KEY constraint violation)`);
}
}
/**
@@ -197,18 +355,19 @@ export class HooksDatabase {
sdkSessionId: string,
project: string,
type: string,
text: string
text: string,
promptNumber?: number
): void {
const now = new Date();
const nowEpoch = now.getTime();
const stmt = this.db.prepare(`
INSERT INTO observations
(sdk_session_id, project, text, type, created_at, created_at_epoch)
VALUES (?, ?, ?, ?, ?, ?)
(sdk_session_id, project, text, type, prompt_number, created_at, created_at_epoch)
VALUES (?, ?, ?, ?, ?, ?, ?)
`);
stmt.run(sdkSessionId, project, text, type, now.toISOString(), nowEpoch);
stmt.run(sdkSessionId, project, text, type, promptNumber || null, now.toISOString(), nowEpoch);
}
/**
@@ -226,7 +385,8 @@ export class HooksDatabase {
files_read?: string;
files_edited?: string;
notes?: string;
}
},
promptNumber?: number
): void {
const now = new Date();
const nowEpoch = now.getTime();
@@ -234,8 +394,8 @@ export class HooksDatabase {
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
next_steps, files_read, files_edited, notes, prompt_number, created_at, created_at_epoch)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
stmt.run(
@@ -249,6 +409,7 @@ export class HooksDatabase {
summary.files_read || null,
summary.files_edited || null,
summary.notes || null,
promptNumber || null,
now.toISOString(),
nowEpoch
);