diff --git a/packages/core/src/plugin/warming.ts b/packages/core/src/plugin/warming.ts index b6a58fb328e..3b3e1dd540c 100644 --- a/packages/core/src/plugin/warming.ts +++ b/packages/core/src/plugin/warming.ts @@ -3,7 +3,7 @@ export * as WarmingPlugin from "./warming.js" import { define } from "@opencode/plugin/effect/plugin" import type { SessionHooks } from "@opencode/plugin/effect/session" import type { Session } from "@opencode/schema/session" -import { Clock, Duration, Effect, Scope } from "effect" +import { Clock, Duration, Effect, Scope, Stream } from "effect" import { Config } from "../config.js" const defaults = { @@ -28,6 +28,31 @@ export const Plugin = define({ const scope = yield* Scope.Scope const sessions = new Map() + // A ping during a model request would compete with the request it keeps warm, which stalls single-slot local models. + // A request counts from its context hook, because `session.step.started` waits for the first response chunk. + const requesting = new Set() + // A turn always ends with an execution event, so a step that ends without its own event can't stay in flight. + yield* ctx.event + .subscribe({ + types: [ + "session.step.streamed", + "session.step.failed", + "session.execution.succeeded", + "session.execution.failed", + "session.execution.interrupted", + ], + }) + .pipe( + Stream.runForEach((event) => + Effect.gen(function* () { + requesting.delete(event.data.sessionID) + // Count the next interval from the end of the response, not from when a long request started. + const current = sessions.get(event.data.sessionID) + if (current) current.last = yield* Clock.currentTimeMillis + }), + ), + Effect.forkScoped({ startImmediately: true }), + ) const loop: (sessionID: Session.ID) => Effect.Effect = Effect.fn("WarmingPlugin.loop")( function* (sessionID) { const current = sessions.get(sessionID) @@ -43,6 +68,10 @@ export const Plugin = define({ sessions.delete(sessionID) return } + if (requesting.has(sessionID)) { + yield* Effect.sleep(current.settings.interval) + return yield* loop(sessionID) + } const last = current.last yield* Effect.logInfo("warming session", { sessionID, last }) @@ -100,7 +129,9 @@ export const Plugin = define({ Effect.forkIn(scope), ) }) - yield* ctx.session.hook("context", hook) + yield* ctx.session.hook("context", (event) => + Effect.sync(() => requesting.add(event.sessionID)).pipe(Effect.andThen(hook(event))), + ) yield* ctx.session.hook("compaction", hook) yield* ctx.session.hook("generate", hook) }),