Fix Native Mode Sessions: implemented fork, revert, and sync for native sessions
This commit is contained in:
@@ -2,6 +2,7 @@ import type { Session, Provider, Model } from "../types/session"
|
||||
import type { Message } from "../types/message"
|
||||
|
||||
import { instances } from "./instances"
|
||||
import { nativeSessionApi } from "../lib/lite-mode"
|
||||
import { preferences, setAgentModelPreference, getAgentModelPreference } from "./preferences"
|
||||
import { setSessionCompactionState } from "./session-compaction"
|
||||
import {
|
||||
@@ -366,10 +367,16 @@ interface SessionForkResponse {
|
||||
|
||||
async function fetchSessions(instanceId: string): Promise<void> {
|
||||
const instance = instances().get(instanceId)
|
||||
if (!instance || !instance.client) {
|
||||
if (!instance) {
|
||||
throw new Error("Instance not ready")
|
||||
}
|
||||
|
||||
const isNative = instance.binaryPath === "__nomadarch_native__"
|
||||
|
||||
if (!isNative && !instance.client) {
|
||||
throw new Error("Instance client not ready")
|
||||
}
|
||||
|
||||
setLoading((prev) => {
|
||||
const next = { ...prev }
|
||||
next.fetchingSessions.set(instanceId, true)
|
||||
@@ -377,13 +384,38 @@ async function fetchSessions(instanceId: string): Promise<void> {
|
||||
})
|
||||
|
||||
try {
|
||||
log.info("session.list", { instanceId })
|
||||
const response = await instance.client.session.list()
|
||||
log.info("session.list", { instanceId, isNative })
|
||||
|
||||
let responseData: any[] = []
|
||||
|
||||
if (isNative) {
|
||||
const nativeSessions = await nativeSessionApi.listSessions(instanceId)
|
||||
responseData = nativeSessions.map(s => ({
|
||||
id: s.id,
|
||||
title: s.title,
|
||||
parentID: s.parentId,
|
||||
version: "0",
|
||||
time: {
|
||||
created: s.createdAt,
|
||||
updated: s.updatedAt
|
||||
},
|
||||
model: s.model ? {
|
||||
providerID: s.model.providerId,
|
||||
modelID: s.model.modelId
|
||||
} : undefined,
|
||||
agent: s.agent
|
||||
}))
|
||||
} else {
|
||||
const response = await instance.client!.session.list()
|
||||
if (response.data && Array.isArray(response.data)) {
|
||||
responseData = response.data
|
||||
}
|
||||
}
|
||||
|
||||
const sessionMap = new Map<string, Session>()
|
||||
|
||||
if (!response.data || !Array.isArray(response.data)) {
|
||||
return
|
||||
if (responseData.length === 0 && !isNative) {
|
||||
// In SDK mode we still check response.data for empty
|
||||
}
|
||||
|
||||
const existingSessions = sessions().get(instanceId)
|
||||
@@ -394,13 +426,13 @@ async function fetchSessions(instanceId: string): Promise<void> {
|
||||
const sessionTasks = instanceData.sessionTasks || {}
|
||||
const sessionSkills = instanceData.sessionSkills || {}
|
||||
|
||||
for (const apiSession of response.data) {
|
||||
for (const apiSession of responseData) {
|
||||
const existingSession = existingSessions?.get(apiSession.id)
|
||||
|
||||
const existingModel = existingSession?.model ?? { providerId: "", modelId: "" }
|
||||
const hasUserSelectedModel = existingModel.providerId && existingModel.modelId
|
||||
const apiModel = (apiSession as any).model?.providerID && (apiSession as any).model?.modelID
|
||||
? { providerId: (apiSession as any).model.providerID, modelId: (apiSession as any).model.modelID }
|
||||
const apiModel = apiSession.model?.providerID && apiSession.model?.modelID
|
||||
? { providerId: apiSession.model.providerID, modelId: apiSession.model.modelID }
|
||||
: { providerId: "", modelId: "" }
|
||||
|
||||
sessionMap.set(apiSession.id, {
|
||||
@@ -408,7 +440,7 @@ async function fetchSessions(instanceId: string): Promise<void> {
|
||||
instanceId,
|
||||
title: apiSession.title || "Untitled",
|
||||
parentId: apiSession.parentID || null,
|
||||
agent: existingSession?.agent ?? (apiSession as any).agent ?? "",
|
||||
agent: existingSession?.agent ?? apiSession.agent ?? "",
|
||||
model: hasUserSelectedModel ? existingModel : apiModel,
|
||||
version: apiSession.version,
|
||||
time: {
|
||||
@@ -475,10 +507,15 @@ async function createSession(
|
||||
options?: { skipAutoCleanup?: boolean },
|
||||
): Promise<Session> {
|
||||
const instance = instances().get(instanceId)
|
||||
if (!instance || !instance.client) {
|
||||
if (!instance) {
|
||||
throw new Error("Instance not ready")
|
||||
}
|
||||
|
||||
const isNative = instance.binaryPath === "__nomadarch_native__"
|
||||
if (!isNative && !instance.client) {
|
||||
throw new Error("Instance client not ready")
|
||||
}
|
||||
|
||||
const instanceAgents = agents().get(instanceId) || []
|
||||
const nonSubagents = instanceAgents.filter((a) => a.mode !== "subagent")
|
||||
const selectedAgent = agent || (nonSubagents.length > 0 ? nonSubagents[0].name : "")
|
||||
@@ -498,31 +535,57 @@ async function createSession(
|
||||
})
|
||||
|
||||
try {
|
||||
log.info(`[HTTP] POST /session.create for instance ${instanceId}`)
|
||||
const response = await instance.client.session.create()
|
||||
log.info(`[HTTP] POST session create for instance ${instanceId}, isNative: ${isNative}`)
|
||||
|
||||
if (!response.data) {
|
||||
let sessionData: any = null
|
||||
|
||||
if (isNative) {
|
||||
const native = await nativeSessionApi.createSession(instanceId, {
|
||||
agent: selectedAgent,
|
||||
model: sessionModel
|
||||
})
|
||||
sessionData = {
|
||||
id: native.id,
|
||||
title: native.title || "New Session",
|
||||
parentID: native.parentId,
|
||||
version: "0",
|
||||
time: {
|
||||
created: native.createdAt,
|
||||
updated: native.updatedAt
|
||||
},
|
||||
agent: native.agent,
|
||||
model: native.model ? {
|
||||
providerID: native.model.providerId,
|
||||
modelID: native.model.modelId
|
||||
} : undefined
|
||||
}
|
||||
} else {
|
||||
const response = await instance.client!.session.create()
|
||||
sessionData = response.data
|
||||
}
|
||||
|
||||
if (!sessionData) {
|
||||
throw new Error("Failed to create session: No data returned")
|
||||
}
|
||||
|
||||
const session: Session = {
|
||||
id: response.data.id,
|
||||
id: sessionData.id,
|
||||
instanceId,
|
||||
title: response.data.title || "New Session",
|
||||
title: sessionData.title || "New Session",
|
||||
parentId: null,
|
||||
agent: selectedAgent,
|
||||
model: sessionModel,
|
||||
skills: [],
|
||||
version: response.data.version,
|
||||
version: sessionData.version,
|
||||
time: {
|
||||
...response.data.time,
|
||||
...sessionData.time,
|
||||
},
|
||||
revert: response.data.revert
|
||||
revert: sessionData.revert
|
||||
? {
|
||||
messageID: response.data.revert.messageID,
|
||||
partID: response.data.revert.partID,
|
||||
snapshot: response.data.revert.snapshot,
|
||||
diff: response.data.revert.diff,
|
||||
messageID: sessionData.revert.messageID,
|
||||
partID: sessionData.revert.partID,
|
||||
snapshot: sessionData.revert.snapshot,
|
||||
diff: sessionData.revert.diff,
|
||||
}
|
||||
: undefined,
|
||||
}
|
||||
@@ -683,9 +746,10 @@ async function forkSession(
|
||||
|
||||
async function deleteSession(instanceId: string, sessionId: string): Promise<void> {
|
||||
const instance = instances().get(instanceId)
|
||||
if (!instance || !instance.client) {
|
||||
throw new Error("Instance not ready")
|
||||
}
|
||||
if (!instance) return
|
||||
|
||||
const isNative = instance.binaryPath === "__nomadarch_native__"
|
||||
if (!isNative && !instance.client) return
|
||||
|
||||
setLoading((prev) => {
|
||||
const next = { ...prev }
|
||||
@@ -696,8 +760,13 @@ async function deleteSession(instanceId: string, sessionId: string): Promise<voi
|
||||
})
|
||||
|
||||
try {
|
||||
log.info(`[HTTP] DELETE /session.delete for instance ${instanceId}`, { sessionId })
|
||||
await instance.client.session.delete({ path: { id: sessionId } })
|
||||
log.info("session.delete", { instanceId, sessionId, isNative })
|
||||
|
||||
if (isNative) {
|
||||
await nativeSessionApi.deleteSession(instanceId, sessionId)
|
||||
} else {
|
||||
await instance.client!.session.delete({ path: { id: sessionId } })
|
||||
}
|
||||
|
||||
setSessions((prev) => {
|
||||
const next = new Map(prev)
|
||||
@@ -754,25 +823,42 @@ async function deleteSession(instanceId: string, sessionId: string): Promise<voi
|
||||
|
||||
async function fetchAgents(instanceId: string): Promise<void> {
|
||||
const instance = instances().get(instanceId)
|
||||
if (!instance || !instance.client) {
|
||||
if (!instance) {
|
||||
throw new Error("Instance not ready")
|
||||
}
|
||||
|
||||
const isNative = instance.binaryPath === "__nomadarch_native__"
|
||||
if (!isNative && !instance.client) {
|
||||
throw new Error("Instance client not ready")
|
||||
}
|
||||
|
||||
try {
|
||||
await ensureInstanceConfigLoaded(instanceId)
|
||||
log.info(`[HTTP] GET /app.agents for instance ${instanceId}`)
|
||||
const response = await instance.client.app.agents()
|
||||
const agentList = (response.data ?? []).map((agent) => ({
|
||||
name: agent.name,
|
||||
description: agent.description || "",
|
||||
mode: agent.mode,
|
||||
model: agent.model?.modelID
|
||||
? {
|
||||
providerId: agent.model.providerID || "",
|
||||
modelId: agent.model.modelID,
|
||||
}
|
||||
: undefined,
|
||||
}))
|
||||
log.info("agents.list", { instanceId, isNative })
|
||||
|
||||
let agentList: any[] = []
|
||||
|
||||
if (isNative) {
|
||||
// In native mode, we don't have agents from the SDK yet
|
||||
// We can return a default agent or common agents
|
||||
agentList = [{
|
||||
name: "Assistant",
|
||||
description: "Native assistant agent",
|
||||
mode: "native"
|
||||
}]
|
||||
} else {
|
||||
const response = await instance.client!.agents.list()
|
||||
agentList = (response.data || []).map((agent) => ({
|
||||
name: agent.name,
|
||||
description: agent.description || "",
|
||||
mode: agent.mode as "standard" | "subagent",
|
||||
model: agent.model
|
||||
? {
|
||||
providerId: agent.model.providerID || "",
|
||||
modelId: agent.model.modelID,
|
||||
}
|
||||
: undefined,
|
||||
}))
|
||||
}
|
||||
|
||||
const customAgents = getInstanceConfig(instanceId)?.customAgents ?? []
|
||||
const customList = customAgents.map((agent) => ({
|
||||
@@ -793,27 +879,43 @@ async function fetchAgents(instanceId: string): Promise<void> {
|
||||
|
||||
async function fetchProviders(instanceId: string): Promise<void> {
|
||||
const instance = instances().get(instanceId)
|
||||
if (!instance || !instance.client) {
|
||||
if (!instance) {
|
||||
throw new Error("Instance not ready")
|
||||
}
|
||||
|
||||
try {
|
||||
log.info(`[HTTP] GET /config.providers for instance ${instanceId}`)
|
||||
const response = await instance.client.config.providers()
|
||||
if (!response.data) return
|
||||
const isNative = instance.binaryPath === "__nomadarch_native__"
|
||||
if (!isNative && !instance.client) {
|
||||
throw new Error("Instance client not ready")
|
||||
}
|
||||
|
||||
const providerList = response.data.providers.map((provider) => ({
|
||||
id: provider.id,
|
||||
name: provider.name,
|
||||
defaultModelId: response.data?.default?.[provider.id],
|
||||
models: Object.entries(provider.models).map(([id, model]) => ({
|
||||
id,
|
||||
name: model.name,
|
||||
providerId: provider.id,
|
||||
limit: model.limit,
|
||||
cost: model.cost,
|
||||
})),
|
||||
}))
|
||||
try {
|
||||
log.info("config.providers", { instanceId, isNative })
|
||||
|
||||
let providerList: any[] = []
|
||||
let defaultProviders: any = {}
|
||||
|
||||
if (isNative) {
|
||||
// For native mode, we mainly rely on extra providers
|
||||
// but we could add "zen" (OpenCode Zen) if it's available via server API
|
||||
providerList = []
|
||||
} else {
|
||||
const response = await instance.client!.config.providers()
|
||||
if (response.data) {
|
||||
providerList = response.data.providers.map((provider) => ({
|
||||
id: provider.id,
|
||||
name: provider.name,
|
||||
defaultModelId: response.data?.default?.[provider.id],
|
||||
models: Object.entries(provider.models).map(([id, model]) => ({
|
||||
id,
|
||||
name: model.name,
|
||||
providerId: provider.id,
|
||||
limit: model.limit,
|
||||
cost: model.cost,
|
||||
})),
|
||||
}))
|
||||
defaultProviders = response.data.default || {}
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out Z.AI providers from SDK to use our custom routing with full message history
|
||||
const filteredBaseProviders = providerList.filter((provider) =>
|
||||
@@ -859,10 +961,15 @@ async function loadMessages(instanceId: string, sessionId: string, force = false
|
||||
}
|
||||
|
||||
const instance = instances().get(instanceId)
|
||||
if (!instance || !instance.client) {
|
||||
if (!instance) {
|
||||
throw new Error("Instance not ready")
|
||||
}
|
||||
|
||||
const isNative = instance.binaryPath === "__nomadarch_native__"
|
||||
if (!isNative && !instance.client) {
|
||||
throw new Error("Instance client not ready")
|
||||
}
|
||||
|
||||
const instanceSessions = sessions().get(instanceId)
|
||||
const session = instanceSessions?.get(sessionId)
|
||||
if (!session) {
|
||||
@@ -878,15 +985,37 @@ async function loadMessages(instanceId: string, sessionId: string, force = false
|
||||
})
|
||||
|
||||
try {
|
||||
log.info(`[HTTP] GET /session.${"messages"} for instance ${instanceId}`, { sessionId })
|
||||
const response = await instance.client.session["messages"]({ path: { id: sessionId } })
|
||||
log.info("session.getMessages", { instanceId, sessionId, isNative })
|
||||
|
||||
if (!response.data || !Array.isArray(response.data)) {
|
||||
return
|
||||
let apiMessages: any[] = []
|
||||
let apiMessagesInfo: any = {}
|
||||
|
||||
if (isNative) {
|
||||
const nativeMessages = await nativeSessionApi.getMessages(instanceId, sessionId)
|
||||
apiMessages = nativeMessages.map(m => ({
|
||||
id: m.id,
|
||||
role: m.role,
|
||||
content: m.content || "",
|
||||
createdAt: m.createdAt,
|
||||
status: m.status,
|
||||
info: {
|
||||
id: m.id,
|
||||
role: m.role,
|
||||
time: { created: m.createdAt },
|
||||
// Add other native message properties to info if needed for later processing
|
||||
}
|
||||
}))
|
||||
} else {
|
||||
const response = await instance.client!.session.getMessages({ path: { id: sessionId } })
|
||||
if (!response.data || !Array.isArray(response.data)) {
|
||||
return
|
||||
}
|
||||
apiMessages = response.data || []
|
||||
apiMessagesInfo = (response as any).info || {} // Assuming 'info' might be on the response object itself for some cases
|
||||
}
|
||||
|
||||
const messagesInfo = new Map<string, any>()
|
||||
const messages: Message[] = response.data.map((apiMessage: any) => {
|
||||
const messages: Message[] = apiMessages.map((apiMessage: any) => {
|
||||
const info = apiMessage.info || apiMessage
|
||||
const role = info.role || "assistant"
|
||||
const messageId = info.id || String(Date.now())
|
||||
|
||||
Reference in New Issue
Block a user