chore: update kilo-vscode visual regression baselines

This commit is contained in:
kilo-maintainer[bot]
2026-07-30 19:55:30 +00:00
committed by Johnny Amancio
parent 76d06fd8ba
commit 5ca97417a0
65 changed files with 1277 additions and 916 deletions
+2
View File
@@ -8,6 +8,7 @@ import { Flag } from "../flag/flag"
import { isAbsolute, join } from "path"
import { existsSync } from "fs" // kilocode_change
import { DbPreflight } from "../kilocode/db-preflight" // kilocode_change
import { ensure as compat } from "../kilocode/database-compat" // kilocode_change
import { DatabaseMigration } from "./migration"
import { InstallationChannel } from "../installation/version"
import { makeGlobalNode } from "../effect/app-node"
@@ -33,6 +34,7 @@ const layer = Layer.effect(
yield* db.run("PRAGMA foreign_keys = ON")
yield* db.run("PRAGMA wal_checkpoint(PASSIVE)")
yield* DatabaseMigration.apply(db)
yield* compat(db) // kilocode_change - keep the shared database usable by released CLIs
return { db }
}).pipe(Effect.orDie),
@@ -0,0 +1,22 @@
import { Effect } from "effect"
import type { Database } from "../database/database"
type Db = Database.Interface["db"]
export function ensure(db: Db) {
return db.transaction(
(tx) =>
Effect.gen(function* () {
const rows = yield* tx.all<{ name: string }>("PRAGMA table_info('session_context_epoch')")
const names = new Set(rows.map((row) => row.name))
if (!names.has("agent"))
yield* tx.run("ALTER TABLE `session_context_epoch` ADD `agent` text DEFAULT 'build' NOT NULL")
if (!names.has("replacement_seq"))
yield* tx.run("ALTER TABLE `session_context_epoch` ADD `replacement_seq` integer")
if (!names.has("revision"))
yield* tx.run("ALTER TABLE `session_context_epoch` ADD `revision` integer DEFAULT 0 NOT NULL")
}),
{ behavior: "immediate" },
)
}
+18
View File
@@ -10,6 +10,7 @@ import { FSUtil } from "../fs-util"
import { LocationMutation } from "../location-mutation"
import { Patch } from "../patch"
import { PermissionV2 } from "../permission"
import { ToolOutputStore } from "../tool-output-store" // kilocode_change
import { ToolRegistry } from "./registry"
import { Tool } from "./tool"
import { Tools } from "./tools"
@@ -34,6 +35,21 @@ export const Output = Schema.Struct({
})
export type Output = typeof Output.Type
// kilocode_change start - keep durable/SSE tool records bounded without hiding normal-sized diff previews
const compact = (output: Output): Output => {
if (Buffer.byteLength(JSON.stringify(output), "utf-8") <= ToolOutputStore.MAX_BYTES) return output
return {
...output,
files: output.files.map((file) => ({
additions: file.additions,
deletions: file.deletions,
...(file.file === undefined ? {} : { file: file.file }),
...(file.status === undefined ? {} : { status: file.status }),
})),
}
}
// kilocode_change end
export const toModelOutput = (output: Output) =>
[
"Applied patch sequentially:",
@@ -72,6 +88,8 @@ const layer = Layer.effectDiscard(
"Apply one patch containing add, update, and delete file operations. All targets are resolved and approved before target contents are read. Operations apply sequentially; if a later operation fails, earlier operations remain applied and the failure reports them explicitly. Moves and atomic rollback are not supported yet.",
input: Input,
output: Output,
structured: Output, // kilocode_change
toStructuredOutput: ({ output }) => compact(output), // kilocode_change
toModelOutput: ({ output }) => [{ type: "text", text: toModelOutput(output) }],
execute: (input, context) => {
const applied: Array<typeof Applied.Type> = []
+18
View File
@@ -17,6 +17,7 @@ import { FileMutation } from "../file-mutation"
import { FSUtil } from "../fs-util"
import { LocationMutation } from "../location-mutation"
import { PermissionV2 } from "../permission"
import { ToolOutputStore } from "../tool-output-store" // kilocode_change
import { ToolRegistry } from "./registry"
import { Tool } from "./tool"
import { Tools } from "./tools"
@@ -41,6 +42,21 @@ export const Output = Schema.Struct({
})
export type Output = typeof Output.Type
// kilocode_change start - keep durable/SSE tool records bounded without hiding normal-sized diff previews
const compact = (output: Output): Output => {
if (Buffer.byteLength(JSON.stringify(output), "utf-8") <= ToolOutputStore.MAX_BYTES) return output
return {
...output,
files: output.files.map((file) => ({
additions: file.additions,
deletions: file.deletions,
...(file.file === undefined ? {} : { file: file.file }),
...(file.status === undefined ? {} : { status: file.status }),
})),
}
}
// kilocode_change end
const normalizeLineEndings = (text: string) => text.replaceAll("\r\n", "\n")
const detectLineEnding = (text: string): "\n" | "\r\n" => (text.includes("\r\n") ? "\r\n" : "\n")
const convertToLineEnding = (text: string, ending: "\n" | "\r\n") =>
@@ -105,6 +121,8 @@ const layer = Layer.effectDiscard(
"Replace exact text in one file. Relative paths resolve within the active Location. Absolute paths inside the Location are accepted. Explicit external absolute paths require external_directory approval before edit approval.",
input: Input,
output: Output,
structured: Output, // kilocode_change
toStructuredOutput: ({ output }) => compact(output), // kilocode_change
toModelOutput: ({ input, output }) => [
{ type: "text", text: toModelOutput(output, input.oldString, input.newString) },
],
@@ -11,6 +11,8 @@ import path from "path"
import { tmpdir } from "../fixture/tmpdir"
import { SessionHistory } from "@opencode-ai/core/session/history"
import { SessionV2 } from "@opencode-ai/core/session"
import { ensure } from "@opencode-ai/core/kilocode/database-compat"
import { Database } from "@opencode-ai/core/database/database"
const make = EffectDrizzleSqlite.makeWithDefaults()
const run = <A, E>(effect: Effect.Effect<A, E, SqlClientService>) =>
@@ -43,18 +45,30 @@ describe("database migration compatibility", () => {
)
yield* DatabaseMigration.applyOnly(db, migrations.slice(split))
yield* ensure(db)
// This is the projection shape written by the CLI bundled with VS Code v7.4.7.
yield* db.run(
sql`INSERT INTO session_message (id, session_id, type, time_created, time_updated, data) VALUES ('message', 'ses_session', 'user', 1, 1, '{}')`,
)
yield* db.run(sql`UPDATE session_message SET data = '{"text":"updated"}' WHERE id = 'message'`)
yield* db.run(
sql`INSERT INTO session_context_epoch (session_id, baseline, agent, snapshot, baseline_seq, revision) VALUES ('ses_session', 'baseline', 'build', '{}', 0, 0)`,
)
yield* db.run(
sql`UPDATE session_context_epoch SET replacement_seq = 4, revision = revision + 1 WHERE session_id = 'ses_session'`,
)
expect(yield* db.get(sql`SELECT id, seq, data FROM session_message WHERE id = 'message'`)).toEqual({
id: "message",
seq: null,
data: '{"text":"updated"}',
})
expect(
yield* db.get(
sql`SELECT agent, replacement_seq AS replacementSeq, revision FROM session_context_epoch WHERE session_id = 'ses_session'`,
),
).toEqual({ agent: "build", replacementSeq: 4, revision: 1 })
yield* db.run(
sql`INSERT INTO session_message (id, session_id, type, seq, time_created, time_updated, data) VALUES ('msg_sequenced', 'ses_session', 'user', 1, 2, 2, '{"text":"current","files":[],"agents":[],"time":{"created":2}}')`,
)
@@ -119,6 +133,37 @@ describe("database migration compatibility", () => {
)
})
test("keeps released context epoch writes compatible on fresh current databases", async () => {
await using tmp = await tmpdir()
const filename = path.join(tmp.path, "kilo.db")
await Effect.runPromise(
Database.Service.use((service) =>
Effect.gen(function* () {
const db = service.db
yield* ensure(db)
yield* db.run(
sql`INSERT INTO project (id, worktree, time_created, time_updated, sandboxes) VALUES ('project', '/repo', 1, 1, '[]')`,
)
yield* db.run(
sql`INSERT INTO session (id, project_id, slug, directory, title, version, time_created, time_updated) VALUES ('session', 'project', 'session', '/repo', 'Session', '7.4.7', 1, 1)`,
)
yield* db.run(
sql`INSERT INTO session_context_epoch (session_id, baseline, agent, snapshot, baseline_seq, revision) VALUES ('session', 'baseline', 'build', '{}', 0, 0)`,
)
yield* db.run(
sql`UPDATE session_context_epoch SET replacement_seq = 4, revision = revision + 1 WHERE session_id = 'session'`,
)
expect(
yield* db.get(
sql`SELECT agent, replacement_seq AS replacementSeq, revision FROM session_context_epoch WHERE session_id = 'session'`,
),
).toEqual({ agent: "build", replacementSeq: 4, revision: 1 })
}),
).pipe(Effect.provide(Database.layerFromPath(filename)), Effect.scoped),
)
})
test("preserves sequenced projections when repairing an already-migrated database", async () => {
await run(
Effect.gen(function* () {
@@ -202,6 +202,33 @@ describe("ApplyPatchTool", () => {
),
)
// kilocode_change start
it.live("omits oversized patches from durable structured output", () =>
Effect.acquireUseRelease(
Effect.promise(() => tmpdir()),
(tmp) => {
reset()
const content = "x".repeat(ToolOutputStore.MAX_BYTES)
const patch = `*** Begin Patch\n*** Add File: large.txt\n+${content}\n*** End Patch`
return withTool(tmp.path, (registry) => settleTool(registry, call(patch))).pipe(
Effect.tap((settled) =>
Effect.sync(() => {
expect(settled.output?.structured).toEqual({
applied: [{ type: "add", resource: "large.txt", target: path.join(tmp.path, "large.txt") }],
files: [{ file: "large.txt", status: "added", additions: 1, deletions: 0 }],
})
expect(Buffer.byteLength(JSON.stringify(settled.output?.structured), "utf-8")).toBeLessThanOrEqual(
ToolOutputStore.MAX_BYTES,
)
}),
),
)
},
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
),
)
// kilocode_change end
it.live("rejects moves before applying any hunk", () =>
Effect.acquireUseRelease(
Effect.promise(() => tmpdir()),
+33
View File
@@ -159,6 +159,39 @@ describe("EditTool", () => {
),
)
// kilocode_change start
it.live("omits an oversized patch from durable structured output", () =>
Effect.acquireUseRelease(
Effect.promise(() => tmpdir()),
(tmp) => {
reset()
const target = path.join(tmp.path, "large.txt")
const before = "a".repeat(ToolOutputStore.MAX_BYTES)
const after = "b".repeat(ToolOutputStore.MAX_BYTES)
return Effect.promise(() => fs.writeFile(target, before)).pipe(
Effect.andThen(
withTool(tmp.path, (registry) =>
settleTool(registry, call({ path: "large.txt", oldString: before, newString: after })),
),
),
Effect.tap((settled) =>
Effect.sync(() => {
expect(settled.output?.structured).toEqual({
replacements: 1,
files: [{ file: "large.txt", status: "modified", additions: 1, deletions: 1 }],
})
expect(Buffer.byteLength(JSON.stringify(settled.output?.structured), "utf-8")).toBeLessThanOrEqual(
ToolOutputStore.MAX_BYTES,
)
}),
),
)
},
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
),
)
// kilocode_change end
it.live("accepts an absolute file path inside the active Location", () =>
Effect.acquireUseRelease(
Effect.promise(() => tmpdir()),
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:58fef726cab9c37deeb8f3e4fac8f3d6c943acf11c46c0d8d53af93df028a4c2
size 46120
oid sha256:cef414cf59da1f632a7d5d9a2e44f792e7e2ea6b4041bcc214cb32c052f851d8
size 46930
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b21ced6db14cbc68efe2f996542b7810e000a9c8a5e714c0aa434928865046ab
size 32798
oid sha256:bd700edca458baefb4ce6cea68ed78f8c002bfb6edd842dbea318c2fa443b814
size 32789
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:57e417f0d51ea0d9b15ab1dcee4d5b49fe064bfd6d9dad6c8d5cc38e59eea049
size 27189
oid sha256:d7fc23fcb7adf483c0b771ef601b23cb365dc7f40033b00fce703b35910aa4fc
size 27159
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8ae5166647579b4ebe139206f34ce2227d7fa8c5fde537b7808355fd8a86c70a
size 27309
oid sha256:d11f4004ed3170647d385c14df077d235f5bb9bd6e5dec07557ee2014d553233
size 27302
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0fc65d066f7ec387cb4f1a8980178281ad961b9b57df2bf1261d6650637414b8
size 29764
oid sha256:f8bd56ba87d0c2bbef8a2325ab2e9f0e956c89e2602ff8ca7415d5fb7c9fa1f3
size 29709
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:60de70c3e280fde6486647a2d037bbdb83070b9d1a34e96d4deac8835bb11bcf
size 4238
oid sha256:222790e6c32346cf6b6cc52a0859ee01d364b52a1ef8b9f385a6e9798adfccfb
size 4256
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d86411a4a93770b39e06881761864f24bf7890b8da5faf21d065125cb4662b52
size 6899
oid sha256:e854dd761ae03a44210467d2550849b1e49dd9a3ee4efef50d5116962a9a4059
size 6907
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7020df6386028c96fac00bedf8df1170146bb8ea851d2511944d6cee37365597
size 34013
oid sha256:c3ebab71919a7bafe609933ba8909f22e68171d3546473cca7ad5e3d859787ef
size 30655
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3e104fbc1ff457317f7715937fb939142374ea20e20cda6aeb1d412867cf66c4
size 7481
oid sha256:76b359a64ad4dede420eb4669f973f80f87be63b5d0a1dfe36829f0c19698381
size 7486
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5594e89d1abea2e4a0b534fe99d8802fa14edb60479b418bc4b9bee1fa85f7b6
size 5172
oid sha256:eecccfc0bbd53bd52261ff8cfc02c444c6d97f6438e6693d8cd998c36909708a
size 5178
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:393b2bf1cfeb3ab01e864084f520bdc8ef1cbbeba87e47bb5037e90ca71e109e
size 6757
oid sha256:43ea789b53b0bd402231f30970e9f8f1648ac1d0bcd971102ed4769bd002d9e6
size 6764
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:49ecad51e50b6fcdb3c9f5eac8ef516032844de2beb77fee68d206216c1be8fe
size 669184
oid sha256:b7482c1c495c5f96fe1cda00a956e2e0480f261af043e15441f047e687c4b063
size 660234
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1c09f8d7adef66fd01ec50a13dd8fdad1fe92f789216fd5bd8f83b92dc2e4ac1
size 18496
oid sha256:77964c111d0454d7985e3368b821e161e4d6d9fffee5438ee094ab34ff464d52
size 18623
+24
View File
@@ -64,6 +64,9 @@ 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"
import * as DateTime from "effect/DateTime" // kilocode_change
import { SessionEvent } from "@opencode-ai/core/session/event" // kilocode_change
import { SessionMessage } from "@opencode-ai/core/session/message" // kilocode_change
import { InstanceState } from "@/effect/instance-state"
import { InstanceRef } from "@/effect/instance-ref"
import { Instance } from "@/kilocode/instance"
@@ -628,6 +631,17 @@ export const layer = Layer.effect(
},
}
yield* sessions.updatePart(part)
// kilocode_change start - dual-write the v2 shell record so the durable timeline correlates with the tool part
if (flags.experimentalEventSystem) {
yield* events.publish(SessionEvent.Shell.Started, {
sessionID: input.sessionID,
messageID: SessionMessage.ID.create(),
timestamp: DateTime.makeUnsafe(started),
callID: part.callID,
command: input.command,
})
}
// kilocode_change end
return { msg, part, cwd: ctx.directory }
}).pipe(Effect.ensuring(markReady))
@@ -645,6 +659,16 @@ export const layer = Layer.effect(
}
if (timeout) output += "\n\n" + ["<metadata>", timeout, "</metadata>"].join("\n") // kilocode_change
const completed = Date.now()
// kilocode_change start
if (flags.experimentalEventSystem) {
yield* events.publish(SessionEvent.Shell.Ended, {
sessionID: input.sessionID,
timestamp: DateTime.makeUnsafe(completed),
callID: part.callID,
output,
})
}
// kilocode_change end
if (!msg.time.completed) {
msg.time.completed = completed
yield* sessions.updateMessage(msg)
@@ -157,13 +157,13 @@ describe("opencode run (non-interactive subprocess)", () => {
yield* llm.tool("bash", { command: "sed -n 1,5p README.md" })
const result = yield* opencode.run("run sed on readme", {
format: "json",
timeoutMs: 45_000,
timeoutMs: 75_000,
})
opencode.expectExit(result, 1)
expect(result.stderr).toContain("run ended with an auto-rejected permission; pass --auto for autonomous use")
expect(opencode.parseJsonEvents(result.stdout).some((event) => event.type === "error")).toBe(true)
}),
60_000,
90_000,
)
// kilocode_change end
@@ -302,13 +302,17 @@ describe("opencode run (non-interactive subprocess)", () => {
expect(result.exitCode).toBe(0)
expect(events.map((event) => event.type)).toEqual([
"step_start",
"text",
"tool_use",
"text", // kilocode_change - a pre-denied tool settles before the SDK closes its preceding text part
"step_finish",
"step_start",
"step_finish",
])
expect(events[1]?.part).toEqual(expect.objectContaining({ type: "text", text: "partial json" }))
// kilocode_change start - the pre-denied tool completes before text-end
expect(events.find((event) => event.type === "text")?.part).toEqual(
expect.objectContaining({ type: "text", text: "partial json" }),
)
// kilocode_change end
// kilocode_change - upstream asserts reason "unknown" here. Reaching that requires the bash call
// to proceed without permission friction, which a Kilo headless run never does: left alone the ask
// is auto-rejected (exit 1, no second step), and settling it up front changes the request sequence
@@ -399,12 +403,13 @@ describe("opencode run (non-interactive subprocess)", () => {
yield* llm.hang
const run = yield* opencode.startRun("wait forever")
yield* llm.wait(1)
const interrupted = Date.now() // kilocode_change - assert signal handling, independent of contended CLI startup
run.interrupt()
const result = yield* run.result
expect(result.exitCode).not.toBe(0)
expect(result.durationMs).toBeLessThan(30_000)
expect(Date.now() - interrupted).toBeLessThan(10_000) // kilocode_change
}),
30_000,
60_000, // kilocode_change
)
})
@@ -37,11 +37,11 @@ describe("tui thread", () => {
await check(".")
})
test("resolves a relative mini project from PWD when cwd differs", async () => {
test("ignores stale PWD when resolving a relative mini project", async () => { // kilocode_change
await using pwd = await tmpdir({ git: true })
await using cwd = await tmpdir({ git: true })
expect(resolveThreadDirectory(".", pwd.path, cwd.path)).toBe(pwd.path)
expect(resolveThreadDirectory(".", pwd.path, cwd.path)).toBe(cwd.path) // kilocode_change
expect(resolveThreadDirectory(undefined, pwd.path, cwd.path)).toBe(cwd.path)
})
@@ -9,12 +9,13 @@ describe("public event manifest", () => {
expect(EventManifest.Definitions).toBe(SchemaEventManifest.Definitions)
expect(EventManifest.Latest).toBe(SchemaEventManifest.Latest)
expect(EventManifest.Durable).toBe(SchemaEventManifest.Durable)
expect(EventManifest.Latest.size).toBe(88)
expect(EventManifest.Latest.size).toBe(89) // kilocode_change - include global.config.updated
expect(EventManifest.Latest.get("session.next.step.ended")).toBe(SessionEvent.Step.Ended)
expect(EventManifest.Latest.get("todo.updated")).toBe(Todo.Event.Updated)
expect(EventManifest.Latest.has("ide.installed")).toBe(false)
expect(EventManifest.Latest.has("server.connected")).toBe(true)
expect(EventManifest.Latest.has("global.disposed")).toBe(true)
expect(EventManifest.Latest.has("global.config.updated")).toBe(true) // kilocode_change
})
test("contains only the current step settlement versions", () => {
@@ -22,7 +22,7 @@
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"gpt-5.5\",\"input\":[{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"What is the weather in Paris?\"}]}],\"instructions\":\"You are Kilo, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices.\\n\\n# Personality\\n\\n- Your goal is to accomplish the user's task, NOT engage in a back and forth conversation.\\n- You accomplish tasks iteratively, breaking them down into clear steps and working through them methodically.\\n- Do not ask for more information than necessary. Use the tools provided to accomplish the user's request efficiently and effectively.\\n- Use the `question` tool only when you need an actual answer from the user.\\n- You are STRICTLY FORBIDDEN from starting your messages with \\\"Great\\\", \\\"Certainly\\\", \\\"Okay\\\", \\\"Sure\\\". You should NOT be conversational in your responses, but rather direct and to the point. For example you should NOT say \\\"Great, I've updated the CSS\\\" but instead something like \\\"I've updated the CSS\\\". It is important you be clear and technical in your messages.\\n- NEVER end your result with a question or request to engage in further conversation. Formulate the end of your result in a way that is final and does not require further input from the user.\\n- The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance.\\n\\n# Code\\n\\n- When making changes to code, always consider the context in which the code is being used. Ensure that your changes are compatible with the existing codebase and that they follow the project's coding standards and best practices.\\nAnswer using tools when appropriate.\\nUse the get_weather tool exactly once to look up Paris, then reply with exactly: Paris is sunny.\",\"tools\":[{\"type\":\"function\",\"name\":\"get_weather\",\"description\":\"Get the current weather for a city.\",\"parameters\":{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false}}],\"store\":false,\"prompt_cache_key\":\"session-recorded-openai-oauth-loop\",\"reasoning\":{\"effort\":\"medium\",\"summary\":\"auto\"},\"text\":{\"verbosity\":\"low\"},\"stream\":true,\"include\":[\"reasoning.encrypted_content\"]}"
"body": "{\"model\":\"gpt-5.5\",\"input\":[{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"What is the weather in Paris?\"}]}],\"instructions\":\"You are Kilo, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices.\\n\\n# Personality\\n\\n- Your goal is to accomplish the user's task, NOT engage in a back and forth conversation.\\n- You accomplish tasks iteratively, breaking them down into clear steps and working through them methodically.\\n- Do not ask for more information than necessary. Use the tools provided to accomplish the user's request efficiently and effectively.\\n- Use the `question` tool only when you need an actual answer from the user.\\n- You are STRICTLY FORBIDDEN from starting your messages with \\\"Great\\\", \\\"Certainly\\\", \\\"Okay\\\", \\\"Sure\\\". You should NOT be conversational in your responses, but rather direct and to the point. For example you should NOT say \\\"Great, I've updated the CSS\\\" but instead something like \\\"I've updated the CSS\\\". It is important you be clear and technical in your messages.\\n- NEVER end your result with a question or request to engage in further conversation. Formulate the end of your result in a way that is final and does not require further input from the user.\\n- The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance.\\n\\n# Code\\n\\n- When making changes to code, always consider the context in which the code is being used. Ensure that your changes are compatible with the existing codebase and that they follow the project's coding standards and best practices.\\nAnswer using tools when appropriate.\\nUse the get_weather tool exactly once to look up Paris, then reply with exactly: Paris is sunny.\",\"tools\":[{\"type\":\"function\",\"name\":\"get_weather\",\"description\":\"Get the current weather for a city.\",\"parameters\":{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false},\"strict\":false}],\"store\":false,\"prompt_cache_key\":\"session-recorded-openai-oauth-loop\",\"reasoning\":{\"effort\":\"medium\",\"summary\":\"auto\"},\"text\":{\"verbosity\":\"low\"},\"stream\":true,\"include\":[\"reasoning.encrypted_content\"]}"
},
"response": {
"status": 200,
@@ -38,7 +38,7 @@
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"gpt-5.5\",\"input\":[{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"What is the weather in Paris?\"}]},{\"type\":\"function_call\",\"call_id\":\"call_uoEsDnHNhxMLpCUy6hqEyHme\",\"name\":\"get_weather\",\"arguments\":\"{\\\"city\\\":{}}\"},{\"type\":\"function_call_output\",\"call_id\":\"call_uoEsDnHNhxMLpCUy6hqEyHme\",\"output\":\"{\\\"temperature\\\":22,\\\"condition\\\":\\\"sunny\\\"}\"}],\"instructions\":\"You are Kilo, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices.\\n\\n# Personality\\n\\n- Your goal is to accomplish the user's task, NOT engage in a back and forth conversation.\\n- You accomplish tasks iteratively, breaking them down into clear steps and working through them methodically.\\n- Do not ask for more information than necessary. Use the tools provided to accomplish the user's request efficiently and effectively.\\n- Use the `question` tool only when you need an actual answer from the user.\\n- You are STRICTLY FORBIDDEN from starting your messages with \\\"Great\\\", \\\"Certainly\\\", \\\"Okay\\\", \\\"Sure\\\". You should NOT be conversational in your responses, but rather direct and to the point. For example you should NOT say \\\"Great, I've updated the CSS\\\" but instead something like \\\"I've updated the CSS\\\". It is important you be clear and technical in your messages.\\n- NEVER end your result with a question or request to engage in further conversation. Formulate the end of your result in a way that is final and does not require further input from the user.\\n- The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance.\\n\\n# Code\\n\\n- When making changes to code, always consider the context in which the code is being used. Ensure that your changes are compatible with the existing codebase and that they follow the project's coding standards and best practices.\\nAnswer using tools when appropriate.\\nUse the get_weather tool exactly once to look up Paris, then reply with exactly: Paris is sunny.\",\"tools\":[{\"type\":\"function\",\"name\":\"get_weather\",\"description\":\"Get the current weather for a city.\",\"parameters\":{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false}}],\"store\":false,\"prompt_cache_key\":\"session-recorded-openai-oauth-loop\",\"reasoning\":{\"effort\":\"medium\",\"summary\":\"auto\"},\"text\":{\"verbosity\":\"low\"},\"stream\":true,\"include\":[\"reasoning.encrypted_content\"]}"
"body": "{\"model\":\"gpt-5.5\",\"input\":[{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"What is the weather in Paris?\"}]},{\"type\":\"function_call\",\"call_id\":\"call_uoEsDnHNhxMLpCUy6hqEyHme\",\"name\":\"get_weather\",\"arguments\":\"{\\\"city\\\":{}}\"},{\"type\":\"function_call_output\",\"call_id\":\"call_uoEsDnHNhxMLpCUy6hqEyHme\",\"output\":\"{\\\"temperature\\\":22,\\\"condition\\\":\\\"sunny\\\"}\"}],\"instructions\":\"You are Kilo, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices.\\n\\n# Personality\\n\\n- Your goal is to accomplish the user's task, NOT engage in a back and forth conversation.\\n- You accomplish tasks iteratively, breaking them down into clear steps and working through them methodically.\\n- Do not ask for more information than necessary. Use the tools provided to accomplish the user's request efficiently and effectively.\\n- Use the `question` tool only when you need an actual answer from the user.\\n- You are STRICTLY FORBIDDEN from starting your messages with \\\"Great\\\", \\\"Certainly\\\", \\\"Okay\\\", \\\"Sure\\\". You should NOT be conversational in your responses, but rather direct and to the point. For example you should NOT say \\\"Great, I've updated the CSS\\\" but instead something like \\\"I've updated the CSS\\\". It is important you be clear and technical in your messages.\\n- NEVER end your result with a question or request to engage in further conversation. Formulate the end of your result in a way that is final and does not require further input from the user.\\n- The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance.\\n\\n# Code\\n\\n- When making changes to code, always consider the context in which the code is being used. Ensure that your changes are compatible with the existing codebase and that they follow the project's coding standards and best practices.\\nAnswer using tools when appropriate.\\nUse the get_weather tool exactly once to look up Paris, then reply with exactly: Paris is sunny.\",\"tools\":[{\"type\":\"function\",\"name\":\"get_weather\",\"description\":\"Get the current weather for a city.\",\"parameters\":{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false},\"strict\":false}],\"store\":false,\"prompt_cache_key\":\"session-recorded-openai-oauth-loop\",\"reasoning\":{\"effort\":\"medium\",\"summary\":\"auto\"},\"text\":{\"verbosity\":\"low\"},\"stream\":true,\"include\":[\"reasoning.encrypted_content\"]}"
},
"response": {
"status": 200,
@@ -22,7 +22,7 @@
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"gpt-5.2-codex\",\"input\":[{\"role\":\"system\",\"content\":\"You are Kilo, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices.\\n\\n# Personality\\n\\n- Your goal is to accomplish the user's task, NOT engage in a back and forth conversation.\\n- You accomplish tasks iteratively, breaking them down into clear steps and working through them methodically.\\n- Do not ask for more information than necessary. Use the tools provided to accomplish the user's request efficiently and effectively.\\n- Use the `question` tool only when you need an actual answer from the user.\\n- You are STRICTLY FORBIDDEN from starting your messages with \\\"Great\\\", \\\"Certainly\\\", \\\"Okay\\\", \\\"Sure\\\". You should NOT be conversational in your responses, but rather direct and to the point. For example you should NOT say \\\"Great, I've updated the CSS\\\" but instead something like \\\"I've updated the CSS\\\". It is important you be clear and technical in your messages.\\n- NEVER end your result with a question or request to engage in further conversation. Formulate the end of your result in a way that is final and does not require further input from the user.\\n- The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance.\\n\\n# Code\\n\\n- When making changes to code, always consider the context in which the code is being used. Ensure that your changes are compatible with the existing codebase and that they follow the project's coding standards and best practices.\\nAnswer using tools when appropriate.\\nUse the get_weather tool exactly once to look up Paris, then reply with exactly: Paris is sunny.\"},{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"What is the weather in Paris?\"}]}],\"tools\":[{\"type\":\"function\",\"name\":\"get_weather\",\"description\":\"Get the current weather for a city.\",\"parameters\":{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false}}],\"store\":false,\"prompt_cache_key\":\"session-recorded-opencode-loop\",\"reasoning\":{\"effort\":\"medium\",\"summary\":\"auto\"},\"max_output_tokens\":32000,\"stream\":true,\"include\":[\"reasoning.encrypted_content\"]}"
"body": "{\"model\":\"gpt-5.2-codex\",\"input\":[{\"role\":\"system\",\"content\":\"You are Kilo, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices.\\n\\n# Personality\\n\\n- Your goal is to accomplish the user's task, NOT engage in a back and forth conversation.\\n- You accomplish tasks iteratively, breaking them down into clear steps and working through them methodically.\\n- Do not ask for more information than necessary. Use the tools provided to accomplish the user's request efficiently and effectively.\\n- Use the `question` tool only when you need an actual answer from the user.\\n- You are STRICTLY FORBIDDEN from starting your messages with \\\"Great\\\", \\\"Certainly\\\", \\\"Okay\\\", \\\"Sure\\\". You should NOT be conversational in your responses, but rather direct and to the point. For example you should NOT say \\\"Great, I've updated the CSS\\\" but instead something like \\\"I've updated the CSS\\\". It is important you be clear and technical in your messages.\\n- NEVER end your result with a question or request to engage in further conversation. Formulate the end of your result in a way that is final and does not require further input from the user.\\n- The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance.\\n\\n# Code\\n\\n- When making changes to code, always consider the context in which the code is being used. Ensure that your changes are compatible with the existing codebase and that they follow the project's coding standards and best practices.\\nAnswer using tools when appropriate.\\nUse the get_weather tool exactly once to look up Paris, then reply with exactly: Paris is sunny.\"},{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"What is the weather in Paris?\"}]}],\"tools\":[{\"type\":\"function\",\"name\":\"get_weather\",\"description\":\"Get the current weather for a city.\",\"parameters\":{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false},\"strict\":false}],\"store\":false,\"prompt_cache_key\":\"session-recorded-opencode-loop\",\"reasoning\":{\"effort\":\"medium\",\"summary\":\"auto\"},\"max_output_tokens\":32000,\"stream\":true,\"include\":[\"reasoning.encrypted_content\"]}"
},
"response": {
"status": 200,
@@ -40,7 +40,7 @@
"headers": {
"content-type": "application/json"
},
"body": "{\"model\":\"gpt-5.2-codex\",\"input\":[{\"role\":\"system\",\"content\":\"You are Kilo, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices.\\n\\n# Personality\\n\\n- Your goal is to accomplish the user's task, NOT engage in a back and forth conversation.\\n- You accomplish tasks iteratively, breaking them down into clear steps and working through them methodically.\\n- Do not ask for more information than necessary. Use the tools provided to accomplish the user's request efficiently and effectively.\\n- Use the `question` tool only when you need an actual answer from the user.\\n- You are STRICTLY FORBIDDEN from starting your messages with \\\"Great\\\", \\\"Certainly\\\", \\\"Okay\\\", \\\"Sure\\\". You should NOT be conversational in your responses, but rather direct and to the point. For example you should NOT say \\\"Great, I've updated the CSS\\\" but instead something like \\\"I've updated the CSS\\\". It is important you be clear and technical in your messages.\\n- NEVER end your result with a question or request to engage in further conversation. Formulate the end of your result in a way that is final and does not require further input from the user.\\n- The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance.\\n\\n# Code\\n\\n- When making changes to code, always consider the context in which the code is being used. Ensure that your changes are compatible with the existing codebase and that they follow the project's coding standards and best practices.\\nAnswer using tools when appropriate.\\nUse the get_weather tool exactly once to look up Paris, then reply with exactly: Paris is sunny.\"},{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"What is the weather in Paris?\"}]},{\"type\":\"function_call\",\"call_id\":\"call_DfI0RwTrlaizfnQ9zkJC8rks\",\"name\":\"get_weather\",\"arguments\":\"{\\\"city\\\":{}}\"},{\"type\":\"function_call_output\",\"call_id\":\"call_DfI0RwTrlaizfnQ9zkJC8rks\",\"output\":\"{\\\"temperature\\\":22,\\\"condition\\\":\\\"sunny\\\"}\"}],\"tools\":[{\"type\":\"function\",\"name\":\"get_weather\",\"description\":\"Get the current weather for a city.\",\"parameters\":{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false}}],\"store\":false,\"prompt_cache_key\":\"session-recorded-opencode-loop\",\"reasoning\":{\"effort\":\"medium\",\"summary\":\"auto\"},\"max_output_tokens\":32000,\"stream\":true,\"include\":[\"reasoning.encrypted_content\"]}"
"body": "{\"model\":\"gpt-5.2-codex\",\"input\":[{\"role\":\"system\",\"content\":\"You are Kilo, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices.\\n\\n# Personality\\n\\n- Your goal is to accomplish the user's task, NOT engage in a back and forth conversation.\\n- You accomplish tasks iteratively, breaking them down into clear steps and working through them methodically.\\n- Do not ask for more information than necessary. Use the tools provided to accomplish the user's request efficiently and effectively.\\n- Use the `question` tool only when you need an actual answer from the user.\\n- You are STRICTLY FORBIDDEN from starting your messages with \\\"Great\\\", \\\"Certainly\\\", \\\"Okay\\\", \\\"Sure\\\". You should NOT be conversational in your responses, but rather direct and to the point. For example you should NOT say \\\"Great, I've updated the CSS\\\" but instead something like \\\"I've updated the CSS\\\". It is important you be clear and technical in your messages.\\n- NEVER end your result with a question or request to engage in further conversation. Formulate the end of your result in a way that is final and does not require further input from the user.\\n- The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance.\\n\\n# Code\\n\\n- When making changes to code, always consider the context in which the code is being used. Ensure that your changes are compatible with the existing codebase and that they follow the project's coding standards and best practices.\\nAnswer using tools when appropriate.\\nUse the get_weather tool exactly once to look up Paris, then reply with exactly: Paris is sunny.\"},{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"What is the weather in Paris?\"}]},{\"type\":\"function_call\",\"call_id\":\"call_DfI0RwTrlaizfnQ9zkJC8rks\",\"name\":\"get_weather\",\"arguments\":\"{\\\"city\\\":{}}\"},{\"type\":\"function_call_output\",\"call_id\":\"call_DfI0RwTrlaizfnQ9zkJC8rks\",\"output\":\"{\\\"temperature\\\":22,\\\"condition\\\":\\\"sunny\\\"}\"}],\"tools\":[{\"type\":\"function\",\"name\":\"get_weather\",\"description\":\"Get the current weather for a city.\",\"parameters\":{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false},\"strict\":false}],\"store\":false,\"prompt_cache_key\":\"session-recorded-opencode-loop\",\"reasoning\":{\"effort\":\"medium\",\"summary\":\"auto\"},\"max_output_tokens\":32000,\"stream\":true,\"include\":[\"reasoning.encrypted_content\"]}"
},
"response": {
"status": 200,
@@ -1,34 +1,28 @@
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { SessionProjector } from "@opencode-ai/core/session/projector"
import { afterEach, describe, expect, mock, test } from "bun:test"
import { APICallError } from "ai"
import { Effect, Layer, ManagedRuntime, Scope } from "effect"
import * as Stream from "effect/Stream"
import { LLMEvent, type LLMEvent as Event } from "@opencode-ai/llm"
import { Database } from "@opencode-ai/core/database/database"
import { Agent } from "../../src/agent/agent"
import { Bus } from "../../src/bus"
import { Config } from "../../src/config/config"
import { RuntimeFlags } from "../../src/effect/runtime-flags"
import { EventV2Bridge } from "../../src/event-v2-bridge"
import { Image } from "../../src/image/image"
import { KiloCompactionPayloadRecovery } from "../../src/kilocode/session/compaction-payload-recovery"
import { KiloSessionCompaction } from "../../src/kilocode/session/compaction"
import { Permission } from "../../src/permission"
import { Plugin } from "../../src/plugin"
import { provideTestInstance } from "../fixture/fixture"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
import { Snapshot } from "../../src/snapshot"
import { LLM } from "../../src/session/llm"
import { MessageV2 } from "../../src/session/message-v2"
import * as SessionProcessorModule from "../../src/session/processor"
import { Session as SessionNs } from "../../src/session/session"
import { MessageID, PartID, SessionID } from "../../src/session/schema"
import { SessionCompaction } from "../../src/session/compaction"
import { SessionStatus } from "../../src/session/status"
import { SessionSummary } from "../../src/session/summary"
import { SyncEvent } from "../../src/sync"
import { ProviderTest } from "../fake/provider"
import { Provider } from "../../src/provider/provider"
import { tmpdir } from "../fixture/fixture"
const sessionID = SessionID.make("ses_payload_recovery")
@@ -68,6 +62,7 @@ const summary = Layer.succeed(
computeDiff: () => Effect.succeed([]),
}),
)
const summaryNode = LayerNode.make({ service: SessionSummary.Service, layer: summary, deps: [] })
function base(id: MessageID) {
return {
@@ -156,29 +151,26 @@ function reply(
const scope = Layer.effect(Scope.Scope, Scope.make())
function runtime(layer: Layer.Layer<LLM.Service>, config = AppNodeBuilder.build(Config.node)) {
const bus = Bus.layer
const status = AppNodeBuilder.build(SessionStatus.node)
const processor = AppNodeBuilder.build(SessionProcessorModule.SessionProcessor.node, [
[SessionSummary.node, summary],
])
const model = ProviderTest.model({ providerID, id: modelID, limit: { context: 100_000, output: 32_000 } })
const llm = LayerNode.make({ service: LLM.Service, layer, deps: [] })
const cfg = LayerNode.make({ service: Config.Service, layer: config, deps: [] })
const provider = LayerNode.make({
service: Provider.Service,
layer: ProviderTest.fake({ model }).layer,
deps: [],
})
const flags = LayerNode.make({ service: RuntimeFlags.Service, layer: RuntimeFlags.layer(), deps: [] })
return ManagedRuntime.make(
Layer.mergeAll(AppNodeBuilder.build(SessionCompaction.node, [[SessionProcessorModule.SessionProcessor.node, processor], [SessionSummary.node, summary]]), processor, bus, status).pipe(
Layer.provide(ProviderTest.fake({ model }).layer),
Layer.provideMerge(AppNodeBuilder.build(SessionNs.node)),
Layer.provide(AppNodeBuilder.build(Snapshot.node)),
Layer.provide(layer),
Layer.provide(AppNodeBuilder.build(Permission.node)),
Layer.provide(AppNodeBuilder.build(Agent.node)),
Layer.provide(AppNodeBuilder.build(Plugin.node)),
Layer.provide(status),
Layer.provide(bus),
Layer.provide(config),
Layer.provide(RuntimeFlags.layer()),
LayerNode.compile(LayerNode.group([SessionCompaction.node, SessionNs.node, SessionProjector.node]), [
[LLM.node, llm],
[Config.node, cfg],
[Provider.node, provider],
[RuntimeFlags.node, flags],
[SessionSummary.node, summaryNode],
]).pipe(
Layer.provide(Bus.layer),
Layer.provide(scope),
Layer.provide(SyncEvent.defaultLayer),
Layer.provide(AppNodeBuilder.build(EventV2Bridge.node)),
Layer.provide(AppNodeBuilder.build(Database.node)),
),
)
}
@@ -268,132 +260,136 @@ describe("KiloCompactionPayloadRecovery", () => {
}
})
test("retries compaction without media and tool outputs after payload-size failure", async () => {
await using tmp = await tmpdir({ git: true })
const stub = llm()
const captures: string[] = []
stub.push((input) => {
captures.push(JSON.stringify(input.messages))
return Stream.fail(
new APICallError({
message: "Request Entity Too Large",
url: "https://api.kilo.ai/api/openrouter/responses",
requestBodyValues: {},
statusCode: 413,
responseHeaders: { "content-type": "text/plain" },
responseBody: "Request Entity Too Large\n\nFUNCTION_PAYLOAD_TOO_LARGE",
isRetryable: false,
}),
)
})
stub.push(
reply("summary", (input) => {
test(
"retries compaction without media and tool outputs after payload-size failure",
async () => {
await using tmp = await tmpdir({ git: true })
const stub = llm()
const captures: string[] = []
stub.push((input) => {
captures.push(JSON.stringify(input.messages))
}),
)
await provideTestInstance({
directory: tmp.path,
fn: async () => {
const rt = runtime(stub.layer, AppNodeBuilder.build(Config.node))
const svc = service(rt)
const session = await svc.create({})
const old = await user(svc, session.id, "old image turn")
await svc.updatePart({
id: PartID.ascending(),
messageID: old.id,
sessionID: session.id,
type: "file",
mime: "image/png",
filename: "old.png",
url: `data:image/png;base64,${"a".repeat(8_000)}`,
})
const oldReply = await assistant(svc, session.id, old.id, tmp.path)
await svc.updatePart({
id: PartID.ascending(),
messageID: oldReply.id,
sessionID: session.id,
type: "tool",
callID: crypto.randomUUID(),
tool: "bash",
state: {
status: "completed",
input: {},
output: "old output".repeat(10_000),
title: "old",
metadata: {},
time: { start: Date.now(), end: Date.now() },
},
})
await user(svc, session.id, "latest turn")
const keep = await user(svc, session.id, "preserved tail turn")
const keepReply = await assistant(svc, session.id, keep.id, tmp.path)
await svc.updatePart({
id: PartID.ascending(),
messageID: keepReply.id,
sessionID: session.id,
type: "tool",
callID: crypto.randomUUID(),
tool: "bash",
state: {
status: "completed",
input: {},
output: "keep output",
title: "keep",
metadata: {},
time: { start: Date.now(), end: Date.now() },
},
})
await Effect.runPromise(
KiloSessionCompaction.create({
session: {
updateMessage: (msg) => Effect.promise(() => svc.updateMessage(msg)),
updatePart: (part) => Effect.promise(() => svc.updatePart(part)),
},
sessionID: session.id,
agent: "build",
model: ref,
auto: false,
return Stream.fail(
new APICallError({
message: "Request Entity Too Large",
url: "https://api.kilo.ai/api/openrouter/responses",
requestBodyValues: {},
statusCode: 413,
responseHeaders: { "content-type": "text/plain" },
responseBody: "Request Entity Too Large\n\nFUNCTION_PAYLOAD_TOO_LARGE",
isRetryable: false,
}),
)
})
stub.push(
reply("summary", (input) => {
captures.push(JSON.stringify(input.messages))
}),
)
try {
const msgs = await svc.messages({ sessionID: session.id })
const parent = msgs.at(-1)?.info.id
expect(parent).toBeTruthy()
const result = await rt.runPromise(
SessionCompaction.Service.use((svc) =>
svc.process({
parentID: parent!,
messages: msgs,
sessionID: session.id,
auto: false,
}),
),
await provideTestInstance({
directory: tmp.path,
fn: async () => {
const rt = runtime(stub.layer, AppNodeBuilder.build(Config.node))
const svc = service(rt)
const session = await svc.create({})
const old = await user(svc, session.id, "old image turn")
await svc.updatePart({
id: PartID.ascending(),
messageID: old.id,
sessionID: session.id,
type: "file",
mime: "image/png",
filename: "old.png",
url: `data:image/png;base64,${"a".repeat(8_000)}`,
})
const oldReply = await assistant(svc, session.id, old.id, tmp.path)
await svc.updatePart({
id: PartID.ascending(),
messageID: oldReply.id,
sessionID: session.id,
type: "tool",
callID: crypto.randomUUID(),
tool: "bash",
state: {
status: "completed",
input: {},
output: "old output".repeat(10_000),
title: "old",
metadata: {},
time: { start: Date.now(), end: Date.now() },
},
})
await user(svc, session.id, "latest turn")
const keep = await user(svc, session.id, "preserved tail turn")
const keepReply = await assistant(svc, session.id, keep.id, tmp.path)
await svc.updatePart({
id: PartID.ascending(),
messageID: keepReply.id,
sessionID: session.id,
type: "tool",
callID: crypto.randomUUID(),
tool: "bash",
state: {
status: "completed",
input: {},
output: "keep output",
title: "keep",
metadata: {},
time: { start: Date.now(), end: Date.now() },
},
})
await Effect.runPromise(
KiloSessionCompaction.create({
session: {
updateMessage: (msg) => Effect.promise(() => svc.updateMessage(msg)),
updatePart: (part) => Effect.promise(() => svc.updatePart(part)),
},
sessionID: session.id,
agent: "build",
model: ref,
auto: false,
}),
)
expect(result).toBe("continue")
expect(captures).toHaveLength(2)
expect(captures[0]).toContain("Attached image/png: old.png")
expect(captures[0]).toContain("old output")
expect(captures[0]).not.toContain("keep output")
expect(captures[1]).not.toContain("data:image/png;base64")
expect(captures[1]).not.toContain("old output")
expect(captures[1]).not.toContain("keep output")
expect(captures[1]).toContain("Attached image/png: old.png")
expect(captures[1]).toContain("Old tool result content cleared")
const tools = (await svc.messages({ sessionID: session.id }))
.flatMap((msg) => msg.parts)
.filter((part): part is MessageV2.ToolPart => part.type === "tool")
expect(tools).toHaveLength(2)
expect(tools[0]?.type).toBe("tool")
if (tools[0]?.state.status === "completed") expect(tools[0].state.time.compacted).toBeNumber()
expect(tools[1]?.type).toBe("tool")
if (tools[1]?.state.status === "completed") expect(tools[1].state.time.compacted).toBeUndefined()
} finally {
await rt.dispose()
}
},
})
})
try {
const msgs = await svc.messages({ sessionID: session.id })
const parent = msgs.at(-1)?.info.id
expect(parent).toBeTruthy()
const result = await rt.runPromise(
SessionCompaction.Service.use((svc) =>
svc.process({
parentID: parent!,
messages: msgs,
sessionID: session.id,
auto: false,
}),
),
)
expect(result).toBe("continue")
expect(captures).toHaveLength(2)
expect(captures[0]).toContain("Attached image/png: old.png")
expect(captures[0]).toContain("old output")
expect(captures[0]).not.toContain("keep output")
expect(captures[1]).not.toContain("data:image/png;base64")
expect(captures[1]).not.toContain("old output")
expect(captures[1]).not.toContain("keep output")
expect(captures[1]).toContain("Attached image/png: old.png")
expect(captures[1]).toContain("Old tool result content cleared")
const tools = (await svc.messages({ sessionID: session.id }))
.flatMap((msg) => msg.parts)
.filter((part): part is MessageV2.ToolPart => part.type === "tool")
expect(tools).toHaveLength(2)
expect(tools[0]?.type).toBe("tool")
if (tools[0]?.state.status === "completed") expect(tools[0].state.time.compacted).toBeNumber()
expect(tools[1]?.type).toBe("tool")
if (tools[1]?.state.status === "completed") expect(tools[1].state.time.compacted).toBeUndefined()
} finally {
await rt.dispose()
}
},
})
},
30_000,
)
})
@@ -2,15 +2,15 @@
// the same parent assistant message. Without the internal lock, parallel
// subagent completions race on read-modify-write and lose deltas (#6321).
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { afterEach, describe, expect } from "bun:test"
import { Effect, Layer } from "effect"
import { Effect } from "effect"
import { Bus } from "../../src/bus"
import * as CrossSpawnSpawner from "@opencode-ai/core/cross-spawn-spawner"
import { KiloCostPropagation } from "../../src/kilocode/session/cost-propagation"
import { Instance } from "../../src/kilocode/instance"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
import { SessionProjector } from "@opencode-ai/core/session/projector"
import { Session } from "../../src/session/session"
import { MessageV2 } from "../../src/session/message-v2"
import { MessageID } from "../../src/session/schema"
@@ -31,7 +31,16 @@ const ref = {
}
const it = testEffect(
Layer.mergeAll(AppNodeBuilder.build(Session.node), Bus.layer, AppNodeBuilder.build(Database.node), AppNodeBuilder.build(CrossSpawnSpawner.node)),
LayerNode.compile(
LayerNode.group([
Session.node,
SessionProjector.node,
MessageV2.node,
Bus.node,
Database.node,
CrossSpawnSpawner.node,
]),
),
)
const seed = Effect.fn("CostPropagationTest.seed")(function* () {
@@ -16,6 +16,8 @@
// Progress is mirrored to a JSON file so tests can assert what the provider saw
// without sharing module state with the plugin that loads this file.
import { rename } from "node:fs/promises"
export type StallState = { calls: number; stalls: number; recovered: number }
const HEAD = { id: "chatcmpl-stall", object: "chat.completion.chunk", created: 0, model: "mock-model" }
@@ -84,7 +86,16 @@ function stalling() {
export function createStallTransport(input: { state: string; answer?: string; command?: string }) {
const state: StallState = { calls: 0, stalls: 0, recovered: 0 }
const save = () => Bun.write(input.state, JSON.stringify(state))
let pending = Promise.resolve()
const save = () => {
const json = JSON.stringify(state)
const tmp = `${input.state}.${crypto.randomUUID()}.tmp`
pending = pending.then(async () => {
await Bun.write(tmp, json)
await rename(tmp, input.state)
})
return pending
}
return async (_input: unknown, init?: { body?: unknown }) => {
const body = typeof init?.body === "string" ? init.body : ""
@@ -4,12 +4,17 @@ import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { Deferred, Effect, Fiber, Layer } from "effect"
import { GlobalBus, type GlobalEvent } from "../../src/bus/global"
import { Git } from "../../src/git"
import { InstanceBootstrap } from "../../src/project/bootstrap"
import { InstanceStore } from "../../src/project/instance-store"
import { KilocodeWatcher } from "../../src/kilocode/watcher"
import { tmpdirScoped } from "../fixture/fixture"
import { awaitWithTimeout, testEffect } from "../lib/effect"
const layer = Layer.mergeAll(AppNodeBuilder.build(InstanceStore.node), AppNodeBuilder.build(Git.node), AppNodeBuilder.build(CrossSpawnSpawner.node))
const layer = Layer.mergeAll(
AppNodeBuilder.build(InstanceStore.node, [[InstanceStore.bootstrapNode, InstanceBootstrap.node]]),
AppNodeBuilder.build(Git.node),
AppNodeBuilder.build(CrossSpawnSpawner.node),
)
const it = testEffect(layer)
// The suite disables the file watcher (see test/preload.ts); this file tests it, so opt back in.
@@ -77,6 +77,7 @@ const realToast = await import("@tui/ui/toast")
const realEvent = await import("@tui/context/event")
const realRoute = await import("@tui/context/route")
const realRuntime = await import("@tui/context/runtime")
const realPermission = await import("@tui/context/permission")
let capturedInit: (() => any) | undefined
@@ -169,6 +170,11 @@ mock.module("@tui/context/route", () => ({
useRoute: () => ({ data: { type: "home" }, navigate: () => {} }),
}))
mock.module("@tui/context/permission", () => ({
...realPermission,
usePermission: () => ({ mode: "normal", set: () => {}, toggle: () => {} }),
}))
// Import the real Global to get the state path (set by test preload via XDG_STATE_HOME)
const { Global } = await import("@opencode-ai/core/global")
const modelJsonPath = path.join(Global.Path.state, "model.json")
@@ -7,6 +7,8 @@ import { Identifier } from "../../src/id/id"
import { SessionID, MessageID, PartID } from "../../src/session/schema"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { SessionProjector } from "@opencode-ai/core/session/projector"
import { Instance } from "../../src/kilocode/instance"
import { provideTestInstance } from "../fixture/fixture"
import { PlanFollowup } from "../../src/kilocode/plan-followup"
@@ -21,7 +23,10 @@ import { tmpdir } from "../fixture/fixture"
Log.init({ print: false })
const session = makeRuntime(Session.Service, Session.defaultLayer)
const session = makeRuntime(
Session.Service,
LayerNode.compile(LayerNode.group([Session.node, SessionProjector.node])),
)
const sessions = {
create: (input?: Parameters<Session.Interface["create"]>[0]) =>
session.runPromise((svc) => svc.create(input)),
@@ -1,6 +1,6 @@
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { describe, expect, test } from "bun:test"
import { Effect, Layer, ManagedRuntime } from "effect"
import { Effect, ManagedRuntime } from "effect"
import path from "path"
import { Agent } from "../../src/agent/agent"
import { PlanFile } from "../../src/kilocode/plan-file"
@@ -10,12 +10,15 @@ import { Session } from "../../src/session/session"
import { MessageID, PartID } from "../../src/session/schema"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
import { SessionProjector } from "@opencode-ai/core/session/projector"
import { PlanExitTool } from "../../src/tool/plan"
import { Tool } from "../../src/tool/tool"
import { Truncate } from "../../src/tool/truncate"
import { tmpdir } from "../fixture/fixture"
const rt = ManagedRuntime.make(Layer.mergeAll(AppNodeBuilder.build(Agent.node), AppNodeBuilder.build(Session.node), AppNodeBuilder.build(Truncate.node)))
const rt = ManagedRuntime.make(
LayerNode.compile(LayerNode.group([Agent.node, Session.node, SessionProjector.node, Truncate.node])),
)
async function init() {
return rt.runPromise(
@@ -11,6 +11,8 @@ import { SessionID, MessageID, PartID } from "../../src/session/schema"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
import { EventV2 } from "@opencode-ai/core/event"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { SessionProjector } from "@opencode-ai/core/session/projector"
import { formatTodos, generateHandover, PlanFollowup, PlanFollowupRuntime } from "../../src/kilocode/plan-followup"
import { Instance } from "../../src/kilocode/instance"
import * as KiloInstance from "../../src/kilocode/instance"
@@ -67,7 +69,10 @@ const todo = {
},
}
const session = makeRuntime(Session.Service, Session.defaultLayer)
const session = makeRuntime(
Session.Service,
LayerNode.compile(LayerNode.group([Session.node, SessionProjector.node])),
)
const store = {
create: (input?: Parameters<Session.Interface["create"]>[0]) => session.runPromise((svc) => svc.create(input)),
get: (id: SessionID) => session.runPromise((svc) => svc.get(id)),
@@ -11,7 +11,10 @@ import { awaitWithTimeout, testEffect } from "../../lib/effect"
const bootstrap = Layer.succeed(InstanceBootstrap.Service, InstanceBootstrap.Service.of({ run: Effect.void }))
const it = testEffect(
Layer.mergeAll(AppNodeBuilder.build(InstanceStore.node), AppNodeBuilder.build(CrossSpawnSpawner.node)).pipe(Layer.provide(bootstrap)),
Layer.mergeAll(
AppNodeBuilder.build(InstanceStore.node, [[InstanceStore.bootstrapNode, bootstrap]]),
AppNodeBuilder.build(CrossSpawnSpawner.node),
),
)
const register = (disposer: (directory: string) => Promise<void>) =>
@@ -25,7 +25,7 @@ import fs from "node:fs"
import path from "node:path"
import { Option } from "../../src/question"
const SOURCE = path.resolve(import.meta.dir, "../../src/question/index.ts")
const SOURCE = path.resolve(import.meta.dir, "../../../schema/src/v1/question.ts")
describe("QuestionOption schema — Kilo-specific field contract", () => {
test("Option class accepts and round-trips the mode field", () => {
@@ -56,14 +56,14 @@ describe("QuestionOption schema — Kilo-specific field contract", () => {
// resolution that drops the fields is caught immediately.
test("source declares mode as an optional field inside a kilocode_change block", () => {
const src = fs.readFileSync(SOURCE, "utf-8")
expect(src).toMatch(/kilocode_change start[^\n]*hint to UI clients/)
expect(src).toMatch(/kilocode_change start[^\n]*localization and mode selection hints/)
expect(src).toMatch(/mode:\s*Schema\.optional\(Schema\.String\)/)
expect(src).toMatch(/kilocode_change end/)
})
test("source declares labelKey and descriptionKey inside a kilocode_change block", () => {
const src = fs.readFileSync(SOURCE, "utf-8")
expect(src).toMatch(/kilocode_change start[^\n]*i18n keys/)
expect(src).toMatch(/kilocode_change start[^\n]*localization and mode selection hints/)
expect(src).toMatch(/labelKey:\s*Schema\.optional\(Schema\.String\)/)
expect(src).toMatch(/descriptionKey:\s*Schema\.optional\(Schema\.String\)/)
})
@@ -1,6 +1,7 @@
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { SessionProjector } from "@opencode-ai/core/session/projector"
import { expect } from "bun:test"
import { Effect, Layer } from "effect"
import { Effect } from "effect"
import { RecallSearch } from "../../src/kilocode/session/recall-search"
import { Instance } from "../../src/kilocode/instance"
import { Session } from "../../src/session/session"
@@ -16,7 +17,7 @@ import { testEffect } from "../lib/effect"
type Stored<T> = T extends unknown ? Omit<T, "id" | "sessionID" | "messageID"> : never
const it = testEffect(Layer.mergeAll(AppNodeBuilder.build(Session.node), AppNodeBuilder.build(Database.node)))
const it = testEffect(LayerNode.compile(LayerNode.group([Session.node, SessionProjector.node, Database.node])))
const add = Effect.fn("RecallSearchTest.add")(function* (
sessionID: SessionID,
@@ -106,9 +106,19 @@ function data(diff = false) {
async function request(directory: string, body: unknown) {
const auth = process.env.KILO_AUTH_CONTENT
const fetcher = globalThis.fetch
const token = `test-${crypto.randomUUID()}`
bodies.set(token, body)
process.env.KILO_AUTH_CONTENT = JSON.stringify({ kilo: { type: "api", key: token } })
globalThis.fetch = Object.assign(
(input: URL | RequestInfo, init?: BunFetchRequestInit | RequestInit) => {
const req = new Request(input, init)
const url = new URL(req.url)
if (url.pathname !== "/api/session/ses_cloud/export") return fetcher(req)
return fetcher(new Request(`${ingest.url.origin}${url.pathname}${url.search}`, req))
},
{ preconnect: fetcher.preconnect },
)
try {
return await HttpApiApp.webHandler().handler(
new Request(`http://localhost${KiloGatewayPaths.cloudSessionImport}`, {
@@ -120,6 +130,7 @@ async function request(directory: string, body: unknown) {
)
} finally {
bodies.delete(token)
globalThis.fetch = fetcher
if (auth === undefined) delete process.env.KILO_AUTH_CONTENT
else process.env.KILO_AUTH_CONTENT = auth
}
@@ -1,28 +1,29 @@
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { SessionProjector } from "@opencode-ai/core/session/projector"
import { afterEach, describe, expect, test } from "bun:test"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Cause, Effect, Exit, Fiber, Layer } from "effect"
import { Cause, Effect, Exit, Fiber } from "effect"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { Bus } from "../../../src/bus"
import * as Config from "../../../src/config/config"
import { AllowEverythingPermission } from "../../../src/kilocode/permission/allow-everything"
import { Permission } from "../../../src/permission"
import { EventV2Bridge } from "../../../src/event-v2-bridge"
import { PermissionV1 } from "@opencode-ai/core/v1/permission"
import { Database } from "@opencode-ai/core/database/database"
import { provideTestInstance } from "../../fixture/fixture"
import { Server } from "../../../src/server/server"
import { Session } from "../../../src/session/session"
import { provideTmpdirInstance, tmpdir } from "../../fixture/fixture"
import { testEffect } from "../../lib/effect"
const bus = Bus.layer
const env = Layer.mergeAll(
AppNodeBuilder.build(Permission.node),
AppNodeBuilder.build(Config.node),
AppNodeBuilder.build(Session.node),
bus,
AppNodeBuilder.build(CrossSpawnSpawner.node),
const env = LayerNode.compile(
LayerNode.group([
Permission.node,
Config.node,
Session.node,
SessionProjector.node,
Bus.node,
CrossSpawnSpawner.node,
]),
)
const it = testEffect(env)
const original = {
@@ -3,17 +3,15 @@
// overflows the model context, and that the exhausted turn surfaces as an
// error (rather than silently completing).
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { NodeFileSystem } from "@effect/platform-node"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { SessionProjector } from "@opencode-ai/core/session/projector"
import { describe, expect } from "bun:test"
import { Deferred, Effect, Layer } from "effect"
import { FetchHttpClient } from "effect/unstable/http"
import { Database } from "@opencode-ai/core/database/database"
import { Agent as AgentSvc } from "../../src/agent/agent"
import { BackgroundJob } from "../../src/background/job"
import { Bus } from "../../src/bus"
import { Command } from "../../src/command"
import { Auth } from "../../src/auth"
import { Config } from "../../src/config/config"
import { RuntimeFlags } from "../../src/effect/runtime-flags"
import { EventV2Bridge } from "../../src/event-v2-bridge"
@@ -35,7 +33,6 @@ import { Provider as ProviderSvc } from "../../src/provider/provider"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
import { Question } from "../../src/question"
import { RepositoryCache } from "@opencode-ai/core/repository-cache"
import { Session } from "../../src/session/session"
import { SessionCompaction } from "../../src/session/compaction"
import { Instruction } from "../../src/session/instruction"
@@ -52,8 +49,6 @@ import { SessionSummary } from "../../src/session/summary"
import { Todo } from "../../src/session/todo"
import { Skill } from "../../src/skill"
import { Snapshot } from "../../src/snapshot"
import { Storage } from "../../src/storage/storage"
import { SyncEvent } from "../../src/sync"
import { ToolRegistry } from "../../src/tool/registry"
import { Truncate } from "../../src/tool/truncate"
import * as Log from "@opencode-ai/core/util/log"
@@ -133,72 +128,59 @@ const lsp = Layer.succeed(
}),
)
const status = Layer.mergeAll(AppNodeBuilder.build(SessionStatus.node), Bus.layer)
const runState = SessionRunState.layer.pipe(Layer.provide(status))
const infra = Layer.mergeAll(NodeFileSystem.layer, AppNodeBuilder.build(CrossSpawnSpawner.node))
const memoryNode = LayerNode.make({ service: MemoryService.Service, layer: MemoryService.layer, deps: [] })
const serverNode = LayerNode.make({ service: TestLLMServer, layer: TestLLMServer.layer, deps: [] })
const root = LayerNode.group([
SessionPrompt.node,
Session.node,
SessionProjector.node,
MessageV2.node,
Snapshot.node,
LLM.node,
Env.node,
AgentSvc.node,
Command.node,
Permission.node,
Plugin.node,
Config.node,
ProviderSvc.node,
LSP.node,
MCP.node,
FSUtil.node,
BackgroundJob.node,
SessionStatus.node,
SessionRunState.node,
Database.node,
EventV2Bridge.node,
Question.node,
Todo.node,
ToolRegistry.node,
Skill.node,
Git.node,
Ripgrep.node,
Format.node,
Truncate.node,
SessionProcessor.node,
Image.node,
SessionCompaction.node,
SessionRevert.node,
Instruction.node,
SystemPrompt.node,
CrossSpawnSpawner.node,
RuntimeFlags.node,
memoryNode,
serverNode,
])
function makeHttp() {
const deps = Layer.mergeAll(
AppNodeBuilder.build(Session.node),
AppNodeBuilder.build(BackgroundJob.node),
AppNodeBuilder.build(Snapshot.node),
AppNodeBuilder.build(LLM.node),
AppNodeBuilder.build(Env.node),
AppNodeBuilder.build(AgentSvc.node),
AppNodeBuilder.build(Command.node),
AppNodeBuilder.build(Permission.node),
plugin,
AppNodeBuilder.build(Config.node),
RuntimeFlags.layer(),
AppNodeBuilder.build(ProviderSvc.node),
lsp,
mcp,
AppNodeBuilder.build(FSUtil.node),
SyncEvent.defaultLayer,
AppNodeBuilder.build(EventV2Bridge.node),
AppNodeBuilder.build(Database.node),
status,
MemoryService.layer,
).pipe(Layer.provideMerge(infra))
const question = Question.layer.pipe(Layer.provideMerge(deps))
const todo = Todo.layer.pipe(Layer.provideMerge(deps))
const registry = AppNodeBuilder.build(ToolRegistry.node, [[KiloSessions.node, KiloSessions.testLayer]])
const trunc = AppNodeBuilder.build(Truncate.node)
const proc = AppNodeBuilder.build(SessionProcessor.node, [[SessionSummary.node, summary]])
const compact = AppNodeBuilder.build(SessionCompaction.node, [
[SessionProcessor.node, proc],
return LayerNode.compile(root, [
[SessionSummary.node, summary],
[Plugin.node, plugin],
[LSP.node, lsp],
[MCP.node, mcp],
[RuntimeFlags.node, RuntimeFlags.layer()],
[KiloSessions.node, KiloSessions.testLayer],
])
return Layer.mergeAll(
TestLLMServer.layer,
SessionPrompt.layer.pipe(
Layer.provide(AppNodeBuilder.build(SessionRevert.node)),
Layer.provide(AppNodeBuilder.build(Image.node)),
Layer.provide(summary),
Layer.provideMerge(runState),
Layer.provideMerge(compact),
Layer.provideMerge(proc),
Layer.provideMerge(registry),
Layer.provideMerge(trunc),
Layer.provideMerge(question),
Layer.provide(AppNodeBuilder.build(Instruction.node)),
Layer.provide(AppNodeBuilder.build(SystemPrompt.node)),
Layer.provideMerge(deps),
),
).pipe(
Layer.provide(
Layer.mergeAll(
summary,
deps,
AppNodeBuilder.build(Config.node),
RuntimeFlags.layer(),
AppNodeBuilder.build(BackgroundJob.node),
Bus.layer,
infra,
AppNodeBuilder.build(Storage.node),
),
),
)
}
const it = testEffect(makeHttp())
@@ -1,4 +1,4 @@
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { SessionProjector } from "@opencode-ai/core/session/projector"
import { afterAll, afterEach, beforeAll, describe, expect, mock, test } from "bun:test"
import { Effect, Layer, ManagedRuntime } from "effect"
import fs from "fs/promises"
@@ -35,6 +35,7 @@ import { ProviderTest } from "../fake/provider"
import { tmpdir } from "../fixture/fixture"
import { Flag } from "@opencode-ai/core/flag/flag"
import { AppRuntime } from "../../src/effect/app-runtime"
import { makeRuntime } from "../../src/effect/run-service"
import { remove as cleanup } from "./cleanup"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Provider } from "../../src/provider/provider"
@@ -47,6 +48,8 @@ const agents = Layer.mock(Agent.Service)({
})
const previous = Flag.KILO_DB
const dbfile = path.join(os.tmpdir(), `kilo-compaction-chunks-${process.pid}-${crypto.randomUUID()}.db`)
const layer = LayerNode.compile(LayerNode.group([SessionNs.node, SessionProjector.node]))
const runtime = makeRuntime(SessionNs.Service, layer)
beforeAll(async () => {
await fs.rm(dbfile, { force: true })
@@ -54,16 +57,13 @@ beforeAll(async () => {
})
afterAll(async () => {
await runtime.dispose()
await AppRuntime.dispose()
await disposeTestRuntime()
Flag.KILO_DB = previous
await Promise.all([dbfile, `${dbfile}-wal`, `${dbfile}-shm`].map(cleanup))
})
function run<A, E>(fx: Effect.Effect<A, E, SessionNs.Service>) {
return Effect.runPromise(fx.pipe(Effect.provide(AppNodeBuilder.build(SessionNs.node))))
}
const store = {
updateMessage: <T extends MessageV2.Info>(msg: T) => Effect.promise(() => svc.updateMessage(msg)),
updatePart: <T extends MessageV2.Part>(part: T) => Effect.promise(() => svc.updatePart(part)),
@@ -71,16 +71,16 @@ const store = {
const svc = {
create(input?: SessionNs.CreateInput) {
return run(SessionNs.Service.use((svc) => svc.create(input)))
return runtime.runPromise((svc) => svc.create(input))
},
messages(input: Parameters<SessionNs.Interface["messages"]>[0]) {
return run(SessionNs.Service.use((svc) => svc.messages(input)))
return runtime.runPromise((svc) => svc.messages(input))
},
updateMessage<T extends MessageV2.Info>(msg: T) {
return run(SessionNs.Service.use((svc) => svc.updateMessage(msg)))
return runtime.runPromise((svc) => svc.updateMessage(msg))
},
updatePart<T extends MessageV2.Part>(part: T) {
return run(SessionNs.Service.use((svc) => svc.updatePart(part)))
return runtime.runPromise((svc) => svc.updatePart(part))
},
}
@@ -224,13 +224,18 @@ function fakeRuntime(outputTokenMax?: number, error?: MessageV2.Assistant["error
})
}),
)
const processorNode = LayerNode.make({
service: SessionProcessorModule.SessionProcessor.Service,
layer: processor,
deps: [SessionNs.node],
})
const model = ProviderTest.model({ providerID, id: modelID, limit: { context: 10_000, output: 1_000 } })
return {
calls,
outputs,
rt: ManagedRuntime.make(
AppNodeBuilder.build(LayerNode.group([SessionCompaction.node, Bus.node]), [
[SessionProcessorModule.SessionProcessor.node, processor],
LayerNode.compile(LayerNode.group([SessionCompaction.node, SessionNs.node, SessionProjector.node, Bus.node]), [
[SessionProcessorModule.SessionProcessor.node, processorNode],
[Provider.node, ProviderTest.fake({ model }).layer],
[Agent.node, agents],
[RuntimeFlags.node, RuntimeFlags.layer({ outputTokenMax })],
@@ -238,6 +243,7 @@ function fakeRuntime(outputTokenMax?: number, error?: MessageV2.Assistant["error
Config.node,
Layer.mock(Config.Service)({
get: () => Effect.succeed({ ...{}, compaction: { reserved: 1_000 } }),
directories: () => Effect.succeed([]),
}),
],
]),
@@ -290,8 +296,15 @@ async function failure(error?: MessageV2.Assistant["error"], empty = false) {
function liveRuntime(layer: Layer.Layer<LLM.Service>, context = 10_000) {
const model = ProviderTest.model({ providerID, id: modelID, limit: { context, output: 1_000 } })
return ManagedRuntime.make(
AppNodeBuilder.build(
LayerNode.group([SessionCompaction.node, SessionProcessorModule.SessionProcessor.node, Bus.node, SessionStatus.node]),
LayerNode.compile(
LayerNode.group([
SessionCompaction.node,
SessionProcessorModule.SessionProcessor.node,
SessionNs.node,
SessionProjector.node,
Bus.node,
SessionStatus.node,
]),
[
[SessionSummary.node, summary],
[Provider.node, ProviderTest.fake({ model }).layer],
@@ -301,6 +314,7 @@ function liveRuntime(layer: Layer.Layer<LLM.Service>, context = 10_000) {
Config.node,
Layer.mock(Config.Service)({
get: () => Effect.succeed({ ...{}, compaction: { reserved: 1_000 } }),
directories: () => Effect.succeed([]),
}),
],
],
@@ -698,82 +712,85 @@ describe("KiloCompactionChunks", () => {
})
})
test(
"compaction must not leak maxOutputTokens into agent options",
async () => {
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(20_000))
await assistant(session.id, first.id, tmp.path, "reply " + "b".repeat(20_000))
const second = await user(session.id, "second " + "c".repeat(20_000))
await assistant(session.id, second.id, tmp.path, "reply " + "d".repeat(20_000))
await Effect.runPromise(
KiloSessionCompaction.create({
session: store,
sessionID: session.id,
agent: "build",
model: ref,
auto: false,
}),
)
test("compaction must not leak maxOutputTokens into agent options", async () => {
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(20_000))
await assistant(session.id, first.id, tmp.path, "reply " + "b".repeat(20_000))
const second = await user(session.id, "second " + "c".repeat(20_000))
await assistant(session.id, second.id, tmp.path, "reply " + "d".repeat(20_000))
await Effect.runPromise(
KiloSessionCompaction.create({
session: store,
sessionID: session.id,
agent: "build",
model: ref,
auto: false,
}),
)
const captured: Array<{ opts: Record<string, unknown>; modelLimitOutput: number }> = []
const bus = Bus.layer
const processor = Layer.effect(
SessionProcessorModule.SessionProcessor.Service,
Effect.gen(function* () {
const sessions = yield* SessionNs.Service
return SessionProcessorModule.SessionProcessor.Service.of({
create: Effect.fn("TestSessionProcessorLeak.create")((input) =>
Effect.succeed({
get message() {
return input.assistantMessage
},
updateToolCall: Effect.fn("TestSessionProcessorLeak.updateToolCall")(() =>
Effect.succeed(undefined),
),
metadata: Effect.fn("TestSessionProcessorLeak.metadata")(() => Effect.void),
completeToolCall: Effect.fn("TestSessionProcessorLeak.completeToolCall")(() => Effect.void),
process: Effect.fn("TestSessionProcessorLeak.process")((stream: LLM.StreamInput) =>
Effect.gen(function* () {
captured.push({
opts: stream.agent.options as Record<string, unknown>,
modelLimitOutput: stream.model.limit.output,
})
const text = stream.messages.some((msg) =>
JSON.stringify(msg).includes("Create a new anchored summary"),
)
? "final summary"
: "chunk summary"
yield* sessions.updatePart({
id: PartID.ascending(),
messageID: input.assistantMessage.id,
sessionID: input.sessionID,
type: "text",
text,
})
input.assistantMessage.finish = "stop"
return "continue" as const
}),
),
} satisfies SessionProcessor.Handle),
),
})
}),
)
const captured: Array<{ opts: Record<string, unknown>; modelLimitOutput: number }> = []
const bus = Bus.layer
const processor = Layer.effect(
SessionProcessorModule.SessionProcessor.Service,
Effect.gen(function* () {
const sessions = yield* SessionNs.Service
return SessionProcessorModule.SessionProcessor.Service.of({
create: Effect.fn("TestSessionProcessorLeak.create")((input) =>
Effect.succeed({
get message() {
return input.assistantMessage
},
updateToolCall: Effect.fn("TestSessionProcessorLeak.updateToolCall")(() => Effect.succeed(undefined)),
metadata: Effect.fn("TestSessionProcessorLeak.metadata")(() => Effect.void),
completeToolCall: Effect.fn("TestSessionProcessorLeak.completeToolCall")(() => Effect.void),
process: Effect.fn("TestSessionProcessorLeak.process")((stream: LLM.StreamInput) =>
Effect.gen(function* () {
captured.push({
opts: stream.agent.options as Record<string, unknown>,
modelLimitOutput: stream.model.limit.output,
})
const text = stream.messages.some((msg) =>
JSON.stringify(msg).includes("Create a new anchored summary"),
)
? "final summary"
: "chunk summary"
yield* sessions.updatePart({
id: PartID.ascending(),
messageID: input.assistantMessage.id,
sessionID: input.sessionID,
type: "text",
text,
})
input.assistantMessage.finish = "stop"
return "continue" as const
}),
),
} satisfies SessionProcessor.Handle),
),
})
}),
)
const model = ProviderTest.model({
providerID,
id: modelID,
limit: { context: 10_000, output: 1_000 },
})
const outputTokenMax = 512
const rt = ManagedRuntime.make(
AppNodeBuilder.build(LayerNode.group([SessionCompaction.node, Bus.node]), [
[SessionProcessorModule.SessionProcessor.node, processor],
const model = ProviderTest.model({
providerID,
id: modelID,
limit: { context: 10_000, output: 1_000 },
})
const processorNode = LayerNode.make({
service: SessionProcessorModule.SessionProcessor.Service,
layer: processor,
deps: [SessionNs.node],
})
const outputTokenMax = 512
const rt = ManagedRuntime.make(
LayerNode.compile(
LayerNode.group([SessionCompaction.node, SessionNs.node, SessionProjector.node, Bus.node]),
[
[SessionProcessorModule.SessionProcessor.node, processorNode],
[Provider.node, ProviderTest.fake({ model }).layer],
[Agent.node, agents],
[RuntimeFlags.node, RuntimeFlags.layer({ outputTokenMax })],
@@ -781,49 +798,49 @@ describe("KiloCompactionChunks", () => {
Config.node,
Layer.mock(Config.Service)({
get: () => Effect.succeed({ ...{}, compaction: { reserved: 1_000 } }),
directories: () => Effect.succeed([]),
}),
],
]),
],
),
)
try {
const msgs = await svc.messages({ sessionID: session.id })
const parent = msgs.at(-1)?.info.id
expect(parent).toBeTruthy()
const result = await rt.runPromise(
SessionCompaction.Service.use((svc) =>
svc.process({
parentID: parent!,
messages: msgs,
sessionID: session.id,
auto: false,
}),
),
)
try {
const msgs = await svc.messages({ sessionID: session.id })
const parent = msgs.at(-1)?.info.id
expect(parent).toBeTruthy()
const result = await rt.runPromise(
SessionCompaction.Service.use((svc) =>
svc.process({
parentID: parent!,
messages: msgs,
sessionID: session.id,
auto: false,
}),
),
)
expect(result).toBe("continue")
expect(captured.length).toBeGreaterThan(0)
// Negative assertion (the bug surfacing):
// maxOutputTokens must not appear in agent.options that the
// worker hands to the LLM. Today a strict OpenAI-compatible
// upstream rejects that field with
// Unsupported parameter(s): maxOutputTokens`.
for (const c of captured) {
expect(c.opts.maxOutputTokens).toBeUndefined()
}
// Positive assertion (budget preserved through an independent path):
// the constrained model still threads a tightened output limit
// through to every worker. If a future "fix" accidentally severs
// the only budget source along with the leak, this fails.
for (const c of captured) {
expect(c.modelLimitOutput).toBeLessThanOrEqual(outputTokenMax)
}
} finally {
await rt.dispose()
expect(result).toBe("continue")
expect(captured.length).toBeGreaterThan(0)
// Negative assertion (the bug surfacing):
// maxOutputTokens must not appear in agent.options that the
// worker hands to the LLM. Today a strict OpenAI-compatible
// upstream rejects that field with
// Unsupported parameter(s): maxOutputTokens`.
for (const c of captured) {
expect(c.opts.maxOutputTokens).toBeUndefined()
}
},
})
},
30_000,
)
// Positive assertion (budget preserved through an independent path):
// the constrained model still threads a tightened output limit
// through to every worker. If a future "fix" accidentally severs
// the only budget source along with the leak, this fails.
for (const c of captured) {
expect(c.modelLimitOutput).toBeLessThanOrEqual(outputTokenMax)
}
} finally {
await rt.dispose()
}
},
})
}, 30_000)
})
@@ -1,4 +1,6 @@
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { SessionProjector } from "@opencode-ai/core/session/projector"
import { afterAll, afterEach, beforeAll, describe, expect, test } from "bun:test"
import { Effect } from "effect"
import { HttpRouter } from "effect/unstable/http"
@@ -23,12 +25,15 @@ import path from "path"
import os from "os"
import fs from "fs/promises"
import { AppRuntime } from "../../src/effect/app-runtime"
import { makeRuntime } from "../../src/effect/run-service"
import { remove as cleanup } from "./cleanup"
Log.init({ print: false })
const previous = Flag.KILO_DB
const dbfile = path.join(os.tmpdir(), `kilo-fork-${process.pid}-${crypto.randomUUID()}.db`)
const layer = LayerNode.compile(LayerNode.group([Session.node, SessionProjector.node]))
const runtime = makeRuntime(Session.Service, layer)
beforeAll(async () => {
await fs.rm(dbfile, { force: true })
@@ -36,6 +41,7 @@ beforeAll(async () => {
})
afterAll(async () => {
await runtime.dispose()
await AppRuntime.dispose()
await disposeTestRuntime()
Flag.KILO_DB = previous
@@ -44,14 +50,14 @@ afterAll(async () => {
const sessions = {
create: (input?: Parameters<Session.Interface["create"]>[0]) =>
Effect.runPromise(Session.Service.use((svc) => svc.create(input)).pipe(Effect.provide(AppNodeBuilder.build(Session.node)))),
list: () => Effect.runPromise(Session.Service.use((svc) => svc.list()).pipe(Effect.provide(AppNodeBuilder.build(Session.node)))),
runtime.runPromise((svc) => svc.create(input)),
list: () => runtime.runPromise((svc) => svc.list()),
messages: (input: Parameters<Session.Interface["messages"]>[0]) =>
Effect.runPromise(Session.Service.use((svc) => svc.messages(input)).pipe(Effect.provide(AppNodeBuilder.build(Session.node)))),
runtime.runPromise((svc) => svc.messages(input)),
updateMessage: <T extends MessageV2.Info>(msg: T) =>
Effect.runPromise(Session.Service.use((svc) => svc.updateMessage(msg)).pipe(Effect.provide(AppNodeBuilder.build(Session.node)))),
runtime.runPromise((svc) => svc.updateMessage(msg)),
updatePart: <T extends MessageV2.Part>(part: T) =>
Effect.runPromise(Session.Service.use((svc) => svc.updatePart(part)).pipe(Effect.provide(AppNodeBuilder.build(Session.node)))),
runtime.runPromise((svc) => svc.updatePart(part)),
}
afterEach(async () => {
@@ -1,6 +1,7 @@
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { SessionProjector } from "@opencode-ai/core/session/projector"
import { describe, expect } from "bun:test"
import { Effect, Layer } from "effect"
import { Effect } from "effect"
import path from "path"
import { seedProject } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
@@ -15,7 +16,7 @@ import { AbsolutePath } from "@opencode-ai/core/schema"
import * as Log from "@opencode-ai/core/util/log"
Log.init({ print: false })
const layer = Layer.mergeAll(AppNodeBuilder.build(Session.node), AppNodeBuilder.build(Database.node))
const layer = LayerNode.compile(LayerNode.group([Session.node, SessionProjector.node, Database.node]))
const it = testEffect(layer)
describe("Kilo Session.list", () => {
@@ -1,6 +1,7 @@
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { SessionProjector } from "@opencode-ai/core/session/projector"
import { describe, expect } from "bun:test"
import { Effect, Layer } from "effect"
import { Effect } from "effect"
import { ModelUsage } from "@/kilocode/session/model-usage"
import { ProjectTable } from "@opencode-ai/core/project/sql"
import { ProjectV2 } from "@opencode-ai/core/project"
@@ -16,7 +17,7 @@ import { eq } from "drizzle-orm"
import { TestInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
const it = testEffect(Layer.mergeAll(AppNodeBuilder.build(Session.node), AppNodeBuilder.build(Database.node)))
const it = testEffect(LayerNode.compile(LayerNode.group([Session.node, SessionProjector.node, Database.node])))
const ref = (providerID: string, modelID: string) => ({
providerID: ProviderV2.ID.make(providerID),
@@ -1,4 +1,5 @@
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { SessionProjector } from "@opencode-ai/core/session/projector"
import { NodeFileSystem } from "@effect/platform-node"
import { describe, expect } from "bun:test"
import { Context, Effect, Layer } from "effect"
@@ -49,6 +50,8 @@ class TestLLM extends Context.Service<
}
>()("@test/EmptyToolCallsLLM") {}
class State extends Context.Service<State, { readonly queue: Script[] }>()("@test/EmptyToolCallsState") {}
function model(selection = ref): Provider.Model {
return {
id: selection.modelID,
@@ -77,48 +80,58 @@ function usage() {
}
}
const llm = Layer.unwrap(
Effect.gen(function* () {
const queue: Script[] = []
const push = (item: Script) => {
queue.push(item)
return Effect.void
}
const reply = (...items: Event[]) => push(Stream.make(...items))
return Layer.mergeAll(
Layer.succeed(
LLM.Service,
LLM.Service.of({
stream: () => {
const item = queue.shift() ?? Stream.empty
return item
},
}),
),
Layer.succeed(TestLLM, TestLLM.of({ reply, script: push })),
)
}),
)
const status = Layer.mergeAll(AppNodeBuilder.build(SessionStatus.node), Bus.layer)
const infra = Layer.mergeAll(NodeFileSystem.layer, AppNodeBuilder.build(CrossSpawnSpawner.node))
const deps = Layer.mergeAll(
AppNodeBuilder.build(Session.node),
AppNodeBuilder.build(Snapshot.node),
AppNodeBuilder.build(AgentSvc.node),
AppNodeBuilder.build(Permission.node),
AppNodeBuilder.build(Plugin.node),
AppNodeBuilder.build(Config.node),
RuntimeFlags.layer(),
AppNodeBuilder.build(SessionSummary.node),
AppNodeBuilder.build(Image.node),
SyncEvent.defaultLayer,
AppNodeBuilder.build(EventV2Bridge.node),
AppNodeBuilder.build(Database.node),
status,
llm,
).pipe(Layer.provideMerge(infra))
const env = AppNodeBuilder.build(SessionProcessor.node).pipe(Layer.provideMerge(deps))
const stateNode = LayerNode.make({
service: State,
layer: Layer.sync(State, () => State.of({ queue: [] })),
deps: [],
})
const llmNode = LayerNode.make({
service: LLM.Service,
layer: Layer.effect(
LLM.Service,
Effect.gen(function* () {
const state = yield* State
return LLM.Service.of({ stream: () => state.queue.shift() ?? Stream.empty })
}),
),
deps: [stateNode],
})
const testNode = LayerNode.make({
service: TestLLM,
layer: Layer.effect(
TestLLM,
Effect.gen(function* () {
const state = yield* State
const push = (item: Script) => Effect.sync(() => state.queue.push(item)).pipe(Effect.asVoid)
return TestLLM.of({ reply: (...items) => push(Stream.make(...items)), script: push })
}),
),
deps: [stateNode],
})
const root = LayerNode.group([
SessionProcessor.node,
Session.node,
SessionProjector.node,
MessageV2.node,
Snapshot.node,
AgentSvc.node,
Permission.node,
Plugin.node,
Config.node,
SessionSummary.node,
Image.node,
SessionStatus.node,
EventV2Bridge.node,
Database.node,
CrossSpawnSpawner.node,
RuntimeFlags.node,
LLM.node,
testNode,
])
const env = LayerNode.compile(root, [
[LLM.node, llmNode],
[RuntimeFlags.node, RuntimeFlags.layer()],
]).pipe(Layer.provideMerge(Layer.mergeAll(NodeFileSystem.layer, Bus.layer, SyncEvent.defaultLayer)))
const it = testEffect(env)
@@ -1,4 +1,5 @@
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { SessionProjector } from "@opencode-ai/core/session/projector"
import { NodeFileSystem } from "@effect/platform-node"
import { describe, expect, spyOn } from "bun:test"
import { APICallError } from "ai"
@@ -53,6 +54,10 @@ class TestLLM extends Context.Service<
}
>()("@test/IncompleteResponseRetryLLM") {}
class State extends Context.Service<State, { readonly queue: Script[]; calls: number }>()(
"@test/IncompleteResponseRetryState",
) {}
function model(): Provider.Model {
return {
id: ref.modelID,
@@ -108,63 +113,72 @@ function retryable429() {
})
}
const llm = Layer.unwrap(
Effect.gen(function* () {
const queue: Script[] = []
let calls = 0
const push = (stream: Script) => {
queue.push(stream)
return Effect.void
}
return Layer.mergeAll(
Layer.succeed(
LLM.Service,
LLM.Service.of({
stream: () => {
calls += 1
return queue.shift() ?? Stream.fail(new Error("unexpected extra llm call"))
},
}),
),
Layer.succeed(
TestLLM,
TestLLM.of({
push,
reply: (...events) => push(Stream.make(...events)),
calls: Effect.sync(() => calls),
}),
),
)
}),
)
const reference = Layer.mock(Reference.Service, {
list: () => Effect.succeed([]),
})
const status = Layer.mergeAll(AppNodeBuilder.build(SessionStatus.node), Bus.layer)
const infra = Layer.mergeAll(NodeFileSystem.layer, AppNodeBuilder.build(CrossSpawnSpawner.node))
const stateNode = LayerNode.make({
service: State,
layer: Layer.sync(State, () => State.of({ queue: [], calls: 0 })),
deps: [],
})
const llmNode = LayerNode.make({
service: LLM.Service,
layer: Layer.effect(
LLM.Service,
Effect.gen(function* () {
const state = yield* State
return LLM.Service.of({
stream: () => {
state.calls += 1
return state.queue.shift() ?? Stream.fail(new Error("unexpected extra llm call"))
},
})
}),
),
deps: [stateNode],
})
const testNode = LayerNode.make({
service: TestLLM,
layer: Layer.effect(
TestLLM,
Effect.gen(function* () {
const state = yield* State
const push = (stream: Script) => Effect.sync(() => state.queue.push(stream)).pipe(Effect.asVoid)
return TestLLM.of({
push,
reply: (...events) => push(Stream.make(...events)),
calls: Effect.sync(() => state.calls),
})
}),
),
deps: [stateNode],
})
const root = LayerNode.group([
SessionProcessor.node,
Session.node,
SessionProjector.node,
MessageV2.node,
Snapshot.node,
AgentSvc.node,
Permission.node,
Plugin.node,
Config.node,
SessionSummary.node,
Image.node,
SessionStatus.node,
EventV2Bridge.node,
Database.node,
CrossSpawnSpawner.node,
RuntimeFlags.node,
LLM.node,
testNode,
])
const env = (event = false) =>
AppNodeBuilder.build(SessionProcessor.node).pipe(
Layer.provideMerge(
Layer.mergeAll(
AppNodeBuilder.build(Session.node),
AppNodeBuilder.build(Snapshot.node),
AppNodeBuilder.build(AgentSvc.node),
AppNodeBuilder.build(Permission.node),
AppNodeBuilder.build(Plugin.node),
AppNodeBuilder.build(Config.node),
RuntimeFlags.layer({ experimentalEventSystem: event }),
reference,
AppNodeBuilder.build(SessionSummary.node),
AppNodeBuilder.build(Image.node),
SyncEvent.defaultLayer,
AppNodeBuilder.build(EventV2Bridge.node),
AppNodeBuilder.build(Database.node),
status,
llm,
).pipe(Layer.provideMerge(infra)),
),
Layer.provide(reference),
LayerNode.compile(root, [
[LLM.node, llmNode],
[RuntimeFlags.node, RuntimeFlags.layer({ experimentalEventSystem: event })],
]).pipe(
Layer.provideMerge(Layer.mergeAll(NodeFileSystem.layer, Bus.layer, SyncEvent.defaultLayer, reference)),
)
const it = testEffect(env())
@@ -560,16 +574,23 @@ describe("session processor incomplete response retry", () => {
),
)
eventIt.effect("does not retry when Event V2 mirroring is enabled", () =>
eventIt.effect("retries an empty response the same way when the event system flag is on", () =>
provideTmpdirProject(
(dir) =>
Effect.gen(function* () {
const ctx = yield* setup(dir)
yield* ctx.test.reply(...empty())
yield* ctx.test.reply(...success())
const delay = spyOn(SessionRetry, "delay").mockReturnValue(0)
expect(yield* ctx.handle.process(ctx.input)).toBe("continue")
expect(yield* ctx.test.calls).toBe(1)
expect(ctx.handle.message.finish).toBe("unknown")
try {
expect(yield* ctx.handle.process(ctx.input)).toBe("continue")
} finally {
delay.mockRestore()
}
expect(yield* ctx.test.calls).toBe(2)
expect(ctx.handle.message.finish).toBe("stop")
}),
{ git: true },
),
@@ -1,4 +1,5 @@
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { SessionProjector } from "@opencode-ai/core/session/projector"
import { NodeFileSystem } from "@effect/platform-node"
import { describe, expect, spyOn } from "bun:test"
import { Context, Effect, Layer } from "effect"
@@ -48,6 +49,8 @@ class TestLLM extends Context.Service<
}
>()("@test/OfflineLLM") {}
class State extends Context.Service<State, { readonly queue: Script[] }>()("@test/OfflineLLMState") {}
function model(): Provider.Model {
return {
id: "test-model",
@@ -76,47 +79,59 @@ function usage() {
}
}
const llm = Layer.unwrap(
Effect.gen(function* () {
const queue: Script[] = []
const push = (item: Script) => {
queue.push(item)
return Effect.void
}
return Layer.mergeAll(
Layer.succeed(
LLM.Service,
LLM.Service.of({
stream: () => {
const item = queue.shift() ?? Stream.empty
return item
},
}),
),
Layer.succeed(TestLLM, TestLLM.of({ push })),
)
}),
)
const status = Layer.mergeAll(AppNodeBuilder.build(SessionStatus.node), Bus.layer)
const infra = Layer.mergeAll(NodeFileSystem.layer, AppNodeBuilder.build(CrossSpawnSpawner.node))
const deps = Layer.mergeAll(
AppNodeBuilder.build(Session.node),
AppNodeBuilder.build(Snapshot.node),
AppNodeBuilder.build(AgentSvc.node),
AppNodeBuilder.build(Permission.node),
AppNodeBuilder.build(Plugin.node),
AppNodeBuilder.build(Config.node),
RuntimeFlags.layer(),
AppNodeBuilder.build(SessionSummary.node),
AppNodeBuilder.build(Image.node),
SyncEvent.defaultLayer,
AppNodeBuilder.build(EventV2Bridge.node),
AppNodeBuilder.build(Database.node),
status,
llm,
).pipe(Layer.provideMerge(infra))
const env = AppNodeBuilder.build(SessionProcessor.node).pipe(Layer.provideMerge(deps))
const stateNode = LayerNode.make({
service: State,
layer: Layer.sync(State, () => State.of({ queue: [] })),
deps: [],
})
const llmNode = LayerNode.make({
service: LLM.Service,
layer: Layer.effect(
LLM.Service,
Effect.gen(function* () {
const state = yield* State
return LLM.Service.of({ stream: () => state.queue.shift() ?? Stream.empty })
}),
),
deps: [stateNode],
})
const testNode = LayerNode.make({
service: TestLLM,
layer: Layer.effect(
TestLLM,
Effect.gen(function* () {
const state = yield* State
return TestLLM.of({
push: (item) => Effect.sync(() => state.queue.push(item)).pipe(Effect.asVoid),
})
}),
),
deps: [stateNode],
})
const root = LayerNode.group([
SessionProcessor.node,
Session.node,
SessionProjector.node,
MessageV2.node,
Snapshot.node,
AgentSvc.node,
Permission.node,
Plugin.node,
Config.node,
SessionSummary.node,
Image.node,
SessionStatus.node,
EventV2Bridge.node,
Database.node,
CrossSpawnSpawner.node,
RuntimeFlags.node,
LLM.node,
testNode,
])
const env = LayerNode.compile(root, [
[LLM.node, llmNode],
[RuntimeFlags.node, RuntimeFlags.layer()],
]).pipe(Layer.provideMerge(Layer.mergeAll(NodeFileSystem.layer, Bus.layer, SyncEvent.defaultLayer)))
const it = testEffect(env)
@@ -4,7 +4,8 @@
// transitively load flag.ts to ensure the env is captured at load time.
process.env.KILO_SESSION_RETRY_LIMIT = "2"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { SessionProjector } from "@opencode-ai/core/session/projector"
import { NodeFileSystem } from "@effect/platform-node"
import { afterEach, describe, expect, spyOn } from "bun:test"
import { APICallError } from "ai"
@@ -56,6 +57,8 @@ class TestLLM extends Context.Service<
}
>()("@test/RetryLimitLLM") {}
class State extends Context.Service<State, { readonly queue: Script[]; calls: number }>()("@test/RetryLimitLLMState") {}
function model(): Provider.Model {
return {
id: "test-model",
@@ -87,49 +90,65 @@ function retryable429() {
})
}
const llm = Layer.unwrap(
Effect.gen(function* () {
const queue: Script[] = []
let calls = 0
const push = (item: Script) => {
queue.push(item)
return Effect.void
}
return Layer.mergeAll(
Layer.succeed(
LLM.Service,
LLM.Service.of({
stream: () => {
calls += 1
const item = queue.shift() ?? Stream.fail(new Error("unexpected extra llm call"))
return item
},
}),
),
Layer.succeed(TestLLM, TestLLM.of({ push, calls: Effect.sync(() => calls) })),
)
}),
)
const status = Layer.mergeAll(AppNodeBuilder.build(SessionStatus.node), Bus.layer)
const infra = Layer.mergeAll(NodeFileSystem.layer, AppNodeBuilder.build(CrossSpawnSpawner.node))
const deps = Layer.mergeAll(
AppNodeBuilder.build(Session.node),
AppNodeBuilder.build(Snapshot.node),
AppNodeBuilder.build(AgentSvc.node),
AppNodeBuilder.build(Permission.node),
AppNodeBuilder.build(Plugin.node),
AppNodeBuilder.build(Config.node),
RuntimeFlags.layer(),
AppNodeBuilder.build(SessionSummary.node),
AppNodeBuilder.build(Image.node),
SyncEvent.defaultLayer,
AppNodeBuilder.build(EventV2Bridge.node),
AppNodeBuilder.build(Database.node),
status,
llm,
).pipe(Layer.provideMerge(infra))
const env = AppNodeBuilder.build(SessionProcessor.node).pipe(Layer.provideMerge(deps))
const stateNode = LayerNode.make({
service: State,
layer: Layer.sync(State, () => State.of({ queue: [], calls: 0 })),
deps: [],
})
const llmNode = LayerNode.make({
service: LLM.Service,
layer: Layer.effect(
LLM.Service,
Effect.gen(function* () {
const state = yield* State
return LLM.Service.of({
stream: () => {
state.calls += 1
return state.queue.shift() ?? Stream.fail(new Error("unexpected extra llm call"))
},
})
}),
),
deps: [stateNode],
})
const testNode = LayerNode.make({
service: TestLLM,
layer: Layer.effect(
TestLLM,
Effect.gen(function* () {
const state = yield* State
return TestLLM.of({
push: (item) => Effect.sync(() => state.queue.push(item)).pipe(Effect.asVoid),
calls: Effect.sync(() => state.calls),
})
}),
),
deps: [stateNode],
})
const root = LayerNode.group([
SessionProcessor.node,
Session.node,
SessionProjector.node,
MessageV2.node,
Snapshot.node,
AgentSvc.node,
Permission.node,
Plugin.node,
Config.node,
SessionSummary.node,
Image.node,
SessionStatus.node,
EventV2Bridge.node,
Database.node,
CrossSpawnSpawner.node,
RuntimeFlags.node,
LLM.node,
testNode,
])
const env = LayerNode.compile(root, [
[LLM.node, llmNode],
[RuntimeFlags.node, RuntimeFlags.layer()],
]).pipe(Layer.provideMerge(Layer.mergeAll(NodeFileSystem.layer, Bus.layer, SyncEvent.defaultLayer)))
const it = testEffect(env)
@@ -2,17 +2,15 @@
// Ensures Kilo's post-filterCompacted trim and post-summary media strip are
// applied before messages are serialized for the provider request.
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { NodeFileSystem } from "@effect/platform-node"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { SessionProjector } from "@opencode-ai/core/session/projector"
import { describe, expect } from "bun:test"
import { Effect, Layer } from "effect"
import { FetchHttpClient } from "effect/unstable/http"
import { Database } from "@opencode-ai/core/database/database"
import { Agent as AgentSvc } from "../../src/agent/agent"
import { BackgroundJob } from "../../src/background/job"
import { Bus } from "../../src/bus"
import { Command } from "../../src/command"
import { Auth } from "../../src/auth"
import { Config } from "../../src/config/config"
import { RuntimeFlags } from "../../src/effect/runtime-flags"
import { EventV2Bridge } from "../../src/event-v2-bridge"
@@ -31,7 +29,6 @@ import { Provider as ProviderSvc } from "../../src/provider/provider"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
import { Question } from "../../src/question"
import { RepositoryCache } from "@opencode-ai/core/repository-cache"
import { Session } from "../../src/session/session"
import { SessionCompaction } from "../../src/session/compaction"
import { Instruction } from "../../src/session/instruction"
@@ -48,8 +45,6 @@ import { SessionSummary } from "../../src/session/summary"
import { Todo } from "../../src/session/todo"
import { Skill } from "../../src/skill"
import { Snapshot } from "../../src/snapshot"
import { Storage } from "../../src/storage/storage"
import { SyncEvent } from "../../src/sync"
import { ToolRegistry } from "../../src/tool/registry"
import { Truncate } from "../../src/tool/truncate"
import { KiloSessions } from "../../src/kilo-sessions/kilo-sessions"
@@ -126,72 +121,59 @@ const lsp = Layer.succeed(
}),
)
const status = Layer.mergeAll(AppNodeBuilder.build(SessionStatus.node), Bus.layer)
const run = SessionRunState.layer.pipe(Layer.provide(status))
const infra = Layer.mergeAll(NodeFileSystem.layer, AppNodeBuilder.build(CrossSpawnSpawner.node))
const memoryNode = LayerNode.make({ service: MemoryService.Service, layer: MemoryService.layer, deps: [] })
const serverNode = LayerNode.make({ service: TestLLMServer, layer: TestLLMServer.layer, deps: [] })
const root = LayerNode.group([
SessionPrompt.node,
Session.node,
SessionProjector.node,
MessageV2.node,
Snapshot.node,
LLM.node,
Env.node,
AgentSvc.node,
Command.node,
Permission.node,
Plugin.node,
Config.node,
ProviderSvc.node,
LSP.node,
MCP.node,
FSUtil.node,
BackgroundJob.node,
SessionStatus.node,
SessionRunState.node,
Database.node,
EventV2Bridge.node,
Question.node,
Todo.node,
ToolRegistry.node,
Skill.node,
Git.node,
Ripgrep.node,
Format.node,
Truncate.node,
SessionProcessor.node,
Image.node,
SessionCompaction.node,
SessionRevert.node,
Instruction.node,
SystemPrompt.node,
CrossSpawnSpawner.node,
RuntimeFlags.node,
memoryNode,
serverNode,
])
function makeHttp() {
const deps = Layer.mergeAll(
AppNodeBuilder.build(Session.node),
AppNodeBuilder.build(BackgroundJob.node),
AppNodeBuilder.build(Snapshot.node),
AppNodeBuilder.build(LLM.node),
AppNodeBuilder.build(Env.node),
AppNodeBuilder.build(AgentSvc.node),
AppNodeBuilder.build(Command.node),
AppNodeBuilder.build(Permission.node),
plugin,
AppNodeBuilder.build(Config.node),
RuntimeFlags.layer(),
AppNodeBuilder.build(ProviderSvc.node),
lsp,
mcp,
AppNodeBuilder.build(FSUtil.node),
SyncEvent.defaultLayer,
AppNodeBuilder.build(EventV2Bridge.node),
AppNodeBuilder.build(Database.node),
status,
MemoryService.layer,
).pipe(Layer.provideMerge(infra))
const question = Question.layer.pipe(Layer.provideMerge(deps))
const todo = Todo.layer.pipe(Layer.provideMerge(deps))
const registry = AppNodeBuilder.build(ToolRegistry.node, [[KiloSessions.node, KiloSessions.testLayer]])
const trunc = AppNodeBuilder.build(Truncate.node)
const proc = AppNodeBuilder.build(SessionProcessor.node, [[SessionSummary.node, summary]])
const compact = AppNodeBuilder.build(SessionCompaction.node, [
[SessionProcessor.node, proc],
return LayerNode.compile(root, [
[SessionSummary.node, summary],
[Plugin.node, plugin],
[LSP.node, lsp],
[MCP.node, mcp],
[RuntimeFlags.node, RuntimeFlags.layer()],
[KiloSessions.node, KiloSessions.testLayer],
])
return Layer.mergeAll(
TestLLMServer.layer,
SessionPrompt.layer.pipe(
Layer.provide(AppNodeBuilder.build(SessionRevert.node)),
Layer.provide(AppNodeBuilder.build(Image.node)),
Layer.provide(summary),
Layer.provideMerge(run),
Layer.provideMerge(compact),
Layer.provideMerge(proc),
Layer.provideMerge(registry),
Layer.provideMerge(trunc),
Layer.provideMerge(question),
Layer.provide(AppNodeBuilder.build(Instruction.node)),
Layer.provide(AppNodeBuilder.build(SystemPrompt.node)),
Layer.provideMerge(deps),
),
).pipe(
Layer.provide(
Layer.mergeAll(
summary,
deps,
AppNodeBuilder.build(Config.node),
RuntimeFlags.layer(),
AppNodeBuilder.build(BackgroundJob.node),
Bus.layer,
infra,
AppNodeBuilder.build(Storage.node),
),
),
)
}
const it = testEffect(makeHttp())
@@ -1,4 +1,5 @@
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { SessionProjector } from "@opencode-ai/core/session/projector"
import { NodeFileSystem } from "@effect/platform-node"
import { expect } from "bun:test"
import { Cause, Effect, Exit, Fiber, Layer } from "effect"
@@ -34,6 +35,7 @@ import { RepositoryCache } from "@opencode-ai/core/repository-cache"
import { SessionCompaction } from "../../src/session/compaction"
import { Instruction } from "../../src/session/instruction"
import { LLM } from "../../src/session/llm"
import { MessageV2 } from "../../src/session/message-v2"
import { SessionProcessor } from "../../src/session/processor"
import { SessionPrompt } from "../../src/session/prompt"
import { SessionRevert } from "../../src/session/revert"
@@ -45,8 +47,6 @@ import { SessionSummary } from "../../src/session/summary"
import { Todo } from "../../src/session/todo"
import { Skill } from "../../src/skill"
import { Snapshot } from "../../src/snapshot"
import { Storage } from "../../src/storage/storage"
import { SyncEvent } from "../../src/sync"
import { Ripgrep } from "@opencode-ai/core/ripgrep"
import { ToolRegistry } from "../../src/tool/registry"
import { Truncate } from "../../src/tool/truncate"
@@ -126,74 +126,62 @@ const lsp = Layer.succeed(
}),
)
const status = Layer.mergeAll(AppNodeBuilder.build(SessionStatus.node), Bus.layer)
const run = SessionRunState.layer.pipe(Layer.provide(status))
const infra = Layer.mergeAll(NodeFileSystem.layer, AppNodeBuilder.build(CrossSpawnSpawner.node))
// One compiled graph, mirroring test/session/prompt.test.ts. Effect v4 does
// not memoize nested layers, so per-service AppNodeBuilder.build calls each stood up their own
// Database and sessions written by the test were invisible to the prompt loop.
const memoryNode = LayerNode.make({ service: MemoryService.Service, layer: MemoryService.layer, deps: [] })
const testLLMServerNode = LayerNode.make({ service: TestLLMServer, layer: TestLLMServer.layer, deps: [] })
const promptRoot = LayerNode.group([
SessionPrompt.node,
Session.node,
SessionProjector.node,
MessageV2.node,
Snapshot.node,
LLM.node,
Env.node,
AgentSvc.node,
Command.node,
Permission.node,
Plugin.node,
Config.node,
ProviderSvc.node,
LSP.node,
MCP.node,
FSUtil.node,
BackgroundJob.node,
SessionStatus.node,
SessionRunState.node,
Database.node,
EventV2Bridge.node,
Question.node,
Todo.node,
ToolRegistry.node,
Skill.node,
Git.node,
Ripgrep.node,
Format.node,
Truncate.node,
SessionProcessor.node,
Image.node,
SessionCompaction.node,
SessionRevert.node,
Instruction.node,
SystemPrompt.node,
CrossSpawnSpawner.node,
RuntimeFlags.node,
memoryNode,
testLLMServerNode,
])
function makeHttp() {
const deps = Layer.mergeAll(
AppNodeBuilder.build(Session.node),
AppNodeBuilder.build(BackgroundJob.node),
AppNodeBuilder.build(Snapshot.node),
AppNodeBuilder.build(LLM.node),
AppNodeBuilder.build(Env.node),
AppNodeBuilder.build(AgentSvc.node),
AppNodeBuilder.build(Command.node),
AppNodeBuilder.build(Permission.node),
AppNodeBuilder.build(Plugin.node),
AppNodeBuilder.build(Config.node),
RuntimeFlags.layer(),
AppNodeBuilder.build(ProviderSvc.node),
lsp,
mcp,
AppNodeBuilder.build(FSUtil.node),
SyncEvent.defaultLayer,
AppNodeBuilder.build(EventV2Bridge.node),
AppNodeBuilder.build(Database.node),
status,
MemoryService.layer,
).pipe(Layer.provideMerge(infra))
const question = Question.layer.pipe(Layer.provideMerge(deps))
const todo = Todo.layer.pipe(Layer.provideMerge(deps))
const registry = AppNodeBuilder.build(ToolRegistry.node, [[KiloSessions.node, KiloSessions.testLayer]])
const trunc = AppNodeBuilder.build(Truncate.node)
const proc = AppNodeBuilder.build(SessionProcessor.node, [[SessionSummary.node, summary]])
const compact = AppNodeBuilder.build(SessionCompaction.node, [
[SessionProcessor.node, proc],
return LayerNode.compile(promptRoot, [
[SessionSummary.node, summary],
[LSP.node, lsp],
[MCP.node, mcp],
[KiloSessions.node, KiloSessions.testLayer],
])
return Layer.mergeAll(
TestLLMServer.layer,
SessionPrompt.layer.pipe(
Layer.provide(AppNodeBuilder.build(SessionRevert.node)),
Layer.provide(AppNodeBuilder.build(Image.node)),
Layer.provide(summary),
Layer.provideMerge(run),
Layer.provideMerge(compact),
Layer.provideMerge(proc),
Layer.provideMerge(registry),
Layer.provideMerge(trunc),
Layer.provideMerge(question),
Layer.provide(AppNodeBuilder.build(Instruction.node)),
Layer.provide(AppNodeBuilder.build(SystemPrompt.node)),
Layer.provideMerge(deps),
),
).pipe(
Layer.provide(
Layer.mergeAll(
summary,
deps,
AppNodeBuilder.build(Config.node),
RuntimeFlags.layer(),
AppNodeBuilder.build(BackgroundJob.node),
Bus.layer,
infra,
AppNodeBuilder.build(Storage.node),
),
),
)
}
const it = testEffect(makeHttp())
const symlinkIt = process.platform === "win32" ? it.live.skip : it.live
@@ -1,10 +1,11 @@
import path from "path"
import { afterAll, beforeAll, describe, expect, test } from "bun:test"
import { afterAll, beforeAll, describe, expect, setDefaultTimeout, test } from "bun:test"
import { Effect } from "effect"
import fs from "fs/promises"
import os from "os"
import { Bus } from "../../src/bus"
import { AppRuntime } from "../../src/effect/app-runtime"
import { makeRuntime } from "../../src/effect/run-service"
import { InstanceRef } from "../../src/effect/instance-ref"
import { KiloSessionCompaction } from "@/kilocode/session/compaction"
import { KiloSessionPromptQueue } from "@/kilocode/session/prompt-queue"
@@ -12,6 +13,8 @@ import { KiloSession } from "@/kilocode/session"
import { Suggestion } from "../../src/kilocode/suggestion"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { SessionProjector } from "@opencode-ai/core/session/projector"
import { InstanceStore } from "../../src/project/instance-store"
import { provideTestInstance } from "../fixture/fixture"
import { Session } from "../../src/session/session"
@@ -26,9 +29,13 @@ import { remove as cleanup } from "./cleanup"
import { pollWithTimeout } from "../lib/effect"
Log.init({ print: false })
setDefaultTimeout(15_000)
const previous = Flag.KILO_DB
const dbfile = path.join(os.tmpdir(), `kilo-prompt-queue-${process.pid}-${crypto.randomUUID()}.db`)
const layer = LayerNode.compile(LayerNode.group([Session.node, SessionProjector.node]))
const prompt = LayerNode.compile(LayerNode.group([SessionPrompt.node, SessionProjector.node]))
const runtime = makeRuntime(Session.Service, layer)
beforeAll(async () => {
await fs.rm(dbfile, { force: true })
@@ -36,6 +43,7 @@ beforeAll(async () => {
})
afterAll(async () => {
await runtime.dispose()
await AppRuntime.dispose()
await disposeTestRuntime()
Flag.KILO_DB = previous
@@ -49,13 +57,13 @@ const store = {
const sessions = {
create: (input?: Parameters<Session.Interface["create"]>[0]) =>
Effect.runPromise(Session.Service.use((svc) => svc.create(input)).pipe(Effect.provide(Session.defaultLayer))),
runtime.runPromise((svc) => svc.create(input)),
messages: (input: Parameters<Session.Interface["messages"]>[0]) =>
Effect.runPromise(Session.Service.use((svc) => svc.messages(input)).pipe(Effect.provide(Session.defaultLayer))),
runtime.runPromise((svc) => svc.messages(input)),
updateMessage: <T extends MessageV2.Info>(msg: T) =>
Effect.runPromise(Session.Service.use((svc) => svc.updateMessage(msg)).pipe(Effect.provide(Session.defaultLayer))),
runtime.runPromise((svc) => svc.updateMessage(msg)),
updatePart: <T extends MessageV2.Part>(part: T) =>
Effect.runPromise(Session.Service.use((svc) => svc.updatePart(part)).pipe(Effect.provide(Session.defaultLayer))),
runtime.runPromise((svc) => svc.updatePart(part)),
}
function line(input: unknown) {
@@ -121,7 +129,7 @@ function hasText(msg: MessageV2.WithParts, text: string) {
function scoped<T>(dir: string, fn: (prompt: SessionPrompt.Interface) => Promise<T>) {
return Effect.runPromise(
SessionPrompt.Service.use((prompt) => Effect.promise(() => fn(prompt))).pipe(
Effect.provide(SessionPrompt.defaultLayer),
Effect.provide(prompt),
provideInstance(dir),
Effect.provide(testInstanceStoreLayer),
Effect.scoped,
@@ -4,7 +4,10 @@ import fs from "fs/promises"
import os from "os"
import { Effect } from "effect"
import { Flag } from "@opencode-ai/core/flag/flag"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { SessionProjector } from "@opencode-ai/core/session/projector"
import { AppRuntime } from "../../src/effect/app-runtime"
import { makeRuntime } from "../../src/effect/run-service"
import { MessageV2 } from "../../src/session/message-v2"
import { Session } from "../../src/session/session"
import { SessionPrompt } from "../../src/session/prompt"
@@ -20,6 +23,9 @@ import { remove as cleanup } from "./cleanup"
const previous = Flag.KILO_DB
const dbfile = path.join(os.tmpdir(), `kilo-prompt-steering-${process.pid}-${crypto.randomUUID()}.db`)
const layer = LayerNode.compile(LayerNode.group([Session.node, SessionProjector.node]))
const prompt = LayerNode.compile(LayerNode.group([SessionPrompt.node, SessionProjector.node]))
const runtime = makeRuntime(Session.Service, layer)
beforeAll(async () => {
await fs.rm(dbfile, { force: true })
@@ -27,6 +33,7 @@ beforeAll(async () => {
})
afterAll(async () => {
await runtime.dispose()
await AppRuntime.dispose()
await disposeTestRuntime()
Flag.KILO_DB = previous
@@ -105,11 +112,9 @@ function question() {
const sessions = {
create: (input: Parameters<Session.Interface["create"]>[0]) =>
Effect.runPromise(Session.Service.use((svc) => svc.create(input)).pipe(Effect.provide(Session.defaultLayer))),
runtime.runPromise((svc) => svc.create(input)),
messages: (sessionID: SessionID) =>
Effect.runPromise(
Session.Service.use((svc) => svc.messages({ sessionID })).pipe(Effect.provide(Session.defaultLayer)),
),
runtime.runPromise((svc) => svc.messages({ sessionID })),
}
async function wait(sessionID: SessionID) {
@@ -130,7 +135,7 @@ async function wait(sessionID: SessionID) {
function scoped<T>(dir: string, fn: (prompt: SessionPrompt.Interface) => Promise<T>) {
return Effect.runPromise(
SessionPrompt.Service.use((prompt) => Effect.promise(() => fn(prompt))).pipe(
Effect.provide(SessionPrompt.defaultLayer),
Effect.provide(prompt),
provideInstance(dir),
Effect.provide(testInstanceStoreLayer),
Effect.scoped,
@@ -1,22 +1,27 @@
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { SessionProjector } from "@opencode-ai/core/session/projector"
import { expect, spyOn } from "bun:test"
import { Effect, Layer } from "effect"
import { Effect } from "effect"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { Auth } from "../../src/auth"
import { Config } from "../../src/config/config"
import { RuntimeFlags } from "../../src/effect/runtime-flags"
import { Session } from "../../src/session/session"
import { SessionShare } from "../../src/share/session"
import { Storage } from "../../src/storage/storage"
import { SyncEvent } from "../../src/sync"
import { testEffect } from "../lib/effect"
const it = testEffect(
Layer.mergeAll(AppNodeBuilder.build(Auth.node), AppNodeBuilder.build(Storage.node), AppNodeBuilder.build(CrossSpawnSpawner.node), RuntimeFlags.layer()),
)
const layer = AppNodeBuilder.build(SessionShare.node).pipe(
Layer.provideMerge(AppNodeBuilder.build(Session.node)),
LayerNode.compile(
LayerNode.group([
SessionShare.node,
Session.node,
SessionProjector.node,
Auth.node,
Storage.node,
CrossSpawnSpawner.node,
RuntimeFlags.node,
]),
),
)
it.instance("shares and unshares sessions through Kilo public URLs", () => {
@@ -60,6 +65,5 @@ it.instance("shares and unshares sessions through Kilo public URLs", () => {
request.mockRestore()
}),
),
Effect.provide(layer),
)
})
@@ -1,6 +1,7 @@
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { SessionProjector } from "@opencode-ai/core/session/projector"
import { describe, expect } from "bun:test"
import { Effect, Exit, Layer } from "effect"
import { Effect, Exit } from "effect"
import fs from "node:fs/promises"
import path from "node:path"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
@@ -15,11 +16,14 @@ import { Snapshot } from "@/snapshot"
import { provideInstance, provideTmpdirInstance } from "../../fixture/fixture"
import { testEffect } from "../../lib/effect"
const env = Layer.mergeAll(
AppNodeBuilder.build(Session.node),
AppNodeBuilder.build(SessionRevert.node),
AppNodeBuilder.build(Snapshot.node),
AppNodeBuilder.build(CrossSpawnSpawner.node),
const env = LayerNode.compile(
LayerNode.group([
Session.node,
SessionProjector.node,
SessionRevert.node,
Snapshot.node,
CrossSpawnSpawner.node,
]),
)
const it = testEffect(env)
const guarded = process.platform === "win32" ? it.live.skip : it.live
@@ -1,6 +1,7 @@
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { SessionProjector } from "@opencode-ai/core/session/projector"
import { describe, expect } from "bun:test"
import { Effect, Layer } from "effect"
import { Effect } from "effect"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
@@ -10,7 +11,7 @@ import { MessageID, PartID, SessionID } from "@/session/schema"
import { provideTmpdirInstance } from "../../fixture/fixture"
import { testEffect } from "../../lib/effect"
const env = Layer.mergeAll(AppNodeBuilder.build(Session.node), AppNodeBuilder.build(CrossSpawnSpawner.node))
const env = LayerNode.compile(LayerNode.group([Session.node, SessionProjector.node, CrossSpawnSpawner.node]))
const it = testEffect(env)
const providerID = ProviderV2.ID.make("test")
@@ -6,9 +6,8 @@ import { ProviderV2 } from "@opencode-ai/core/provider"
import { Provider } from "@/provider/provider"
import { Env } from "@/env"
import { Plugin } from "@/plugin"
import { provideInstanceEffect, tmpdirScoped } from "../fixture/fixture"
import { provideInstanceEffect, testInstanceStoreLayer, tmpdirScoped } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
import { InstanceStore } from "@/project/instance-store"
const it = testEffect(
Layer.mergeAll(AppNodeBuilder.build(Provider.node), AppNodeBuilder.build(Env.node), AppNodeBuilder.build(Plugin.node), AppNodeBuilder.build(CrossSpawnSpawner.node)),
@@ -50,7 +49,7 @@ it.effect("loads Snowflake Cortex from OAuth credentials", () =>
})
const provider = yield* Provider.use
.getProvider(ProviderV2.ID.make("snowflake-cortex"))
.pipe(provideInstanceEffect(directory), Effect.provide(AppNodeBuilder.build(InstanceStore.node)))
.pipe(provideInstanceEffect(directory), Effect.provide(testInstanceStoreLayer))
expect(provider.options.baseURL).toBe("https://test-account.snowflakecomputing.com/api/v2/cortex/v1")
expect(provider.options.apiKey).toBe("access-token")
@@ -3,9 +3,10 @@
// tool propagates each child session's total cost up to the parent's
// tool-wrapper assistant message (#6321).
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { SessionProjector } from "@opencode-ai/core/session/projector"
import { describe, expect } from "bun:test"
import { Effect, Layer } from "effect"
import { Effect } from "effect"
import { Database } from "@opencode-ai/core/database/database"
import { aggregateSessionStats } from "../../src/cli/cmd/stats"
import { MessageV2 } from "../../src/session/message-v2"
@@ -18,7 +19,7 @@ import { testEffect } from "../lib/effect"
void Log.init({ print: false })
const it = testEffect(Layer.mergeAll(AppNodeBuilder.build(Session.node), AppNodeBuilder.build(Database.node)))
const it = testEffect(LayerNode.compile(LayerNode.group([Session.node, SessionProjector.node, Database.node])))
const ref = {
providerID: ProviderV2.ID.make("test"),
@@ -1,6 +1,7 @@
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { SessionProjector } from "@opencode-ai/core/session/projector"
import { afterEach, describe, expect, test } from "bun:test"
import { Effect, Exit, Layer } from "effect"
import { Effect, Exit } from "effect"
import { Database } from "@opencode-ai/core/database/database"
import { Agent } from "../../src/agent/agent"
import { BackgroundJob } from "../../src/background/job"
@@ -35,20 +36,23 @@ const ref = {
}
const it = testEffect(
Layer.mergeAll(
AppNodeBuilder.build(Agent.node),
AppNodeBuilder.build(BackgroundJob.node),
AppNodeBuilder.build(Bus.node),
AppNodeBuilder.build(Config.node),
RuntimeFlags.layer(),
AppNodeBuilder.build(SessionRunState.node),
AppNodeBuilder.build(SessionStatus.node),
AppNodeBuilder.build(CrossSpawnSpawner.node),
AppNodeBuilder.build(Session.node),
AppNodeBuilder.build(Truncate.node),
AppNodeBuilder.build(Provider.node),
AppNodeBuilder.build(ToolRegistry.node),
AppNodeBuilder.build(Database.node),
LayerNode.compile(
LayerNode.group([
Agent.node,
BackgroundJob.node,
Bus.node,
Config.node,
RuntimeFlags.node,
SessionRunState.node,
SessionStatus.node,
CrossSpawnSpawner.node,
Session.node,
SessionProjector.node,
Truncate.node,
Provider.node,
ToolRegistry.node,
Database.node,
]),
),
)
@@ -1,6 +1,7 @@
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { SessionProjector } from "@opencode-ai/core/session/projector"
import { afterEach, beforeAll, describe, expect } from "bun:test"
import { Effect, Layer } from "effect"
import { Effect } from "effect"
import { Database } from "@opencode-ai/core/database/database"
import fs from "fs/promises"
import path from "path"
@@ -100,20 +101,23 @@ const catalog = {
}
const it = testEffect(
Layer.mergeAll(
AppNodeBuilder.build(Agent.node),
AppNodeBuilder.build(BackgroundJob.node),
AppNodeBuilder.build(Bus.node),
AppNodeBuilder.build(Config.node),
RuntimeFlags.layer(),
AppNodeBuilder.build(SessionRunState.node),
AppNodeBuilder.build(SessionStatus.node),
AppNodeBuilder.build(CrossSpawnSpawner.node),
AppNodeBuilder.build(Session.node),
AppNodeBuilder.build(Truncate.node),
AppNodeBuilder.build(Provider.node),
AppNodeBuilder.build(ToolRegistry.node),
AppNodeBuilder.build(Database.node),
LayerNode.compile(
LayerNode.group([
Agent.node,
BackgroundJob.node,
Bus.node,
Config.node,
RuntimeFlags.node,
SessionRunState.node,
SessionStatus.node,
CrossSpawnSpawner.node,
Session.node,
SessionProjector.node,
Truncate.node,
Provider.node,
ToolRegistry.node,
Database.node,
]),
),
)
@@ -5,11 +5,19 @@ import * as fs from "fs/promises"
import path from "path"
import { Effect, Layer } from "effect"
import * as CrossSpawnSpawner from "@opencode-ai/core/cross-spawn-spawner"
import { InstanceBootstrap } from "../../src/project/bootstrap-service"
import { InstanceStore } from "../../src/project/instance-store"
import { Worktree } from "../../src/worktree"
import { provideTmpdirInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
const it = testEffect(Layer.mergeAll(AppNodeBuilder.build(Worktree.node), AppNodeBuilder.build(CrossSpawnSpawner.node)))
const bootstrap = Layer.succeed(InstanceBootstrap.Service, InstanceBootstrap.Service.of({ run: Effect.void }))
const it = testEffect(
Layer.mergeAll(
AppNodeBuilder.build(Worktree.node, [[InstanceStore.bootstrapNode, bootstrap]]),
AppNodeBuilder.build(CrossSpawnSpawner.node),
),
)
describe("Worktree.remove lock retries", () => {
it.live("retries transient git remove lock failures", () =>
+3 -3
View File
@@ -212,7 +212,7 @@ export function withCliFixture<A, E>(
const spawn = Effect.fn("opencode.spawn")(function* (args: string[], opts?: SpawnOpts) {
const start = Date.now()
const timeoutMs = opts?.timeoutMs ?? 30_000
const timeoutMs = opts?.timeoutMs ?? 45_000 // kilocode_change - current full CLI startup leaves less than 30s for multi-step runs
// stdin: "ignore" so the child doesn't see a piped stdin and block
// on `Bun.stdin.text()` (see src/cli/cmd/run.ts — non-TTY stdin is
// consumed as the prompt). The old Process.run wrapper defaulted to
@@ -537,8 +537,8 @@ export const cliIt = {
body: (input: CliFixture) => Effect.Effect<A, E, Scope.Scope | HttpClient.HttpClient>,
opts?: number | TestOptions,
) =>
// kilocode_change start - Windows CI cannot reliably start nested CLI trees concurrently
(process.platform === "win32" ? test : test.concurrent)(
// kilocode_change start - full CLI processes contend heavily during startup after the Effect graph migration
test.serial(
name,
() => Effect.runPromise(Effect.scoped(withCliFixture(body))),
opts,
@@ -21,7 +21,7 @@ const env = Layer.mergeAll(
AppNodeBuilder.build(Permission.node),
events,
AppNodeBuilder.build(CrossSpawnSpawner.node),
AppNodeBuilder.build(InstanceStore.node).pipe(Layer.provide(noopBootstrap)),
AppNodeBuilder.build(InstanceStore.node, [[InstanceStore.bootstrapNode, noopBootstrap]]), // kilocode_change
).pipe(Layer.provide(RuntimeFlags.layer()), Layer.provide(AppNodeBuilder.build(Config.node)))
const it = testEffect(Layer.mergeAll(env, RuntimeFlags.layer()))
@@ -1,49 +1,41 @@
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { describe, expect, test } from "bun:test"
import path from "path"
import { pathToFileURL } from "url"
import { Effect, Layer } from "effect"
import { Effect } from "effect"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { provideInstance, TestInstance, tmpdirScoped } from "../fixture/fixture"
import { ProviderAuth } from "@/provider/auth"
import { Plugin } from "@/plugin"
import { RuntimeFlags } from "@/effect/runtime-flags"
import { Auth } from "@/auth"
import { ModelCache } from "@/provider/model-cache" // kilocode_change
import { EventV2Bridge } from "@/event-v2-bridge"
import { TestConfig } from "../fixture/config"
import { testEffect } from "../lib/effect"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { Config } from "@/config/config"
const it = testEffect(Layer.mergeAll(AppNodeBuilder.build(CrossSpawnSpawner.node), AppNodeBuilder.build(FSUtil.node)))
const it = testEffect(AppNodeBuilder.build(LayerNode.group([CrossSpawnSpawner.node, FSUtil.node])))
function layer(directory: string, plugins: string[]) {
return AppNodeBuilder.build(ProviderAuth.node).pipe(
Layer.provide(AppNodeBuilder.build(Auth.node)),
Layer.provide(AppNodeBuilder.build(ModelCache.node)), // kilocode_change
Layer.provide(
AppNodeBuilder.build(Plugin.node).pipe(
Layer.provide(AppNodeBuilder.build(EventV2Bridge.node)),
Layer.provide(RuntimeFlags.layer()),
Layer.provide(
TestConfig.layer({
get: () =>
Effect.succeed({
plugin: plugins,
plugin_origins: plugins.map((plugin) => ({
spec: plugin,
source: path.join(directory, "opencode.json"),
scope: "local" as const,
})),
}),
directories: () => Effect.succeed([directory]),
function providerAuthLayer(directory: string, plugins: string[]) {
return AppNodeBuilder.build(ProviderAuth.node, [
[
Config.node,
TestConfig.layer({
get: () =>
Effect.succeed({
plugin: plugins,
plugin_origins: plugins.map((plugin) => ({
spec: plugin,
source: path.join(directory, "opencode.json"),
scope: "local" as const,
})),
}),
),
),
),
)
directories: () => Effect.succeed([directory]),
}),
],
[RuntimeFlags.node, RuntimeFlags.layer()],
])
}
describe("plugin.auth-override", () => {
@@ -76,10 +68,12 @@ describe("plugin.auth-override", () => {
const plain = yield* tmpdirScoped({ git: true })
const plugin = pathToFileURL(path.join(pluginDir, "custom-copilot-auth.ts")).href
const methods = yield* ProviderAuth.use.methods().pipe(Effect.provide(layer(tmp.directory, [plugin])))
const methods = yield* ProviderAuth.use
.methods()
.pipe(Effect.provide(providerAuthLayer(tmp.directory, [plugin])))
const plainMethods = yield* ProviderAuth.use
.methods()
.pipe(Effect.provide(layer(plain, [])), provideInstance(plain))
.pipe(Effect.provide(providerAuthLayer(plain, [])), provideInstance(plain))
const copilot = methods[ProviderV2.ID.make("github-copilot")]
expect(copilot).toBeDefined()
@@ -727,11 +727,13 @@ it.instance(
)
it.instance(
"getSmallModel ignores model IDs without family metadata",
// kilocode_change start - Kilo always has an auto-routed small-model fallback
"getSmallModel falls back to Kilo auto when model IDs lack family metadata",
Effect.gen(function* () {
const model = yield* Provider.use.getSmallModel(ProviderV2.ID.make("test-provider"))
expect(model).toBeUndefined()
expect(model).toMatchObject({ providerID: "kilo", id: "kilo-auto/small" })
}),
// kilocode_change end
{
config: {
provider: {
+92 -61
View File
@@ -40,6 +40,7 @@ import { SessionCompaction } from "../../src/session/compaction"
import { SessionSummary } from "../../src/session/summary"
import { Instruction } from "../../src/session/instruction"
import { SessionProcessor } from "../../src/session/processor"
import { SessionProjector } from "@opencode-ai/core/session/projector" // kilocode_change
import { SessionPrompt } from "../../src/session/prompt"
import { SessionRevert } from "../../src/session/revert"
import { SessionRunState } from "../../src/session/run-state"
@@ -167,10 +168,10 @@ const lsp = Layer.succeed(
}),
)
const status = SessionStatus.layer.pipe(Layer.provideMerge(AppNodeBuilder.build(EventV2Bridge.node)))
const run = SessionRunState.layer.pipe(Layer.provide(status))
const infra = Layer.mergeAll(NodeFileSystem.layer, AppNodeBuilder.build(CrossSpawnSpawner.node))
// kilocode_change start - one compiled graph per env. Effect v4 does not memoize nested layers, so
// LayerNode.compile's cache is the only dedupe; building services with separate AppNodeBuilder.build
// calls gave this file three Database instances and every prompt died with "Session not found".
// Mirrors upstream's harness, with Kilo's KiloSessions/MemoryService/fastAgents deltas.
const agent: AgentSvc.Info = {
name: "build",
mode: "primary",
@@ -200,72 +201,93 @@ const blockingProcessor = Layer.succeed(
}),
)
const runtimeFlags = RuntimeFlags.layer({ experimentalEventSystem: true })
const memoryNode = LayerNode.make({ service: MemoryService.Service, layer: MemoryService.layer, deps: [] })
const testLLMServerNode = LayerNode.make({ service: TestLLMServer, layer: TestLLMServer.layer, deps: [] })
const promptRoot = LayerNode.group([
SessionPrompt.node,
Session.node,
SessionProjector.node,
MessageV2.node,
Snapshot.node,
LLM.node,
Env.node,
AgentSvc.node,
Command.node,
Permission.node,
Plugin.node,
Config.node,
ProviderSvc.node,
LSP.node,
MCP.node,
FSUtil.node,
BackgroundJob.node,
SessionStatus.node,
SessionRunState.node,
Database.node,
EventV2Bridge.node,
Question.node,
Todo.node,
ToolRegistry.node,
Skill.node,
Git.node,
Ripgrep.node,
Format.node,
Truncate.node,
SessionProcessor.node,
Image.node,
SessionCompaction.node,
SessionRevert.node,
Instruction.node,
SystemPrompt.node,
CrossSpawnSpawner.node,
RuntimeFlags.node,
memoryNode,
])
function makePrompt(input?: { processor?: "blocking" }) {
const deps = Layer.mergeAll(
AppNodeBuilder.build(Session.node),
AppNodeBuilder.build(Snapshot.node),
AppNodeBuilder.build(LLM.node),
AppNodeBuilder.build(Env.node),
input?.processor === "blocking" ? fastAgents : AppNodeBuilder.build(AgentSvc.node),
AppNodeBuilder.build(Command.node),
AppNodeBuilder.build(Permission.node),
AppNodeBuilder.build(Plugin.node),
AppNodeBuilder.build(Config.node),
AppNodeBuilder.build(ProviderSvc.node),
lsp,
makeMcp(),
AppNodeBuilder.build(FSUtil.node),
AppNodeBuilder.build(BackgroundJob.node),
status,
AppNodeBuilder.build(Database.node),
AppNodeBuilder.build(EventV2Bridge.node),
Bus.layer,
MemoryService.layer,
).pipe(Layer.provideMerge(infra))
const question = Question.layer.pipe(Layer.provideMerge(deps))
const todo = Todo.layer.pipe(Layer.provideMerge(deps))
const registry = AppNodeBuilder.build(ToolRegistry.node, [
[KiloSessions.node, KiloSessions.testLayer],
[RuntimeFlags.node, RuntimeFlags.layer({ experimentalEventSystem: true })],
])
const trunc = AppNodeBuilder.build(Truncate.node)
const proc =
input?.processor === "blocking"
? blockingProcessor
: AppNodeBuilder.build(SessionProcessor.node, [
[SessionSummary.node, summary],
[RuntimeFlags.node, RuntimeFlags.layer({ experimentalEventSystem: true })],
])
const compact = AppNodeBuilder.build(SessionCompaction.node, [
[SessionProcessor.node, proc],
const replacements = [
[SessionSummary.node, summary],
[RuntimeFlags.node, RuntimeFlags.layer({ experimentalEventSystem: true })],
])
return SessionPrompt.layer.pipe(
Layer.provide(AppNodeBuilder.build(SessionRevert.node)),
Layer.provide(AppNodeBuilder.build(Image.node)),
Layer.provide(summary),
Layer.provideMerge(run),
Layer.provideMerge(compact),
Layer.provideMerge(proc),
Layer.provideMerge(registry),
Layer.provideMerge(trunc),
Layer.provideMerge(question),
Layer.provide(AppNodeBuilder.build(Instruction.node)),
Layer.provide(AppNodeBuilder.build(SystemPrompt.node)),
Layer.provide(RuntimeFlags.layer({ experimentalEventSystem: true })),
Layer.provideMerge(deps),
Layer.provide(summary),
)
[LSP.node, lsp],
[MCP.node, makeMcp()],
[RuntimeFlags.node, runtimeFlags],
[KiloSessions.node, KiloSessions.testLayer],
] as const
if (input?.processor === "blocking") {
return LayerNode.compile(promptRoot, [
...replacements,
[SessionProcessor.node, blockingProcessor],
[AgentSvc.node, fastAgents],
])
}
return LayerNode.compile(promptRoot, replacements)
}
function makeHttp(input?: { processor?: "blocking" }) {
return Layer.mergeAll(TestLLMServer.layer, makePrompt(input))
const root = LayerNode.group([promptRoot, testLLMServerNode])
const replacements = [
[SessionSummary.node, summary],
[LSP.node, lsp],
[MCP.node, makeMcp()],
[RuntimeFlags.node, runtimeFlags],
[KiloSessions.node, KiloSessions.testLayer],
] as const
if (input?.processor === "blocking") {
return LayerNode.compile(root, [
...replacements,
[SessionProcessor.node, blockingProcessor],
[AgentSvc.node, fastAgents],
])
}
return LayerNode.compile(root, replacements)
}
function makeHttpNoLLMServer(input?: { processor?: "blocking" }) {
return makePrompt(input)
}
// kilocode_change end
const it = testEffect(makeHttp())
const noLLMServer = testEffect(makeHttpNoLLMServer())
@@ -777,6 +799,7 @@ noLLMServer.instance.skip(
],
})
// kilocode_change start - compile the v2 reader against this test's database graph
const messages = yield* SessionV2.Service.use((session) => session.messages({ sessionID: chat.id })).pipe(
Effect.provide(
LayerNode.compile(SessionV2.node, [
@@ -785,6 +808,7 @@ noLLMServer.instance.skip(
]),
),
)
// kilocode_change end
const { db } = yield* Database.Service
const row = yield* db
.select()
@@ -1829,9 +1853,16 @@ unixNoLLMServer(
const tool = completedTool(result.parts)
if (!tool) return
// kilocode_change start - bind v2 execution and location services in the consolidated test graph
const messages = yield* SessionV2.Service.use((session) => session.messages({ sessionID: chat.id })).pipe(
Effect.provide(AppNodeBuilder.build(SessionV2.node)),
Effect.provide(
LayerNode.compile(SessionV2.node, [
[SessionExecution.node, SessionExecution.noopLayer],
[LocationServiceMap.node, locationServiceMapLayer],
]),
),
)
// kilocode_change end
const shell = messages.find((message) => message.type === "shell")
expect(shell).toMatchObject({