mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-29 03:32:08 +08:00
fix(cli): handle sqlite lock errors
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Show a concise retryable message when concurrent Kilo processes temporarily lock the SQLite database instead of printing the full server error trace.
|
||||
@@ -0,0 +1,7 @@
|
||||
import { isSqlError } from "effect/unstable/sql/SqlError"
|
||||
|
||||
export const busyMessage = "Database is busy. Please try again in a moment."
|
||||
|
||||
export function isBusy(error: unknown) {
|
||||
return isSqlError(error) && error.reason._tag === "LockTimeoutError"
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Image } from "@/image/image" // kilocode_change - classify user image validation defects
|
||||
import { busyMessage, isBusy } from "@/kilocode/database/sqlite-error" // kilocode_change
|
||||
import { KiloSessionHttpApi } from "@/kilocode/server/httpapi/session-fork" // kilocode_change
|
||||
import { BlockedError as AgentRequirementError } from "@/kilocode/agent-requirements" // kilocode_change
|
||||
import { KiloSessionPromptQueue } from "@/kilocode/session/prompt-queue" // kilocode_change
|
||||
@@ -322,13 +323,21 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
|
||||
Effect.catchCause((cause) => {
|
||||
if (Cause.hasInterruptsOnly(cause)) return Effect.void // kilocode_change - Stop is not an error
|
||||
return Effect.gen(function* () {
|
||||
yield* Effect.logError("prompt_async failed", { sessionID: ctx.params.sessionID, cause })
|
||||
const error = Cause.squash(cause)
|
||||
// kilocode_change start - keep SQLite lock failures out of local CLI logs
|
||||
const busy = isBusy(error)
|
||||
if (busy) {
|
||||
yield* Effect.logWarning("prompt_async database busy", { sessionID: ctx.params.sessionID })
|
||||
}
|
||||
if (!busy) yield* Effect.logError("prompt_async failed", { sessionID: ctx.params.sessionID, cause })
|
||||
// kilocode_change end
|
||||
yield* events.publish(Session.Event.Error, {
|
||||
sessionID: ctx.params.sessionID,
|
||||
error: AgentRequirementError.isInstance(error)
|
||||
? error.toObject()
|
||||
: new NamedError.Unknown({ message: Cause.pretty(cause) }).toObject(),
|
||||
: busy // kilocode_change
|
||||
? new NamedError.Unknown({ message: busyMessage }).toObject() // kilocode_change
|
||||
: new NamedError.Unknown({ message: Cause.pretty(cause) }).toObject(), // kilocode_change
|
||||
})
|
||||
})
|
||||
}),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NamedError } from "@opencode-ai/core/util/error"
|
||||
import { ConfigErrorV1 } from "@opencode-ai/core/v1/config/error"
|
||||
import { busyMessage, isBusy } from "@/kilocode/database/sqlite-error" // kilocode_change
|
||||
import { Cause, Effect } from "effect"
|
||||
import { HttpRouter, HttpServerError, HttpServerRespondable, HttpServerResponse } from "effect/unstable/http"
|
||||
|
||||
@@ -16,6 +17,19 @@ export const errorLayer = HttpRouter.middleware<{ handles: unknown }>()((effect)
|
||||
if (!defect) return Effect.failCause(cause)
|
||||
|
||||
const error = defect.defect
|
||||
// kilocode_change start - SQLite lock contention is expected with multiple local clients
|
||||
if (isBusy(error)) {
|
||||
const ref = `err_${crypto.randomUUID().slice(0, 8)}`
|
||||
return Effect.logWarning("database busy", { ref }).pipe(
|
||||
Effect.as(
|
||||
HttpServerResponse.jsonUnsafe(
|
||||
new NamedError.Unknown({ message: busyMessage, ref }).toObject(),
|
||||
{ status: 503 },
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
// kilocode_change end
|
||||
if (
|
||||
ConfigErrorV1.JsonError.isInstance(error) ||
|
||||
ConfigErrorV1.InvalidError.isInstance(error) ||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { LockTimeoutError, SqlError, UnknownError } from "effect/unstable/sql/SqlError"
|
||||
import { busyMessage, isBusy } from "@/kilocode/database/sqlite-error"
|
||||
|
||||
describe("SQLite errors", () => {
|
||||
test("recognizes lock timeouts as busy database errors", () => {
|
||||
const error = new SqlError({
|
||||
reason: new LockTimeoutError({
|
||||
cause: new Error("database is locked"),
|
||||
message: "Failed to execute statement",
|
||||
operation: "execute",
|
||||
}),
|
||||
})
|
||||
|
||||
expect(isBusy(error)).toBe(true)
|
||||
expect(busyMessage).toBe("Database is busy. Please try again in a moment.")
|
||||
})
|
||||
|
||||
test("does not classify other SQLite errors as busy", () => {
|
||||
const error = new SqlError({
|
||||
reason: new UnknownError({
|
||||
cause: new Error("constraint failed"),
|
||||
message: "Failed to execute statement",
|
||||
operation: "execute",
|
||||
}),
|
||||
})
|
||||
|
||||
expect(isBusy(error)).toBe(false)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user