Add auto-sync of SDK sessions: directly read from OpenCode storage and import to Native mode
Some checks failed
Release Binaries / release (push) Has been cancelled
Some checks failed
Release Binaries / release (push) Has been cancelled
This commit is contained in:
@@ -26,6 +26,7 @@ import { registerAntigravityRoutes } from "./routes/antigravity"
|
||||
import { registerSkillsRoutes } from "./routes/skills"
|
||||
import { registerContextEngineRoutes } from "./routes/context-engine"
|
||||
import { registerNativeSessionsRoutes } from "./routes/native-sessions"
|
||||
import { registerSdkSyncRoutes } from "./routes/sdk-sync"
|
||||
import { initSessionManager } from "../storage/session-store"
|
||||
import { ServerMeta } from "../api-types"
|
||||
import { InstanceStore } from "../storage/instance-store"
|
||||
@@ -144,6 +145,12 @@ export function createHttpServer(deps: HttpServerDeps) {
|
||||
eventBus: deps.eventBus,
|
||||
})
|
||||
|
||||
// Register SDK session sync routes (for auto-migration from OpenCode to Native)
|
||||
registerSdkSyncRoutes(app, {
|
||||
logger: deps.logger,
|
||||
dataDir,
|
||||
})
|
||||
|
||||
registerInstanceProxyRoutes(app, { workspaceManager: deps.workspaceManager, logger: proxyLogger })
|
||||
|
||||
|
||||
|
||||
206
packages/server/src/server/routes/sdk-sync.ts
Normal file
206
packages/server/src/server/routes/sdk-sync.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* SDK Session Sync - Reads sessions from OpenCode's storage and syncs to Native mode
|
||||
*
|
||||
* OpenCode stores sessions in:
|
||||
* - Windows: %USERPROFILE%\.local\share\opencode\storage\session\{projectId}\
|
||||
* - Linux/Mac: ~/.local/share/opencode/storage/session/{projectId}/
|
||||
*
|
||||
* The projectId is a hash of the workspace folder path.
|
||||
*/
|
||||
|
||||
import { FastifyInstance } from "fastify"
|
||||
import { readdir, readFile } from "fs/promises"
|
||||
import { existsSync } from "fs"
|
||||
import { join } from "path"
|
||||
import { createHash } from "crypto"
|
||||
import { homedir } from "os"
|
||||
import { Logger } from "../../logger"
|
||||
import { getSessionManager } from "../../storage/session-store"
|
||||
|
||||
interface SdkSyncRouteDeps {
|
||||
logger: Logger
|
||||
dataDir: string
|
||||
}
|
||||
|
||||
interface OpenCodeSession {
|
||||
id: string
|
||||
version: string
|
||||
projectID: string
|
||||
directory: string
|
||||
title: string
|
||||
parentID?: string
|
||||
time: {
|
||||
created: number
|
||||
updated: number
|
||||
}
|
||||
summary?: {
|
||||
additions: number
|
||||
deletions: number
|
||||
files: number
|
||||
}
|
||||
share?: {
|
||||
url: string
|
||||
version: number
|
||||
}
|
||||
revert?: {
|
||||
messageID: string
|
||||
reason?: string
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the OpenCode storage directory
|
||||
*/
|
||||
function getOpenCodeStorageDir(): string {
|
||||
const homeDir = homedir()
|
||||
|
||||
// Windows: %USERPROFILE%\.local\share\opencode
|
||||
// Linux/Mac: ~/.local/share/opencode
|
||||
return join(homeDir, ".local", "share", "opencode", "storage")
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the project ID hash that OpenCode uses
|
||||
* OpenCode uses a SHA1 hash of the folder path
|
||||
*/
|
||||
function generateProjectId(folderPath: string): string {
|
||||
return createHash("sha1").update(folderPath).digest("hex")
|
||||
}
|
||||
|
||||
/**
|
||||
* Read all sessions for a project from OpenCode's storage
|
||||
*/
|
||||
async function readOpenCodeSessions(folderPath: string, logger: Logger): Promise<OpenCodeSession[]> {
|
||||
const storageDir = getOpenCodeStorageDir()
|
||||
const projectId = generateProjectId(folderPath)
|
||||
const sessionDir = join(storageDir, "session", projectId)
|
||||
|
||||
logger.info({ folderPath, projectId, sessionDir }, "Looking for OpenCode sessions")
|
||||
|
||||
if (!existsSync(sessionDir)) {
|
||||
logger.info({ sessionDir }, "OpenCode session directory not found")
|
||||
return []
|
||||
}
|
||||
|
||||
const sessions: OpenCodeSession[] = []
|
||||
|
||||
try {
|
||||
const files = await readdir(sessionDir)
|
||||
const sessionFiles = files.filter(f => f.startsWith("ses_") && f.endsWith(".json"))
|
||||
|
||||
logger.info({ count: sessionFiles.length }, "Found OpenCode session files")
|
||||
|
||||
for (const file of sessionFiles) {
|
||||
try {
|
||||
const filePath = join(sessionDir, file)
|
||||
const content = await readFile(filePath, "utf-8")
|
||||
const session = JSON.parse(content) as OpenCodeSession
|
||||
sessions.push(session)
|
||||
} catch (error) {
|
||||
logger.warn({ file, error }, "Failed to read session file")
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error({ error }, "Failed to read OpenCode sessions directory")
|
||||
}
|
||||
|
||||
return sessions
|
||||
}
|
||||
|
||||
export function registerSdkSyncRoutes(app: FastifyInstance, deps: SdkSyncRouteDeps) {
|
||||
const logger = deps.logger.child({ component: "sdk-sync" })
|
||||
const sessionManager = getSessionManager(deps.dataDir)
|
||||
|
||||
/**
|
||||
* Sync sessions from SDK (OpenCode) to Native mode
|
||||
* This reads sessions directly from OpenCode's storage directory
|
||||
*/
|
||||
app.post<{
|
||||
Params: { workspaceId: string }
|
||||
Body: { folderPath: string }
|
||||
}>("/api/native/workspaces/:workspaceId/sync-sdk", async (request, reply) => {
|
||||
const { workspaceId } = request.params
|
||||
const { folderPath } = request.body
|
||||
|
||||
if (!folderPath) {
|
||||
return reply.status(400).send({ error: "Missing folderPath" })
|
||||
}
|
||||
|
||||
logger.info({ workspaceId, folderPath }, "Starting SDK session sync")
|
||||
|
||||
try {
|
||||
// Read sessions from OpenCode's storage
|
||||
const sdkSessions = await readOpenCodeSessions(folderPath, logger)
|
||||
|
||||
if (sdkSessions.length === 0) {
|
||||
return {
|
||||
success: true,
|
||||
imported: 0,
|
||||
skipped: 0,
|
||||
message: "No SDK sessions found for this folder"
|
||||
}
|
||||
}
|
||||
|
||||
// Convert OpenCode sessions to our format
|
||||
const sessionsToImport = sdkSessions.map(s => ({
|
||||
id: s.id,
|
||||
title: s.title,
|
||||
parentId: s.parentID || null,
|
||||
createdAt: s.time.created,
|
||||
updatedAt: s.time.updated,
|
||||
// We don't have model/agent info in the SDK session format
|
||||
// Those are stored in OpenCode's config, not session
|
||||
}))
|
||||
|
||||
// Import to native session store
|
||||
const result = await sessionManager.importSessions(workspaceId, sessionsToImport)
|
||||
|
||||
logger.info({ workspaceId, ...result }, "SDK session sync completed")
|
||||
|
||||
return {
|
||||
success: true,
|
||||
imported: result.imported,
|
||||
skipped: result.skipped,
|
||||
total: sdkSessions.length
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error({ error }, "SDK session sync failed")
|
||||
return reply.status(500).send({
|
||||
error: "Failed to sync SDK sessions",
|
||||
details: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Check if OpenCode sessions exist for a folder
|
||||
*/
|
||||
app.post<{
|
||||
Body: { folderPath: string }
|
||||
}>("/api/native/check-sdk-sessions", async (request, reply) => {
|
||||
const { folderPath } = request.body
|
||||
|
||||
if (!folderPath) {
|
||||
return reply.status(400).send({ error: "Missing folderPath" })
|
||||
}
|
||||
|
||||
try {
|
||||
const sdkSessions = await readOpenCodeSessions(folderPath, logger)
|
||||
|
||||
return {
|
||||
found: sdkSessions.length > 0,
|
||||
count: sdkSessions.length,
|
||||
sessions: sdkSessions.map(s => ({
|
||||
id: s.id,
|
||||
title: s.title,
|
||||
created: s.time.created
|
||||
}))
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error({ error }, "Failed to check SDK sessions")
|
||||
return { found: false, count: 0, sessions: [] }
|
||||
}
|
||||
})
|
||||
|
||||
logger.info("SDK sync routes registered")
|
||||
}
|
||||
@@ -192,6 +192,43 @@ export const nativeSessionApi = {
|
||||
return response.json()
|
||||
},
|
||||
|
||||
/**
|
||||
* Sync sessions from SDK (OpenCode) to Native mode
|
||||
* This reads sessions directly from OpenCode's storage
|
||||
*/
|
||||
async syncFromSdk(workspaceId: string, folderPath: string): Promise<{
|
||||
success: boolean
|
||||
imported: number
|
||||
skipped: number
|
||||
total?: number
|
||||
message?: string
|
||||
}> {
|
||||
const response = await fetch(`${CODENOMAD_API_BASE}/api/native/workspaces/${encodeURIComponent(workspaceId)}/sync-sdk`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ folderPath })
|
||||
})
|
||||
if (!response.ok) throw new Error("Failed to sync SDK sessions")
|
||||
return response.json()
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if SDK sessions exist for a folder
|
||||
*/
|
||||
async checkSdkSessions(folderPath: string): Promise<{
|
||||
found: boolean
|
||||
count: number
|
||||
sessions: Array<{ id: string; title: string; created: number }>
|
||||
}> {
|
||||
const response = await fetch(`${CODENOMAD_API_BASE}/api/native/check-sdk-sessions`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ folderPath })
|
||||
})
|
||||
if (!response.ok) throw new Error("Failed to check SDK sessions")
|
||||
return response.json()
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Send a prompt to the session and get a streaming response
|
||||
|
||||
@@ -390,15 +390,28 @@ async function fetchSessions(instanceId: string): Promise<void> {
|
||||
let responseData: any[] = []
|
||||
|
||||
if (isNative) {
|
||||
// Auto-import cached SDK sessions on native mode startup
|
||||
// Auto-sync SDK sessions from OpenCode's storage on native mode startup
|
||||
if (needsMigration(instanceId)) {
|
||||
try {
|
||||
const result = await autoImportCachedSessions(instanceId)
|
||||
if (result.imported > 0) {
|
||||
log.info({ instanceId, result }, "Auto-imported SDK sessions to native mode")
|
||||
// First try to sync directly from OpenCode's storage (most reliable)
|
||||
const folderPath = instance.folder
|
||||
if (folderPath) {
|
||||
log.info({ instanceId, folderPath }, "Syncing SDK sessions from OpenCode storage")
|
||||
const syncResult = await nativeSessionApi.syncFromSdk(instanceId, folderPath)
|
||||
if (syncResult.imported > 0) {
|
||||
log.info({ instanceId, syncResult }, "Synced SDK sessions from OpenCode storage")
|
||||
} else if (syncResult.message) {
|
||||
log.info({ instanceId, message: syncResult.message }, "SDK sync info")
|
||||
}
|
||||
}
|
||||
|
||||
// Also try the localStorage cache as fallback
|
||||
const cacheResult = await autoImportCachedSessions(instanceId)
|
||||
if (cacheResult.imported > 0) {
|
||||
log.info({ instanceId, cacheResult }, "Auto-imported cached SDK sessions")
|
||||
}
|
||||
} catch (error) {
|
||||
log.error({ instanceId, error }, "Failed to auto-import SDK sessions")
|
||||
log.error({ instanceId, error }, "Failed to sync SDK sessions")
|
||||
markMigrated(instanceId) // Mark as migrated to prevent repeated failures
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user