fix(core): make session warming observable
This commit is contained in:
parent
c821d49386
commit
e84938b309
6 changed files with 166 additions and 45 deletions
|
|
@ -15,65 +15,86 @@ export const Plugin = define({
|
|||
id: "opencode.warming",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
const config = yield* Config.Service
|
||||
const warming = Config.latest(yield* config.entries(), "warming")
|
||||
if (!warming) return
|
||||
const settings = warming === true ? defaults : { ...defaults, ...warming }
|
||||
const interval = Duration.toMillis(settings.interval)
|
||||
const duration = Duration.toMillis(settings.duration)
|
||||
if (!Number.isFinite(interval) || interval <= 0 || !Number.isFinite(duration) || duration <= 0) {
|
||||
const loadSettings = Effect.fn("WarmingPlugin.loadSettings")(function* () {
|
||||
const warming = Config.latest(yield* config.entries(), "warming")
|
||||
if (!warming) return
|
||||
const settings = warming === true ? defaults : { ...defaults, ...warming }
|
||||
const interval = Duration.toMillis(settings.interval)
|
||||
const duration = Duration.toMillis(settings.duration)
|
||||
if (Number.isFinite(interval) && interval > 0 && Number.isFinite(duration) && duration > 0) return settings
|
||||
yield* Effect.logWarning("warming interval and duration must be finite positive durations")
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
const scope = yield* Scope.Scope
|
||||
const sessions = new Map<SessionSchema.ID, { last: number; expires: number }>()
|
||||
const loop: (sessionID: SessionSchema.ID) => Effect.Effect<void> = Effect.fn("WarmingPlugin.loop")(function* (
|
||||
sessionID,
|
||||
) {
|
||||
const current = sessions.get(sessionID)
|
||||
if (!current) return
|
||||
const sessions = new Map<SessionSchema.ID, { last: number; expires: number; settings: typeof defaults }>()
|
||||
const loop: (sessionID: SessionSchema.ID) => Effect.Effect<void> = Effect.fn("WarmingPlugin.loop")(
|
||||
function* (sessionID) {
|
||||
const current = sessions.get(sessionID)
|
||||
if (!current) return
|
||||
|
||||
const now = yield* Clock.currentTimeMillis
|
||||
const next = Math.min(current.last + interval, current.expires)
|
||||
if (now < next) {
|
||||
yield* Effect.sleep(Duration.millis(next - now))
|
||||
const now = yield* Clock.currentTimeMillis
|
||||
const next = Math.min(current.last + Duration.toMillis(current.settings.interval), current.expires)
|
||||
if (now < next) {
|
||||
yield* Effect.sleep(Duration.millis(next - now))
|
||||
return yield* loop(sessionID)
|
||||
}
|
||||
if (now >= current.expires) {
|
||||
sessions.delete(sessionID)
|
||||
return
|
||||
}
|
||||
|
||||
const last = current.last
|
||||
yield* Effect.logInfo("warming session", { sessionID, last })
|
||||
yield* ctx.session
|
||||
.generate({ sessionID, prompt: current.settings.prompt })
|
||||
.pipe(Effect.catchCause((cause) => Effect.logWarning("failed to warm session", { sessionID, cause })))
|
||||
const latest = sessions.get(sessionID)
|
||||
if (latest === current && latest.last === last) latest.last = yield* Clock.currentTimeMillis
|
||||
return yield* loop(sessionID)
|
||||
}
|
||||
if (now >= current.expires) {
|
||||
sessions.delete(sessionID)
|
||||
return
|
||||
}
|
||||
|
||||
const last = current.last
|
||||
yield* ctx.session.generate({ sessionID, prompt: settings.prompt }).pipe(
|
||||
Effect.catchCause((cause) => Effect.logWarning("failed to warm session", { sessionID, cause })),
|
||||
)
|
||||
const latest = sessions.get(sessionID)
|
||||
if (latest === current && latest.last === last) latest.last = yield* Clock.currentTimeMillis
|
||||
return yield* loop(sessionID)
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
yield* ctx.session.hook("context", (event) =>
|
||||
Effect.gen(function* () {
|
||||
const active = sessions.get(event.sessionID)
|
||||
const settings = yield* loadSettings()
|
||||
if (!settings) {
|
||||
sessions.delete(event.sessionID)
|
||||
return
|
||||
}
|
||||
|
||||
// Once generate exposes request metadata to context hooks, tag warm requests instead of matching the prompt.
|
||||
const message = event.messages.at(-1)
|
||||
if (
|
||||
message?.role === "user" &&
|
||||
message.content.length === 1 &&
|
||||
message.content[0]?.type === "text" &&
|
||||
message.content[0].text === settings.prompt
|
||||
)
|
||||
(message.content[0].text === active?.settings.prompt || message.content[0].text === settings.prompt)
|
||||
) {
|
||||
if (active) active.settings = settings
|
||||
return
|
||||
}
|
||||
|
||||
const now = yield* Clock.currentTimeMillis
|
||||
const active = sessions.get(event.sessionID)
|
||||
const duration = Duration.toMillis(settings.duration)
|
||||
if (active) {
|
||||
active.last = now
|
||||
active.expires = now + duration
|
||||
active.settings = settings
|
||||
return
|
||||
}
|
||||
sessions.set(event.sessionID, { last: now, expires: now + duration })
|
||||
yield* loop(event.sessionID).pipe(Effect.forkIn(scope))
|
||||
sessions.set(event.sessionID, { last: now, expires: now + duration, settings })
|
||||
yield* Effect.logInfo("scheduled session warming", {
|
||||
sessionID: event.sessionID,
|
||||
interval: settings.interval,
|
||||
expires: now + duration,
|
||||
})
|
||||
yield* loop(event.sessionID).pipe(
|
||||
Effect.catchCause((cause) =>
|
||||
Effect.logError("session warming loop failed", { sessionID: event.sessionID, cause }),
|
||||
),
|
||||
Effect.forkIn(scope),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue