feat: add background AI sessions with persistent storage and SSE streaming
Introduces ai_sessions table (schema v9), AiSessionStore, and background session support for mission interviews, planning, and subtask breakdown. Adds BackgroundTasksIndicator component, SSE-based progress streaming, and dashboard API routes for session management. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -69,6 +69,7 @@ describe("Database", () => {
|
||||
expect(tableNames).toContain("milestones");
|
||||
expect(tableNames).toContain("slices");
|
||||
expect(tableNames).toContain("mission_features");
|
||||
expect(tableNames).toContain("ai_sessions");
|
||||
});
|
||||
|
||||
it("creates all expected indexes", () => {
|
||||
@@ -83,10 +84,12 @@ describe("Database", () => {
|
||||
expect(indexNames).toContain("idxArchivedTasksId");
|
||||
expect(indexNames).toContain("idxAgentHeartbeatsAgentId");
|
||||
expect(indexNames).toContain("idxAgentHeartbeatsRunId");
|
||||
expect(indexNames).toContain("idxAiSessionsStatus");
|
||||
expect(indexNames).toContain("idxAiSessionsType");
|
||||
});
|
||||
|
||||
it("seeds schema version", () => {
|
||||
expect(db.getSchemaVersion()).toBe(8);
|
||||
expect(db.getSchemaVersion()).toBe(9);
|
||||
});
|
||||
|
||||
it("seeds lastModified", () => {
|
||||
@@ -109,7 +112,7 @@ describe("Database", () => {
|
||||
|
||||
it("is idempotent - calling init() twice does not fail", () => {
|
||||
expect(() => db.init()).not.toThrow();
|
||||
expect(db.getSchemaVersion()).toBe(8);
|
||||
expect(db.getSchemaVersion()).toBe(9);
|
||||
});
|
||||
|
||||
it("does not overwrite existing config on re-init", () => {
|
||||
@@ -716,7 +719,7 @@ describe("schema migrations", () => {
|
||||
db.init();
|
||||
|
||||
// Verify version bumped to 5 (includes v1→v2, v2→v3, v3→v4, and v4→v5 migrations)
|
||||
expect(db.getSchemaVersion()).toBe(8);
|
||||
expect(db.getSchemaVersion()).toBe(9);
|
||||
|
||||
// Verify new columns exist and existing data is intact
|
||||
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
|
||||
@@ -741,11 +744,11 @@ describe("schema migrations", () => {
|
||||
const db = new Database(kbDir);
|
||||
db.init();
|
||||
|
||||
expect(db.getSchemaVersion()).toBe(8);
|
||||
expect(db.getSchemaVersion()).toBe(9);
|
||||
|
||||
// Re-init should not fail
|
||||
db.init();
|
||||
expect(db.getSchemaVersion()).toBe(8);
|
||||
expect(db.getSchemaVersion()).toBe(9);
|
||||
|
||||
db.close();
|
||||
});
|
||||
@@ -840,7 +843,7 @@ describe("schema migrations", () => {
|
||||
db.init();
|
||||
|
||||
// Verify version bumped to 5
|
||||
expect(db.getSchemaVersion()).toBe(8);
|
||||
expect(db.getSchemaVersion()).toBe(9);
|
||||
|
||||
// Verify new columns exist and existing data is intact
|
||||
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
|
||||
@@ -1050,7 +1053,7 @@ describe("createDatabase factory", () => {
|
||||
const db = createDatabase(kbDir);
|
||||
db.init();
|
||||
|
||||
expect(db.getSchemaVersion()).toBe(8);
|
||||
expect(db.getSchemaVersion()).toBe(9);
|
||||
expect(db.getLastModified()).toBeGreaterThan(0);
|
||||
|
||||
db.close();
|
||||
|
||||
@@ -59,7 +59,7 @@ export function fromJson<T>(json: string | null | undefined): T | undefined {
|
||||
|
||||
// ── Schema Definition ────────────────────────────────────────────────
|
||||
|
||||
const SCHEMA_VERSION = 8;
|
||||
const SCHEMA_VERSION = 9;
|
||||
|
||||
function normalizeTaskComments(
|
||||
steeringComments: SteeringComment[] | undefined,
|
||||
@@ -424,8 +424,32 @@ export class Database {
|
||||
});
|
||||
}
|
||||
|
||||
if (version < 9) {
|
||||
this.applyMigration(9, () => {
|
||||
this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS ai_sessions (
|
||||
id TEXT PRIMARY KEY,
|
||||
type TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
inputPayload TEXT NOT NULL,
|
||||
conversationHistory TEXT DEFAULT '[]',
|
||||
currentQuestion TEXT,
|
||||
result TEXT,
|
||||
thinkingOutput TEXT DEFAULT '',
|
||||
error TEXT,
|
||||
projectId TEXT,
|
||||
createdAt TEXT NOT NULL,
|
||||
updatedAt TEXT NOT NULL
|
||||
)
|
||||
`);
|
||||
this.db.exec(`CREATE INDEX IF NOT EXISTS idxAiSessionsStatus ON ai_sessions(status)`);
|
||||
this.db.exec(`CREATE INDEX IF NOT EXISTS idxAiSessionsType ON ai_sessions(type)`);
|
||||
});
|
||||
}
|
||||
|
||||
// Future migrations go here:
|
||||
// if (version < 9) { this.applyMigration(9, () => { ... }); }
|
||||
// if (version < 10) { this.applyMigration(10, () => { ... }); }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2753,6 +2753,11 @@ ${stepsSection}`;
|
||||
return this.tasksDir;
|
||||
}
|
||||
|
||||
/** Expose the shared Database instance for co-located stores (e.g. AiSessionStore). */
|
||||
getDatabase(): Database {
|
||||
return this.db;
|
||||
}
|
||||
|
||||
private generateSpecifiedPrompt(task: Task): string {
|
||||
const deps =
|
||||
task.dependencies.length > 0
|
||||
|
||||
Reference in New Issue
Block a user