mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-29 03:44:06 +08:00
fix: restore cloud session previews
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
"@kilocode/kilo-gateway": patch
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Restore Cloud Agent transcripts in VS Code session previews and stop cloud session previews or continuation from loading indefinitely when a request stalls.
|
||||
@@ -7,6 +7,7 @@ export interface DrizzleDb {
|
||||
}
|
||||
|
||||
const INGEST_BASE = process.env.KILO_SESSION_INGEST_URL ?? "https://ingest.kilosessions.ai"
|
||||
const TIMEOUT = 30_000
|
||||
|
||||
function exportUrl(sessionId: string) {
|
||||
return UUID_RE.test(sessionId)
|
||||
@@ -18,6 +19,7 @@ export type FetchResult = { ok: true; data: any } | { ok: false; status: number;
|
||||
|
||||
export async function fetchCloudSession(token: string, sessionId: string): Promise<FetchResult> {
|
||||
const response = await fetch(exportUrl(sessionId), {
|
||||
signal: AbortSignal.timeout(TIMEOUT),
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
...buildKiloHeaders(),
|
||||
@@ -33,6 +35,7 @@ export async function fetchCloudSession(token: string, sessionId: string): Promi
|
||||
|
||||
export async function fetchCloudSessionForImport(token: string, sessionId: string): Promise<FetchResult> {
|
||||
const response = await fetch(exportUrl(sessionId), {
|
||||
signal: AbortSignal.timeout(TIMEOUT),
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
...buildKiloHeaders(),
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { fetchCloudSession, fetchCloudSessionForImport } from "../src/cloud-sessions"
|
||||
|
||||
async function expectStalledFetchToTimeOut(run: () => Promise<unknown>) {
|
||||
const fetch = globalThis.fetch
|
||||
const timeout = AbortSignal.timeout
|
||||
let delay: number | undefined
|
||||
|
||||
AbortSignal.timeout = (ms) => {
|
||||
delay = ms
|
||||
const controller = new AbortController()
|
||||
queueMicrotask(() => controller.abort(new DOMException("The operation timed out", "TimeoutError")))
|
||||
return controller.signal
|
||||
}
|
||||
globalThis.fetch = ((_input: RequestInfo | URL, init?: RequestInit) =>
|
||||
new Promise<Response>((_resolve, reject) => {
|
||||
init?.signal?.addEventListener("abort", () => reject(init.signal?.reason), { once: true })
|
||||
})) as typeof globalThis.fetch
|
||||
|
||||
try {
|
||||
const outcome = await Promise.race([
|
||||
run().then(
|
||||
() => "resolved" as const,
|
||||
(err) => {
|
||||
if (err instanceof DOMException && err.name === "TimeoutError") return "timed-out" as const
|
||||
throw err
|
||||
},
|
||||
),
|
||||
Bun.sleep(50).then(() => "still-pending" as const),
|
||||
])
|
||||
|
||||
expect(outcome).toBe("timed-out")
|
||||
expect(delay).toBe(30_000)
|
||||
} finally {
|
||||
globalThis.fetch = fetch
|
||||
AbortSignal.timeout = timeout
|
||||
}
|
||||
}
|
||||
|
||||
describe("cloud session export requests", () => {
|
||||
test("times out a stalled preview request", async () => {
|
||||
await expectStalledFetchToTimeOut(() => fetchCloudSession("token", "session-id"))
|
||||
})
|
||||
|
||||
test("times out a stalled import request", async () => {
|
||||
await expectStalledFetchToTimeOut(() => fetchCloudSessionForImport("token", "session-id"))
|
||||
})
|
||||
})
|
||||
@@ -23,6 +23,7 @@ const kiloVscodeDir = join(import.meta.dir, "..")
|
||||
const packagesDir = join(kiloVscodeDir, "..")
|
||||
const opencodeDir = join(packagesDir, "opencode")
|
||||
const coreDir = join(packagesDir, "core")
|
||||
const gatewayDir = join(packagesDir, "kilo-gateway")
|
||||
const indexingDir = join(packagesDir, "kilo-indexing")
|
||||
|
||||
const targetBinDir = join(kiloVscodeDir, "bin")
|
||||
@@ -38,8 +39,12 @@ async function cliSourceHash(): Promise<string | null> {
|
||||
try {
|
||||
const opencodeResult = await $`git log -1 --format=%H -- .`.cwd(opencodeDir).quiet()
|
||||
const coreResult = await $`git log -1 --format=%H -- .`.cwd(coreDir).quiet()
|
||||
const gatewayResult = await $`git log -1 --format=%H -- .`.cwd(gatewayDir).quiet()
|
||||
const indexingResult = await $`git log -1 --format=%H -- .`.cwd(indexingDir).quiet()
|
||||
return `${opencodeResult.text().trim()}-${coreResult.text().trim()}-${indexingResult.text().trim()}` || null
|
||||
return (
|
||||
`${opencodeResult.text().trim()}-${coreResult.text().trim()}-${gatewayResult.text().trim()}-${indexingResult.text().trim()}` ||
|
||||
null
|
||||
)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
@@ -49,10 +54,12 @@ async function isDirty(): Promise<boolean> {
|
||||
try {
|
||||
const opencodeResult = await $`git status --porcelain -- .`.cwd(opencodeDir).quiet()
|
||||
const coreResult = await $`git status --porcelain -- .`.cwd(coreDir).quiet()
|
||||
const gatewayResult = await $`git status --porcelain -- .`.cwd(gatewayDir).quiet()
|
||||
const indexingResult = await $`git status --porcelain -- .`.cwd(indexingDir).quiet()
|
||||
return (
|
||||
opencodeResult.text().trim().length > 0 ||
|
||||
coreResult.text().trim().length > 0 ||
|
||||
gatewayResult.text().trim().length > 0 ||
|
||||
indexingResult.text().trim().length > 0
|
||||
)
|
||||
} catch {
|
||||
|
||||
@@ -10,6 +10,8 @@ import type { CloudSessionData, EditorContext } from "../../services/cli-backend
|
||||
import { getErrorMessage, sessionToWebview, mapCloudSessionMessageToWebviewMessage } from "../../kilo-provider-utils"
|
||||
import type { MessageFile } from "../message-files"
|
||||
|
||||
const TIMEOUT = 30_000
|
||||
|
||||
export interface CloudSessionContext {
|
||||
readonly client: KiloClient | null
|
||||
currentSession: Session | null
|
||||
@@ -73,7 +75,7 @@ export async function handleRequestCloudSessionData(ctx: CloudSessionContext, se
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await ctx.client.kilo.cloud.session.get({ id: sessionId })
|
||||
const result = await ctx.client.kilo.cloud.session.get({ id: sessionId }, { signal: AbortSignal.timeout(TIMEOUT) })
|
||||
const data = result.data as CloudSessionData | undefined
|
||||
if (!data) {
|
||||
ctx.postMessage({
|
||||
@@ -135,10 +137,13 @@ export async function handleImportAndSend(
|
||||
// Step 1: Import the cloud session with fresh IDs
|
||||
let session: Session | undefined
|
||||
try {
|
||||
const result = await ctx.client.kilo.cloud.session.import({
|
||||
sessionId: cloudSessionId,
|
||||
directory: dir,
|
||||
})
|
||||
const result = await ctx.client.kilo.cloud.session.import(
|
||||
{
|
||||
sessionId: cloudSessionId,
|
||||
directory: dir,
|
||||
},
|
||||
{ signal: AbortSignal.timeout(TIMEOUT) },
|
||||
)
|
||||
session = result.data as Session | undefined
|
||||
} catch (error) {
|
||||
console.error("[Kilo New] KiloProvider: ❌ Cloud session import failed:", error)
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import { describe, expect, it } from "bun:test"
|
||||
import {
|
||||
handleImportAndSend,
|
||||
handleRequestCloudSessionData,
|
||||
type CloudSessionContext,
|
||||
} from "../../src/kilo-provider/handlers/cloud-session"
|
||||
|
||||
function stalled(options?: { signal?: AbortSignal }) {
|
||||
return new Promise<never>((_resolve, reject) => {
|
||||
options?.signal?.addEventListener("abort", () => reject(options.signal?.reason), { once: true })
|
||||
})
|
||||
}
|
||||
|
||||
function context(sent: unknown[]) {
|
||||
return {
|
||||
client: {
|
||||
kilo: {
|
||||
cloud: {
|
||||
session: {
|
||||
get: (_params: { id: string }, options?: { signal?: AbortSignal }) => stalled(options),
|
||||
import: (_params: { sessionId: string; directory: string }, options?: { signal?: AbortSignal }) =>
|
||||
stalled(options),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
currentSession: null,
|
||||
trackedSessionIds: new Set<string>(),
|
||||
connectionService: { recordMessageSessionId: () => undefined },
|
||||
postMessage: (message: unknown) => sent.push(message),
|
||||
getWorkspaceDirectory: () => "/repo",
|
||||
gatherEditorContext: async () => ({}),
|
||||
} as unknown as CloudSessionContext
|
||||
}
|
||||
|
||||
describe("cloud session preview handler", () => {
|
||||
it("reports a failure when the CLI preview request stalls", async () => {
|
||||
const timeout = AbortSignal.timeout
|
||||
AbortSignal.timeout = () => {
|
||||
const controller = new AbortController()
|
||||
queueMicrotask(() => controller.abort(new DOMException("The operation timed out", "TimeoutError")))
|
||||
return controller.signal
|
||||
}
|
||||
|
||||
try {
|
||||
const sent: unknown[] = []
|
||||
const outcome = await Promise.race([
|
||||
handleRequestCloudSessionData(context(sent), "cloud-session").then(() => "resolved" as const),
|
||||
Bun.sleep(50).then(() => "still-pending" as const),
|
||||
])
|
||||
|
||||
expect(outcome).toBe("resolved")
|
||||
expect(sent).toEqual([
|
||||
{
|
||||
type: "cloudSessionImportFailed",
|
||||
cloudSessionId: "cloud-session",
|
||||
error: "The operation timed out",
|
||||
},
|
||||
])
|
||||
} finally {
|
||||
AbortSignal.timeout = timeout
|
||||
}
|
||||
})
|
||||
|
||||
it("reports a failure when the CLI import request stalls", async () => {
|
||||
const timeout = AbortSignal.timeout
|
||||
AbortSignal.timeout = () => {
|
||||
const controller = new AbortController()
|
||||
queueMicrotask(() => controller.abort(new DOMException("The operation timed out", "TimeoutError")))
|
||||
return controller.signal
|
||||
}
|
||||
|
||||
try {
|
||||
const sent: unknown[] = []
|
||||
const outcome = await Promise.race([
|
||||
handleImportAndSend(context(sent), "cloud-session", "Continue").then(() => "resolved" as const),
|
||||
Bun.sleep(50).then(() => "still-pending" as const),
|
||||
])
|
||||
|
||||
expect(outcome).toBe("resolved")
|
||||
expect(sent).toEqual([
|
||||
{
|
||||
type: "cloudSessionImportFailed",
|
||||
cloudSessionId: "cloud-session",
|
||||
error: "The operation timed out",
|
||||
},
|
||||
])
|
||||
} finally {
|
||||
AbortSignal.timeout = timeout
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -188,35 +188,49 @@ export const TranscriptionResponse = Schema.Struct({
|
||||
usage: Schema.optional(Schema.Unknown),
|
||||
})
|
||||
|
||||
export const CloudMessage = Schema.Struct({
|
||||
info: Schema.Struct({
|
||||
id: Schema.String,
|
||||
sessionID: Schema.String,
|
||||
role: Schema.Literals(["user", "assistant"]),
|
||||
time: Schema.Struct({
|
||||
created: Schema.Finite,
|
||||
completed: Schema.optional(Schema.Finite),
|
||||
}),
|
||||
const UnknownRecord = Schema.Record(Schema.String, Schema.Unknown)
|
||||
|
||||
export const CloudMessage = Schema.StructWithRest(
|
||||
Schema.Struct({
|
||||
info: Schema.StructWithRest(
|
||||
Schema.Struct({
|
||||
id: Schema.String,
|
||||
sessionID: Schema.String,
|
||||
role: Schema.Literals(["user", "assistant"]),
|
||||
time: Schema.Struct({
|
||||
created: Schema.Finite,
|
||||
completed: Schema.optional(Schema.Finite),
|
||||
}),
|
||||
}),
|
||||
[UnknownRecord],
|
||||
),
|
||||
parts: Schema.Array(
|
||||
Schema.StructWithRest(
|
||||
Schema.Struct({
|
||||
id: Schema.String,
|
||||
sessionID: Schema.String,
|
||||
messageID: Schema.String,
|
||||
type: Schema.String,
|
||||
}),
|
||||
[UnknownRecord],
|
||||
),
|
||||
),
|
||||
}),
|
||||
parts: Schema.Array(
|
||||
Schema.Struct({
|
||||
id: Schema.String,
|
||||
sessionID: Schema.String,
|
||||
messageID: Schema.String,
|
||||
type: Schema.String,
|
||||
}),
|
||||
),
|
||||
})
|
||||
[UnknownRecord],
|
||||
)
|
||||
|
||||
export const CloudSessionData = Schema.Struct({
|
||||
info: Schema.Struct({
|
||||
id: Schema.String,
|
||||
title: Schema.String,
|
||||
time: Schema.Struct({
|
||||
created: Schema.Finite,
|
||||
updated: Schema.Finite,
|
||||
info: Schema.StructWithRest(
|
||||
Schema.Struct({
|
||||
id: Schema.String,
|
||||
title: Schema.String,
|
||||
time: Schema.Struct({
|
||||
created: Schema.Finite,
|
||||
updated: Schema.Finite,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
[UnknownRecord],
|
||||
),
|
||||
messages: Schema.Array(CloudMessage),
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Schema } from "effect"
|
||||
import { CloudSessionData } from "../../src/kilocode/server/httpapi/groups/kilo-gateway"
|
||||
|
||||
describe("cloud session HTTP schema", () => {
|
||||
test("preserves transcript fields needed by the VS Code preview", () => {
|
||||
const input = {
|
||||
info: {
|
||||
id: "ses_cloud",
|
||||
title: "Cloud transcript",
|
||||
slug: "cloud-transcript",
|
||||
time: { created: 1, updated: 2 },
|
||||
},
|
||||
messages: [
|
||||
{
|
||||
info: {
|
||||
id: "msg_user",
|
||||
sessionID: "ses_cloud",
|
||||
role: "user" as const,
|
||||
agent: "code",
|
||||
time: { created: 3 },
|
||||
},
|
||||
parts: [
|
||||
{
|
||||
id: "prt_text",
|
||||
sessionID: "ses_cloud",
|
||||
messageID: "msg_user",
|
||||
type: "text",
|
||||
text: "Show this cloud message",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
expect(Schema.encodeUnknownSync(CloudSessionData)(input)).toEqual(input)
|
||||
})
|
||||
})
|
||||
@@ -12025,8 +12025,7 @@
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["id", "title", "time"],
|
||||
"additionalProperties": false
|
||||
"required": ["id", "title", "time"]
|
||||
},
|
||||
"messages": {
|
||||
"type": "array",
|
||||
@@ -12060,8 +12059,7 @@
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["id", "sessionID", "role", "time"],
|
||||
"additionalProperties": false
|
||||
"required": ["id", "sessionID", "role", "time"]
|
||||
},
|
||||
"parts": {
|
||||
"type": "array",
|
||||
@@ -12081,13 +12079,11 @@
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["id", "sessionID", "messageID", "type"],
|
||||
"additionalProperties": false
|
||||
"required": ["id", "sessionID", "messageID", "type"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["info", "parts"],
|
||||
"additionalProperties": false
|
||||
"required": ["info", "parts"]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -12170,7 +12166,6 @@
|
||||
}
|
||||
},
|
||||
"required": ["id", "title", "time"],
|
||||
"additionalProperties": false,
|
||||
"description": "Imported session info"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user