feat(opencode): add tool metadata buffering and migrate virtua to v0.42.3

Introduce a `metadata` method on SessionProcessor.Handle that buffers
metadata emitted before tool-call registration, then applies it on the
running transition. This decouples metadata emission timing from
tool-call lifecycle.

Downgrade virtua from 0.49.1 to 0.42.3 and migrate the virtualizer API:
- Replace `findItemIndex(scrollOffset)` with `findStartIndex()`
- Replace `bufferSize` prop with `overscan` (count-based)

Additional changes:
- Change Permission.reply return type from Promise<boolean> to Promise<void>
- Make Ruleset type readonly and remove unnecessary array spreads
- Update nvidia provider headers to reference Kilo branding
- Reorder SDK event type definitions for installation events
- Reduce promise facade allowlist in check script
This commit is contained in:
Imanol Maiztegui
2026-06-17 16:41:43 +02:00
parent b1045ac091
commit abdef9a7ee
20 changed files with 171 additions and 140 deletions
+2 -2
View File
@@ -767,7 +767,7 @@
"tailwindcss": "4.1.11",
"typescript": "5.8.2",
"ulid": "3.0.1",
"virtua": "0.49.1",
"virtua": "0.42.3",
"vite": "7.3.2",
"vite-plugin-solid": "2.11.10",
"zod": "4.1.8",
@@ -4237,7 +4237,7 @@
"vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
"virtua": ["virtua@0.49.1", "", { "peerDependencies": { "react": ">=16.14.0", "react-dom": ">=16.14.0", "solid-js": ">=1.0", "svelte": ">=5.0", "vue": ">=3.2" }, "optionalPeers": ["react", "react-dom", "solid-js", "svelte", "vue"] }, "sha512-6f79msqg3jzNFdqJiS0FSzhRN1EHlDhR7EvW7emp6z5qQ22VdsReiDHflkpMEMhoAyUuYr69nwT0aagiM7NrUg=="],
"virtua": ["virtua@0.42.3", "", { "peerDependencies": { "react": ">=16.14.0", "react-dom": ">=16.14.0", "solid-js": ">=1.0", "svelte": ">=5.0", "vue": ">=3.2" }, "optionalPeers": ["react", "react-dom", "solid-js", "svelte", "vue"] }, "sha512-5FoAKcEvh05qsUF97Yz42SWJ7bwnPExjUYHGuoxz1EUtfWtaOgXaRwnylJbDpA0QcH1rKvJ2qsGRi9MK1fpQbg=="],
"vite": ["vite@7.3.2", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg=="],
+1 -1
View File
@@ -69,7 +69,7 @@
"shiki": "3.20.0",
"solid-list": "0.3.0",
"tailwindcss": "4.1.11",
"virtua": "0.49.1",
"virtua": "0.42.3",
"vite": "7.3.2",
"@solidjs/meta": "0.29.4",
"@solidjs/router": "0.15.4",
+2
View File
@@ -51,6 +51,8 @@
<!-- packages/opencode/src/plugin/digitalocean.ts -->
- <https://cloudflare.com/cdn-cgi/trace>
<!-- packages/opencode/src/session/network.ts -->
- <https://console.opencode.ai>
<!-- packages/opencode/src/cli/cmd/account.ts -->
- <https://docs.github.com/en/actions/how-tos/security-for-github-actions/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services>
<!-- packages/opencode/src/cli/cmd/github.ts -->
- <https://docs.inceptionlabs.ai/capabilities/next-edit>
@@ -287,7 +287,7 @@ describe("Collapsed deferred tool details contract (source)", () => {
it("uses an explicit details hint before touching deferred children", () => {
expect(basic).toContain("hasDetails?: boolean")
expect(basic).toContain("props.hasDetails ?? !!props.children")
expect(basic).toContain("props.hasDetails ?? !!hasChildren()")
expect(basic).toMatch(/<Show when=\{!props\.defer \|\| ready\(\)\}>\{props\.children\}<\/Show>/)
})
@@ -174,7 +174,7 @@ export const DiffPanel: Component<DiffPanelProps> = (props) => {
// row. Raw scrollTop is not stable once the virtualizer remeasures dynamic rows.
const preserveScroll = (fn: () => void) => {
const handle = virtualizer()
const index = handle?.findItemIndex(handle.scrollOffset)
const index = handle?.findStartIndex()
const file = index === undefined ? undefined : rows()[index]?.file
const offset = index === undefined ? 0 : (handle?.scrollOffset ?? 0) - (handle?.getItemOffset(index) ?? 0)
fn()
@@ -173,7 +173,7 @@ export const FullScreenDiffView: Component<FullScreenDiffViewProps> = (props) =>
const preserveScroll = (fn: () => void) => {
const handle = virtualizer()
const index = handle?.findItemIndex(handle.scrollOffset)
const index = handle?.findStartIndex()
const file = index === undefined ? undefined : rows()[index]?.file
const offset = index === undefined ? 0 : (handle?.scrollOffset ?? 0) - (handle?.getItemOffset(index) ?? 0)
fn()
@@ -473,9 +473,8 @@ export const FullScreenDiffView: Component<FullScreenDiffViewProps> = (props) =>
requestAnimationFrame(() => {
const index = rows().findIndex((diff) => diff.file === path)
if (index < 0) return
const handle = virtualizer()
const current = handle?.findItemIndex(handle.scrollOffset) ?? index
handle?.scrollToIndex(index, { offset: -8, smooth: Math.abs(index - current) <= 8 })
const current = virtualizer()?.findStartIndex() ?? index
virtualizer()?.scrollToIndex(index, { offset: -8, smooth: Math.abs(index - current) <= 8 })
})
}
@@ -486,7 +485,7 @@ export const FullScreenDiffView: Component<FullScreenDiffViewProps> = (props) =>
const syncActiveFileFromScroll = () => {
const handle = virtualizer()
if (!handle) return
const file = rows()[handle.findItemIndex(handle.scrollOffset)]?.file
const file = rows()[handle.findStartIndex()]?.file
if (file) setActiveFile(file)
}
@@ -29,8 +29,7 @@ export function VirtualDiffList<T>(props: VirtualDiffListProps<T>) {
data={props.data}
scrollRef={state.scroll}
keepMounted={props.keep}
// Use a 4-row overscan, so 4x the estimated row size.
bufferSize={1680}
overscan={4}
itemSize={420}
>
{props.render}
@@ -170,7 +170,7 @@ export const MessageList: Component<MessageListProps> = (props) => {
return
}
if (!handle || saved.keys.length === 0) return
const index = handle.findItemIndex(handle.scrollOffset)
const index = handle.findStartIndex()
const key = saved.keys[index]
if (!key) return
setScroll(id, { type: "anchor", key, offset: handle.scrollOffset - handle.getItemOffset(index) })
@@ -313,8 +313,7 @@ export const MessageList: Component<MessageListProps> = (props) => {
scrollRef={scrollEl()}
shift={session.messageMutation() === "prepend"}
cache={measurement()}
// Use a 2-row overscan, so 2x the estimated row size.
bufferSize={520}
overscan={2}
itemSize={260}
>
{(row, index) => (
@@ -75,7 +75,7 @@ export namespace RemoteSender {
provide?: Provide
permission?: {
readonly list: () => Promise<ReadonlyArray<Permission.Request>>
readonly reply: (input: Permission.ReplyInput) => Promise<boolean>
readonly reply: (input: Permission.ReplyInput) => Promise<void>
}
question?: {
readonly list: () => Promise<ReadonlyArray<Question.Request>>
@@ -4,7 +4,7 @@ export type Rule = {
action: "allow" | "deny" | "ask"
}
export type Ruleset = Rule[]
export type Ruleset = ReadonlyArray<Rule>
export namespace PermissionRule {
export function broad(rule: Rule) {
+2 -2
View File
@@ -179,7 +179,7 @@ export function resolve(permission: string, pattern: string, ruleset: Ruleset, .
const evalFn =
permission === "external_directory"
? (permission: string, pattern: string, ...sets: Ruleset[]) =>
ExternalDirectoryPermission.evaluate(permission, pattern, ...sets.map((set) => [...set]))
ExternalDirectoryPermission.evaluate(permission, pattern, ...sets)
: evaluate
const base = ReadPermission.harden(permission, pattern, evalFn(permission, pattern, ruleset))
const saved = evalFn(permission, pattern, ...overrides)
@@ -195,7 +195,7 @@ export function resolve(permission: string, pattern: string, ruleset: Ruleset, .
function veto(permission: string, pattern: string, ruleset?: Ruleset) {
if (!ruleset) return false
return ExternalDirectoryPermission.evaluate(permission, pattern, [...ruleset]).action === "deny"
return ExternalDirectoryPermission.evaluate(permission, pattern, ruleset).action === "deny"
}
function subset(permission: string, ruleset: Ruleset) {
+51 -1
View File
@@ -45,6 +45,12 @@ export interface Handle {
toolCallID: string,
update: (part: MessageV2.ToolPart) => MessageV2.ToolPart,
) => Effect.Effect<MessageV2.ToolPart | undefined>
// kilocode_change start
readonly metadata: (
toolCallID: string,
input: { title?: string; metadata?: Record<string, any> },
) => Effect.Effect<void>
// kilocode_change end
readonly completeToolCall: (
toolCallID: string,
output: {
@@ -82,6 +88,7 @@ type ToolCall = {
interface ProcessorContext extends Input {
toolcalls: Record<string, ToolCall>
toolmeta: Record<string, { title?: string; metadata?: Record<string, any> }> // kilocode_change
shouldBreak: boolean
snapshot: string | undefined
blocked: boolean
@@ -133,6 +140,7 @@ export const layer = Layer.effect(
sessionID: input.sessionID,
model: input.model,
toolcalls: {},
toolmeta: {}, // kilocode_change
shouldBreak: false,
snapshot: initialSnapshot,
blocked: false,
@@ -159,6 +167,7 @@ export const layer = Layer.effect(
const settleToolCall = Effect.fn("SessionProcessor.settleToolCall")(function* (toolCallID: string) {
const done = ctx.toolcalls[toolCallID]?.done
delete ctx.toolcalls[toolCallID]
delete ctx.toolmeta[toolCallID] // kilocode_change
if (done) yield* Deferred.succeed(done, undefined).pipe(Effect.ignore)
})
@@ -172,6 +181,7 @@ export const layer = Layer.effect(
})
if (!part || part.type !== "tool") {
delete ctx.toolcalls[toolCallID]
delete ctx.toolmeta[toolCallID] // kilocode_change
return undefined
}
return { call, part }
@@ -205,6 +215,33 @@ export const layer = Layer.effect(
return part
})
// kilocode_change start - buffer metadata emitted before tool-call registration
const metadata = Effect.fn("SessionProcessor.metadata")(function* (
toolCallID: string,
input: { title?: string; metadata?: Record<string, any> },
) {
const match = yield* readToolCall(toolCallID)
if (!match || match.part.state.status !== "running") {
ctx.toolmeta[toolCallID] = {
...ctx.toolmeta[toolCallID],
...input,
}
return
}
yield* updateToolCall(toolCallID, (part) => {
if (part.state.status !== "running") return part
return {
...part,
state: {
...part.state,
title: input.title ?? part.state.title,
metadata: input.metadata ?? part.state.metadata,
},
}
})
})
// kilocode_change end
const completeToolCall = Effect.fn("SessionProcessor.completeToolCall")(function* (
toolCallID: string,
output: {
@@ -462,21 +499,32 @@ export const layer = Layer.effect(
timestamp: DateTime.makeUnsafe(Date.now()),
})
}
// kilocode_change start - apply metadata buffered before the running transition
const meta = ctx.toolmeta[value.id]
yield* updateToolCall(value.id, (match) => ({
...match,
tool: value.name,
state:
match.state.status === "running"
? { ...match.state, input }
? {
...match.state,
input,
title: meta?.title ?? match.state.title,
metadata: meta?.metadata ?? match.state.metadata,
}
: {
status: "running",
input,
title: meta?.title,
metadata: meta?.metadata,
time: { start: Date.now() },
},
metadata: match.metadata?.providerExecuted
? { ...value.providerMetadata, providerExecuted: true }
: value.providerMetadata,
}))
delete ctx.toolmeta[value.id]
// kilocode_change end
const parts = MessageV2.parts(ctx.assistantMessage.id)
const recentParts = parts.slice(-DOOM_LOOP_THRESHOLD)
@@ -871,6 +919,7 @@ export const layer = Layer.effect(
})
}
ctx.toolcalls = {}
ctx.toolmeta = {} // kilocode_change
KiloSessionProcessor.guardEmptyToolCalls(ctx.assistantMessage, MessageV2.parts(ctx.assistantMessage.id)) // kilocode_change
ctx.assistantMessage.time.completed = Date.now()
// kilocode_change start - reconcile cost with any subagent propagation written during tool calls (#6321)
@@ -1010,6 +1059,7 @@ export const layer = Layer.effect(
return ctx.assistantMessage
},
updateToolCall,
metadata, // kilocode_change
completeToolCall,
...output, // kilocode_change
process,
+2 -15
View File
@@ -26,7 +26,7 @@ export const resolve = Effect.fn("SessionTools.resolve")(function* (input: {
agent: Agent.Info
model: Provider.Model
session: Session.Info
processor: Pick<SessionProcessor.Handle, "message" | "updateToolCall" | "completeToolCall">
processor: Pick<SessionProcessor.Handle, "message" | "metadata" | "completeToolCall"> // kilocode_change
bypassAgentCheck: boolean
messages: MessageV2.WithParts[]
promptOps: TaskPromptOps
@@ -50,20 +50,7 @@ export const resolve = Effect.fn("SessionTools.resolve")(function* (input: {
extra: { model: input.model, bypassAgentCheck: input.bypassAgentCheck, promptOps: input.promptOps },
agent: input.agent.name,
messages: input.messages,
metadata: (val) =>
input.processor.updateToolCall(options.toolCallId, (match) => {
if (!["running", "pending"].includes(match.state.status)) return match
return {
...match,
state: {
title: val.title,
metadata: val.metadata,
status: "running",
input: args,
time: { start: Date.now() },
},
}
}),
metadata: (val) => input.processor.metadata(options.toolCallId, val), // kilocode_change
// kilocode_change start - resolve permissions at ask time so active tools see config edits
ask: (req) =>
KiloSessionPrompt.askPermission({
@@ -161,6 +161,7 @@ function fakeRuntime(outputTokenMax?: number) {
return input.assistantMessage
},
updateToolCall: Effect.fn("TestSessionProcessor.updateToolCall")(() => Effect.succeed(undefined)),
metadata: Effect.fn("TestSessionProcessor.metadata")(() => Effect.void),
completeToolCall: Effect.fn("TestSessionProcessor.completeToolCall")(() => Effect.void),
process: Effect.fn("TestSessionProcessor.process")((stream: LLM.StreamInput) =>
Effect.gen(function* () {
@@ -54,7 +54,7 @@ const nolog = {
function permissions(items: Permission.Request[] = []) {
return {
list: async () => items,
reply: async () => true,
reply: async () => {},
}
}
@@ -515,7 +515,6 @@ describe("RemoteSender", () => {
list: async () => [],
reply: async (input) => {
calls.push(input)
return true
},
},
})
@@ -1114,9 +1114,9 @@ it.instance(
Effect.gen(function* () {
const providers = yield* list
expect(providers[ProviderID.make("nvidia")].options.headers).toEqual({
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
"X-BILLING-INVOKE-ORIGIN": "OpenCode",
"HTTP-Referer": "https://kilo.ai/", // kilocode_change
"X-Title": "Kilo Code", // kilocode_change
"X-BILLING-INVOKE-ORIGIN": "KiloCode", // kilocode_change
})
}),
{ config: { provider: { nvidia: { options: { apiKey: "test-api-key" } } } } },
@@ -1127,9 +1127,9 @@ it.instance(
Effect.gen(function* () {
const providers = yield* list
expect(providers[ProviderID.make("nvidia")].options.headers).toEqual({
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
"X-BILLING-INVOKE-ORIGIN": "OpenCode",
"HTTP-Referer": "https://kilo.ai/", // kilocode_change
"X-Title": "Kilo Code", // kilocode_change
"X-BILLING-INVOKE-ORIGIN": "KiloCode", // kilocode_change
})
}),
{ config: { provider: { nvidia: { options: { apiKey: "test-api-key", baseURL: "http://localhost:8000/v1" } } } } },
@@ -203,6 +203,7 @@ function fake(
return msg
},
updateToolCall: Effect.fn("TestSessionProcessor.updateToolCall")(() => Effect.succeed(undefined)),
metadata: Effect.fn("TestSessionProcessor.metadata")(() => Effect.void), // kilocode_change
completeToolCall: Effect.fn("TestSessionProcessor.completeToolCall")(() => Effect.void),
process: Effect.fn("TestSessionProcessor.process")(() => Effect.succeed(result)),
} satisfies SessionProcessorModule.SessionProcessor.Handle
+20 -20
View File
@@ -41,6 +41,8 @@ export type Event =
| EventTodoUpdated
| EventSessionStatus
| EventSessionIdle
| EventInstallationUpdated
| EventInstallationUpdateAvailable
| EventSuggestionShown
| EventSuggestionAccepted
| EventSuggestionDismissed
@@ -58,8 +60,6 @@ export type Event =
| EventPtyUpdated
| EventPtyExited
| EventPtyDeleted
| EventInstallationUpdated
| EventInstallationUpdateAvailable
| EventMessageUpdated
| EventMessageRemoved
| EventMessagePartUpdated
@@ -951,6 +951,8 @@ export type GlobalEvent = {
| EventTodoUpdated
| EventSessionStatus
| EventSessionIdle
| EventInstallationUpdated
| EventInstallationUpdateAvailable
| EventSuggestionShown
| EventSuggestionAccepted
| EventSuggestionDismissed
@@ -968,8 +970,6 @@ export type GlobalEvent = {
| EventPtyUpdated
| EventPtyExited
| EventPtyDeleted
| EventInstallationUpdated
| EventInstallationUpdateAvailable
| EventMessageUpdated
| EventMessageRemoved
| EventMessagePartUpdated
@@ -3203,6 +3203,22 @@ export type EventSessionIdle = {
}
}
export type EventInstallationUpdated = {
id: string
type: "installation.updated"
properties: {
version: string
}
}
export type EventInstallationUpdateAvailable = {
id: string
type: "installation.update-available"
properties: {
version: string
}
}
export type EventSuggestionShown = {
id: string
type: "suggestion.shown"
@@ -3356,22 +3372,6 @@ export type EventPtyDeleted = {
}
}
export type EventInstallationUpdated = {
id: string
type: "installation.updated"
properties: {
version: string
}
}
export type EventInstallationUpdateAvailable = {
id: string
type: "installation.update-available"
properties: {
version: string
}
}
export type EventMessageUpdated = {
id: string
type: "message.updated"
+70 -70
View File
@@ -51,42 +51,42 @@ Repeated setup work, long sleeps/timeouts, serial integration tests, filesystem/
## Hypothesis Loop
| Hypothesis | Change | Before | After | Decision | Notes |
| --------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | --------- | ------- | -------- | -------------------------------------------------------------------------------------------------------------------------------- |
| Repeated full-suite runs are too expensive for discovery | Switched full-suite benchmark to one run and added per-file profiler | ~250s/run | pending | keep | Bun has no slowest-test reporter in this version; profile files directly. |
| Plugin install concurrency test spends time spawning more workers than needed to exercise lock contention | Reduced worker counts from 12/10/8 to 6/6/5; kept `holdMs: 30` | 7.800s | 6.204s | keep | Median from 3 targeted runs; still covers concurrent cross-process writes to server, server+tui, and existing json config. |
| `httpapi-listen` PTY route tests pay for git repositories they do not assert on | Removed `git: true` from temp dirs while keeping config setup | 10.554s | 7.818s | keep | Median from 3 targeted runs; HTTP routes, tickets, websocket upgrade, restart, and no-auth paths still pass. |
| `workspace.waitForSync` timeout test waits the full production timeout | Added optional timeout parameter defaulting to production timeout; timeout test uses 25ms | 12.949s | 8.305s | keep | Median from 3 targeted runs; production callers keep the 5000ms default. |
| `config.test` waits after dependencies even though `.gitignore` is written synchronously | Removed obsolete 1000ms sleep from writable `KILO_CONFIG_DIR` test | 10.270s | 9.433s | keep | Median from 5 targeted runs because one run was noisy; simpler test and no fixed sleep. |
| SDK parity helpers create git repos for tests that only need files/config/session state | Changed `withProject` default to no git; explicit git init test still opts into no-git fixture | 8.011s | 5.180s | keep | Median from 5 targeted runs because first run was cold/noisy. |
| Provider plugin filter test waits on plugin dependency readiness setup | Marked local plugin dependencies ready using the existing fixture helper | 7.543s | 6.366s | keep | Median from 3 targeted runs; matches neighboring plugin provider test setup. |
| HTTP provider tests generate local plugins without dependency-ready fixture state | Marked generated `.opencode` plugin fixtures dependency-ready | 7.905s | 2.980s | keep | Median from 3 targeted runs; avoids unrelated plugin dependency setup in route tests. |
| TUI plugin lifecycle timeout coverage waits the full production cleanup timeout | Added optional runtime dispose timeout override and used 25ms in the timeout test | 7.330s | 1.507s | keep | Median from 3 targeted runs; production default remains 5000ms. |
| Skill tool test initializes git even though it only reads local skill files | Removed `git: true` from the temporary directory fixture | 2.320s | 1.425s | keep | Single targeted rerun; still exercises skill discovery, permission request, and bundled file output. |
| Prompt shell semantics tests initialize git though they only assert shell/session behavior | Removed `git: true` from shell-focused prompt fixtures while preserving config setup | 26.930s | 23.400s | keep | Three targeted reruns passed after the change: 23.80s, 23.55s, 23.40s. |
| Remaining prompt behavior tests mostly do not require repository state | Removed git setup from safe loop/reference/error fixtures; restored shell queue/cancel cases | 23.400s | 19.610s | keep | Safety review found shell runner readiness depends on git-backed setup in several tests; current single rerun passes. |
| Session processor effect tests do not require repository state | Removed git setup from all processor-effect temp server fixtures | 12.500s | 9.230s | keep | Two targeted reruns passed after the change: 9.61s, 9.23s. |
| HTTP listen PTY ticket tests restart the same listener topology twice | Folded directory-scoped ticket regression into the broader unsafe-ticket test | 7.051s | 6.170s | keep | Two targeted reruns passed after the change: 6.76s, 6.17s; still covers mint failure and successful same-directory upgrade. |
| File watcher readiness can write before async native subscriptions are active | Retried short readiness writes and accepted symlink-realpath HEAD events | failed | 4.62s | keep | Three sequential focused watcher runs passed: 4.62s, 4.57s, 4.64s; full suite no longer failed in `watcher.test.ts`. |
| First provider config/env/filtering block can use Effect-aware instance fixtures | Migrated six `tmpdir` + `withTestInstance` cases to `it.instance` | 6.06s | 6.07s | keep | Neutral timing, but removes manual config file writes and instance plumbing; use as the pattern for later provider slices. |
| Custom provider/model config cases can use Effect-aware instance fixtures | Migrated three more config-heavy provider cases to `it.instance` | 6.07s | 6.12s | keep | Neutral timing within noise, but continues removing manual config file writes on top of the first provider fixture PR. |
| Provider env precedence and model lookup cases can use Effect-aware instance fixtures | Migrated four more provider lookup/default-model cases to `it.instance` | 6.12s | 6.36s | keep | Noisy 5-run median; kept as a small stacked cleanup slice but do not claim speedup from this migration. |
| Simple config load cases can use Effect-aware instance fixtures | Migrated JSON, shell, formatter, and lsp config load cases to `it.instance` | 14.18s | 3.93s | keep | Three-run medians before/after; removes manual `tmpdir` + `withTestInstance` setup from the first simple config block. |
| Config template, file include, and simple agent cases can use Effect-aware instance fixtures | Migrated JSONC, env/file substitution, invalid config, and agent config cases to `it.instance` | 1.87s | 1.90s | keep | Stacked on the first config slice; neutral timing but removes more manual `tmpdir` + instance plumbing. |
| Agent option, command, and legacy migration config cases can use Effect-aware instance fixtures | Migrated agent variant, command, autoshare, and mode migration cases to `it.instance` | 1.90s | 1.83s | keep | Stacked on the config template slice; small neutral-to-positive timing and less manual setup. |
| Local config update and directory cases can use Effect-aware instance fixtures | Migrated local `update` and `directories` cases to `it.instance` | 1.77s | 1.71s | keep | Three-run medians; small positive/neutral timing, removes manual instance plumbing, and eliminates one existing unsafe cast. |
| `.opencode` agent and command file-loading cases can use Effect-aware instance fixtures | Migrated singular/plural agent and command markdown fixture cases to `it.instance` | 7.21s | 1.87s | keep | Parent baseline was noisy (7.42, 7.21, 2.83); after runs were stable at 1.87, 1.98, 1.83. Keep as cleanup with no broad claim. |
| Legacy tools and permission-order config cases can use Effect-aware instance fixtures | Migrated legacy `tools` migration and permission order cases to `it.instance` | 1.87s | 1.87s | keep | Neutral timing; removes more manual temp-instance plumbing from legacy config migration coverage. |
| Remaining simple config load cases can use Effect-aware instance fixtures | Migrated default config load and legacy TUI-key cases to `it.instance` | 7.78s | 6.39s | keep | Single baseline before edit; after median from three sequential reruns (5.76, 6.39, 6.53). Keep as cleanup with cautious timing. |
| Managed settings config cases can use Effect-aware instance fixtures | Migrated managed override and missing-managed-file cases to `it.instance` | 2.40s | 1.76s | keep | Single baseline before edit; after median from three sequential reruns (1.75, 1.76, 1.80). |
| Local plugin and subagent config fixtures can use Effect-aware instance fixtures | Migrated scoped npm plugin and custom subagent markdown cases to `it.instance` | 2.37s | 1.67s | keep | Single baseline before edit; after median from three sequential reruns (1.66, 1.67, 1.67). |
| MCP merge config cases can use Effect-aware instance fixtures | Migrated three MCP merge/override cases to `it.instance` | 1.98s | 1.95s | keep | Neutral timing within noise; removes manual `tmpdir` + `withTestInstance` setup from isolated filesystem-only config cases. |
| Remaining legacy tools config cases can use Effect-aware instance fixtures | Migrated allow/deny legacy `tools` permission cases to `it.instance` | 2.65s | 1.90s | keep | Single baseline before edit; after median from three sequential reruns (2.58, 1.90, 1.90). |
| Oversized snapshot batch tests only need to cross the 100-file boundary | Reduced large diff/revert fixture sizes while keeping each case above the batch boundary | 4.32s | 3.66s | keep | Three affected snapshot tests; after median from three reruns (4.32, 3.66, 3.66) while still crossing the 100-file boundary. |
| Prompt tests without LLM calls do not need the test LLM server | Added a no-server runner and moved obvious non-LLM prompt/shell cases to it | 25.41s | 21.03s | keep | Full prompt file after simplify pass median from three reruns (20.66, 21.03, 21.64); LLM-backed tests stay on original runner. |
| CLI run subprocess cases can run independently | Marked `run-process.test.ts` subprocess cases concurrent | 11.87s | 4.13s | keep | Newest-dev single baseline; after median from three reruns (4.13, 4.17, 4.11). Each case has an isolated temp home and LLM port. |
| Snapshot initialization does not need to commit seeded files in the source repo | Removed extra `git add`/`commit` from the snapshot test `initialize()` helper | 22.22s | 20.23s | keep | Newest-dev single baseline; after median from three reruns (20.23, 22.59, 20.11). Fixture still creates a git repo root commit. |
| Processor AI SDK tool-call case does not assert git behavior | Removed `git: true` from the non-native tool-call processor test | 10.22s | 9.48s | keep | Newest-dev single baseline; full-file after median from three reruns (9.48, 9.60, 9.36); focused case passes in 1.39s. |
| Hypothesis | Change | Before | After | Decision | Notes |
|---|---|---|---|---|---|
| Repeated full-suite runs are too expensive for discovery | Switched full-suite benchmark to one run and added per-file profiler | ~250s/run | pending | keep | Bun has no slowest-test reporter in this version; profile files directly. |
| Plugin install concurrency test spends time spawning more workers than needed to exercise lock contention | Reduced worker counts from 12/10/8 to 6/6/5; kept `holdMs: 30` | 7.800s | 6.204s | keep | Median from 3 targeted runs; still covers concurrent cross-process writes to server, server+tui, and existing json config. |
| `httpapi-listen` PTY route tests pay for git repositories they do not assert on | Removed `git: true` from temp dirs while keeping config setup | 10.554s | 7.818s | keep | Median from 3 targeted runs; HTTP routes, tickets, websocket upgrade, restart, and no-auth paths still pass. |
| `workspace.waitForSync` timeout test waits the full production timeout | Added optional timeout parameter defaulting to production timeout; timeout test uses 25ms | 12.949s | 8.305s | keep | Median from 3 targeted runs; production callers keep the 5000ms default. |
| `config.test` waits after dependencies even though `.gitignore` is written synchronously | Removed obsolete 1000ms sleep from writable `KILO_CONFIG_DIR` test | 10.270s | 9.433s | keep | Median from 5 targeted runs because one run was noisy; simpler test and no fixed sleep. |
| SDK parity helpers create git repos for tests that only need files/config/session state | Changed `withProject` default to no git; explicit git init test still opts into no-git fixture | 8.011s | 5.180s | keep | Median from 5 targeted runs because first run was cold/noisy. |
| Provider plugin filter test waits on plugin dependency readiness setup | Marked local plugin dependencies ready using the existing fixture helper | 7.543s | 6.366s | keep | Median from 3 targeted runs; matches neighboring plugin provider test setup. |
| HTTP provider tests generate local plugins without dependency-ready fixture state | Marked generated `.opencode` plugin fixtures dependency-ready | 7.905s | 2.980s | keep | Median from 3 targeted runs; avoids unrelated plugin dependency setup in route tests. |
| TUI plugin lifecycle timeout coverage waits the full production cleanup timeout | Added optional runtime dispose timeout override and used 25ms in the timeout test | 7.330s | 1.507s | keep | Median from 3 targeted runs; production default remains 5000ms. |
| Skill tool test initializes git even though it only reads local skill files | Removed `git: true` from the temporary directory fixture | 2.320s | 1.425s | keep | Single targeted rerun; still exercises skill discovery, permission request, and bundled file output. |
| Prompt shell semantics tests initialize git though they only assert shell/session behavior | Removed `git: true` from shell-focused prompt fixtures while preserving config setup | 26.930s | 23.400s | keep | Three targeted reruns passed after the change: 23.80s, 23.55s, 23.40s. |
| Remaining prompt behavior tests mostly do not require repository state | Removed git setup from safe loop/reference/error fixtures; restored shell queue/cancel cases | 23.400s | 19.610s | keep | Safety review found shell runner readiness depends on git-backed setup in several tests; current single rerun passes. |
| Session processor effect tests do not require repository state | Removed git setup from all processor-effect temp server fixtures | 12.500s | 9.230s | keep | Two targeted reruns passed after the change: 9.61s, 9.23s. |
| HTTP listen PTY ticket tests restart the same listener topology twice | Folded directory-scoped ticket regression into the broader unsafe-ticket test | 7.051s | 6.170s | keep | Two targeted reruns passed after the change: 6.76s, 6.17s; still covers mint failure and successful same-directory upgrade. |
| File watcher readiness can write before async native subscriptions are active | Retried short readiness writes and accepted symlink-realpath HEAD events | failed | 4.62s | keep | Three sequential focused watcher runs passed: 4.62s, 4.57s, 4.64s; full suite no longer failed in `watcher.test.ts`. |
| First provider config/env/filtering block can use Effect-aware instance fixtures | Migrated six `tmpdir` + `withTestInstance` cases to `it.instance` | 6.06s | 6.07s | keep | Neutral timing, but removes manual config file writes and instance plumbing; use as the pattern for later provider slices. |
| Custom provider/model config cases can use Effect-aware instance fixtures | Migrated three more config-heavy provider cases to `it.instance` | 6.07s | 6.12s | keep | Neutral timing within noise, but continues removing manual config file writes on top of the first provider fixture PR. |
| Provider env precedence and model lookup cases can use Effect-aware instance fixtures | Migrated four more provider lookup/default-model cases to `it.instance` | 6.12s | 6.36s | keep | Noisy 5-run median; kept as a small stacked cleanup slice but do not claim speedup from this migration. |
| Simple config load cases can use Effect-aware instance fixtures | Migrated JSON, shell, formatter, and lsp config load cases to `it.instance` | 14.18s | 3.93s | keep | Three-run medians before/after; removes manual `tmpdir` + `withTestInstance` setup from the first simple config block. |
| Config template, file include, and simple agent cases can use Effect-aware instance fixtures | Migrated JSONC, env/file substitution, invalid config, and agent config cases to `it.instance` | 1.87s | 1.90s | keep | Stacked on the first config slice; neutral timing but removes more manual `tmpdir` + instance plumbing. |
| Agent option, command, and legacy migration config cases can use Effect-aware instance fixtures | Migrated agent variant, command, autoshare, and mode migration cases to `it.instance` | 1.90s | 1.83s | keep | Stacked on the config template slice; small neutral-to-positive timing and less manual setup. |
| Local config update and directory cases can use Effect-aware instance fixtures | Migrated local `update` and `directories` cases to `it.instance` | 1.77s | 1.71s | keep | Three-run medians; small positive/neutral timing, removes manual instance plumbing, and eliminates one existing unsafe cast. |
| `.opencode` agent and command file-loading cases can use Effect-aware instance fixtures | Migrated singular/plural agent and command markdown fixture cases to `it.instance` | 7.21s | 1.87s | keep | Parent baseline was noisy (7.42, 7.21, 2.83); after runs were stable at 1.87, 1.98, 1.83. Keep as cleanup with no broad claim. |
| Legacy tools and permission-order config cases can use Effect-aware instance fixtures | Migrated legacy `tools` migration and permission order cases to `it.instance` | 1.87s | 1.87s | keep | Neutral timing; removes more manual temp-instance plumbing from legacy config migration coverage. |
| Remaining simple config load cases can use Effect-aware instance fixtures | Migrated default config load and legacy TUI-key cases to `it.instance` | 7.78s | 6.39s | keep | Single baseline before edit; after median from three sequential reruns (5.76, 6.39, 6.53). Keep as cleanup with cautious timing. |
| Managed settings config cases can use Effect-aware instance fixtures | Migrated managed override and missing-managed-file cases to `it.instance` | 2.40s | 1.76s | keep | Single baseline before edit; after median from three sequential reruns (1.75, 1.76, 1.80). |
| Local plugin and subagent config fixtures can use Effect-aware instance fixtures | Migrated scoped npm plugin and custom subagent markdown cases to `it.instance` | 2.37s | 1.67s | keep | Single baseline before edit; after median from three sequential reruns (1.66, 1.67, 1.67). |
| MCP merge config cases can use Effect-aware instance fixtures | Migrated three MCP merge/override cases to `it.instance` | 1.98s | 1.95s | keep | Neutral timing within noise; removes manual `tmpdir` + `withTestInstance` setup from isolated filesystem-only config cases. |
| Remaining legacy tools config cases can use Effect-aware instance fixtures | Migrated allow/deny legacy `tools` permission cases to `it.instance` | 2.65s | 1.90s | keep | Single baseline before edit; after median from three sequential reruns (2.58, 1.90, 1.90). |
| Oversized snapshot batch tests only need to cross the 100-file boundary | Reduced large diff/revert fixture sizes while keeping each case above the batch boundary | 4.32s | 3.66s | keep | Three affected snapshot tests; after median from three reruns (4.32, 3.66, 3.66) while still crossing the 100-file boundary. |
| Prompt tests without LLM calls do not need the test LLM server | Added a no-server runner and moved obvious non-LLM prompt/shell cases to it | 25.41s | 21.03s | keep | Full prompt file after simplify pass median from three reruns (20.66, 21.03, 21.64); LLM-backed tests stay on original runner. |
| CLI run subprocess cases can run independently | Marked `run-process.test.ts` subprocess cases concurrent | 11.87s | 4.13s | keep | Newest-dev single baseline; after median from three reruns (4.13, 4.17, 4.11). Each case has an isolated temp home and LLM port. |
| Snapshot initialization does not need to commit seeded files in the source repo | Removed extra `git add`/`commit` from the snapshot test `initialize()` helper | 22.22s | 20.23s | keep | Newest-dev single baseline; after median from three reruns (20.23, 22.59, 20.11). Fixture still creates a git repo root commit. |
| Processor AI SDK tool-call case does not assert git behavior | Removed `git: true` from the non-native tool-call processor test | 10.22s | 9.48s | keep | Newest-dev single baseline; full-file after median from three reruns (9.48, 9.60, 9.36); focused case passes in 1.39s. |
## Profiling Results
@@ -98,48 +98,48 @@ TEST_PROFILE_GLOB='test/<area>/**/*.test.ts' TEST_PROFILE_TOP=15 bun run profile
Initial slowest files observed during discovery:
| File | Seconds | Scope |
| ----------------------------------------- | ------: | ------------- |
| `test/config/config.test.ts` | 23.546 | config |
| `test/provider/provider.test.ts` | 18.747 | provider |
| `test/control-plane/workspace.test.ts` | 16.447 | control-plane |
| `test/plugin/install-concurrency.test.ts` | 14.804 | plugin |
| `test/server/httpapi-cors.test.ts` | 14.620 | server |
| `test/server/httpapi-listen.test.ts` | 10.073 | server |
| `test/server/httpapi-sdk.test.ts` | 8.661 | server |
| `test/server/httpapi-provider.test.ts` | 7.905 | server |
| `test/cli/tui/plugin-lifecycle.test.ts` | 7.330 | cli/tui |
| `test/file/index.test.ts` | 7.214 | file |
| File | Seconds | Scope |
|---|---:|---|
| `test/config/config.test.ts` | 23.546 | config |
| `test/provider/provider.test.ts` | 18.747 | provider |
| `test/control-plane/workspace.test.ts` | 16.447 | control-plane |
| `test/plugin/install-concurrency.test.ts` | 14.804 | plugin |
| `test/server/httpapi-cors.test.ts` | 14.620 | server |
| `test/server/httpapi-listen.test.ts` | 10.073 | server |
| `test/server/httpapi-sdk.test.ts` | 8.661 | server |
| `test/server/httpapi-provider.test.ts` | 7.905 | server |
| `test/cli/tui/plugin-lifecycle.test.ts` | 7.330 | cli/tui |
| `test/file/index.test.ts` | 7.214 | file |
This table is historical profiling input, not the current ranking after kept changes.
Targeted 3-run baselines:
| File | Runs | Median | Notes |
| ----------------------------------------- | ---------------------- | -----: | ---------------------------------------------------------------------------- |
| `test/control-plane/workspace.test.ts` | 12.949, 12.949, 12.773 | 12.949 | Stable slow target. |
| `test/server/httpapi-listen.test.ts` | 10.554, 10.631, 10.479 | 10.554 | Stable slow target; WebSocket/listener lifecycle. |
| `test/config/config.test.ts` | 10.270, 9.042, 10.737 | 10.270 | Large serial file; initial 23s was mixed-scope contention/noise. |
| `test/server/httpapi-sdk.test.ts` | 7.600, 8.011, 8.035 | 8.011 | Stable slow target. |
| `test/plugin/install-concurrency.test.ts` | 7.949, 7.800, 7.712 | 7.800 | Stable slow target; many subprocesses. |
| `test/provider/provider.test.ts` | 8.323, 7.543, 7.474 | 7.543 | Large serial file. |
| `test/server/httpapi-cors.test.ts` | 2.621, 1.682, 1.518 | 1.682 | Not a standalone top target; initial 14s was mixed-scope noise/order effect. |
| File | Runs | Median | Notes |
|---|---|---:|---|
| `test/control-plane/workspace.test.ts` | 12.949, 12.949, 12.773 | 12.949 | Stable slow target. |
| `test/server/httpapi-listen.test.ts` | 10.554, 10.631, 10.479 | 10.554 | Stable slow target; WebSocket/listener lifecycle. |
| `test/config/config.test.ts` | 10.270, 9.042, 10.737 | 10.270 | Large serial file; initial 23s was mixed-scope contention/noise. |
| `test/server/httpapi-sdk.test.ts` | 7.600, 8.011, 8.035 | 8.011 | Stable slow target. |
| `test/plugin/install-concurrency.test.ts` | 7.949, 7.800, 7.712 | 7.800 | Stable slow target; many subprocesses. |
| `test/provider/provider.test.ts` | 8.323, 7.543, 7.474 | 7.543 | Large serial file. |
| `test/server/httpapi-cors.test.ts` | 2.621, 1.682, 1.518 | 1.682 | Not a standalone top target; initial 14s was mixed-scope noise/order effect. |
Full-suite sanity checks:
| Command | Result | Notes |
| -------------------- | -------: | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `bun run bench:test` | 225.069s | Before continuing prompt/session work. |
| `bun run bench:test` | 186.729s | After prompt, processor, and PTY wins before safety review restores. |
| `bun run bench:test` | 202.317s | After restoring prompt shell coverage and SDK VCS parity coverage. |
| `bun run bench:test` | failed | Watcher blocker cleared; current run later failed in focused-passing `tool/skill.test.ts` and prompt shell timeout cases under full-suite load. |
| Command | Result | Notes |
|---|---:|---|
| `bun run bench:test` | 225.069s | Before continuing prompt/session work. |
| `bun run bench:test` | 186.729s | After prompt, processor, and PTY wins before safety review restores. |
| `bun run bench:test` | 202.317s | After restoring prompt shell coverage and SDK VCS parity coverage. |
| `bun run bench:test` | failed | Watcher blocker cleared; current run later failed in focused-passing `tool/skill.test.ts` and prompt shell timeout cases under full-suite load. |
## Dead Ends
| Hypothesis | Change Tried | Before | After | Decision | Notes |
| ---------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | -----: | -----: | -------- | --------------------------------------------------------------------------------------------- |
| `file/index.test.ts` pays unnecessary per-test global instance cleanup | Removed `afterEach(disposeAllInstances)` while keeping the explicit disposal test import | 5.262s | 5.089s | discard | Improvement was within noise and the cleanup is a safety guard for many instance-state tests. |
| Socket reset retry test can shorten its idle-timeout path | Reduced Bun server idle timeout and tried forced server close | 16.46s | failed | discard | Shorter idle timeout changed the error shape; forced close hung. Keep the real socket reset. |
| `tool/webfetch` can avoid per-test instance setup | Switched local HTTP tests from `it.instance` to `it.live` | 1.219s | failed | discard | Tool execution reads instance-local agent state, so the temp instance is required. |
| LSP client interop tests can shorten coarse request-handling sleeps | Reduced fixed post-notification waits from 100ms to 10ms | 4.270s | 4.740s | discard | First run improved to 3.870s but verification was slower than baseline; not a clear win. |
| Config content env cases can use Effect-aware instance fixtures | Migrated two `KILO_CONFIG_CONTENT` token substitution cases to `it.instance` | 1.95s | 2.06s | discard | Passing but not neutral-or-better in focused reruns; keep existing explicit env cleanup. |
| Hypothesis | Change Tried | Before | After | Decision | Notes |
|---|---|---:|---:|---|---|
| `file/index.test.ts` pays unnecessary per-test global instance cleanup | Removed `afterEach(disposeAllInstances)` while keeping the explicit disposal test import | 5.262s | 5.089s | discard | Improvement was within noise and the cleanup is a safety guard for many instance-state tests. |
| Socket reset retry test can shorten its idle-timeout path | Reduced Bun server idle timeout and tried forced server close | 16.46s | failed | discard | Shorter idle timeout changed the error shape; forced close hung. Keep the real socket reset. |
| `tool/webfetch` can avoid per-test instance setup | Switched local HTTP tests from `it.instance` to `it.live` | 1.219s | failed | discard | Tool execution reads instance-local agent state, so the temp instance is required. |
| LSP client interop tests can shorten coarse request-handling sleeps | Reduced fixed post-notification waits from 100ms to 10ms | 4.270s | 4.740s | discard | First run improved to 3.870s but verification was slower than baseline; not a clear win. |
| Config content env cases can use Effect-aware instance fixtures | Migrated two `KILO_CONFIG_CONTENT` token substitution cases to `it.instance` | 1.95s | 2.06s | discard | Passing but not neutral-or-better in focused reruns; keep existing explicit env cleanup. |
+1 -7
View File
@@ -29,18 +29,12 @@ const allow: Record<string, string> = {
}
const testAllow: Record<string, { count: number; reason: string }> = {
"control-plane/workspace.test.ts": { count: 5, reason: "existing runtime integration test" },
"kilocode/config-resilience.test.ts": { count: 4, reason: "existing runtime integration test" },
"kilocode/config-validation.test.ts": { count: 2, reason: "existing runtime integration test" },
"kilocode/plan-followup.test.ts": { count: 4, reason: "existing runtime integration test" },
"kilocode/session/platform-attribution.test.ts": { count: 5, reason: "existing runtime integration test" },
"kilocode/session/platform-attribution.test.ts": { count: 3, reason: "existing runtime integration test" },
"kilocode/session-prompt-queue.test.ts": { count: 5, reason: "prompt queue legacy instance bridge regression" },
"kilocode/session/session.test.ts": { count: 5, reason: "existing runtime integration test" },
"provider/amazon-bedrock.test.ts": { count: 2, reason: "existing runtime integration test" },
"provider/provider.test.ts": { count: 3, reason: "existing runtime integration test" },
"server/experimental-session-list.test.ts": { count: 2, reason: "Kilo session list integration test" },
"server/httpapi-event.test.ts": { count: 6, reason: "event stream integration test" },
"session/llm.test.ts": { count: 2, reason: "existing runtime integration test" },
"tool/recall.test.ts": { count: 11, reason: "existing runtime integration test" },
}