Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/cloud-fork-import-errors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@kilocode/cli": patch
---

Surface the underlying reason when `kilo --cloud-fork` fails to import a cloud session (HTTP status, server message, or fetch error) in both the user-visible message and the DEBUG log stream.
44 changes: 24 additions & 20 deletions packages/opencode/src/cli/cmd/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,14 @@ import { FormatError, FormatUnknownError } from "../error"
import { INTERACTIVE_INPUT_ERROR, resolveInteractiveStdin } from "./run/runtime.stdin"
import { event as normalizeEvent } from "./run/event"
import { importCloudSession, validateCloudFork } from "@/kilocode/cloud-session" // kilocode_change
import { errorMessage } from "@/util/error" // kilocode_change
import { Log } from "@opencode-ai/core/util/log" // kilocode_change
import { KiloRunAuto } from "@/kilocode/cli/run-auto" // kilocode_change
import { KiloHeadless } from "@/kilocode/permission/headless" // kilocode_change
import { KiloRun, KiloRunDaemon } from "@/kilocode/cli/cmd/run" // kilocode_change

const runtimeTask = import("./run/runtime")
const log = Log.create({ service: "cli.run" }) // kilocode_change
type ModelInput = Parameters<KiloClient["session"]["prompt"]>[0]["model"]

function pick(value: string | undefined): ModelInput | undefined {
Expand Down Expand Up @@ -441,29 +444,30 @@ export const RunCommand = effectCmd({
async function session(sdk: KiloClient): Promise<SessionInfo | undefined> {
// kilocode_change start - import cloud session before local lookup
if (args.session && args["cloud-fork"]) {
const id = await importCloudSession(sdk, args.session).catch(() => undefined)
if (!id) {
UI.error("Failed to import session from cloud")
process.exit(1)
}

const current = await sdk.session
.get({
sessionID: id,
})
.catch(() => undefined)
try {
const id = await importCloudSession(sdk, args.session)
const current = await sdk.session
.get({
sessionID: id,
})
.catch(() => undefined)

if (!current?.data) {
UI.error("Session not found")
process.exit(1)
}

if (!current?.data) {
UI.error("Session not found")
return {
id: current.data.id,
title: current.data.title,
directory: current.data.directory,
model: current.data.model,
}
} catch (err) {
log.debug("failed to import cloud session", { err })
UI.error(`Failed to import session from cloud: ${errorMessage(err)}`)
process.exit(1)
}

return {
id: current.data.id,
title: current.data.title,
directory: current.data.directory,
model: current.data.model,
}
}
// kilocode_change end

Expand Down
15 changes: 10 additions & 5 deletions packages/opencode/src/cli/cmd/tui/attach.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { cmd } from "../cmd"
import { Log } from "@opencode-ai/core/util/log" // kilocode_change
import { UI } from "@/cli/ui"
import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32"
import { TuiConfig } from "@/cli/cmd/tui/config/tui"
Expand All @@ -8,6 +9,8 @@ import { errorMessage } from "@/util/error"
import { validateSession } from "./validate-session"
import { ServerAuth } from "@/server/auth"

const log = Log.create({ service: "tui.attach" }) // kilocode_change

export const AttachCommand = cmd({
command: "attach <url>",
describe: "attach to a running kilo server", // kilocode_change
Expand Down Expand Up @@ -89,14 +92,16 @@ export const AttachCommand = cmd({
directory,
headers,
})
const id = await importCloudSession(sdk, args.session).catch(() => undefined)
if (!id) {
UI.error("Failed to import session from cloud")
try {
const id = await importCloudSession(sdk, args.session)
args.session = id
args.cloudFork = false
} catch (err) {
log.debug("failed to import cloud session", { err })
UI.error(`Failed to import session from cloud: ${errorMessage(err)}`)
process.exitCode = 1
return
}
args.session = id
args.cloudFork = false
}
// kilocode_change end
const config = await TuiConfig.get()
Expand Down
12 changes: 7 additions & 5 deletions packages/opencode/src/cli/cmd/tui/thread.ts
Original file line number Diff line number Diff line change
Expand Up @@ -334,14 +334,16 @@ export const TuiThreadCommand = cmd({
headers: transport.headers, // kilocode_change
directory: cwd,
})
const id = await importCloudSession(sdk, args.session).catch(() => undefined)
if (!id) {
UI.error("Failed to import session from cloud")
try {
const id = await importCloudSession(sdk, args.session)
args.session = id
args.cloudFork = false
} catch (err) {
Log.Default.debug("failed to import cloud session", { err })
UI.error(`Failed to import session from cloud: ${errorMessage(err)}`)
shutdownAndExit({ reason: "cloud-fork-failed", code: 1 })
return
}
args.session = id
args.cloudFork = false
}
// kilocode_change end

Expand Down
18 changes: 12 additions & 6 deletions packages/opencode/src/kilocode/cli/cmd/tui/thread.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { randomUUID } from "node:crypto"
import { Log } from "@opencode-ai/core/util/log"
import { UI } from "@/cli/ui"
import type { NetworkOptions } from "@/cli/network"
import { ServerAuth } from "@/server/auth"
Expand All @@ -10,6 +11,8 @@ import { importCloudSession } from "@/kilocode/cloud-session"
import { DaemonClient } from "@/kilocode/daemon/client"
import { createKiloClient } from "@kilocode/sdk/v2"

const log = Log.create({ service: "kilo.tui.thread" })

type TuiInput = Parameters<typeof import("@/cli/cmd/tui/app").tui>[0]
export type StartInput = Omit<TuiInput, "renderer">

Expand Down Expand Up @@ -39,12 +42,15 @@ async function session(input: Input, daemon: DaemonClient.Connection) {
directory: input.cwd,
headers: daemon.headers,
})
const id = await importCloudSession(client, input.args.session).catch(() => undefined)
if (id) return { ok: true as const, id }

UI.error("Failed to import session from cloud")
process.exitCode = 1
return { ok: false as const }
try {
const id = await importCloudSession(client, input.args.session)
return { ok: true as const, id }
} catch (err) {
log.debug("failed to import cloud session", { err })
UI.error(`Failed to import session from cloud: ${errorMessage(err)}`)
process.exitCode = 1
return { ok: false as const }
}
}

export namespace KiloTuiThreadDaemon {
Expand Down
14 changes: 11 additions & 3 deletions packages/opencode/src/kilocode/cloud-session.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { errorMessage } from "@/util/error"

/**
* Validate --cloud-fork flag combinations and return an error message if invalid.
*/
Expand All @@ -21,20 +23,26 @@ export function localSessionID(args: { cloudFork?: boolean; session?: string })
* Import a cloud session to local storage and return the new local session ID.
* Wraps the SDK's `.kilo.cloud.session.import()` which returns `unknown` due to
* the OpenAPI spec not typing the response.
*
* Throws when the import fails: with the server's error message on an HTTP
* error, or with "cloud session import returned no session id" when the
* response was malformed.
*/
export async function importCloudSession(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: Consider consolidating the duplicated try/catch pattern

The try/catch + log.debug("failed to import cloud session", { err }) + UI.error(\Failed to import session from cloud: ${errorMessage(err)}`) sequence is repeated near-verbatim across four call sites, three of which are shared upstream files (run.ts, attach.ts, thread.ts). Since each site's only real difference is the exit mechanism (process.exit, process.exitCode, shutdownAndExit), a small helper here (e.g. importCloudSessionOrThrowthat logs before rethrowing, or a shareddescribeCloudImportError(err)formatter) could shrink the diff each shared file carries against upstream — this repo's stated top priority for files outsidekilocode`-named paths.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — addressed in commit 7138f93. Extracted a shared
eportCloudImportError(err)\ helper in \packages/opencode/src/kilocode/cloud-session.ts\ that logs the failure at DEBUG, prints the real reason via \UI.error, then rethrows. The four call sites now delegate to it and keep their own exit semantics, so the three shared upstream files only carry a single marked import instead of a duplicated try/catch — shrinking the diff against upstream as suggested.

client: {
kilo: {
cloud: {
session: {
import: (params: { sessionId: string }) => Promise<{ data?: unknown }>
import: (params: { sessionId: string }) => Promise<{ data?: unknown; error?: unknown }>
}
}
}
},
sessionId: string,
): Promise<string | undefined> {
): Promise<string> {
const result = await client.kilo.cloud.session.import({ sessionId })
if (result.error) throw new Error(errorMessage(result.error))
const id = (result.data as Record<string, unknown>)?.id
return typeof id === "string" ? id : undefined
if (typeof id !== "string") throw new Error("cloud session import returned no session id")
return id
}
35 changes: 35 additions & 0 deletions packages/opencode/test/kilocode/cloud-session.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { describe, expect, test } from "bun:test"
import { importCloudSession } from "../../src/kilocode/cloud-session"

type ImportResult = { data?: unknown; error?: unknown }

const client = (imp: (params: { sessionId: string }) => Promise<ImportResult>) =>
({ kilo: { cloud: { session: { import: imp } } } }) as Parameters<typeof importCloudSession>[0]

describe("importCloudSession", () => {
test("returns local id on success", async () => {
const c = client(async () => ({ data: { id: "ses_local" } }))
const id = await importCloudSession(c, "ses_cloud")
expect(id).toBe("ses_local")
})

test("throws when server returns HTTP error", async () => {
const c = client(async () => ({
data: undefined,
error: { name: "GatewayError", message: "session not found", status: 404 },
}))
await expect(importCloudSession(c, "ses_cloud")).rejects.toThrow("session not found")
})

test("throws when data.id is missing", async () => {
const c = client(async () => ({ data: {} }))
await expect(importCloudSession(c, "ses_cloud")).rejects.toThrow()
})

test("propagates thrown fetch exceptions", async () => {
const c = client(async () => {
throw new Error("network down")
})
await expect(importCloudSession(c, "ses_cloud")).rejects.toThrow("network down")
})
})