fix: complete session persistence overhaul (Codex 5.2)
Some checks failed
Release Binaries / release (push) Has been cancelled

1. Implemented auto-selection of tasks in MultiXV2 to prevent empty initial state.
2. Added force-loading logic for task session messages with debouncing.
3. Updated session-actions to return full assistant text and immediately persist native messages.
4. Fixed caching logic in instance-shell2 to retain active task sessions in memory.
This commit is contained in:
Gemini AI
2025-12-27 20:36:43 +04:00
Unverified
parent 5022a23aeb
commit 1e991d9ebd
8 changed files with 235 additions and 20 deletions

View File

@@ -555,7 +555,7 @@ async function streamOllamaChat(
messageId: string,
assistantMessageId: string,
assistantPartId: string,
): Promise<void> {
): Promise<string> {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), STREAM_TIMEOUT_MS)
@@ -682,6 +682,8 @@ async function streamOllamaChat(
isEphemeral: false,
})
})
return fullText
}
async function streamQwenChat(
@@ -695,7 +697,7 @@ async function streamQwenChat(
messageId: string,
assistantMessageId: string,
assistantPartId: string,
): Promise<void> {
): Promise<string> {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), STREAM_TIMEOUT_MS)
@@ -831,6 +833,8 @@ async function streamQwenChat(
updatedAt: Date.now(),
isEphemeral: false,
})
return fullText
}
async function streamOpenCodeZenChat(
@@ -842,7 +846,7 @@ async function streamOpenCodeZenChat(
messageId: string,
assistantMessageId: string,
assistantPartId: string,
): Promise<void> {
): Promise<string> {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), STREAM_TIMEOUT_MS)
@@ -978,6 +982,8 @@ async function streamOpenCodeZenChat(
updatedAt: Date.now(),
isEphemeral: false,
})
return fullText
}
async function streamZAIChat(
@@ -989,7 +995,7 @@ async function streamZAIChat(
messageId: string,
assistantMessageId: string,
assistantPartId: string,
): Promise<void> {
): Promise<string> {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), STREAM_TIMEOUT_MS)
@@ -1116,6 +1122,8 @@ async function streamZAIChat(
updatedAt: Date.now(),
isEphemeral: false,
})
return fullText
}
async function streamAntigravityChat(
@@ -1127,7 +1135,7 @@ async function streamAntigravityChat(
messageId: string,
assistantMessageId: string,
assistantPartId: string,
): Promise<void> {
): Promise<string> {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), STREAM_TIMEOUT_MS)
@@ -1254,6 +1262,26 @@ async function streamAntigravityChat(
updatedAt: Date.now(),
isEphemeral: false,
})
return fullText
}
async function persistNativeMessages(
instanceId: string,
sessionId: string,
messages: Array<{
id: string
role: "user" | "assistant" | "system" | "tool"
content: string
createdAt: number
updatedAt: number
}>,
): Promise<void> {
try {
await nativeSessionApi.appendMessages(instanceId, sessionId, messages)
} catch (error) {
log.warn("Failed to persist native messages", { instanceId, sessionId, error })
}
}
async function sendMessage(
@@ -1267,6 +1295,7 @@ async function sendMessage(
if (!instance || !instance.client) {
throw new Error("Instance not ready")
}
const isNative = instance.binaryPath === "__nomadarch_native__"
const instanceSessions = sessions().get(instanceId)
const session = instanceSessions?.get(sessionId)
@@ -1450,10 +1479,11 @@ async function sendMessage(
})
})
let assistantText = ""
try {
if (providerId === "ollama-cloud") {
const tStream1 = performance.now()
await streamOllamaChat(
assistantText = await streamOllamaChat(
instanceId,
sessionId,
providerId,
@@ -1466,7 +1496,7 @@ async function sendMessage(
const tStream2 = performance.now()
addDebugLog(`Stream Complete: ${Math.round(tStream2 - tStream1)}ms`, "info")
} else if (providerId === "opencode-zen") {
await streamOpenCodeZenChat(
assistantText = await streamOpenCodeZenChat(
instanceId,
sessionId,
providerId,
@@ -1477,7 +1507,7 @@ async function sendMessage(
assistantPartId,
)
} else if (providerId === "zai") {
await streamZAIChat(
assistantText = await streamZAIChat(
instanceId,
sessionId,
providerId,
@@ -1488,7 +1518,7 @@ async function sendMessage(
assistantPartId,
)
} else if (providerId === "antigravity") {
await streamAntigravityChat(
assistantText = await streamAntigravityChat(
instanceId,
sessionId,
providerId,
@@ -1526,7 +1556,7 @@ async function sendMessage(
return messageId
}
await streamQwenChat(
assistantText = await streamQwenChat(
instanceId,
sessionId,
providerId,
@@ -1539,6 +1569,25 @@ async function sendMessage(
assistantPartId,
)
}
if (isNative) {
const completedAt = Date.now()
await persistNativeMessages(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) {
if (providerId === "opencode-zen") {

View File

@@ -486,6 +486,28 @@ async function fetchSessions(instanceId: string): Promise<void> {
})
}
if (isNative) {
const updates: Promise<unknown>[] = []
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,
})
updates.push(nativeSessionApi.updateSession(instanceId, childId, { parentId }).catch(() => undefined))
}
}
if (updates.length > 0) {
await Promise.allSettled(updates)
}
}
const validSessionIds = new Set(sessionMap.keys())
setSessions((prev) => {
@@ -1055,7 +1077,10 @@ async function loadMessages(instanceId: string, sessionId: string, force = false
messagesInfo.set(messageId, info)
const parts: any[] = (apiMessage.parts || []).map((part: any) => normalizeMessagePart(part))
let parts: any[] = (apiMessage.parts || []).map((part: any) => normalizeMessagePart(part))
if (parts.length === 0 && typeof apiMessage.content === "string" && apiMessage.content.trim().length > 0) {
parts = [normalizeMessagePart({ id: `part-${messageId}`, type: "text", text: apiMessage.content })]
}
const message: Message = {
id: messageId,

View File

@@ -3,6 +3,8 @@ import { Task, TaskStatus } from "../types/session"
import { nanoid } from "nanoid"
import { createSession } from "./session-api"
import { showToastNotification } from "../lib/notifications"
import { instances } from "./instances"
import { nativeSessionApi } from "../lib/lite-mode"
export function setActiveTask(instanceId: string, sessionId: string, taskId: string | undefined): void {
withSession(instanceId, sessionId, (session) => {
@@ -35,6 +37,16 @@ export async function addTask(
taskSession.model = { ...parentModel }
}
})
const instance = instances().get(instanceId)
if (instance?.binaryPath === "__nomadarch_native__") {
try {
await nativeSessionApi.updateSession(instanceId, taskSessionId, {
parentId: sessionId,
})
} catch (error) {
console.warn("[task-actions] Failed to persist parent session", error)
}
}
// console.log("[task-actions] task session created", { taskSessionId });
} catch (error) {
console.error("[task-actions] Failed to create session for task", error)