diff --git a/packages/opencode/src/cli/cmd/import.ts b/packages/opencode/src/cli/cmd/import.ts index 9696d17add..99976e1185 100644 --- a/packages/opencode/src/cli/cmd/import.ts +++ b/packages/opencode/src/cli/cmd/import.ts @@ -1,7 +1,6 @@ import type { Argv } from "yargs" import type { Session as SDKSession, Message, Part } from "@kilocode/sdk/v2" import { Session } from "../../session" -import { Bus } from "../../bus" import { cmd } from "./cmd" import { bootstrap } from "../bootstrap" import { Database } from "../../storage/db" @@ -10,6 +9,9 @@ import { Instance } from "../../project/instance" import { ShareNext } from "../../share/share-next" import { EOL } from "os" import { Filesystem } from "../../util/filesystem" +import { Log } from "../../util/log" + +const log = Log.create({ service: "import-command" }) /** Discriminated union returned by the ShareNext API (GET /api/share/:id/data) */ export type ShareData = @@ -65,6 +67,44 @@ export function transformShareData(shareData: ShareData[]): { } } +// kilocode_change start +export function ingestBootstrapWarning(sessionId: string, error: unknown) { + const details = error instanceof Error ? error.message : String(error) + return `Warning: imported session ${sessionId} locally, but ingest bootstrap failed: ${details}` +} + +async function ingestBootstrap(sessionId: string) { + const { KiloSessions } = await import("../../kilo-sessions/kilo-sessions") + return KiloSessions.bootstrap(sessionId) +} + +export async function bootstrapImportedSessionIngest( + sessionId: string, + input?: { + bootstrap?: (sessionId: string) => Promise + warn?: (message: string) => void + }, +) { + const run = input?.bootstrap ?? ingestBootstrap + const warn = + input?.warn ?? + ((message: string) => { + process.stderr.write(message) + process.stderr.write(EOL) + }) + + log.info("ingest bootstrap started", { sessionId }) + await run(sessionId) + .then(() => { + log.info("ingest bootstrap completed", { sessionId }) + }) + .catch((error) => { + log.error("ingest bootstrap failed", { sessionId, error }) + warn(ingestBootstrapWarning(sessionId, error)) + }) +} +// kilocode_change end + export const ImportCommand = cmd({ command: "import ", describe: "import session data from JSON file or URL", @@ -134,7 +174,6 @@ export const ImportCommand = cmd({ Database.use((db) => { db.insert(SessionTable).values(Session.toRow(exportData.info)).onConflictDoNothing().run() - Database.effect(() => Bus.publish(Session.Event.Created, { info: exportData.info })) }) for (const msg of exportData.messages) { @@ -167,6 +206,10 @@ export const ImportCommand = cmd({ } } + // kilocode_change start + await bootstrapImportedSessionIngest(exportData.info.id) + // kilocode_change end + process.stdout.write(`Imported session: ${exportData.info.id}`) process.stdout.write(EOL) }) diff --git a/packages/opencode/src/kilo-sessions/kilo-sessions.ts b/packages/opencode/src/kilo-sessions/kilo-sessions.ts index 7a22b3e78c..62288eba6f 100644 --- a/packages/opencode/src/kilo-sessions/kilo-sessions.ts +++ b/packages/opencode/src/kilo-sessions/kilo-sessions.ts @@ -198,8 +198,25 @@ export namespace KiloSessions { } export async function create(sessionId: string) { + const result = await bootstrap(sessionId) + if (!result) return { id: "", ingestPath: "" } + + void fullSync(sessionId).catch((error) => log.error("share full sync failed", { sessionId, error })) + + return result + } + + export async function bootstrap(sessionId: string) { + if (ingestDisabled) { + log.info("session bootstrap skipped: ingest disabled", { sessionId }) + return + } + const client = await getClient() - if (!client) return { id: "", ingestPath: "" } + if (!client) { + log.info("session bootstrap skipped: no client", { sessionId }) + return + } log.info("creating session", { sessionId }) @@ -216,7 +233,7 @@ export namespace KiloSessions { await Storage.write(["session_share", sessionId], result) - void fullSync(sessionId).catch((error) => log.error("share full sync failed", { sessionId, error })) + log.info("session bootstrap completed", { sessionId }) return result } diff --git a/packages/opencode/test/cli/import.test.ts b/packages/opencode/test/cli/import.test.ts index a1a69dc094..e32de1342e 100644 --- a/packages/opencode/test/cli/import.test.ts +++ b/packages/opencode/test/cli/import.test.ts @@ -1,5 +1,11 @@ import { test, expect } from "bun:test" -import { parseShareUrl, transformShareData, type ShareData } from "../../src/cli/cmd/import" +import { + parseShareUrl, + transformShareData, + bootstrapImportedSessionIngest, + ingestBootstrapWarning, + type ShareData, +} from "../../src/cli/cmd/import" // parseShareUrl tests test("parses valid share URLs", () => { @@ -36,3 +42,41 @@ test("returns null for invalid share data", () => { expect(transformShareData([{ type: "message", data: {} as any }])).toBeNull() expect(transformShareData([{ type: "session", data: { id: "s" } as any }])).toBeNull() // no messages }) + +test("formats ingest bootstrap warning", () => { + expect(ingestBootstrapWarning("session-123", new Error("network failed"))).toContain("session-123") + expect(ingestBootstrapWarning("session-123", new Error("network failed"))).toContain("network failed") + expect(ingestBootstrapWarning("session-123", "oops")).toContain("oops") +}) + +test("bootstrapImportedSessionIngest runs bootstrap and does not warn on success", async () => { + const calls: string[] = [] + const warnings: string[] = [] + + await bootstrapImportedSessionIngest("session-success", { + bootstrap: async (sessionId) => { + calls.push(sessionId) + }, + warn: (message) => warnings.push(message), + }) + + expect(calls).toEqual(["session-success"]) + expect(warnings).toHaveLength(0) +}) + +test("bootstrapImportedSessionIngest warns and continues on failure", async () => { + const warnings: string[] = [] + + await expect( + bootstrapImportedSessionIngest("session-fail", { + bootstrap: async () => { + throw new Error("boom") + }, + warn: (message) => warnings.push(message), + }), + ).resolves.toBeUndefined() + + expect(warnings).toHaveLength(1) + expect(warnings[0]).toContain("session-fail") + expect(warnings[0]).toContain("boom") +})