feat(FN-2449): merge fusion/fn-2449

This commit is contained in:
gsxdsm
2026-04-24 08:38:24 -07:00
parent f69fb4fe76
commit 4f633be65e
10 changed files with 471 additions and 64 deletions

View File

@@ -181,6 +181,110 @@ describe("ChatStore", () => {
});
});
describe("findLatestActiveSessionForTarget", () => {
it("returns newest exact model match for model-specific targets", async () => {
const olderModelMatch = createTestSession(store, {
agentId: "agent-lookup",
projectId: "proj-1",
modelProvider: "openai",
modelId: "gpt-4o",
});
await new Promise((r) => setTimeout(r, 5));
const newestModelMatch = createTestSession(store, {
agentId: "agent-lookup",
projectId: "proj-1",
modelProvider: "openai",
modelId: "gpt-4o",
});
createTestSession(store, {
agentId: "agent-lookup",
projectId: "proj-1",
modelProvider: "anthropic",
modelId: "claude-sonnet-4-5",
});
const found = store.findLatestActiveSessionForTarget({
projectId: "proj-1",
agentId: "agent-lookup",
modelProvider: "openai",
modelId: "gpt-4o",
});
expect(found?.id).toBe(newestModelMatch.id);
expect(found?.id).not.toBe(olderModelMatch.id);
});
it("prefers model-less session for agent-only targets", async () => {
const modelSpecific = createTestSession(store, {
agentId: "agent-lookup",
projectId: "proj-1",
modelProvider: "openai",
modelId: "gpt-4o",
});
await new Promise((r) => setTimeout(r, 5));
const modelLess = createTestSession(store, {
agentId: "agent-lookup",
projectId: "proj-1",
});
const found = store.findLatestActiveSessionForTarget({
projectId: "proj-1",
agentId: "agent-lookup",
});
expect(found?.id).toBe(modelLess.id);
expect(found?.id).not.toBe(modelSpecific.id);
});
it("falls back to newest agent session when no model-less session exists", async () => {
createTestSession(store, {
agentId: "agent-lookup",
projectId: "proj-1",
modelProvider: "openai",
modelId: "gpt-4o-mini",
});
await new Promise((r) => setTimeout(r, 5));
const newestModelSpecific = createTestSession(store, {
agentId: "agent-lookup",
projectId: "proj-1",
modelProvider: "openai",
modelId: "gpt-4o",
});
const found = store.findLatestActiveSessionForTarget({
projectId: "proj-1",
agentId: "agent-lookup",
});
expect(found?.id).toBe(newestModelSpecific.id);
});
it("returns undefined when there is no matching active session", () => {
createTestSession(store, {
agentId: "agent-lookup",
projectId: "proj-1",
});
const found = store.findLatestActiveSessionForTarget({
projectId: "proj-2",
agentId: "agent-lookup",
});
expect(found).toBeUndefined();
});
it("throws for inconsistent model-provider query pairs", () => {
expect(() =>
store.findLatestActiveSessionForTarget({
projectId: "proj-1",
agentId: "agent-lookup",
modelProvider: "openai",
}),
).toThrow("modelProvider and modelId must both be provided together, or neither");
});
});
describe("updateSession", () => {
it("updates title and bumps updatedAt", async () => {
const session = createTestSession(store);

View File

@@ -203,6 +203,74 @@ export class ChatStore extends EventEmitter<ChatStoreEvents> {
return (rows as unknown as ChatSessionRow[]).map((row) => this.rowToSession(row));
}
/**
* Find the newest active session for a specific quick-chat target.
*
* Matching semantics:
* - model target (`modelProvider` + `modelId`): exact agent+model match
* - agent target (no model): prefer model-less sessions, then newest agent session fallback
*/
findLatestActiveSessionForTarget(options: {
agentId: string;
projectId?: string;
modelProvider?: string;
modelId?: string;
}): ChatSession | undefined {
const normalizedAgentId = options.agentId.trim();
if (!normalizedAgentId) {
return undefined;
}
const normalizedProvider = options.modelProvider?.trim();
const normalizedModelId = options.modelId?.trim();
if ((normalizedProvider && !normalizedModelId) || (!normalizedProvider && normalizedModelId)) {
throw new Error("modelProvider and modelId must both be provided together, or neither");
}
const whereClauses: string[] = ["status = ?", "agentId = ?"];
const baseParams: string[] = ["active", normalizedAgentId];
if (options.projectId && options.projectId.trim()) {
whereClauses.push("projectId = ?");
baseParams.push(options.projectId.trim());
}
const baseWhereSql = whereClauses.join(" AND ");
if (normalizedProvider && normalizedModelId) {
const row = this.db.prepare(`
SELECT * FROM chat_sessions
WHERE ${baseWhereSql} AND modelProvider = ? AND modelId = ?
ORDER BY updatedAt DESC
LIMIT 1
`).get(...baseParams, normalizedProvider, normalizedModelId) as ChatSessionRow | undefined;
return row ? this.rowToSession(row) : undefined;
}
const modelLessRow = this.db.prepare(`
SELECT * FROM chat_sessions
WHERE ${baseWhereSql}
AND COALESCE(TRIM(modelProvider), '') = ''
AND COALESCE(TRIM(modelId), '') = ''
ORDER BY updatedAt DESC
LIMIT 1
`).get(...baseParams) as ChatSessionRow | undefined;
if (modelLessRow) {
return this.rowToSession(modelLessRow);
}
const fallbackRow = this.db.prepare(`
SELECT * FROM chat_sessions
WHERE ${baseWhereSql}
ORDER BY updatedAt DESC
LIMIT 1
`).get(...baseParams) as ChatSessionRow | undefined;
return fallbackRow ? this.rowToSession(fallbackRow) : undefined;
}
/**
* Update a chat session.
*