Merge branch 'main' into session/agent_889409bf-00dd-4caf-a3a0-4b1f84fe03ba

This commit is contained in:
Darko Gjorgjievski
2026-05-28 16:28:21 +02:00
committed by GitHub
5 changed files with 110 additions and 97 deletions
+4 -4
View File
@@ -1,8 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-EbXkvlbexL1Gyy4vvGG5j+JWjv7SsVk6T5YnDfo3AQY=",
"aarch64-linux": "sha256-+MuJE4XGr0dArD/HuozaoK9Oymfgvx1CisE/Sm4ZstY=",
"aarch64-darwin": "sha256-Qdk+tZLydGld471ApL1cxfd85QQNFelTfqq7uAznfK4=",
"x86_64-darwin": "sha256-Fo6W65MfAcXaulBGCaE7+GfHtq3VVJyVDJpIENvR7lQ="
"x86_64-linux": "sha256-vI06afIL8mL/Rt33Wk2S2kLzrlR3EHqv5kfy0qgO2Zg=",
"aarch64-linux": "sha256-68uA7dKxOXmIRHJ3BI2K2wc1Pkag2cWpp9fLtbS6Ehk=",
"aarch64-darwin": "sha256-oRy74XjENuCxOmPrfEmM4UXoZQQXM5VQlLXMx5SmX+k=",
"x86_64-darwin": "sha256-TjcaVBh40HZk2kKwAunrrl/KYrvEk13bLiBdcUzWSQA="
}
}
-13
View File
@@ -3,7 +3,6 @@ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
import { formatPatch, structuredPatch } from "diff"
import path from "path"
import z from "zod"
import { makeRuntime } from "@/effect/run-service" // kilocode_change
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { InstanceState } from "@/effect/instance-state"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
@@ -851,16 +850,4 @@ export const defaultLayer = layer.pipe(
Layer.provide(Config.defaultLayer),
)
// kilocode_change start - legacy promise helpers for Kilo callsites
const { runPromise } = makeRuntime(Service, defaultLayer)
export const track = () => runPromise((svc) => svc.track())
export const patch = (hash: string) => runPromise((svc) => svc.patch(hash))
export const restore = (snapshot: string) => runPromise((svc) => svc.restore(snapshot))
export const revert = (patches: Patch[]) => runPromise((svc) => svc.revert(patches))
export const diff = (hash: string) => runPromise((svc) => svc.diff(hash))
export const diffFull = (from: string, to: string) => runPromise((svc) => svc.diffFull(from, to))
export const cleanup = () => runPromise((svc) => svc.cleanup())
export const init = () => runPromise((svc) => svc.init())
// kilocode_change end
export * as Snapshot from "."
@@ -1,12 +1,13 @@
import { test, expect } from "bun:test"
import { $ } from "bun"
import { Effect } from "effect"
import { Snapshot } from "../../src/snapshot"
import { WithInstance } from "../../src/project/with-instance"
import { Filesystem } from "../../src/util/filesystem"
import * as Log from "@opencode-ai/core/util/log"
import { tmpdir } from "../fixture/fixture"
Log.init({ print: false })
void Log.init({ print: false })
async function bootstrap() {
return tmpdir({
@@ -20,26 +21,33 @@ async function bootstrap() {
})
}
function run<A>(body: (snapshot: Snapshot.Interface) => Effect.Effect<A>) {
return Effect.runPromise(Snapshot.Service.use(body).pipe(Effect.provide(Snapshot.defaultLayer)))
}
test("diffFull returns cached result for same hash pair", async () => {
await using tmp = await bootstrap()
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
const before = await Snapshot.track()
expect(before).toBeTruthy()
fn: () =>
run((snapshot) =>
Effect.gen(function* () {
const before = yield* snapshot.track()
expect(before).toBeTruthy()
await Filesystem.write(`${tmp.path}/a.txt`, "MODIFIED")
const after = await Snapshot.track()
expect(after).toBeTruthy()
expect(after).not.toBe(before)
yield* Effect.promise(() => Filesystem.write(`${tmp.path}/a.txt`, "MODIFIED"))
const after = yield* snapshot.track()
expect(after).toBeTruthy()
expect(after).not.toBe(before)
const first = await Snapshot.diffFull(before!, after!)
const second = await Snapshot.diffFull(before!, after!)
const first = yield* snapshot.diffFull(before!, after!)
const second = yield* snapshot.diffFull(before!, after!)
// Should be the exact same array reference (cached)
expect(second).toBe(first)
expect(first.length).toBeGreaterThan(0)
},
// Should be the exact same array reference (cached)
expect(second).toBe(first)
expect(first.length).toBeGreaterThan(0)
}),
),
})
})
@@ -47,13 +55,16 @@ test("diffFull returns empty array when from === to", async () => {
await using tmp = await bootstrap()
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
const hash = await Snapshot.track()
expect(hash).toBeTruthy()
fn: () =>
run((snapshot) =>
Effect.gen(function* () {
const hash = yield* snapshot.track()
expect(hash).toBeTruthy()
const result = await Snapshot.diffFull(hash!, hash!)
expect(result).toEqual([])
},
const result = yield* snapshot.diffFull(hash!, hash!)
expect(result).toEqual([])
}),
),
})
})
@@ -61,24 +72,30 @@ test("diffFull concurrent calls for same pair share one result", async () => {
await using tmp = await bootstrap()
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
const before = await Snapshot.track()
expect(before).toBeTruthy()
fn: () =>
run((snapshot) =>
Effect.gen(function* () {
const before = yield* snapshot.track()
expect(before).toBeTruthy()
await Filesystem.write(`${tmp.path}/a.txt`, "CONCURRENT")
const after = await Snapshot.track()
expect(after).toBeTruthy()
yield* Effect.promise(() => Filesystem.write(`${tmp.path}/a.txt`, "CONCURRENT"))
const after = yield* snapshot.track()
expect(after).toBeTruthy()
// Fire multiple concurrent calls they should all resolve to the same object
const results = await Promise.all([
Snapshot.diffFull(before!, after!),
Snapshot.diffFull(before!, after!),
Snapshot.diffFull(before!, after!),
])
// Fire multiple concurrent calls, they should all resolve to the same object.
const results = yield* Effect.all(
[
snapshot.diffFull(before!, after!),
snapshot.diffFull(before!, after!),
snapshot.diffFull(before!, after!),
],
{ concurrency: "unbounded" },
)
expect(results[0]).toBe(results[1])
expect(results[1]).toBe(results[2])
expect(results[0].length).toBeGreaterThan(0)
},
expect(results[0]).toBe(results[1])
expect(results[1]).toBe(results[2])
expect(results[0].length).toBeGreaterThan(0)
}),
),
})
})
@@ -14,6 +14,7 @@
import { test, expect, afterEach, mock } from "bun:test"
import { $ } from "bun"
import { Effect, Fiber } from "effect"
import { WithInstance } from "../../src/project/with-instance"
import { Server } from "../../src/server/server"
import { Session } from "../../src/session/session"
@@ -22,7 +23,11 @@ import { Filesystem } from "../../src/util/filesystem"
import * as Log from "@opencode-ai/core/util/log"
import { disposeAllInstances, tmpdir } from "../fixture/fixture"
Log.init({ print: false })
void Log.init({ print: false })
function run<A>(body: (snapshot: Snapshot.Interface) => Effect.Effect<A>) {
return Effect.runPromise(Snapshot.Service.use(body).pipe(Effect.provide(Snapshot.defaultLayer)))
}
afterEach(async () => {
mock.restore()
@@ -47,55 +52,60 @@ test("pathological diffFull workload finishes quickly and does not block abort",
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
const session = await Session.create({})
fn: () =>
run((snapshot) =>
Effect.gen(function* () {
const session = yield* Effect.promise(() => Session.create({}))
const before = await Snapshot.track()
expect(before).toBeTruthy()
const before = yield* snapshot.track()
expect(before).toBeTruthy()
await Filesystem.write(`${tmp.path}/fat.json`, v2)
const after = await Snapshot.track()
expect(after).toBeTruthy()
yield* Effect.promise(() => Filesystem.write(`${tmp.path}/fat.json`, v2))
const after = yield* snapshot.track()
expect(after).toBeTruthy()
// Kick off a diffFull that exercises the freeze path.
const diffPromise = Snapshot.diffFull(before!, after!)
// Kick off a diffFull that exercises the freeze path.
const diff = yield* snapshot.diffFull(before!, after!).pipe(Effect.forkChild({ startImmediately: true }))
// Concurrently keep a tick counter running. If the event loop blocks we
// will see this count fall behind wall-clock elapsed.
let ticks = 0
const start = Date.now()
const timer = setInterval(() => {
ticks++
}, 25)
// Concurrently keep a tick counter running. If the event loop blocks we
// will see this count fall behind wall-clock elapsed.
let ticks = 0
const start = Date.now()
const timer = setInterval(() => {
ticks++
}, 25)
// Fire an abort request against the Hono app in the middle of the diff.
const app = Server.Default().app
const abortStart = Date.now()
const res = await app.request(`/session/${session.id}/abort`, { method: "POST" })
const abortLatency = Date.now() - abortStart
expect(res.status).toBe(200)
// The abort endpoint must respond well under a second even under load.
expect(abortLatency).toBeLessThan(2000)
// Fire an abort request against the Hono app in the middle of the diff.
const app = Server.Default().app
const abortStart = Date.now()
const res = yield* Effect.promise(() =>
Promise.resolve(app.request(`/session/${session.id}/abort`, { method: "POST" })),
)
const abortLatency = Date.now() - abortStart
expect(res.status).toBe(200)
// The abort endpoint must respond well under a second even under load.
expect(abortLatency).toBeLessThan(2000)
const diffs = await diffPromise
clearInterval(timer)
const total = Date.now() - start
const diffs = yield* Fiber.join(diff)
clearInterval(timer)
const total = Date.now() - start
// The freeze workload must finish in bounded time. Five seconds is
// generous even for a slow CI box; without the fix this hangs.
expect(total).toBeLessThan(5000)
// And we must have ticked at least a few times during the work proves
// the event loop stayed responsive (ESC would actually arrive).
expect(ticks).toBeGreaterThan(0)
// The freeze workload must finish in bounded time. Five seconds is
// generous even for a slow CI box; without the fix this hangs.
expect(total).toBeLessThan(5000)
// And we must have ticked at least a few times during the work, proving
// the event loop stayed responsive (ESC would actually arrive).
expect(ticks).toBeGreaterThan(0)
// With git-based diff the patch is a real unified diff, not empty.
const hit = diffs.find((d) => d.file === "fat.json")
expect(hit).toBeDefined()
expect(hit!.patch).toMatch(/^diff --git /m)
expect(hit!.patch).toContain("-v1_line_0")
expect(hit!.patch).toContain("+v2_line_0")
expect(hit!.additions).toBeGreaterThan(0)
expect(hit!.deletions).toBeGreaterThan(0)
},
// With git-based diff the patch is a real unified diff, not empty.
const hit = diffs.find((d) => d.file === "fat.json")
expect(hit).toBeDefined()
expect(hit!.patch).toMatch(/^diff --git /m)
expect(hit!.patch).toContain("-v1_line_0")
expect(hit!.patch).toContain("+v2_line_0")
expect(hit!.additions).toBeGreaterThan(0)
expect(hit!.deletions).toBeGreaterThan(0)
}),
),
})
})
-1
View File
@@ -28,7 +28,6 @@ const allow: Record<string, string> = {
"session/prompt.ts": "transitional facade tracked by #10655",
"session/session.ts": "transitional facade tracked by #10655",
"session/summary.ts": "transitional facade removed by #10620",
"snapshot/index.ts": "transitional facade tracked by #10660",
"storage/storage.ts": "transitional facade tracked by #10659",
"sync/index.ts": "sync event runtime boundary",
"tool/registry.ts": "transitional facade removed by #10620",