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

This commit is contained in:
Gemini AI
2025-12-27 11:46:42 +04:00
Unverified
parent eca090d360
commit 251fad85b1
4 changed files with 268 additions and 5 deletions

View File

@@ -26,6 +26,7 @@ import { registerAntigravityRoutes } from "./routes/antigravity"
import { registerSkillsRoutes } from "./routes/skills" import { registerSkillsRoutes } from "./routes/skills"
import { registerContextEngineRoutes } from "./routes/context-engine" import { registerContextEngineRoutes } from "./routes/context-engine"
import { registerNativeSessionsRoutes } from "./routes/native-sessions" import { registerNativeSessionsRoutes } from "./routes/native-sessions"
import { registerSdkSyncRoutes } from "./routes/sdk-sync"
import { initSessionManager } from "../storage/session-store" import { initSessionManager } from "../storage/session-store"
import { ServerMeta } from "../api-types" import { ServerMeta } from "../api-types"
import { InstanceStore } from "../storage/instance-store" import { InstanceStore } from "../storage/instance-store"
@@ -144,6 +145,12 @@ export function createHttpServer(deps: HttpServerDeps) {
eventBus: deps.eventBus, 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 }) registerInstanceProxyRoutes(app, { workspaceManager: deps.workspaceManager, logger: proxyLogger })

View 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")
}

View File

@@ -192,6 +192,43 @@ export const nativeSessionApi = {
return response.json() 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 * Send a prompt to the session and get a streaming response

View File

@@ -390,15 +390,28 @@ async function fetchSessions(instanceId: string): Promise<void> {
let responseData: any[] = [] let responseData: any[] = []
if (isNative) { 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)) { if (needsMigration(instanceId)) {
try { try {
const result = await autoImportCachedSessions(instanceId) // First try to sync directly from OpenCode's storage (most reliable)
if (result.imported > 0) { const folderPath = instance.folder
log.info({ instanceId, result }, "Auto-imported SDK sessions to native mode") 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) { } 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 markMigrated(instanceId) // Mark as migrated to prevent repeated failures
} }
} }