diff --git a/nix/hashes.json b/nix/hashes.json
index e8171c93a6..db28ef6641 100644
--- a/nix/hashes.json
+++ b/nix/hashes.json
@@ -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="
}
}
diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts
index 5826f6947a..0ddbd6f8b4 100644
--- a/packages/opencode/src/snapshot/index.ts
+++ b/packages/opencode/src/snapshot/index.ts
@@ -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 "."
diff --git a/packages/opencode/test/kilocode/snapshot-cache.test.ts b/packages/opencode/test/kilocode/snapshot-cache.test.ts
index c326c59460..6fb9e86dbb 100644
--- a/packages/opencode/test/kilocode/snapshot-cache.test.ts
+++ b/packages/opencode/test/kilocode/snapshot-cache.test.ts
@@ -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(body: (snapshot: Snapshot.Interface) => Effect.Effect) {
+ 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)
+ }),
+ ),
})
})
diff --git a/packages/opencode/test/kilocode/snapshot-freeze-repro.test.ts b/packages/opencode/test/kilocode/snapshot-freeze-repro.test.ts
index 4234efe98b..68b1ceb72b 100644
--- a/packages/opencode/test/kilocode/snapshot-freeze-repro.test.ts
+++ b/packages/opencode/test/kilocode/snapshot-freeze-repro.test.ts
@@ -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(body: (snapshot: Snapshot.Interface) => Effect.Effect) {
+ 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)
+ }),
+ ),
})
})
diff --git a/script/check-opencode-promise-facades.ts b/script/check-opencode-promise-facades.ts
index f7319c53d1..fe36cc5d33 100644
--- a/script/check-opencode-promise-facades.ts
+++ b/script/check-opencode-promise-facades.ts
@@ -28,7 +28,6 @@ const allow: Record = {
"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",