diff --git a/packages/server/src/api-types.ts b/packages/server/src/api-types.ts index adcc102..4c9ceae 100644 --- a/packages/server/src/api-types.ts +++ b/packages/server/src/api-types.ts @@ -192,6 +192,16 @@ export interface InstanceData { agentModelSelections: AgentModelSelection sessionTasks?: SessionTasks // Multi-task chat support: tasks per session sessionSkills?: Record // Selected skills per session + sessionMessages?: Record< + string, + Array<{ + id: string + role: "user" | "assistant" | "system" | "tool" + content?: string + createdAt?: number + updatedAt?: number + }> + > customAgents?: Array<{ name: string description?: string diff --git a/packages/server/src/server/routes/storage.ts b/packages/server/src/server/routes/storage.ts index a12f37a..612f43f 100644 --- a/packages/server/src/server/routes/storage.ts +++ b/packages/server/src/server/routes/storage.ts @@ -26,6 +26,20 @@ const InstanceDataSchema = z.object({ messageHistory: z.array(z.string()).default([]), agentModelSelections: z.record(z.string(), ModelPreferenceSchema).default({}), sessionTasks: z.record(z.string(), z.array(TaskSchema)).optional(), + sessionMessages: z + .record( + z.string(), + z.array( + z.object({ + id: z.string(), + role: z.enum(["user", "assistant", "system", "tool"]), + content: z.string().optional(), + createdAt: z.number().optional(), + updatedAt: z.number().optional(), + }), + ), + ) + .optional(), sessionSkills: z .record( z.string(), @@ -47,6 +61,7 @@ const EMPTY_INSTANCE_DATA: InstanceData = { messageHistory: [], agentModelSelections: {}, sessionTasks: {}, + sessionMessages: {}, sessionSkills: {}, customAgents: [], } diff --git a/packages/server/src/storage/instance-store.ts b/packages/server/src/storage/instance-store.ts index 6361a7d..796aa56 100644 --- a/packages/server/src/storage/instance-store.ts +++ b/packages/server/src/storage/instance-store.ts @@ -8,6 +8,7 @@ const DEFAULT_INSTANCE_DATA: InstanceData = { messageHistory: [], agentModelSelections: {}, sessionTasks: {}, + sessionMessages: {}, } export class InstanceStore { diff --git a/packages/ui/src/lib/storage.ts b/packages/ui/src/lib/storage.ts index f8c01d9..3711cec 100644 --- a/packages/ui/src/lib/storage.ts +++ b/packages/ui/src/lib/storage.ts @@ -11,6 +11,7 @@ const DEFAULT_INSTANCE_DATA: InstanceData = { messageHistory: [], agentModelSelections: {}, sessionTasks: {}, + sessionMessages: {}, } function isDeepEqual(a: unknown, b: unknown): boolean { @@ -157,11 +158,13 @@ export class ServerStorage { const messageHistory = Array.isArray(source.messageHistory) ? [...source.messageHistory] : [] const agentModelSelections = { ...(source.agentModelSelections ?? {}) } const sessionTasks = { ...(source.sessionTasks ?? {}) } + const sessionMessages = { ...(source.sessionMessages ?? {}) } return { ...source, messageHistory, agentModelSelections, sessionTasks, + sessionMessages, } } diff --git a/packages/ui/src/stores/instance-config.tsx b/packages/ui/src/stores/instance-config.tsx index 8cd5cd5..6e3b390 100644 --- a/packages/ui/src/stores/instance-config.tsx +++ b/packages/ui/src/stores/instance-config.tsx @@ -10,6 +10,7 @@ const DEFAULT_INSTANCE_DATA: InstanceData = { agentModelSelections: {}, sessionTasks: {}, sessionSkills: {}, + sessionMessages: {}, customAgents: [], } @@ -25,6 +26,7 @@ function cloneInstanceData(data?: InstanceData | null): InstanceData { agentModelSelections: { ...(source.agentModelSelections ?? {}) }, sessionTasks: { ...(source.sessionTasks ?? {}) }, sessionSkills: { ...(source.sessionSkills ?? {}) }, + sessionMessages: { ...(source.sessionMessages ?? {}) }, customAgents: Array.isArray(source.customAgents) ? [...source.customAgents] : [], } } diff --git a/packages/ui/src/stores/session-actions.ts b/packages/ui/src/stores/session-actions.ts index a0a9304..3eb8864 100644 --- a/packages/ui/src/stores/session-actions.ts +++ b/packages/ui/src/stores/session-actions.ts @@ -26,6 +26,7 @@ import { getUserScopedKey } from "../lib/user-storage" import { loadSkillDetails } from "./skills" import { serverApi } from "../lib/api-client" import { nativeSessionApi } from "../lib/lite-mode" +import { ensureInstanceConfigLoaded, updateInstanceConfig, getInstanceConfig } from "./instance-config" import type { Session } from "../types/session" const log = getLogger("actions") @@ -1284,6 +1285,37 @@ async function persistNativeMessages( } } +async function persistSdkMessages( + instanceId: string, + sessionId: string, + messages: Array<{ + id: string + role: "user" | "assistant" | "system" | "tool" + content?: string + createdAt?: number + updatedAt?: number + }>, +): Promise { + try { + await ensureInstanceConfigLoaded(instanceId) + const existing = getInstanceConfig(instanceId).sessionMessages ?? {} + const current = existing[sessionId] ?? [] + const merged = [...current] + for (const message of messages) { + if (!merged.some((entry) => entry.id === message.id)) { + merged.push(message) + } + } + merged.sort((a, b) => (a.createdAt ?? 0) - (b.createdAt ?? 0)) + const trimmed = merged.length > 200 ? merged.slice(-200) : merged + await updateInstanceConfig(instanceId, (draft) => { + draft.sessionMessages = { ...(draft.sessionMessages ?? {}), [sessionId]: trimmed } + }) + } catch (error) { + log.warn("Failed to persist SDK messages", { instanceId, sessionId, error }) + } +} + async function sendMessage( instanceId: string, sessionId: string, @@ -1296,6 +1328,7 @@ async function sendMessage( throw new Error("Instance not ready") } const isNative = instance.binaryPath === "__nomadarch_native__" + const isSdk = !isNative const instanceSessions = sessions().get(instanceId) const session = instanceSessions?.get(sessionId) @@ -1587,6 +1620,24 @@ async function sendMessage( updatedAt: completedAt, }, ]) + } else if (isSdk) { + const completedAt = Date.now() + await persistSdkMessages(instanceId, sessionId, [ + { + id: messageId, + role: "user", + content: resolvedPrompt, + createdAt: now, + updatedAt: now, + }, + { + id: assistantMessageId, + role: "assistant", + content: assistantText, + createdAt: now, + updatedAt: completedAt, + }, + ]) } return messageId } catch (error: any) { diff --git a/packages/ui/src/stores/session-api.ts b/packages/ui/src/stores/session-api.ts index 148291b..040ce7c 100644 --- a/packages/ui/src/stores/session-api.ts +++ b/packages/ui/src/stores/session-api.ts @@ -486,26 +486,45 @@ async function fetchSessions(instanceId: string): Promise { }) } - if (isNative) { - const updates: Promise[] = [] - for (const [parentId, tasks] of Object.entries(sessionTasks)) { - if (!Array.isArray(tasks)) continue - for (const task of tasks as Array<{ taskSessionId?: string }>) { - const childId = task?.taskSessionId - if (!childId) continue - const childSession = sessionMap.get(childId) - if (!childSession) continue - if (childSession.parentId === parentId) continue - sessionMap.set(childId, { - ...childSession, - parentId, - }) + const updates: Promise[] = [] + for (const [parentId, tasks] of Object.entries(sessionTasks)) { + if (!Array.isArray(tasks)) continue + for (const task of tasks as Array<{ taskSessionId?: string }>) { + const childId = task?.taskSessionId + if (!childId) continue + const childSession = sessionMap.get(childId) + if (!childSession) continue + if (childSession.parentId === parentId) continue + sessionMap.set(childId, { + ...childSession, + parentId, + }) + if (isNative) { updates.push(nativeSessionApi.updateSession(instanceId, childId, { parentId }).catch(() => undefined)) } } - if (updates.length > 0) { - await Promise.allSettled(updates) - } + } + if (updates.length > 0) { + await Promise.allSettled(updates) + } + + for (const [sessionId, tasks] of Object.entries(sessionTasks)) { + if (sessionMap.has(sessionId)) continue + if (!Array.isArray(tasks) || tasks.length === 0) continue + const existingSession = existingSessions?.get(sessionId) + sessionMap.set(sessionId, { + id: sessionId, + instanceId, + title: existingSession?.title ?? "Untitled", + parentId: existingSession?.parentId ?? null, + agent: existingSession?.agent ?? "", + model: existingSession?.model ?? { providerId: "", modelId: "" }, + skills: existingSession?.skills ?? [], + version: existingSession?.version ?? "0", + time: existingSession?.time ?? { created: Date.now(), updated: Date.now() }, + revert: existingSession?.revert, + tasks: tasks as any[], + }) } const validSessionIds = new Set(sessionMap.keys()) @@ -1069,6 +1088,45 @@ async function loadMessages(instanceId: string, sessionId: string, force = false apiMessagesInfo = (response as any).info || {} // Assuming 'info' might be on the response object itself for some cases } + if (!isNative) { + await ensureInstanceConfigLoaded(instanceId) + const cachedMessages = getInstanceConfig(instanceId).sessionMessages?.[sessionId] ?? [] + if (cachedMessages.length > 0) { + const existingIds = new Set() + for (const apiMessage of apiMessages) { + const info = apiMessage.info || apiMessage + if (info?.id) { + existingIds.add(info.id) + } + } + for (const cached of cachedMessages) { + if (!cached?.id || existingIds.has(cached.id)) continue + apiMessages.push({ + id: cached.id, + role: cached.role, + content: cached.content, + createdAt: cached.createdAt, + info: { + id: cached.id, + role: cached.role, + time: { created: cached.createdAt ?? Date.now() }, + }, + parts: cached.content + ? [{ id: `part-${cached.id}`, type: "text", text: cached.content }] + : [], + }) + existingIds.add(cached.id) + } + apiMessages.sort((a, b) => { + const aInfo = a.info || a + const bInfo = b.info || b + const aTime = aInfo.time?.created ?? aInfo.createdAt ?? 0 + const bTime = bInfo.time?.created ?? bInfo.createdAt ?? 0 + return aTime - bTime + }) + } + } + const messagesInfo = new Map() const messages: Message[] = apiMessages.map((apiMessage: any) => { const info = apiMessage.info || apiMessage