fix(opencode): resolve race conditions and update tests for kilo compatibility

Remove eager publish call in indexing initialization that caused
premature event emission. Add foreign key constraint tolerance in
next-message projector to handle writes racing against deleted
sessions. Fix test environment isolation for gateway headers and
permission tests, update auth test assertions to use "kilo" username,
and remove stale indexing route from httpapi-bridge expectations.
This commit is contained in:
Imanol Maiztegui
2026-05-20 12:05:59 +02:00
parent 055a932c66
commit 32777136cd
6 changed files with 44 additions and 13 deletions
@@ -203,7 +203,6 @@ export namespace KiloIndexing {
await Bus.publish(Event, { status: current() })
}
await publish()
return {
current,
publish,
@@ -8,10 +8,22 @@ import { SyncEvent } from "@/sync"
import { SessionMessageTable, SessionTable } from "./session.sql"
import type { SessionID } from "./schema"
import { Schema } from "effect"
import { Log } from "@opencode-ai/core/util/log" // kilocode_change
const decodeMessage = Schema.decodeUnknownSync(SessionMessage.Message)
type SessionMessageData = NonNullable<(typeof SessionMessageTable.$inferInsert)["data"]>
// kilocode_change start - tolerate next-message writes that race deleted sessions
const log = Log.create({ service: "session.projector.next" })
// Duplicated from projectors.ts to minimize merge conflicts and avoid a circular dependency.
function foreign(err: unknown) {
if (typeof err !== "object" || err === null) return false
if ("code" in err && err.code === "SQLITE_CONSTRAINT_FOREIGNKEY") return true
return "message" in err && typeof err.message === "string" && err.message.includes("FOREIGN KEY constraint failed")
}
// kilocode_change end
function encodeDateTimes(value: unknown): unknown {
if (DateTime.isDateTime(value)) return DateTime.toEpochMillis(value)
if (Array.isArray(value)) return value.map(encodeDateTimes)
@@ -115,7 +127,14 @@ function sqlite(db: Database.TxOrDb, sessionID: SessionID): SessionMessageUpdate
}
function update(db: Database.TxOrDb, event: SessionEvent.Event) {
SessionMessageUpdater.update(sqlite(db, event.data.sessionID), event)
// kilocode_change start - tolerate next-message writes that race deleted sessions
try {
SessionMessageUpdater.update(sqlite(db, event.data.sessionID), event)
} catch (err) {
if (!foreign(err)) throw err
log.warn("ignored late next-message update", { eventID: event.id, sessionID: event.data.sessionID })
}
// kilocode_change end
}
export default [
@@ -1,6 +1,6 @@
import { describe, it, expect, afterEach } from "bun:test"
import { describe, it, expect, afterEach, beforeEach } from "bun:test"
import { buildKiloHeaders, getFeatureHeader, getEditorNameHeader } from "@kilocode/kilo-gateway"
import { HEADER_FEATURE, ENV_FEATURE, ENV_VERSION, DEFAULT_EDITOR_NAME } from "@kilocode/kilo-gateway"
import { HEADER_FEATURE, ENV_FEATURE, ENV_EDITOR_NAME, ENV_VERSION, DEFAULT_EDITOR_NAME } from "@kilocode/kilo-gateway"
describe("getFeatureHeader", () => {
const original = process.env[ENV_FEATURE]
@@ -31,6 +31,11 @@ describe("getFeatureHeader", () => {
describe("getEditorNameHeader", () => {
const originalVersion = process.env[ENV_VERSION]
const originalEditor = process.env[ENV_EDITOR_NAME]
beforeEach(() => {
delete process.env[ENV_EDITOR_NAME]
})
afterEach(() => {
if (originalVersion === undefined) {
@@ -38,6 +43,12 @@ describe("getEditorNameHeader", () => {
} else {
process.env[ENV_VERSION] = originalVersion
}
if (originalEditor === undefined) {
delete process.env[ENV_EDITOR_NAME]
} else {
process.env[ENV_EDITOR_NAME] = originalEditor
}
})
it("returns default editor name without version when KILOCODE_VERSION is not set", () => {
@@ -1,17 +1,21 @@
import { afterEach, describe, expect, test } from "bun:test"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Permission } from "../../../src/permission"
import { PermissionID } from "../../../src/permission/schema"
import { WithInstance } from "../../../src/project/with-instance"
import { Session } from "../../../src/session/session"
import { tmpdir } from "../../fixture/fixture"
const original = Flag.KILO_EXPERIMENTAL_HTTPAPI
afterEach(() => {
delete process.env["KILO_EXPERIMENTAL_HTTPAPI"]
Flag.KILO_EXPERIMENTAL_HTTPAPI = original
})
async function app() {
async function app(experimental = false) {
const { Server } = await import("../../../src/server/server")
return Server.Default().app
Flag.KILO_EXPERIMENTAL_HTTPAPI = experimental
return experimental ? Server.Default().app : Server.Legacy().app
}
describe("POST /permission/:requestID/reply", () => {
@@ -119,13 +123,12 @@ describe("POST /permission/:requestID/reply", () => {
})
test("returns 404 for unknown replies when experimental HttpApi is enabled", async () => {
process.env["KILO_EXPERIMENTAL_HTTPAPI"] = "1"
await using tmp = await tmpdir({ git: true })
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
const server = await app()
const server = await app(true)
const response = await server.request("/permission/permission_missing/reply", {
method: "POST",
+3 -3
View File
@@ -22,12 +22,12 @@ describe("ServerAuth", () => {
expect(ServerAuth.headers()).toBeUndefined()
})
test("defaults to the opencode username", () => {
test("defaults to the kilo username", () => { // kilocode_change
Flag.KILO_SERVER_PASSWORD = "secret"
Flag.KILO_SERVER_USERNAME = undefined
expect(ServerAuth.headers()).toEqual({
Authorization: `Basic ${Buffer.from("opencode:secret").toString("base64")}`,
Authorization: `Basic ${Buffer.from("kilo:secret").toString("base64")}`, // kilocode_change
})
})
@@ -54,6 +54,6 @@ describe("ServerAuth", () => {
expect(ServerAuth.required(config)).toBe(true)
expect(ServerAuth.authorized({ username: "alice", password: Redacted.make("secret") }, config)).toBe(true)
expect(ServerAuth.authorized({ username: "opencode", password: Redacted.make("secret") }, config)).toBe(false)
expect(ServerAuth.authorized({ username: "kilo", password: Redacted.make("secret") }, config)).toBe(false) // kilocode_change
})
})
@@ -230,7 +230,6 @@ describe("HttpApi server", () => {
"GET /api/session",
"GET /api/session/{sessionID}/context",
"GET /api/session/{sessionID}/message",
"GET /indexing/status", // kilocode_change - Kilo Effect-only indexing route
"POST /api/session/{sessionID}/compact",
"POST /api/session/{sessionID}/prompt",
"POST /api/session/{sessionID}/wait",