fix(cli): handle sqlite lock errors

This commit is contained in:
marius-kilocode
2026-08-05 10:25:56 +02:00
parent 974f03203a
commit c9199cb529
5 changed files with 67 additions and 2 deletions
+5
View File
@@ -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)
})
})