fix: resolve OpenCode v1.15.4 merge regressions

This commit is contained in:
marius-kilocode
2026-06-12 19:45:14 +02:00
parent 6a1377abaa
commit 06e87cf653
41 changed files with 472 additions and 117 deletions
+1 -1
View File
@@ -285,7 +285,7 @@ for (const item of targets) {
const sessionExportWorkerPath = "./src/kilocode/session-export/worker.ts" // kilocode_change
const indexingWorkerPath = "./src/kilocode/indexing-worker.ts" // kilocode_change
// Use platform-specific bunfs root path based on target OS // kilocode_change
// Use platform-specific bunfs root path based on target OS
const bunfsRoot = item.os === "win32" ? "B:/~BUN/root/" : "/$bunfs/root/"
const workerRelativePath = path.relative(dir, parserWorker).replaceAll("\\", "/")
+2 -2
View File
@@ -544,7 +544,7 @@ export class Agent implements ACPAgent {
"terminal-auth": {
command: "opencode",
args: ["auth", "login"],
label: "Kilo Login", // kilocode_change
label: "Kilo Login",
},
}
}
@@ -570,7 +570,7 @@ export class Agent implements ACPAgent {
},
authMethods: [authMethod],
agentInfo: {
name: "Kilo", // kilocode_change
name: "Kilo",
version: InstallationVersion,
},
}
+2 -2
View File
@@ -1,4 +1,4 @@
import { Effect, Exit, Fiber, Layer, PubSub, Scope, Context, Stream, Schema } from "effect"
import { Effect, Exit, Fiber, Layer, PubSub, Scope, Context, Stream, Schema } from "effect" // kilocode_change
import { EffectBridge } from "@/effect/bridge"
import * as Log from "@opencode-ai/core/util/log"
import { BusEvent } from "./bus-event"
@@ -6,7 +6,7 @@ import { GlobalBus } from "./global"
import { InstanceState } from "@/effect/instance-state"
import { makeRuntime } from "@/effect/run-service"
import { Identifier } from "@/id/id"
import { context as instanceContext, type InstanceContext } from "@/project/instance-context"
import { context as instanceContext, type InstanceContext } from "@/project/instance-context" // kilocode_change
import { InstanceRef } from "@/effect/instance-ref"
import { LocalContext } from "@/util/local-context" // kilocode_change
+2 -2
View File
@@ -488,7 +488,7 @@ export const GithubRunCommand = effectCmd({
? (payload as IssueCommentEvent | IssuesEvent).issue.number
: (payload as PullRequestEvent | PullRequestReviewCommentEvent).pull_request.number
const runUrl = `/${owner}/${repo}/actions/runs/${runId}`
const shareBaseUrl = isMock ? "https://dev.kilo.ai" : "https://kilo.ai" // kilocode_change
const shareBaseUrl = isMock ? "https://dev.kilo.ai" : "https://kilo.ai"
let appToken: string
let octoRest: Octokit
@@ -749,7 +749,7 @@ export const GithubRunCommand = effectCmd({
function normalizeOidcBaseUrl(): string {
const value = process.env["OIDC_BASE_URL"]
if (!value) return "https://api.kilo.ai" // kilocode_change
if (!value) return "https://api.kilo.ai"
return value.replace(/\/+$/, "")
}
+1 -1
View File
@@ -203,7 +203,7 @@ export const RunCommand = effectCmd({
})
.option("attach", {
type: "string",
describe: "attach to a running kilo server (e.g., http://localhost:4096)", // kilocode_change
describe: "attach to a running kilo server (e.g., http://localhost:4096)",
})
.option("password", {
alias: ["p"],
@@ -317,7 +317,7 @@ export function DialogSessionList() {
},
{
command: "session.rename",
title: "rename", // kilocode_change
title: "rename",
// kilocode_change start
onTrigger: async (option) => {
const item = sessions().find((x) => x.id === option.value)
@@ -233,15 +233,15 @@ const TIPS: Tip[] = [
"Tool definitions can invoke scripts written in Python, Go, etc",
"Add {highlight}.ts{/highlight} files to {highlight}.opencode/plugins/{/highlight} for event hooks",
"Use plugins to send OS notifications when sessions complete",
"Create a plugin to prevent Kilo from reading sensitive files", // kilocode_change
"Create a plugin to prevent Kilo from reading sensitive files",
"Use {highlight}kilo run{/highlight} for non-interactive scripting", // kilocode_change
"Use {highlight}kilo --continue{/highlight} to resume the last session", // kilocode_change
"Use {highlight}kilo run -f file.ts{/highlight} to attach files via CLI", // kilocode_change
"Use {highlight}--format json{/highlight} for machine-readable output in scripts",
"Run {highlight}kilo serve{/highlight} for headless API access to Kilo", // kilocode_change
"Run {highlight}kilo serve{/highlight} for headless API access to Kilo",
"Use {highlight}kilo run --attach{/highlight} to connect to a running server", // kilocode_change
"Run {highlight}kilo upgrade{/highlight} to update to the latest version", // kilocode_change
"Run {highlight}kilo auth list{/highlight} to see all configured providers", // kilocode_change
"Run {highlight}kilo upgrade{/highlight} to update to the latest version",
"Run {highlight}kilo auth list{/highlight} to see all configured providers",
"Run {highlight}kilo agent create{/highlight} for guided agent creation", // kilocode_change
"Use {highlight}/opencode{/highlight} in GitHub issues/PRs to trigger AI actions",
"Run {highlight}kilo github install{/highlight} to set up the GitHub workflow", // kilocode_change
+1 -1
View File
@@ -33,7 +33,7 @@ function systemManagedConfigDir(): string {
}
export function managedConfigDir() {
return process.env.KILO_TEST_MANAGED_CONFIG_DIR || systemManagedConfigDir() // kilocode_change
return process.env.KILO_TEST_MANAGED_CONFIG_DIR || systemManagedConfigDir()
}
export function parseManagedPlist(json: string): string {
+2 -2
View File
@@ -24,7 +24,7 @@ export const directories = Effect.fn("ConfigPaths.directories")(function* (direc
const afs = yield* AppFileSystem.Service
return unique([
Global.Path.config,
...(!Flag.KILO_DISABLE_PROJECT_CONFIG // kilocode_change
...(!Flag.KILO_DISABLE_PROJECT_CONFIG
? yield* afs.up({
targets: [".kilocode", ".kilo", ".opencode"], // kilocode_change
start: directory,
@@ -36,7 +36,7 @@ export const directories = Effect.fn("ConfigPaths.directories")(function* (direc
start: Global.Path.home,
stop: Global.Path.home,
})),
...(Flag.KILO_CONFIG_DIR ? [Flag.KILO_CONFIG_DIR] : []), // kilocode_change
...(Flag.KILO_CONFIG_DIR ? [Flag.KILO_CONFIG_DIR] : []),
])
})
+1 -2
View File
@@ -34,8 +34,7 @@ function captureSync() {
export const bind = <Args extends readonly unknown[], Result>(fn: (...args: Args) => Result) => {
const captured = captureSync()
return (...args: Args) =>
restore(captured.instance, captured.workspace, () =>
// kilocode_change
restore(captured.instance, captured.workspace, () => // kilocode_change
Effect.runSync(
attachWith(
Effect.sync(() => fn(...args)),
+2 -2
View File
@@ -26,8 +26,8 @@ export function attachWith<A, E, R>(effect: Effect.Effect<A, E, R>, refs: Refs):
export function attach<A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> {
const workspace = WorkspaceContext.workspaceID
const fiber = Fiber.getCurrent()
const current = fiber ? Context.getReferenceUnsafe(fiber.context, InstanceRef) : undefined
// kilocode_change start - bridge legacy AsyncLocalStorage instance context into Effect runtimes
const current = fiber ? Context.getReferenceUnsafe(fiber.context, InstanceRef) : undefined
const instance = (() => {
if (current) return current
try {
@@ -36,9 +36,9 @@ export function attach<A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A
if (!(err instanceof LocalContext.NotFound)) throw err
}
})()
// kilocode_change end
return attachWith(effect, {
instance,
// kilocode_change end
workspace: workspace ?? (fiber ? Context.getReferenceUnsafe(fiber.context, WorkspaceRef) : undefined),
})
}
+16 -6
View File
@@ -5,11 +5,13 @@ import { Bus as ProjectBus } from "@/bus"
import { GlobalBus } from "@/bus/global"
import { InstanceRef, WorkspaceRef } from "@/effect/instance-ref"
import { InstanceStore } from "@/project/instance-store"
import * as EventWire from "@/kilocode/event-wire" // kilocode_change
import { SyncEvent } from "@/sync"
import { EventV2 } from "@opencode-ai/core/event"
import "@opencode-ai/core/catalog"
import "@opencode-ai/core/session-event"
import { Context, Effect, Layer, Option } from "effect"
import { Schema } from "effect" // kilocode_change - encode EventV2 data at legacy boundaries
export function toSyncDefinition<D extends EventV2.Definition>(definition: D) {
const result = {
@@ -18,6 +20,7 @@ export function toSyncDefinition<D extends EventV2.Definition>(definition: D) {
aggregate: definition.aggregate,
schema: definition.data,
properties: definition.data,
wire: true, // kilocode_change
}
return result as SyncEvent.Definition<D["type"], D["data"], D["data"]>
}
@@ -31,24 +34,26 @@ export const layer = Layer.effect(
const bus = yield* ProjectBus.Service
const sync = yield* SyncEvent.Service
const publishGlobal = (event: EventV2.Payload) =>
// kilocode_change start - legacy bus and SSE consumers require the schema's encoded representation
const publishGlobal = (event: EventV2.Payload, data: unknown) =>
Effect.sync(() => {
GlobalBus.emit("event", {
directory: event.location?.directory ?? "global",
workspace: event.location?.workspaceID,
payload: {
id: event.id,
type: event.type,
properties: event.data,
properties: data,
},
})
})
const provideEventLocation = <E, R>(event: EventV2.Payload, effect: Effect.Effect<void, E, R>) => {
const provideEventLocation = <E, R>(event: EventV2.Payload, data: unknown, effect: Effect.Effect<void, E, R>) => {
return Effect.gen(function* () {
const ctx = yield* InstanceRef
if (ctx) return yield* effect
const store = Option.getOrUndefined(yield* Effect.serviceOption(InstanceStore.Service))
if (!event.location?.directory || !store) return yield* publishGlobal(event)
if (!event.location?.directory || !store) return yield* publishGlobal(event, data)
return yield* store.load({ directory: event.location.directory }).pipe(
Effect.flatMap((ctx) => {
const withInstance = effect.pipe(Effect.provideService(InstanceRef, ctx))
@@ -58,21 +63,26 @@ export const layer = Layer.effect(
)
})
}
// kilocode_change end
const unsubscribe = yield* events.sync((event) => {
const definition = EventV2.registry.get(event.type)
if (!definition) return Effect.void
const data = EventWire.encode(definition.data, event.data) // kilocode_change
const aggregateID = definition.aggregate
? (event.data as Record<string, unknown>)[definition.aggregate]
: undefined
if (definition.version !== undefined && typeof aggregateID === "string") {
return provideEventLocation(event, sync.run(toSyncDefinition(definition), event.data))
return provideEventLocation(event, data, sync.run(toSyncDefinition(definition), event.data)) // kilocode_change
}
return provideEventLocation(
event,
bus.publish({ type: definition.type, properties: definition.data }, event.data, { id: event.id }),
// kilocode_change start
data,
bus.publish({ type: definition.type, properties: Schema.toEncoded(definition.data) }, data, { id: event.id }),
// kilocode_change end
)
})
yield* Effect.addFinalizer(() => unsubscribe)
+3 -3
View File
@@ -16,7 +16,7 @@ import { FileIgnore } from "./ignore"
import { Protected } from "./protected"
import * as Log from "@opencode-ai/core/util/log"
declare const KILO_LIBC: string | undefined // kilocode_change
declare const KILO_LIBC: string | undefined
const log = Log.create({ service: "file.watcher" })
const SUBSCRIBE_TIMEOUT_MS = 10_000
@@ -34,7 +34,7 @@ export const Event = {
const watcher = lazy((): typeof import("@parcel/watcher") | undefined => {
try {
const binding = require(
`@parcel/watcher-${process.platform}-${process.arch}${process.platform === "linux" ? `-${KILO_LIBC || "glibc"}` : ""}`, // kilocode_change
`@parcel/watcher-${process.platform}-${process.arch}${process.platform === "linux" ? `-${KILO_LIBC || "glibc"}` : ""}`,
)
return createWrapper(binding) as typeof import("@parcel/watcher")
} catch (error) {
@@ -73,7 +73,7 @@ export const layer = Layer.effect(
const state = yield* InstanceState.make(
Effect.fn("FileWatcher.state")(
function* () {
if (yield* Flag.KILO_EXPERIMENTAL_DISABLE_FILEWATCHER) return // kilocode_change
if (yield* Flag.KILO_EXPERIMENTAL_DISABLE_FILEWATCHER) return
const ctx = yield* InstanceState.context
@@ -5,6 +5,7 @@ import { ProviderTransform } from "../../../provider/transform"
import { cmd } from "../../../cli/cmd/cmd"
import { UI } from "../../../cli/ui"
import { AppRuntime } from "../../../effect/app-runtime"
import { RuntimeFlags } from "../../../effect/runtime-flags"
import { generateText } from "ai"
import { randomUUID } from "crypto"
@@ -134,6 +135,10 @@ function lang(model: Provider.Model) {
return AppRuntime.runPromise(Provider.Service.use((svc) => svc.getLanguage(model)))
}
export function outputLimit(model: Provider.Model, outputTokenMax?: number) {
return ProviderTransform.maxOutputTokens(model, outputTokenMax)
}
export async function handle(args: ArgumentsCamelCase) {
const load = args.list ?? list
@@ -285,7 +290,9 @@ async function call(
const sessionID = randomUUID()
const options = ProviderTransform.options({ model, sessionID })
const providerOptions = ProviderTransform.providerOptions(model, options)
const maxOutputTokens = ProviderTransform.maxOutputTokens(model)
const maxOutputTokens = await AppRuntime.runPromise(
RuntimeFlags.Service.useSync((flags) => outputLimit(model, flags.outputTokenMax)),
)
const temperature = ProviderTransform.temperature(model)
const topP = ProviderTransform.topP(model)
const topK = ProviderTransform.topK(model)
@@ -0,0 +1,43 @@
import { DateTime, Schema } from "effect"
const codec = (schema: Schema.Top) => schema as Schema.Codec<unknown, unknown>
function wire(value: unknown): unknown {
if (DateTime.isDateTime(value)) return DateTime.toEpochMillis(value)
if (Array.isArray(value)) return value.map(wire)
if (!value || typeof value !== "object") return value
const json = (value as { toJSON?: () => unknown }).toJSON
if (typeof json === "function") {
const result = json.call(value)
if (result !== value) return wire(result)
}
return Object.fromEntries(
Object.entries(value)
.filter(([, item]) => item !== undefined)
.map(([key, item]) => [key, wire(item)]),
)
}
function legacy(value: unknown): unknown {
if (!value || typeof value !== "object" || Array.isArray(value)) return value
const timestamp = (value as { timestamp?: unknown }).timestamp
if (typeof timestamp !== "string") return value
const millis = Date.parse(timestamp)
if (!Number.isFinite(millis)) return value
return { ...value, timestamp: millis }
}
export function encode<S extends Schema.Top>(schema: S, value: unknown): S["Encoded"] {
const target = codec(schema)
const decoded = Schema.decodeUnknownSync(target)(wire(value))
return Schema.encodeUnknownSync(target)(decoded) as S["Encoded"]
}
export function decode<S extends Schema.Top>(schema: S, value: unknown): S["Type"] {
const target = codec(schema)
try {
return Schema.decodeUnknownSync(target)(value) as S["Type"]
} catch {
return Schema.decodeUnknownSync(target)(legacy(value)) as S["Type"]
}
}
@@ -2,6 +2,7 @@ import { Effect } from "effect"
import type { Agent } from "@/agent/agent"
import type { Config } from "@/config/config"
import type { Provider } from "@/provider/provider"
import { ProviderTransform } from "@/provider/transform"
import type { LLM } from "@/session/llm"
import { MessageV2 } from "@/session/message-v2"
import { usable } from "@/session/overflow"
@@ -47,6 +48,7 @@ export namespace KiloCompactionChunks {
messages: MessageV2.WithParts[]
prompt: string
target: MessageV2.Assistant
outputTokenMax?: number
updateMessage: UpdateMessage
updatePart: Update
}
@@ -61,12 +63,18 @@ export namespace KiloCompactionChunks {
return input.result === "stop" && input.error?.name === "ContextOverflowError"
}
export function needed(input: { cfg: Config.Info; model: Provider.Model; tokens: number }) {
export function needed(input: {
cfg: Config.Info
model: Provider.Model
tokens: number
outputTokenMax?: number
}) {
const mdl = model(input.model, input.outputTokenMax)
// Apply 1.3x multiplier to token estimate to compensate for Token.estimate
// under-counting actual provider tokenizer counts by ~15-30%.
return (
Math.ceil(input.tokens * 1.3) + model(input.model).limit.output >
usable({ cfg: input.cfg, model: model(input.model) })
Math.ceil(input.tokens * 1.3) + mdl.limit.output >
usable({ cfg: input.cfg, model: mdl, outputTokenMax: input.outputTokenMax })
)
}
@@ -76,7 +84,7 @@ export namespace KiloCompactionChunks {
index: 0,
messages: [{ info: input.replay.info, parts: input.replay.parts }],
}
const size = budget({ cfg: input.cfg, model: input.model })
const size = budget({ cfg: input.cfg, model: input.model, outputTokenMax: input.outputTokenMax })
if (!(yield* large({ messages: chunk.messages, model: input.model, size }))) return input.replay
const result = yield* summarize({ ...input, chunk, total: 1 })
if (result.result !== "continue" || !result.output) return input.replay
@@ -100,16 +108,21 @@ export namespace KiloCompactionChunks {
})
}
function budget(input: { cfg: Config.Info; model: Provider.Model }) {
return Math.max(1_000, Math.floor(usable({ cfg: input.cfg, model: model(input.model) }) * RATIO))
export function budget(input: { cfg: Config.Info; model: Provider.Model; outputTokenMax?: number }) {
const mdl = model(input.model, input.outputTokenMax)
return Math.max(
1_000,
Math.floor(usable({ cfg: input.cfg, model: mdl, outputTokenMax: input.outputTokenMax }) * RATIO),
)
}
function model(input: Provider.Model) {
function model(input: Provider.Model, outputTokenMax?: number) {
const cap = Math.min(OUTPUT, outputTokenMax ?? OUTPUT)
return {
...input,
limit: {
...input.limit,
output: Math.min(input.limit.output, OUTPUT),
output: ProviderTransform.maxOutputTokens(input, cap),
},
} satisfies Provider.Model
}
@@ -239,14 +252,17 @@ export namespace KiloCompactionChunks {
function run(input: Input & { data: LLM.StreamInput["messages"]; text: string }) {
return Effect.gen(function* () {
const msg = yield* input.session.updateMessage(assistant({ base: input.target, sessionID: input.sessionID }))
const mdl = model(input.model)
const mdl = model(input.model, input.outputTokenMax)
const worker = yield* input.processors.create({ assistantMessage: msg, sessionID: input.sessionID, model: mdl })
const opts = input.agent.options
const agent = {
...input.agent,
options: {
...opts,
maxOutputTokens: Math.min(OUTPUT, typeof opts?.maxOutputTokens === "number" ? opts.maxOutputTokens : OUTPUT),
maxOutputTokens: Math.min(
mdl.limit.output,
typeof opts?.maxOutputTokens === "number" ? opts.maxOutputTokens : mdl.limit.output,
),
},
}
const out = yield* Effect.gen(function* () {
@@ -276,7 +292,7 @@ export namespace KiloCompactionChunks {
function summarize(input: Input & { chunk: Chunk; total: number }) {
return Effect.gen(function* () {
const size = budget({ cfg: input.cfg, model: input.model })
const size = budget({ cfg: input.cfg, model: input.model, outputTokenMax: input.outputTokenMax })
const data = (yield* large({ messages: input.chunk.messages, model: input.model, size }))
? [
{
@@ -325,7 +341,7 @@ export namespace KiloCompactionChunks {
export function process(input: Input) {
return Effect.gen(function* () {
const size = budget({ cfg: input.cfg, model: input.model })
const size = budget({ cfg: input.cfg, model: input.model, outputTokenMax: input.outputTokenMax })
const chunks = yield* split({ messages: input.messages, model: input.model, size })
log.info("fallback", { chunks: chunks.length, concurrency: CONCURRENCY })
+2 -2
View File
@@ -44,8 +44,8 @@ export class McpOAuthProvider implements OAuthClientProvider {
get clientMetadata(): OAuthClientMetadata {
return {
redirect_uris: [this.redirectUrl],
client_name: "Kilo", // kilocode_change
client_uri: "https://kilo.ai", // kilocode_change
client_name: "Kilo",
client_uri: "https://kilo.ai",
grant_types: ["authorization_code", "refresh_token"],
response_types: ["code"],
token_endpoint_auth_method: this.config.clientSecret ? "client_secret_post" : "none",
@@ -4,7 +4,7 @@ import { InstanceRef } from "@/effect/instance-ref"
import { disposeInstance as runDisposers } from "@/effect/instance-registry"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { Context, Deferred, Duration, Effect, Exit, Layer, Scope } from "effect"
import { context as instanceContext, type InstanceContext } from "./instance-context"
import { context as instanceContext, type InstanceContext } from "./instance-context" // kilocode_change
import { InstanceBootstrap } from "./bootstrap-service"
import * as Project from "./project"
@@ -52,10 +52,11 @@ export const layer: Layer.Layer<Service, never, Project.Service | InstanceBootst
project: result.project,
})),
)
// kilocode_change - run bootstrap inside the Instance ALS so KilocodeBootstrap
// kilocode_change start - run bootstrap inside the Instance ALS so KilocodeBootstrap
// (and anything it forks via Effect.forkDetach) sees Instance.directory.
const ready = bootstrap.run.pipe(Effect.provideService(InstanceRef, ctx)) as Effect.Effect<void>
yield* Effect.promise(() => instanceContext.provide(ctx, () => Effect.runPromise(ready)))
// kilocode_change end
return ctx
}).pipe(Effect.withSpan("InstanceStore.boot"))
+1 -1
View File
@@ -1,4 +1,4 @@
// kilocode_change - adapt Kilo model assembly to the upstream core models service
// kilocode_change - new file
import { Config } from "@/config/config"
import { Auth } from "@/auth"
import { ModelCache } from "./model-cache"
+11 -8
View File
@@ -144,12 +144,12 @@ function buildPrompt(input: { previousSummary?: string; context: string[] }) {
return [anchor, SUMMARY_TEMPLATE, ...input.context].join("\n\n")
}
function preserveRecentBudget(input: { cfg: Config.Info; model: Provider.Model; outputTokenMax?: number }) {
function preserveRecentBudget(input: { cfg: Config.Info; model: Provider.Model; outputTokenMax?: number }) { // kilocode_change
return (
input.cfg.compaction?.preserve_recent_tokens ??
Math.min(MAX_PRESERVE_RECENT_TOKENS, Math.max(MIN_PRESERVE_RECENT_TOKENS, Math.floor(usable(input) * 0.25)))
)
} // kilocode_change
}
function turns(messages: MessageV2.WithParts[]) {
const result: Turn[] = []
@@ -260,11 +260,13 @@ export const layer = Layer.effect(
}) {
const limit = input.cfg.compaction?.tail_turns ?? DEFAULT_TAIL_TURNS
if (limit <= 0) return { head: input.messages, tail_start_id: undefined }
// kilocode_change start
const budget = preserveRecentBudget({
cfg: input.cfg,
model: input.model,
outputTokenMax: flags.outputTokenMax,
}) // kilocode_change
})
// kilocode_change end
const all = turns(input.messages)
if (!all.length) return { head: input.messages, tail_start_id: undefined }
const recent = all.slice(-limit)
@@ -466,7 +468,7 @@ export const layer = Layer.effect(
model,
})
// kilocode_change start
const result = KiloCompactionChunks.needed({ cfg, model, tokens })
const result = KiloCompactionChunks.needed({ cfg, model, tokens, outputTokenMax: flags.outputTokenMax })
? "compact"
: yield* KiloCompactionPayloadRecovery.process({
processor,
@@ -493,6 +495,7 @@ export const layer = Layer.effect(
sessionID: input.sessionID,
model,
cfg,
outputTokenMax: flags.outputTokenMax,
messages: selected.head,
prompt: nextPrompt,
target: processor.message,
@@ -519,8 +522,7 @@ export const layer = Layer.effect(
})
}
if (fallback === "continue" && input.auto) {
// kilocode_change
if (fallback === "continue" && input.auto) { // kilocode_change
if (replay) {
// kilocode_change start - compact oversized replay turns instead of looping into replay overflow
replay = yield* KiloCompactionChunks.replay({
@@ -531,6 +533,7 @@ export const layer = Layer.effect(
sessionID: input.sessionID,
model,
cfg,
outputTokenMax: flags.outputTokenMax,
messages: selected.head,
prompt: nextPrompt,
target: processor.message,
@@ -677,8 +680,8 @@ export const layer = Layer.effect(
yield* prune({ sessionID: input.sessionID, reason: "post-compaction" })
yield* bus.publish(Event.Compacted, { sessionID: input.sessionID })
}
return fallback
// kilocode_change end
return fallback // kilocode_change
})
const create = Effect.fn("SessionCompaction.create")(function* (input: {
@@ -719,7 +722,7 @@ export const layer = Layer.effect(
return Service.of({
isOverflow,
prune,
process: (input) => processCompaction(input).pipe(Effect.orDie),
process: (input) => processCompaction(input).pipe(Effect.orDie), // kilocode_change
create,
})
}),
+4 -2
View File
@@ -680,12 +680,14 @@ export const layer = Layer.effect(
.pipe(Effect.ignore, Effect.forkIn(scope))
if (
!ctx.assistantMessage.summary &&
// kilocode_change start
isOverflow({
cfg: yield* config.get(),
tokens: usage.tokens,
model: ctx.model,
outputTokenMax: flags.outputTokenMax,
}) // kilocode_change
})
// kilocode_change end
) {
ctx.needsCompaction = true
// kilocode_change start
@@ -893,8 +895,8 @@ export const layer = Layer.effect(
yield* Effect.gen(function* () {
ctx.currentText = undefined
ctx.reasoningMap = {}
ctx.step = { reasoning: false, text: false, tool: false } // kilocode_change
// kilocode_change start
ctx.step = { reasoning: false, text: false, tool: false }
const stream = llm.stream({
...streamInput,
preflight: !ctx.assistantMessage.summary,
+5 -13
View File
@@ -56,7 +56,7 @@ import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { Truncate } from "@/tool/truncate"
import { Image } from "@/image/image"
import { decodeDataUrl } from "@/util/data-url"
import { Cause, Effect, Exit, Latch, Layer, Option, Scope, Context, Schema, Types } from "effect" // kilocode_change - Process moved to the timeout helper
import { Cause, Effect, Exit, Latch, Layer, Option, Scope, Context, Schema, Types } from "effect"
import * as EffectLogger from "@opencode-ai/core/effect/logger"
import { InstanceState } from "@/effect/instance-state"
import { TaskTool, type TaskPromptOps } from "@/tool/task"
@@ -963,7 +963,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
yield* bus.publish(Session.Event.Error, { sessionID: input.sessionID, error: error.toObject() })
throw error
}
const model = input.model ?? agent.model ?? (yield* currentModel(input.sessionID)) // kilocode_change
const model = input.model ?? agent.model ?? (yield* currentModel(input.sessionID))
const userMsg: MessageV2.User = {
id: input.messageID ?? MessageID.ascending(),
sessionID: input.sessionID,
@@ -1014,7 +1014,6 @@ NOTE: At any point in time through this workflow you should feel free to ask the
},
}
yield* sessions.updatePart(part)
// kilocode_change start - preserve Kilo v2 shell event dual-write
if (flags.experimentalEventSystem) {
yield* events.publish(SessionEvent.Shell.Started, {
sessionID: input.sessionID,
@@ -1023,7 +1022,6 @@ NOTE: At any point in time through this workflow you should feel free to ask the
command: input.command,
})
}
// kilocode_change end
return { msg, part, cwd: ctx.directory }
}).pipe(Effect.ensuring(markReady))
@@ -1041,7 +1039,6 @@ NOTE: At any point in time through this workflow you should feel free to ask the
}
if (timeout) output += "\n\n" + ["<metadata>", timeout, "</metadata>"].join("\n") // kilocode_change
const completed = Date.now()
// kilocode_change start - preserve Kilo v2 shell event dual-write
if (flags.experimentalEventSystem) {
yield* events.publish(SessionEvent.Shell.Ended, {
sessionID: input.sessionID,
@@ -1050,7 +1047,6 @@ NOTE: At any point in time through this workflow you should feel free to ask the
output,
})
}
// kilocode_change end
if (!msg.time.completed) {
msg.time.completed = completed
yield* sessions.updateMessage(msg)
@@ -1137,7 +1133,6 @@ NOTE: At any point in time through this workflow you should feel free to ask the
return yield* Effect.die(err)
})
// kilocode_change start - preserve persisted per-session model selection
const currentModel = Effect.fnUntraced(function* (sessionID: SessionID) {
const current = Database.use((db) =>
db.select({ model: SessionTable.model }).from(SessionTable).where(eq(SessionTable.id, sessionID)).get(),
@@ -1155,7 +1150,6 @@ NOTE: At any point in time through this workflow you should feel free to ask the
if (Option.isSome(match) && match.value.info.role === "user") return match.value.info.model
return yield* provider.defaultModel()
})
// kilocode_change end
const createUserMessage = Effect.fn("SessionPrompt.createUserMessage")(function* (input: PromptInput) {
const agentName = input.agent
@@ -1175,7 +1169,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
.where(eq(SessionTable.id, input.sessionID))
.get(),
)
const model = input.model ?? ag.model ?? (yield* currentModel(input.sessionID)) // kilocode_change
const model = input.model ?? ag.model ?? (yield* currentModel(input.sessionID))
const same = ag.model && model.providerID === ag.model.providerID && model.modelID === ag.model.modelID
const full =
!input.variant && ag.variant && same
@@ -1679,7 +1673,6 @@ NOTE: At any point in time through this workflow you should feel free to ask the
synthetic: [] as string[],
},
)
// kilocode_change start - preserve Kilo v2 prompt event dual-write
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
if (flags.experimentalEventSystem) {
yield* events.publish(SessionEvent.Prompted, {
@@ -1703,7 +1696,6 @@ NOTE: At any point in time through this workflow you should feel free to ask the
})
}
}
// kilocode_change end
return { info, parts }
}, Effect.scoped)
@@ -2265,7 +2257,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
if (cmdAgent?.model) return cmdAgent.model
}
if (input.model) return Provider.parseModel(input.model)
return yield* currentModel(input.sessionID) // kilocode_change
return yield* currentModel(input.sessionID)
})
yield* getModel(taskModel.providerID, taskModel.modelID, input.sessionID)
@@ -2299,7 +2291,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
const userModel = isSubtask
? input.model
? Provider.parseModel(input.model)
: yield* currentModel(input.sessionID) // kilocode_change
: yield* currentModel(input.sessionID)
: taskModel
yield* plugin.trigger(
+2 -2
View File
@@ -648,7 +648,7 @@ export const layer: Layer.Layer<
)
}
// kilocode_change end
yield* sync.run(Event.Deleted, { sessionID, info: session }, { publish: hasInstance }) // kilocode_change
yield* sync.run(Event.Deleted, { sessionID, info: session }, { publish: hasInstance })
// kilocode_change - capture final session-export workspace delta on close/delete
const workspaceKey = hasInstance ? yield* InstanceState.directory : undefined // kilocode_change
yield* Effect.promise(() => SessionExport.onSessionClose(sessionID, workspaceKey)) // kilocode_change
@@ -728,7 +728,7 @@ export const layer: Layer.Layer<
model: input?.model,
permission: input?.permission,
platform: input?.platform, // kilocode_change
workspaceID: input?.workspaceID ?? workspace, // kilocode_change - allow explicit override
workspaceID: input?.workspaceID ?? workspace,
})
return session
})
+2 -2
View File
@@ -268,8 +268,8 @@ export const layer = Layer.effect(
flags.disableExternalSkills,
flags.disableClaudeCodeSkills,
ctx.directory,
ctx.project.worktree,
) // kilocode_change
ctx.project.worktree, // kilocode_change
)
}),
)
const state = yield* InstanceState.make(
+1 -2
View File
@@ -302,8 +302,7 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | AppProce
)
})
const track = Effect.fnUntraced(function* (opts?: Parameters<Interface["track"]>[0]) {
// kilocode_change
const track = Effect.fnUntraced(function* (opts?: Parameters<Interface["track"]>[0]) { // kilocode_change
return yield* locked(
Effect.gen(function* () {
if (!(yield* enabled())) return
+48 -15
View File
@@ -18,6 +18,7 @@ import { serviceUse } from "@/effect/service-use"
import { InstanceState } from "@/effect/instance-state"
import { RuntimeFlags } from "@/effect/runtime-flags"
import { attachWith } from "@/effect/run-service"
import * as EventWire from "@/kilocode/event-wire" // kilocode_change
// Keep `Event["data"]` mutable because projectors mutate the persisted shape
// when writing to the database. Bus payloads (`Properties`) stay readonly —
@@ -36,6 +37,7 @@ export type Definition<
// passed at definition time (see `session.updated`, whose projector
// expands the persisted data to a `{ sessionID, info }` bus payload).
properties: BusSchema
wire?: boolean // kilocode_change - EventV2 rows cross persistence and bus boundaries as encoded data
}
export type Event<Def extends Definition = Definition> = {
@@ -47,7 +49,12 @@ export type Event<Def extends Definition = Definition> = {
export type Properties<Def extends Definition = Definition> = EffectSchema.Schema.Type<Def["properties"]>
export type SerializedEvent<Def extends Definition = Definition> = Event<Def> & { type: string }
// kilocode_change start - serialized rows carry the schema's encoded representation
export type SerializedEvent<Def extends Definition = Definition> = Omit<Event<Def>, "data"> & {
type: string
data: DeepMutable<Def["schema"]["Encoded"]>
}
// kilocode_change end
type ProjectorFunc = (db: Database.TxOrDb, data: unknown, event: Event) => void
type ConvertEvent = (type: string, data: Event["data"]) => unknown | Promise<unknown>
@@ -113,13 +120,20 @@ export const layer = Layer.effect(Service)(
workspace: yield* InstanceState.workspaceID,
}
: undefined
process(def, event, {
bus,
publish,
context,
ownerID: options?.ownerID,
experimentalWorkspaces: flags.experimentalWorkspaces,
})
// kilocode_change start - decode only EventV2 rows
const data = def.wire ? EventWire.decode(def.schema, event.data) : event.data
process(
def,
{ ...event, data },
{
bus,
publish,
context,
ownerID: options?.ownerID,
experimentalWorkspaces: flags.experimentalWorkspaces,
},
)
// kilocode_change end
})
const replayAll: Interface["replayAll"] = Effect.fn("SyncEvent.replayAll")(function* (events, options) {
@@ -238,6 +252,7 @@ export function init(input: { projectors: Array<[Definition, ProjectorFunc]>; co
aggregate: entry.aggregate,
properties: entry.data,
schema: entry.data,
wire: true, // kilocode_change
})
}
@@ -326,6 +341,7 @@ function process<Def extends Definition>(
Database.transaction((tx) => {
projector(tx, event.data, event)
const data = def.wire ? EventWire.encode(def.schema, event.data) : event.data // kilocode_change
if (options.experimentalWorkspaces) {
tx.insert(EventSequenceTable)
@@ -345,7 +361,7 @@ function process<Def extends Definition>(
seq: event.seq,
aggregate_id: event.aggregateID,
type: versionedType(def.type, def.version),
data: event.data as Record<string, unknown>,
data: data as Record<string, unknown>, // kilocode_change
})
.run()
}
@@ -357,13 +373,29 @@ function process<Def extends Definition>(
}
const result = convertEvent(def.type, event.data)
const publish = (data: unknown) =>
Effect.runPromise(
attachWith(options.bus.publish(def, data as Properties<Def>, { id: event.id }), {
instance: options.context?.instance,
workspace: options.context?.workspace,
}),
// kilocode_change start - encode EventV2 properties before crossing the legacy boundary
const publish = (value: unknown) => {
const refs = {
instance: options.context?.instance,
workspace: options.context?.workspace,
}
if (def.wire) {
return Effect.runPromise(
attachWith(
options.bus.publish(
{ type: def.type, properties: EffectSchema.toEncoded(def.properties) },
EventWire.encode(def.properties, value),
{ id: event.id },
),
refs,
),
)
}
return Effect.runPromise(
attachWith(options.bus.publish(def, value as Properties<Def>, { id: event.id }), refs),
)
}
// kilocode_change end
if (result instanceof Promise) {
void result.then(publish)
} else {
@@ -379,6 +411,7 @@ function process<Def extends Definition>(
syncEvent: {
type: versionedType(def.type, def.version),
...event,
data, // kilocode_change
},
},
})
+1 -2
View File
@@ -312,8 +312,7 @@ const ask = Effect.fn("ShellTool.ask")(function* (
function cmd(shell: string, command: string, cwd: string, env: NodeJS.ProcessEnv) {
if (process.platform === "win32" && Shell.ps(shell)) {
return ChildProcess.make(shell, Shell.args(shell, command, cwd), {
// kilocode_change - encoded PowerShell args
return ChildProcess.make(shell, Shell.args(shell, command, cwd), { // kilocode_change - encoded PowerShell args
cwd,
env,
stdin: "ignore",
+1 -1
View File
@@ -61,7 +61,7 @@ export const CodebaseSearchTool = Tool.define(
"Codebase search unavailable: free period ended. Set MORPH_API_KEY to continue. Get your key at https://www.morphllm.com/"
if (isAuthOrRateLimit) {
yield* Effect.promise(() =>
Bus.publish(Instance.current, TuiEvent.ToastShow, {
Bus.publish(Instance.current, TuiEvent.ToastShow, { // kilocode_change
title: "Codebase Search Unavailable",
message: "Free period has ended. Set MORPH_API_KEY to continue. Get your key at morphllm.com",
variant: "error",
@@ -28,6 +28,7 @@ describe("tui sync (#26560)", () => {
directory,
project_id: "proj_test",
}
// kilocode_change start
const { app, sync } = await provideTestInstance({
directory: tmp.path,
fn: () =>
@@ -39,7 +40,8 @@ describe("tui sync (#26560)", () => {
if (url.pathname === "/session") return json([sessionPayload])
return undefined
}),
}) // kilocode_change
})
// kilocode_change end
try {
await expect(sync.session.sync(sessionID)).resolves.toBeUndefined()
+5 -3
View File
@@ -11,7 +11,7 @@ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
import type { Config } from "@/config/config"
import { InstanceRef } from "../../src/effect/instance-ref"
import { InstanceBootstrap } from "../../src/project/bootstrap-service"
import { context as instanceContext, type InstanceContext } from "../../src/project/instance-context"
import { context as instanceContext, type InstanceContext } from "../../src/project/instance-context" // kilocode_change
import { InstanceRuntime } from "../../src/project/instance-runtime"
import { InstanceStore } from "../../src/project/instance-store"
import { TestLLMServer } from "../lib/llm-server"
@@ -32,17 +32,19 @@ export async function provideTestInstance<R>(input: {
const ctx = await runTestInstanceStore((store) => store.load({ directory: input.directory }))
try {
if (input.init) await testInstanceRuntime.runPromise(input.init.pipe(Effect.provideService(InstanceRef, ctx)))
return await instanceContext.provide(ctx, () => input.fn(ctx))
return await instanceContext.provide(ctx, () => input.fn(ctx)) // kilocode_change
} finally {
// kilocode_change start
await instanceContext.provide(ctx, () =>
runTestInstanceStore((store) => store.dispose(ctx).pipe(Effect.provideService(InstanceRef, ctx))),
)
// kilocode_change end
}
}
export async function withTestInstance<R>(input: { directory: string; fn: (ctx: InstanceContext) => R }) {
const ctx = await runTestInstanceStore((store) => store.load({ directory: input.directory }))
return instanceContext.provide(ctx, () => input.fn(ctx))
return instanceContext.provide(ctx, () => input.fn(ctx)) // kilocode_change
}
export async function reloadTestInstance(input: { directory: string }) {
@@ -1,6 +1,6 @@
import { test, expect, describe } from "bun:test"
import { Provider } from "../../../src/provider/provider"
import { formatTable, formatMarkdown, handle, isTextModel } from "../../../src/kilocode/cli/cmd/roll-call"
import { formatTable, formatMarkdown, handle, isTextModel, outputLimit } from "../../../src/kilocode/cli/cmd/roll-call"
const base = {
input: { text: false, audio: false, image: false, video: false, pdf: false },
@@ -100,6 +100,17 @@ describe("isTextModel", () => {
})
})
describe("outputLimit", () => {
test("honors the configured runtime output cap", () => {
const model = {
...caps({ input: { text: true }, output: { text: true } }),
limit: { context: 100_000, input: 90_000, output: 8_000 },
} as Provider.Model
expect(outputLimit(model, 512)).toBe(512)
})
})
describe("formatMarkdown", () => {
test("produces valid markdown table", () => {
const rows = [
@@ -236,7 +236,7 @@ function runtime(layer: Layer.Layer<LLM.Service>, context = 7_000) {
)
}
function fakeRuntime() {
function fakeRuntime(outputTokenMax?: number) {
const calls: string[] = []
const outputs: number[] = []
const bus = Bus.layer
@@ -291,7 +291,7 @@ function fakeRuntime() {
Layer.provide(Plugin.defaultLayer),
Layer.provide(SyncEvent.defaultLayer),
Layer.provide(EventV2Bridge.defaultLayer),
Layer.provide(RuntimeFlags.layer()),
Layer.provide(RuntimeFlags.layer({ outputTokenMax })),
Layer.provide(Reference.defaultLayer),
Layer.provide(bus),
Layer.provide(
@@ -373,6 +373,15 @@ describe("KiloCompactionChunks", () => {
)
})
test("uses runtime output cap for fallback selection and chunk budget", () => {
const model = ProviderTest.model({ providerID, id: modelID, limit: { context: 10_000, output: 8_000 } })
const cfg = {} as Config.Info
const outputTokenMax = 512
expect(KiloCompactionChunks.needed({ cfg, model, tokens: 5_000, outputTokenMax })).toBe(false)
expect(KiloCompactionChunks.budget({ cfg, model, outputTokenMax })).toBe(5_692)
})
test("falls back to chunk workers after the first compaction overflows", async () => {
await using tmp = await tmpdir()
await provideTestInstance({
@@ -569,15 +578,15 @@ describe("KiloCompactionChunks", () => {
})
})
test("caps worker output budget below oversized model output limit", async () => {
const { rt, calls, outputs } = fakeRuntime()
test("caps worker output budget below the configured runtime limit", async () => {
const { rt, calls, outputs } = fakeRuntime(512)
await using tmp = await tmpdir()
await provideTestInstance({
directory: tmp.path,
fn: async () => {
const session = await svc.create({})
const first = await user(session.id, "first " + "a".repeat(1_000))
await assistant(session.id, first.id, tmp.path, "reply " + "b".repeat(1_000))
const first = await user(session.id, "first " + "a".repeat(80_000))
await assistant(session.id, first.id, tmp.path, "reply " + "b".repeat(80_000))
await Effect.runPromise(
KiloSessionCompaction.create({
session: store,
@@ -605,7 +614,7 @@ describe("KiloCompactionChunks", () => {
expect(result).toBe("continue")
expect(calls.length).toBeGreaterThan(0)
expect(outputs.every((value) => value <= 2_048)).toBe(true)
expect(outputs.at(-1)).toBe(512)
} finally {
await rt.dispose()
}
@@ -0,0 +1,110 @@
import { afterEach, describe, expect, test } from "bun:test"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { SessionEvent } from "@opencode-ai/core/session-event"
import { DateTime, Effect, Layer, Schema } from "effect"
import { Bus } from "../../src/bus"
import { RuntimeFlags } from "../../src/effect/runtime-flags"
import { EventV2Bridge } from "../../src/event-v2-bridge"
import * as EventWire from "../../src/kilocode/event-wire"
import { SessionID } from "../../src/session/schema"
import { Database, eq } from "../../src/storage/db"
import { SyncEvent } from "../../src/sync"
import { EventTable } from "../../src/sync/event.sql"
import { resetDatabase } from "../fixture/db"
import { provideTmpdirInstance } from "../fixture/fixture"
import { awaitWithTimeout, testEffect } from "../lib/effect"
const it = testEffect(
Layer.mergeAll(
SyncEvent.layer.pipe(
Layer.provide(RuntimeFlags.layer({ experimentalWorkspaces: true })),
Layer.provideMerge(Bus.layer),
),
CrossSpawnSpawner.defaultLayer,
),
)
afterEach(resetDatabase)
describe("SyncEvent encoding", () => {
test("preserves JSON values nested under unknown schemas", () => {
const schema = Schema.Struct({ value: Schema.Unknown })
expect(EventWire.encode(schema, { value: new Date(0) })).toEqual({ value: "1970-01-01T00:00:00.000Z" })
expect(EventWire.encode(schema, { value: new URL("https://kilo.ai/docs") })).toEqual({
value: "https://kilo.ai/docs",
})
})
test("legacy timestamp decoding leaves unknown payload fields unchanged", () => {
const schema = Schema.Struct({ timestamp: Schema.DateTimeUtcFromMillis, input: Schema.Unknown })
const timestamp = "1970-01-01T00:00:01.234Z"
const decoded = EventWire.decode(schema, { timestamp, input: { created: timestamp, released: timestamp } })
expect(DateTime.toEpochMillis(decoded.timestamp)).toBe(1_234)
expect(decoded.input).toEqual({ created: timestamp, released: timestamp })
})
it.live(
"publishes encoded session data on the legacy bus",
provideTmpdirInstance(() =>
Effect.gen(function* () {
const bus = yield* Bus.Service
const sync = yield* SyncEvent.Service
const def = EventV2Bridge.toSyncDefinition(SessionEvent.Text.Delta)
const sessionID = SessionID.make("ses_event_bus")
const events = new Array<{ type: string; properties: unknown }>()
const received = Promise.withResolvers<void>()
const dispose = yield* bus.subscribeAllCallback((event) => {
if (event.type !== def.type) return
events.push(event)
received.resolve()
})
try {
yield* sync.run(def, { sessionID, timestamp: DateTime.makeUnsafe(1_234), delta: "hello" })
yield* awaitWithTimeout(
Effect.promise(() => received.promise),
"legacy bus did not receive the session event",
)
expect((events[0]?.properties as { timestamp?: unknown }).timestamp).toBe(1_234)
} finally {
dispose()
}
}),
),
)
it.live(
"persists encoded session data and decodes it during replay",
provideTmpdirInstance(() =>
Effect.gen(function* () {
const sync = yield* SyncEvent.Service
const def = EventV2Bridge.toSyncDefinition(SessionEvent.Text.Delta)
const sessionID = SessionID.make("ses_event_replay")
const timestamp = DateTime.makeUnsafe(1_234)
yield* sync.run(def, { sessionID, timestamp, delta: "hello" }, { publish: false })
const row = Database.use((db) =>
db.select().from(EventTable).where(eq(EventTable.aggregate_id, sessionID)).get(),
)
if (!row) throw new Error("missing persisted event")
expect((row.data as { timestamp?: unknown }).timestamp).toBe(1_234)
yield* sync.remove(sessionID)
yield* sync.replay({
id: row.id,
type: row.type,
seq: row.seq,
aggregateID: row.aggregate_id,
data: { ...row.data, timestamp: "1970-01-01T00:00:01.234Z" },
})
const replayed = Database.use((db) =>
db.select().from(EventTable).where(eq(EventTable.aggregate_id, sessionID)).get(),
)
expect((replayed?.data as { timestamp?: unknown }).timestamp).toBe(1_234)
}),
),
)
})
@@ -45,6 +45,11 @@ describe("InstanceStore", () => {
expect(ctx.directory).toBe(dir)
expect(ctx.worktree).toBe(dir)
// kilocode_change start - capture prefers legacy ALS, then falls back to the Effect fiber reference
const fallback = yield* Effect.sync(capture).pipe(Effect.provideService(InstanceRef, ctx))
expect({ ambient: capture(), fallback }).toEqual({ ambient: undefined, fallback: ctx })
// kilocode_change end
}),
)
@@ -62,6 +67,7 @@ describe("InstanceStore", () => {
yield* store.load({ directory: dir })
expect(initializedDirectory).toBe(dir)
expect(capture()).toBeUndefined() // kilocode_change - bootstrap legacy ALS does not leak into the caller
}),
)
@@ -100,7 +100,7 @@ async function markPluginDependenciesReady(dir: string) {
await mkdir(path.join(dir, "node_modules"), { recursive: true })
await Bun.write(
path.join(dir, "package-lock.json"),
JSON.stringify({ packages: { "": { dependencies: { "@kilocode/plugin": "0.0.0" } } } }), // kilocode_change
JSON.stringify({ packages: { "": { dependencies: { "@kilocode/plugin": "0.0.0" } } } }),
)
}
@@ -1,10 +1,10 @@
import { afterEach, expect } from "bun:test" // kilocode_change - blocking behavior now uses the scoped service test helper
import { afterEach, expect } from "bun:test"
import { Cause, Effect, Exit, Fiber, Layer, Queue } from "effect"
import { Question } from "../../src/question"
import { InstanceRef } from "../../src/effect/instance-ref"
import { InstanceRuntime } from "../../src/project/instance-runtime"
import { QuestionID } from "../../src/question/schema"
import { disposeAllInstances, provideInstance, reloadTestInstance, tmpdirScoped } from "../fixture/fixture" // kilocode_change - blocking coverage no longer uses the Promise facade fixture
import { disposeAllInstances, provideInstance, reloadTestInstance, tmpdirScoped } from "../fixture/fixture"
import { SessionID } from "../../src/session/schema"
import { testEffect } from "../lib/effect"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
@@ -59,8 +59,7 @@ describe("config HttpApi", () => {
lsp: false,
})
yield* Fiber.join(disposed)
expect(yield* Effect.promise(() => Bun.file(path.join(tmp.path, "opencode.json")).json())).toMatchObject({
// kilocode_change
expect(yield* Effect.promise(() => Bun.file(path.join(tmp.path, "opencode.json")).json())).toMatchObject({ // kilocode_change
username: "patched-user",
formatter: false,
lsp: false,
@@ -5,8 +5,17 @@ import { InstanceRef } from "../../src/effect/instance-ref"
import { Server } from "../../src/server/server"
import { EventPaths } from "../../src/server/routes/instance/httpapi/groups/event"
import { Event as ServerEvent } from "../../src/server/event"
// kilocode_change start - verify transformed EventV2 values at the legacy SSE boundary
import { Catalog } from "@opencode-ai/core/catalog"
import { EventV2 } from "@opencode-ai/core/event"
import { ModelV2 } from "@opencode-ai/core/model"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { SessionEvent } from "@opencode-ai/core/session-event"
import { GlobalPaths } from "../../src/server/routes/instance/httpapi/groups/global"
import { SessionID } from "../../src/session/schema"
// kilocode_change end
import * as Log from "@opencode-ai/core/util/log"
import { Effect, Schema } from "effect"
import { DateTime, Effect, Schema } from "effect"
import { resetDatabase } from "../fixture/db"
import { disposeAllInstances, reloadTestInstance, tmpdir } from "../fixture/fixture"
@@ -22,6 +31,17 @@ const EventData = Schema.Struct({
properties: Schema.Record(Schema.String, Schema.Any),
})
// kilocode_change start - inspect the real global SSE envelope
const GlobalEventData = Schema.Struct({
directory: Schema.optional(Schema.String),
payload: Schema.Struct({
id: Schema.optional(Schema.String),
type: Schema.String,
properties: Schema.optional(Schema.Record(Schema.String, Schema.Any)),
}),
})
// kilocode_change end
async function readChunk(reader: ReadableStreamDefaultReader<Uint8Array>, delay = 5_000) {
let timeout: ReturnType<typeof setTimeout> | undefined
try {
@@ -90,6 +110,33 @@ async function readEventWithin(reader: ReadableStreamDefaultReader<Uint8Array>,
return Schema.decodeUnknownSync(EventData)(JSON.parse(new TextDecoder().decode(result.value).replace(/^data: /, "")))
}
// kilocode_change start - read transformed values from the global SSE wire payload
async function readGlobal(reader: ReadableStreamDefaultReader<Uint8Array>, delay = 5_000) {
const result = await readChunk(reader, delay)
if (result.done || !result.value) throw new Error("global event stream closed")
return Schema.decodeUnknownSync(GlobalEventData)(
JSON.parse(new TextDecoder().decode(result.value).replace(/^data: /, "")),
)
}
function properties(event: Schema.Schema.Type<typeof GlobalEventData>) {
if (!event.payload.properties) throw new Error(`event ${event.payload.type} has no properties`)
return event.payload.properties
}
async function readGlobalUntil(
reader: ReadableStreamDefaultReader<Uint8Array>,
predicate: (event: Schema.Schema.Type<typeof GlobalEventData>) => boolean,
delay = 5_000,
) {
const end = Date.now() + delay
while (true) {
const event = await readGlobal(reader, end - Date.now())
if (predicate(event)) return event
}
}
// kilocode_change end
afterEach(async () => {
await disposeAllInstances()
await resetDatabase()
@@ -145,4 +192,70 @@ describe("event HttpApi", () => {
await reader.cancel()
}
})
// kilocode_change start - transformed EventV2 data is numeric on legacy SSE while domain data stays decoded
test("encodes catalog and session EventV2 data on the global event stream", async () => {
await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } })
const response = await app().request(GlobalPaths.event)
if (!response.body) throw new Error("missing response body")
const reader = response.body.getReader()
try {
expect(await readGlobal(reader)).toMatchObject({ payload: { type: "server.connected", properties: {} } })
const ctx = await reloadTestInstance({ directory: tmp.path })
const released = DateTime.makeUnsafe(1_750_000_000_123)
const model = new ModelV2.Info({
...ModelV2.Info.empty(ProviderV2.ID.make("test"), ModelV2.ID.make("model")),
time: { released },
})
const catalogID = EventV2.ID.create()
const catalog = readGlobalUntil(reader, (event) => event.payload.id === catalogID)
const catalogDomain = await AppRuntime.runPromise(
EventV2.Service.use((events) => events.publish(Catalog.Event.ModelUpdated, { model }, { id: catalogID })).pipe(
Effect.provideService(InstanceRef, ctx),
),
)
expect(DateTime.isDateTime(catalogDomain.data.model.time.released)).toBe(true)
expect(properties(await catalog).model.time.released).toBe(1_750_000_000_123)
const globalID = EventV2.ID.create()
const global = readGlobalUntil(reader, (event) => event.payload.id === globalID)
await AppRuntime.runPromise(
EventV2.Service.use((events) => events.publish(Catalog.Event.ModelUpdated, { model }, { id: globalID })),
)
expect((await global).directory).toBe("global")
const timestamp = DateTime.makeUnsafe(1_234)
const sessionID = SessionID.make("ses_event_encoding")
const session = readGlobalUntil(
reader,
(event) =>
event.payload.type === SessionEvent.Text.Delta.type && properties(event).sessionID === sessionID,
)
const sessionDomain = await AppRuntime.runPromise(
EventV2.Service.use((events) =>
events.publish(SessionEvent.Text.Delta, { sessionID, timestamp, delta: "hello" }),
).pipe(Effect.provideService(InstanceRef, ctx)),
)
expect(DateTime.isDateTime(sessionDomain.data.timestamp)).toBe(true)
expect(properties(await session).timestamp).toBe(1_234)
const prompted = readGlobalUntil(reader, (event) => event.payload.type === SessionEvent.Prompted.type)
await AppRuntime.runPromise(
EventV2.Service.use((events) =>
events.publish(SessionEvent.Prompted, {
sessionID,
timestamp,
prompt: { text: "hello", files: [], agents: [], references: [] },
}),
).pipe(Effect.provideService(InstanceRef, ctx)),
)
expect(properties(await prompted)).toMatchObject({ timestamp: 1_234, prompt: { text: "hello" } })
} finally {
await reader.cancel()
}
})
// kilocode_change end
})
+2 -2
View File
@@ -295,7 +295,7 @@ describe("tool.registry", () => {
yield* Effect.promise(() =>
Bun.write(
path.join(plugin, "package.json"),
JSON.stringify({ name: "@kilocode/plugin", type: "module", exports: { ".": "./dist/index.js" } }),
JSON.stringify({ name: "@kilocode/plugin", type: "module", exports: { ".": "./dist/index.js" } }), // kilocode_change
),
)
yield* Effect.promise(() =>
@@ -315,7 +315,7 @@ describe("tool.registry", () => {
Bun.write(
path.join(customTools, "addition.ts"),
[
'import { tool } from "@kilocode/plugin"',
'import { tool } from "@kilocode/plugin"', // kilocode_change
"export default tool({",
" description: 'Use this tool to add two numbers and return their sum.',",
" args: {",
+1 -2
View File
@@ -264,8 +264,7 @@ export function Markdown(
key: local.cacheKey,
streaming: local.streaming ?? false,
}),
async (src): Promise<Rendered> => {
// kilocode_change
async (src): Promise<Rendered> => { // kilocode_change
if (isServer) return { content: fallback(src.text), blocks: [] } // kilocode_change
if (!src.text) return { content: "", blocks: [] } // kilocode_change
+1 -1
View File
@@ -39,7 +39,7 @@ const testAllow: Record<string, { count: number; reason: string }> = {
"provider/amazon-bedrock.test.ts": { count: 2, reason: "existing runtime integration test" },
"provider/provider.test.ts": { count: 3, reason: "existing runtime integration test" },
"server/experimental-session-list.test.ts": { count: 2, reason: "Kilo session list integration test" },
"server/httpapi-event.test.ts": { count: 2, reason: "event stream integration test" },
"server/httpapi-event.test.ts": { count: 6, reason: "event stream integration test" },
"session/llm.test.ts": { count: 2, reason: "existing runtime integration test" },
"tool/recall.test.ts": { count: 11, reason: "existing runtime integration test" },
}