fix(cli): address network reconnect review

This commit is contained in:
Catriel Müller
2026-05-18 10:14:43 -03:00
parent 413222f013
commit ad8d57707d
5 changed files with 66 additions and 11 deletions
@@ -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
+20 -5
View File
@@ -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 })
})
},
+38 -3
View File
@@ -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({