Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/quiet-sqlite-lock-errors.md
Original file line number Diff line number Diff line change
@@ -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.
7 changes: 7 additions & 0 deletions packages/opencode/src/kilocode/database/sqlite-error.ts
Original file line number Diff line number Diff line change
@@ -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"
}
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
})
})
}),
Expand Down
Original file line number Diff line number Diff line 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"

Expand All @@ -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) ||
Expand Down
30 changes: 30 additions & 0 deletions packages/opencode/test/kilocode/database/sqlite-error.test.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})
Loading