feat(FN-3927): persist and restore durable chat recovery state
- Persist in-flight chat generation snapshots in core chat store types and DB plumbing - Restore durable recovery state through dashboard chat manager and chat hooks on reload - Add coverage for chat store persistence and chat/useQuickChat recovery behavior - Document durable chat recovery architecture and dashboard behavior Fusion-Task-Id: FN-3927
This commit is contained in:
@@ -71,6 +71,7 @@ describe("ChatStore", () => {
|
||||
expect(session.modelId).toBeNull();
|
||||
expect(session.createdAt).toBeTruthy();
|
||||
expect(session.updatedAt).toBeTruthy();
|
||||
expect(session.inFlightGeneration).toBeNull();
|
||||
});
|
||||
|
||||
it("stores all provided fields", () => {
|
||||
@@ -345,6 +346,27 @@ describe("ChatStore", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("setInFlightGeneration", () => {
|
||||
it("persists and clears in-flight generation snapshot", () => {
|
||||
const session = createTestSession(store);
|
||||
|
||||
const updated = store.setInFlightGeneration(session.id, {
|
||||
status: "generating",
|
||||
streamingText: "partial",
|
||||
streamingThinking: "thinking",
|
||||
toolCalls: [{ toolName: "read", isError: false, status: "running" }],
|
||||
replayFromEventId: 12,
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
expect(updated?.inFlightGeneration?.streamingText).toBe("partial");
|
||||
expect(store.getSession(session.id)?.inFlightGeneration?.replayFromEventId).toBe(12);
|
||||
|
||||
store.setInFlightGeneration(session.id, null);
|
||||
expect(store.getSession(session.id)?.inFlightGeneration).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("archiveSession", () => {
|
||||
it("sets status to archived", () => {
|
||||
const session = createTestSession(store);
|
||||
|
||||
@@ -25,6 +25,7 @@ import type {
|
||||
ChatSessionUpdateInput,
|
||||
ChatMessagesFilter,
|
||||
ChatRoom,
|
||||
ChatInFlightGenerationState,
|
||||
ChatRoomCreateInput,
|
||||
ChatRoomMember,
|
||||
ChatRoomMessage,
|
||||
@@ -82,6 +83,7 @@ interface ChatSessionRow {
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
cliSessionFile: string | null;
|
||||
inFlightGeneration: string | null;
|
||||
}
|
||||
|
||||
/** Database row shape for chat_messages. */
|
||||
@@ -156,6 +158,7 @@ export class ChatStore extends EventEmitter<ChatStoreEvents> {
|
||||
createdAt: row.createdAt,
|
||||
updatedAt: row.updatedAt,
|
||||
cliSessionFile: row.cliSessionFile ?? null,
|
||||
inFlightGeneration: fromJson<ChatInFlightGenerationState>(row.inFlightGeneration) ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -248,11 +251,12 @@ export class ChatStore extends EventEmitter<ChatStoreEvents> {
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
cliSessionFile: null,
|
||||
inFlightGeneration: null,
|
||||
};
|
||||
|
||||
this.db.prepare(`
|
||||
INSERT INTO chat_sessions (id, agentId, title, status, projectId, modelProvider, modelId, createdAt, updatedAt)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO chat_sessions (id, agentId, title, status, projectId, modelProvider, modelId, createdAt, updatedAt, inFlightGeneration)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
session.id,
|
||||
session.agentId,
|
||||
@@ -263,6 +267,7 @@ export class ChatStore extends EventEmitter<ChatStoreEvents> {
|
||||
session.modelId,
|
||||
session.createdAt,
|
||||
session.updatedAt,
|
||||
null,
|
||||
);
|
||||
|
||||
this.db.bumpLastModified();
|
||||
@@ -459,6 +464,20 @@ export class ChatStore extends EventEmitter<ChatStoreEvents> {
|
||||
this.db.bumpLastModified();
|
||||
}
|
||||
|
||||
setInFlightGeneration(id: string, inFlightGeneration: ChatInFlightGenerationState | null): ChatSession | undefined {
|
||||
const existing = this.getSession(id);
|
||||
if (!existing) return undefined;
|
||||
|
||||
this.db
|
||||
.prepare("UPDATE chat_sessions SET inFlightGeneration = ? WHERE id = ?")
|
||||
.run(toJsonNullable(inFlightGeneration), id);
|
||||
|
||||
const updated = this.getSession(id)!;
|
||||
this.db.bumpLastModified();
|
||||
this.emit("chat:session:updated", updated);
|
||||
return updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a chat session and all its messages.
|
||||
* Messages are cascade-deleted via foreign key constraint.
|
||||
|
||||
@@ -19,6 +19,23 @@ export type ChatMessageRole = "user" | "assistant" | "system";
|
||||
* A chat session between a user and an agent.
|
||||
* Contains metadata about the conversation and references to the model used.
|
||||
*/
|
||||
export interface ChatInFlightToolCall {
|
||||
toolName: string;
|
||||
args?: Record<string, unknown>;
|
||||
isError: boolean;
|
||||
result?: unknown;
|
||||
status: "running" | "completed";
|
||||
}
|
||||
|
||||
export interface ChatInFlightGenerationState {
|
||||
status: "generating";
|
||||
streamingText: string;
|
||||
streamingThinking: string;
|
||||
toolCalls: ChatInFlightToolCall[];
|
||||
replayFromEventId: number;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface ChatSession {
|
||||
id: string;
|
||||
/** Session routing kind; legacy sessions default to direct */
|
||||
@@ -51,6 +68,8 @@ export interface ChatSession {
|
||||
* for sessions that have never produced an assistant reply.
|
||||
*/
|
||||
cliSessionFile: string | null;
|
||||
/** Durable in-flight assistant snapshot used to recover streaming UI after refresh. */
|
||||
inFlightGeneration: ChatInFlightGenerationState | null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -912,6 +912,7 @@ export const MIGRATION_ONLY_TABLE_SCHEMAS: Record<string, Record<string, string>
|
||||
createdAt: "TEXT NOT NULL",
|
||||
updatedAt: "TEXT NOT NULL",
|
||||
cliSessionFile: "TEXT",
|
||||
inFlightGeneration: "TEXT",
|
||||
},
|
||||
chat_messages: {
|
||||
id: "TEXT PRIMARY KEY",
|
||||
@@ -1828,7 +1829,8 @@ export class Database {
|
||||
modelProvider TEXT,
|
||||
modelId TEXT,
|
||||
createdAt TEXT NOT NULL,
|
||||
updatedAt TEXT NOT NULL
|
||||
updatedAt TEXT NOT NULL,
|
||||
inFlightGeneration TEXT
|
||||
)
|
||||
`);
|
||||
this.db.exec(`CREATE INDEX IF NOT EXISTS idxChatSessionsAgentId ON chat_sessions(agentId)`);
|
||||
|
||||
@@ -890,6 +890,8 @@ export type {
|
||||
export type {
|
||||
ChatSessionStatus,
|
||||
ChatMessageRole,
|
||||
ChatInFlightToolCall,
|
||||
ChatInFlightGenerationState,
|
||||
ChatSession,
|
||||
ChatSessionSummary,
|
||||
EnrichedChatSession,
|
||||
|
||||
Reference in New Issue
Block a user