feat: Implement Phase 1 of SDK agent architecture with hook integration
- Added CLI commands for context, new session, save observation, and summary. - Created HooksDatabase for managing SDK sessions and observations. - Implemented migration 004 to add new tables: sdk_sessions, observation_queue, observations, and session_summaries. - Developed hook functions for context display, session initialization, observation queuing, and session finalization. - Added comprehensive tests for database schema and hook functionality. - Documented Phase 1 implementation in PHASE1-COMPLETE.md.
This commit is contained in:
@@ -0,0 +1,264 @@
|
||||
import { Database } from 'bun:sqlite';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { PathDiscovery } from '../path-discovery.js';
|
||||
|
||||
/**
|
||||
* Lightweight database interface for hooks
|
||||
* Provides simple, synchronous operations for hook commands
|
||||
* No complex logic - just basic CRUD operations
|
||||
*/
|
||||
export class HooksDatabase {
|
||||
private db: Database;
|
||||
|
||||
constructor() {
|
||||
const dataDir = PathDiscovery.getInstance().getDataDirectory();
|
||||
fs.mkdirSync(dataDir, { recursive: true });
|
||||
|
||||
const dbPath = path.join(dataDir, 'claude-mem.db');
|
||||
this.db = new Database(dbPath, { create: true, readwrite: true });
|
||||
|
||||
// Ensure optimized settings
|
||||
this.db.run('PRAGMA journal_mode = WAL');
|
||||
this.db.run('PRAGMA synchronous = NORMAL');
|
||||
this.db.run('PRAGMA foreign_keys = ON');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent session summaries for a project
|
||||
*/
|
||||
getRecentSummaries(project: string, limit: number = 10): Array<{
|
||||
request: string | null;
|
||||
investigated: string | null;
|
||||
learned: string | null;
|
||||
completed: string | null;
|
||||
next_steps: string | null;
|
||||
files_read: string | null;
|
||||
files_edited: string | null;
|
||||
notes: string | null;
|
||||
created_at: string;
|
||||
}> {
|
||||
const query = this.db.query(`
|
||||
SELECT
|
||||
request, investigated, learned, completed, next_steps,
|
||||
files_read, files_edited, notes, created_at
|
||||
FROM session_summaries
|
||||
WHERE project = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`);
|
||||
|
||||
return query.all(project, limit) as any[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Find active SDK session for a Claude session
|
||||
*/
|
||||
findActiveSDKSession(claudeSessionId: string): {
|
||||
id: number;
|
||||
sdk_session_id: string | null;
|
||||
project: string;
|
||||
} | null {
|
||||
const query = this.db.query(`
|
||||
SELECT id, sdk_session_id, project
|
||||
FROM sdk_sessions
|
||||
WHERE claude_session_id = ? AND status = 'active'
|
||||
LIMIT 1
|
||||
`);
|
||||
|
||||
return query.get(claudeSessionId) as any || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new SDK session
|
||||
*/
|
||||
createSDKSession(claudeSessionId: string, project: string, userPrompt: string): number {
|
||||
const now = new Date();
|
||||
const nowEpoch = now.getTime();
|
||||
|
||||
const query = this.db.query(`
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update SDK session ID (captured from init message)
|
||||
*/
|
||||
updateSDKSessionId(id: number, sdkSessionId: string): void {
|
||||
const query = this.db.query(`
|
||||
UPDATE sdk_sessions
|
||||
SET sdk_session_id = ?
|
||||
WHERE id = ?
|
||||
`);
|
||||
|
||||
query.run(sdkSessionId, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue an observation for SDK processing
|
||||
*/
|
||||
queueObservation(
|
||||
sdkSessionId: string,
|
||||
toolName: string,
|
||||
toolInput: string,
|
||||
toolOutput: string
|
||||
): void {
|
||||
const nowEpoch = Date.now();
|
||||
|
||||
const query = this.db.query(`
|
||||
INSERT INTO observation_queue
|
||||
(sdk_session_id, tool_name, tool_input, tool_output, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
query.run(sdkSessionId, toolName, toolInput, toolOutput, nowEpoch);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pending observations for SDK processing
|
||||
*/
|
||||
getPendingObservations(sdkSessionId: string, limit: number = 10): Array<{
|
||||
id: number;
|
||||
tool_name: string;
|
||||
tool_input: string;
|
||||
tool_output: string;
|
||||
created_at_epoch: number;
|
||||
}> {
|
||||
const query = this.db.query(`
|
||||
SELECT id, tool_name, tool_input, tool_output, created_at_epoch
|
||||
FROM observation_queue
|
||||
WHERE sdk_session_id = ? AND processed_at_epoch IS NULL
|
||||
ORDER BY created_at_epoch ASC
|
||||
LIMIT ?
|
||||
`);
|
||||
|
||||
return query.all(sdkSessionId, limit) as any[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark observation as processed
|
||||
*/
|
||||
markObservationProcessed(id: number): void {
|
||||
const nowEpoch = Date.now();
|
||||
|
||||
const query = this.db.query(`
|
||||
UPDATE observation_queue
|
||||
SET processed_at_epoch = ?
|
||||
WHERE id = ?
|
||||
`);
|
||||
|
||||
query.run(nowEpoch, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store an observation (from SDK parsing)
|
||||
*/
|
||||
storeObservation(
|
||||
sdkSessionId: string,
|
||||
project: string,
|
||||
type: string,
|
||||
text: string
|
||||
): void {
|
||||
const now = new Date();
|
||||
const nowEpoch = now.getTime();
|
||||
|
||||
const query = this.db.query(`
|
||||
INSERT INTO observations
|
||||
(sdk_session_id, project, text, type, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
query.run(sdkSessionId, project, text, type, now.toISOString(), nowEpoch);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a session summary (from SDK parsing)
|
||||
*/
|
||||
storeSummary(
|
||||
sdkSessionId: string,
|
||||
project: string,
|
||||
summary: {
|
||||
request?: string;
|
||||
investigated?: string;
|
||||
learned?: string;
|
||||
completed?: string;
|
||||
next_steps?: string;
|
||||
files_read?: string;
|
||||
files_edited?: string;
|
||||
notes?: string;
|
||||
}
|
||||
): void {
|
||||
const now = new Date();
|
||||
const nowEpoch = now.getTime();
|
||||
|
||||
const query = this.db.query(`
|
||||
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(
|
||||
sdkSessionId,
|
||||
project,
|
||||
summary.request || null,
|
||||
summary.investigated || null,
|
||||
summary.learned || null,
|
||||
summary.completed || null,
|
||||
summary.next_steps || null,
|
||||
summary.files_read || null,
|
||||
summary.files_edited || null,
|
||||
summary.notes || null,
|
||||
now.toISOString(),
|
||||
nowEpoch
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark SDK session as completed
|
||||
*/
|
||||
markSessionCompleted(id: number): void {
|
||||
const now = new Date();
|
||||
const nowEpoch = now.getTime();
|
||||
|
||||
const query = this.db.query(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'completed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE id = ?
|
||||
`);
|
||||
|
||||
query.run(now.toISOString(), nowEpoch, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark SDK session as failed
|
||||
*/
|
||||
markSessionFailed(id: number): void {
|
||||
const now = new Date();
|
||||
const nowEpoch = now.getTime();
|
||||
|
||||
const query = this.db.query(`
|
||||
UPDATE sdk_sessions
|
||||
SET status = 'failed', completed_at = ?, completed_at_epoch = ?
|
||||
WHERE id = ?
|
||||
`);
|
||||
|
||||
query.run(now.toISOString(), nowEpoch, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the database connection
|
||||
*/
|
||||
close(): void {
|
||||
this.db.close();
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,9 @@ export { DiagnosticsStore } from './DiagnosticsStore.js';
|
||||
export { TranscriptEventStore } from './TranscriptEventStore.js';
|
||||
export { StreamingSessionStore } from './StreamingSessionStore.js';
|
||||
|
||||
// Export hooks database
|
||||
export { HooksDatabase } from './HooksDatabase.js';
|
||||
|
||||
// Export types
|
||||
export * from './types.js';
|
||||
|
||||
|
||||
@@ -202,11 +202,115 @@ export const migration003: Migration = {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Migration 004 - Add SDK agent architecture tables
|
||||
* Implements the refactor plan for hook-driven memory with SDK agent synthesis
|
||||
*/
|
||||
export const migration004: Migration = {
|
||||
version: 4,
|
||||
up: (db: Database) => {
|
||||
// SDK sessions table - tracks SDK streaming sessions
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS sdk_sessions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
claude_session_id TEXT UNIQUE NOT NULL,
|
||||
sdk_session_id TEXT UNIQUE,
|
||||
project TEXT NOT NULL,
|
||||
user_prompt TEXT,
|
||||
started_at TEXT NOT NULL,
|
||||
started_at_epoch INTEGER NOT NULL,
|
||||
completed_at TEXT,
|
||||
completed_at_epoch INTEGER,
|
||||
status TEXT CHECK(status IN ('active', 'completed', 'failed')) NOT NULL DEFAULT 'active'
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_claude_id ON sdk_sessions(claude_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_sdk_id ON sdk_sessions(sdk_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_project ON sdk_sessions(project);
|
||||
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_status ON sdk_sessions(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_started ON sdk_sessions(started_at_epoch DESC);
|
||||
`);
|
||||
|
||||
// Observation queue table - tracks pending observations for SDK processing
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS observation_queue (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sdk_session_id TEXT NOT NULL,
|
||||
tool_name TEXT NOT NULL,
|
||||
tool_input TEXT NOT NULL,
|
||||
tool_output TEXT NOT NULL,
|
||||
created_at_epoch INTEGER NOT NULL,
|
||||
processed_at_epoch INTEGER,
|
||||
FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_observation_queue_sdk_session ON observation_queue(sdk_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_observation_queue_processed ON observation_queue(processed_at_epoch);
|
||||
CREATE INDEX IF NOT EXISTS idx_observation_queue_pending ON observation_queue(sdk_session_id, processed_at_epoch);
|
||||
`);
|
||||
|
||||
// Observations table - stores extracted observations (what SDK decides is important)
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS observations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sdk_session_id TEXT NOT NULL,
|
||||
project TEXT NOT NULL,
|
||||
text TEXT NOT NULL,
|
||||
type TEXT NOT NULL CHECK(type IN ('decision', 'bugfix', 'feature', 'refactor', 'discovery')),
|
||||
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
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_observations_sdk_session ON observations(sdk_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_observations_project ON observations(project);
|
||||
CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type);
|
||||
CREATE INDEX IF NOT EXISTS idx_observations_created ON observations(created_at_epoch DESC);
|
||||
`);
|
||||
|
||||
// Session summaries table - stores structured session summaries
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS session_summaries (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sdk_session_id TEXT UNIQUE 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,
|
||||
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
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_session_summaries_sdk_session ON session_summaries(sdk_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_session_summaries_project ON session_summaries(project);
|
||||
CREATE INDEX IF NOT EXISTS idx_session_summaries_created ON session_summaries(created_at_epoch DESC);
|
||||
`);
|
||||
|
||||
console.log('✅ Created SDK agent architecture tables');
|
||||
},
|
||||
|
||||
down: (db: Database) => {
|
||||
db.run(`
|
||||
DROP TABLE IF EXISTS session_summaries;
|
||||
DROP TABLE IF EXISTS observations;
|
||||
DROP TABLE IF EXISTS observation_queue;
|
||||
DROP TABLE IF EXISTS sdk_sessions;
|
||||
`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* All migrations in order
|
||||
*/
|
||||
export const migrations: Migration[] = [
|
||||
migration001,
|
||||
migration002,
|
||||
migration003
|
||||
migration003,
|
||||
migration004
|
||||
];
|
||||
Reference in New Issue
Block a user