mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-21 14:07:20 +08:00
fix(cli): address network reconnect review
This commit is contained in:
@@ -298,7 +298,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
const match = Binary.search(requests, event.properties.requestID, (r) => r.id)
|
||||
if (match.found) {
|
||||
setStore("network", event.properties.sessionID, match.index, "restored", true)
|
||||
setStore("network", event.properties.sessionID, match.index, "time", "restored", event.properties.time)
|
||||
setStore("network", event.properties.sessionID, match.index, "time", "restored", event.properties.time) // kilocode_change
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
@@ -29,7 +29,12 @@ export function NetworkPrompt(props: { request: SessionNetworkWait }) {
|
||||
setCountdown(10)
|
||||
return
|
||||
}
|
||||
const restored = props.request.time.restored ?? Date.now()
|
||||
const restored = props.request.time.restored
|
||||
if (restored === undefined) {
|
||||
console.warn("session.network restored without restore time", props.request.id)
|
||||
setCountdown(0)
|
||||
return
|
||||
}
|
||||
const remaining = () => Math.max(0, 10 - Math.floor((Date.now() - restored) / 1000))
|
||||
setCountdown(remaining())
|
||||
const timer = setInterval(() => {
|
||||
|
||||
@@ -14,7 +14,7 @@ import type { LanguageModelV3 } from "@ai-sdk/provider"
|
||||
import { mapValues, omit, pickBy } from "remeda"
|
||||
|
||||
/** Default timeout (ms) for provider HTTP requests (connection phase). */
|
||||
export const REQUEST_TIMEOUT_MS = 180_000 // 3 minutes
|
||||
export const REQUEST_TIMEOUT_MS = 300_000 // 5 minutes
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Bundled providers
|
||||
|
||||
@@ -24,7 +24,6 @@ export namespace SessionNetwork {
|
||||
"ENETUNREACH",
|
||||
"EHOSTUNREACH",
|
||||
"ENETDOWN",
|
||||
"EPIPE",
|
||||
"UND_ERR_CONNECT_TIMEOUT",
|
||||
"UND_ERR_HEADERS_TIMEOUT",
|
||||
"UND_ERR_SOCKET",
|
||||
@@ -108,6 +107,7 @@ export namespace SessionNetwork {
|
||||
QuestionID,
|
||||
{
|
||||
info: Types.Mutable<Wait>
|
||||
abort: AbortSignal
|
||||
resolve: () => void
|
||||
reject: (e: unknown) => void
|
||||
}
|
||||
@@ -182,7 +182,6 @@ export namespace SessionNetwork {
|
||||
if (match === "ENETUNREACH") return "Network is unreachable"
|
||||
if (match === "EHOSTUNREACH") return "Host is unreachable"
|
||||
if (match === "ENETDOWN") return "Network is down"
|
||||
if (match === "EPIPE") return "Network connection closed"
|
||||
if (match === "UND_ERR_CONNECT_TIMEOUT") return "Connection timed out"
|
||||
if (match === "UND_ERR_HEADERS_TIMEOUT") return "Request timed out"
|
||||
if (match === "UND_ERR_SOCKET") return "Network socket failed"
|
||||
@@ -218,8 +217,23 @@ export namespace SessionNetwork {
|
||||
).catch(() => false)
|
||||
}
|
||||
|
||||
async function resume(input: { requestID: QuestionID }) {
|
||||
await new Promise((resolve) => setTimeout(resolve, RESUME_MS))
|
||||
async function delay(abort: AbortSignal) {
|
||||
if (abort.aborted) return false
|
||||
return new Promise<boolean>((resolve) => {
|
||||
const timer = setTimeout(() => {
|
||||
abort.removeEventListener("abort", onAbort)
|
||||
resolve(true)
|
||||
}, RESUME_MS)
|
||||
function onAbort() {
|
||||
clearTimeout(timer)
|
||||
resolve(false)
|
||||
}
|
||||
abort.addEventListener("abort", onAbort, { once: true })
|
||||
})
|
||||
}
|
||||
|
||||
async function resume(input: { requestID: QuestionID; abort: AbortSignal }) {
|
||||
if (!(await delay(input.abort))) return
|
||||
const s = await state()
|
||||
const req = s.pending.get(input.requestID)
|
||||
if (!req || !req.info.restored) return
|
||||
@@ -266,6 +280,7 @@ export namespace SessionNetwork {
|
||||
}
|
||||
s.pending.set(id, {
|
||||
info,
|
||||
abort: input.abort,
|
||||
resolve: () => {
|
||||
input.abort.removeEventListener("abort", onAbort)
|
||||
resolve()
|
||||
@@ -307,7 +322,7 @@ export namespace SessionNetwork {
|
||||
requestID: req.info.id,
|
||||
time,
|
||||
})
|
||||
void resume({ requestID }).catch((err) => {
|
||||
void resume({ requestID, abort: req.abort }).catch((err) => {
|
||||
log.error("auto resume failed", { err, requestID })
|
||||
})
|
||||
},
|
||||
|
||||
@@ -7,18 +7,30 @@ import { SessionNetwork } from "../../src/session/network"
|
||||
import { SessionID } from "../../src/session/schema"
|
||||
|
||||
const timer = globalThis.setTimeout
|
||||
const clear = globalThis.clearTimeout
|
||||
|
||||
afterEach(() => {
|
||||
globalThis.setTimeout = timer
|
||||
globalThis.clearTimeout = clear
|
||||
})
|
||||
|
||||
function manual() {
|
||||
const jobs: TimerHandler[] = []
|
||||
const state = {
|
||||
next: 0,
|
||||
jobs: new Map<number, TimerHandler>(),
|
||||
}
|
||||
globalThis.setTimeout = ((cb: TimerHandler) => {
|
||||
jobs.push(cb)
|
||||
return 0 as unknown as ReturnType<typeof setTimeout>
|
||||
const id = state.next + 1
|
||||
state.next = id
|
||||
state.jobs.set(id, cb)
|
||||
return id as unknown as ReturnType<typeof setTimeout>
|
||||
}) as unknown as typeof setTimeout
|
||||
globalThis.clearTimeout = ((id: ReturnType<typeof setTimeout>) => {
|
||||
state.jobs.delete(id as unknown as number)
|
||||
}) as unknown as typeof clearTimeout
|
||||
return () => {
|
||||
const jobs = Array.from(state.jobs.values())
|
||||
state.jobs.clear()
|
||||
for (const job of jobs) {
|
||||
if (typeof job === "function") job()
|
||||
}
|
||||
@@ -32,6 +44,7 @@ describe("session.network", () => {
|
||||
expect(SessionNetwork.disconnected({ code: "EAI_AGAIN" })).toBe(true)
|
||||
expect(SessionNetwork.disconnected({ code: "EHOSTUNREACH" })).toBe(true)
|
||||
expect(SessionNetwork.disconnected({ code: "UND_ERR_CONNECT_TIMEOUT" })).toBe(true)
|
||||
expect(SessionNetwork.disconnected({ code: "EPIPE" })).toBe(false)
|
||||
expect(SessionNetwork.disconnected({ code: "ENOENT" })).toBe(false)
|
||||
})
|
||||
|
||||
@@ -136,6 +149,28 @@ describe("session.network", () => {
|
||||
})
|
||||
})
|
||||
|
||||
test("abort cancels restored auto-resume timer", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const run = manual()
|
||||
const abort = new AbortController()
|
||||
const { promise } = await SessionNetwork.ask({
|
||||
sessionID: SessionID.make("ses_test"),
|
||||
message: "Connection refused",
|
||||
abort: abort.signal,
|
||||
})
|
||||
const req = (await SessionNetwork.list())[0]!
|
||||
await SessionNetwork.restore({ requestID: req.id })
|
||||
abort.abort()
|
||||
await expect(promise).rejects.toBeInstanceOf(DOMException)
|
||||
run()
|
||||
expect(await SessionNetwork.list()).toHaveLength(0)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("reject rejects pending request", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
await Instance.provide({
|
||||
|
||||
Reference in New Issue
Block a user