From 2f010a4a8e98e596c7270ff2faaac588715ec790 Mon Sep 17 00:00:00 2001 From: Andrew Date: Sun, 23 Aug 2026 19:44:50 -0700 Subject: [PATCH] perf(opencode): overlap the pre-turn snapshot with the provider request Co-Authored-By: Claude Fable 5 --- packages/opencode/src/session/processor.ts | 30 +++++++++++++------ packages/opencode/src/session/tools.ts | 4 ++- .../opencode/test/session/compaction.test.ts | 1 + 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index c6689252cd51..aba2b621daf8 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -2,7 +2,7 @@ import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { PermissionV1 } from "@opencode-ai/core/v1/permission" import { Image } from "@/image/image" import { SessionV1 } from "@opencode-ai/core/v1/session" -import { Cause, Deferred, Effect, Exit, Layer, Context, Scope, Schema } from "effect" +import { Cause, Deferred, Effect, Exit, Fiber, Layer, Context, Scope, Schema } from "effect" import * as Stream from "effect/Stream" import { Agent } from "@/agent/agent" import { Config } from "@/config/config" @@ -35,6 +35,7 @@ export interface Handle { readonly requestStartAt: number | undefined readonly firstRequestStartAt: number | undefined readonly snapshotMs: number + readonly awaitSnapshot: Effect.Effect readonly updateToolCall: ( toolCallID: string, update: (part: SessionV1.ToolPart) => SessionV1.ToolPart, @@ -104,19 +105,14 @@ const layer = Layer.effect( const database = yield* Database.Service const create = Effect.fn("SessionProcessor.create")(function* (input: Input) { - // Pre-capture snapshot before the LLM stream starts. The AI SDK - // may execute tools internally before emitting start-step events, - // so capturing inside the event handler can be too late. - const snapshotStart = Date.now() - const initialSnapshot = yield* snapshot.track() const ctx: ProcessorContext = { assistantMessage: input.assistantMessage, sessionID: input.sessionID, model: input.model, toolcalls: {}, shouldBreak: false, - snapshot: initialSnapshot, - snapshotMs: Date.now() - snapshotStart, + snapshot: undefined, + snapshotMs: 0, blocked: false, needsCompaction: false, currentText: undefined, @@ -127,6 +123,21 @@ const layer = Layer.effect( } let aborted = false + // Runs concurrently with the provider request; awaitSnapshot is the barrier + // every tool execution and step event takes before touching the result. + const snapshotStart = Date.now() + const snapshotFiber = yield* snapshot.track().pipe( + Effect.tap(() => Effect.sync(() => (ctx.snapshotMs = Date.now() - snapshotStart))), + Effect.orDie, + Effect.forkIn(scope), + ) + const awaitSnapshot = Effect.gen(function* () { + if (ctx.snapshot === undefined) { + ctx.snapshot = yield* Fiber.join(snapshotFiber) + } + return ctx.snapshot + }) + const parse = (e: unknown) => MessageV2.fromError(e, { providerID: input.model.providerID, @@ -436,7 +447,7 @@ const layer = Layer.effect( throw new Error(value.message) case "step-start": - if (!ctx.snapshot) ctx.snapshot = yield* snapshot.track() + yield* awaitSnapshot yield* session.updatePart({ id: PartID.ascending(), messageID: ctx.assistantMessage.id, @@ -716,6 +727,7 @@ const layer = Layer.effect( get snapshotMs() { return ctx.snapshotMs }, + awaitSnapshot, updateToolCall, completeToolCall, process, diff --git a/packages/opencode/src/session/tools.ts b/packages/opencode/src/session/tools.ts index 805d2253b8b6..e2f78fc00f2f 100644 --- a/packages/opencode/src/session/tools.ts +++ b/packages/opencode/src/session/tools.ts @@ -43,7 +43,7 @@ export const resolve = Effect.fn("SessionTools.resolve")(function* (input: { agent: Agent.Info model: Provider.Model session: Session.Info - processor: Pick + processor: Pick bypassAgentCheck: boolean messages: SessionV1.WithParts[] promptOps: TaskPromptOps @@ -104,6 +104,8 @@ export const resolve = Effect.fn("SessionTools.resolve")(function* (input: { return run.promise( Effect.gen(function* () { const ctx = context(args, options) + // Tools can run before any step event; the pre-turn snapshot must land before a tool mutates files. + yield* input.processor.awaitSnapshot yield* plugin.trigger( "tool.execute.before", { tool: item.id, sessionID: ctx.sessionID, callID: ctx.callID }, diff --git a/packages/opencode/test/session/compaction.test.ts b/packages/opencode/test/session/compaction.test.ts index 1878cec50fc9..008f230a2272 100644 --- a/packages/opencode/test/session/compaction.test.ts +++ b/packages/opencode/test/session/compaction.test.ts @@ -206,6 +206,7 @@ function fake( requestStartAt: undefined, firstRequestStartAt: undefined, snapshotMs: 0, + awaitSnapshot: Effect.succeed(undefined), updateToolCall: Effect.fn("TestSessionProcessor.updateToolCall")(() => Effect.succeed(undefined)), completeToolCall: Effect.fn("TestSessionProcessor.completeToolCall")(() => Effect.void), process: Effect.fn("TestSessionProcessor.process")(() => Effect.succeed(result)),