refactor(effect): inline session processor interrupt cleanup (#21593)

This commit is contained in:
Kit Langton
2026-04-08 21:19:01 -04:00
committed by GitHub
parent 2bdd279467
commit 8bdcc22541
9 changed files with 116 additions and 148 deletions
+1 -1
View File
@@ -46,7 +46,7 @@ export namespace FileTime {
const disableCheck = yield* Flag.OPENCODE_DISABLE_FILETIME_CHECK const disableCheck = yield* Flag.OPENCODE_DISABLE_FILETIME_CHECK
const stamp = Effect.fnUntraced(function* (file: string) { const stamp = Effect.fnUntraced(function* (file: string) {
const info = yield* fsys.stat(file).pipe(Effect.catch(() => Effect.succeed(undefined))) const info = yield* fsys.stat(file).pipe(Effect.catch(() => Effect.void))
return { return {
read: yield* DateTime.nowAsDate, read: yield* DateTime.nowAsDate,
mtime: info ? Option.getOrUndefined(info.mtime)?.getTime() : undefined, mtime: info ? Option.getOrUndefined(info.mtime)?.getTime() : undefined,
+1 -1
View File
@@ -501,7 +501,7 @@ export namespace MCP {
return return
} }
const result = yield* create(key, mcp).pipe(Effect.catch(() => Effect.succeed(undefined))) const result = yield* create(key, mcp).pipe(Effect.catch(() => Effect.void))
if (!result) return if (!result) return
s.status[key] = result.status s.status[key] = result.status
+1 -1
View File
@@ -158,7 +158,7 @@ export namespace Project {
return yield* fs.readFileString(pathSvc.join(dir, "opencode")).pipe( return yield* fs.readFileString(pathSvc.join(dir, "opencode")).pipe(
Effect.map((x) => x.trim()), Effect.map((x) => x.trim()),
Effect.map(ProjectID.make), Effect.map(ProjectID.make),
Effect.catch(() => Effect.succeed(undefined)), Effect.catch(() => Effect.void),
) )
}) })
+1 -3
View File
@@ -253,8 +253,7 @@ When constructing the summary, try to stick to this template:
sessionID: input.sessionID, sessionID: input.sessionID,
model, model,
}) })
const result = yield* processor const result = yield* processor.process({
.process({
user: userMessage, user: userMessage,
agent, agent,
sessionID: input.sessionID, sessionID: input.sessionID,
@@ -269,7 +268,6 @@ When constructing the summary, try to stick to this template:
], ],
model, model,
}) })
.pipe(Effect.onInterrupt(() => processor.abort()))
if (result === "compact") { if (result === "compact") {
processor.message.error = new MessageV2.ContextOverflowError({ processor.message.error = new MessageV2.ContextOverflowError({
+10 -21
View File
@@ -30,7 +30,6 @@ export namespace SessionProcessor {
export interface Handle { export interface Handle {
readonly message: MessageV2.Assistant readonly message: MessageV2.Assistant
readonly partFromToolCall: (toolCallID: string) => MessageV2.ToolPart | undefined readonly partFromToolCall: (toolCallID: string) => MessageV2.ToolPart | undefined
readonly abort: () => Effect.Effect<void>
readonly process: (streamInput: LLM.StreamInput) => Effect.Effect<Result> readonly process: (streamInput: LLM.StreamInput) => Effect.Effect<Result>
} }
@@ -429,19 +428,6 @@ export namespace SessionProcessor {
yield* status.set(ctx.sessionID, { type: "idle" }) yield* status.set(ctx.sessionID, { type: "idle" })
}) })
const abort = Effect.fn("SessionProcessor.abort")(() =>
Effect.gen(function* () {
if (!ctx.assistantMessage.error) {
yield* halt(new DOMException("Aborted", "AbortError"))
}
if (!ctx.assistantMessage.time.completed) {
yield* cleanup()
return
}
yield* session.updateMessage(ctx.assistantMessage)
}),
)
const process = Effect.fn("SessionProcessor.process")(function* (streamInput: LLM.StreamInput) { const process = Effect.fn("SessionProcessor.process")(function* (streamInput: LLM.StreamInput) {
log.info("process") log.info("process")
ctx.needsCompaction = false ctx.needsCompaction = false
@@ -459,7 +445,14 @@ export namespace SessionProcessor {
Stream.runDrain, Stream.runDrain,
) )
}).pipe( }).pipe(
Effect.onInterrupt(() => Effect.sync(() => void (aborted = true))), Effect.onInterrupt(() =>
Effect.gen(function* () {
aborted = true
if (!ctx.assistantMessage.error) {
yield* halt(new DOMException("Aborted", "AbortError"))
}
}),
),
Effect.catchCauseIf( Effect.catchCauseIf(
(cause) => !Cause.hasInterruptsOnly(cause), (cause) => !Cause.hasInterruptsOnly(cause),
(cause) => Effect.fail(Cause.squash(cause)), (cause) => Effect.fail(Cause.squash(cause)),
@@ -480,13 +473,10 @@ export namespace SessionProcessor {
Effect.ensuring(cleanup()), Effect.ensuring(cleanup()),
) )
if (aborted && !ctx.assistantMessage.error) {
yield* abort()
}
if (ctx.needsCompaction) return "compact" if (ctx.needsCompaction) return "compact"
if (ctx.blocked || ctx.assistantMessage.error || aborted) return "stop" if (ctx.blocked || ctx.assistantMessage.error) return "stop"
return "continue" return "continue"
}).pipe(Effect.onInterrupt(() => abort().pipe(Effect.asVoid))) })
}) })
return { return {
@@ -496,7 +486,6 @@ export namespace SessionProcessor {
partFromToolCall(toolCallID: string) { partFromToolCall(toolCallID: string) {
return ctx.toolcalls[toolCallID] return ctx.toolcalls[toolCallID]
}, },
abort,
process, process,
} satisfies Handle } satisfies Handle
}) })
+4 -14
View File
@@ -964,9 +964,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
const same = ag.model && model.providerID === ag.model.providerID && model.modelID === ag.model.modelID const same = ag.model && model.providerID === ag.model.providerID && model.modelID === ag.model.modelID
const full = const full =
!input.variant && ag.variant && same !input.variant && ag.variant && same
? yield* provider ? yield* provider.getModel(model.providerID, model.modelID).pipe(Effect.catchDefect(() => Effect.void))
.getModel(model.providerID, model.modelID)
.pipe(Effect.catch(() => Effect.succeed(undefined)))
: undefined : undefined
const variant = input.variant ?? (ag.variant && full?.variants?.[ag.variant] ? ag.variant : undefined) const variant = input.variant ?? (ag.variant && full?.variants?.[ag.variant] ? ag.variant : undefined)
@@ -986,9 +984,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
format: input.format, format: input.format,
} }
yield* Effect.addFinalizer(() => yield* Effect.addFinalizer(() => instruction.clear(info.id))
InstanceState.withALS(() => instruction.clear(info.id)).pipe(Effect.flatMap((x) => x)),
)
type Draft<T> = T extends MessageV2.Part ? Omit<T, "id"> & { id?: string } : never type Draft<T> = T extends MessageV2.Part ? Omit<T, "id"> & { id?: string } : never
const assign = (part: Draft<MessageV2.Part>): MessageV2.Part => ({ const assign = (part: Draft<MessageV2.Part>): MessageV2.Part => ({
@@ -1459,8 +1455,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
model, model,
}) })
const outcome: "break" | "continue" = yield* Effect.onExit( const outcome: "break" | "continue" = yield* Effect.gen(function* () {
Effect.gen(function* () {
const lastUserMsg = msgs.findLast((m) => m.info.role === "user") const lastUserMsg = msgs.findLast((m) => m.info.role === "user")
const bypassAgentCheck = lastUserMsg?.parts.some((p) => p.type === "agent") ?? false const bypassAgentCheck = lastUserMsg?.parts.some((p) => p.type === "agent") ?? false
@@ -1557,12 +1552,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
}) })
} }
return "continue" as const return "continue" as const
}), }).pipe(Effect.ensuring(instruction.clear(handle.message.id)))
Effect.fnUntraced(function* (exit) {
if (Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause)) yield* handle.abort()
yield* InstanceState.withALS(() => instruction.clear(handle.message.id)).pipe(Effect.flatMap((x) => x))
}),
)
if (outcome === "break") break if (outcome === "break") break
continue continue
} }
+1 -3
View File
@@ -67,9 +67,7 @@ export const ReadTool = Tool.defineEffect(
if (item.type === "directory") return item.name + "/" if (item.type === "directory") return item.name + "/"
if (item.type !== "symlink") return item.name if (item.type !== "symlink") return item.name
const target = yield* fs const target = yield* fs.stat(path.join(filepath, item.name)).pipe(Effect.catch(() => Effect.void))
.stat(path.join(filepath, item.name))
.pipe(Effect.catch(() => Effect.succeed(undefined)))
if (target?.type === "Directory") return item.name + "/" if (target?.type === "Directory") return item.name + "/"
return item.name return item.name
}), }),
@@ -139,7 +139,6 @@ function fake(
get message() { get message() {
return msg return msg
}, },
abort: Effect.fn("TestSessionProcessor.abort")(() => Effect.void),
partFromToolCall() { partFromToolCall() {
return { return {
id: PartID.ascending(), id: PartID.ascending(),
@@ -593,9 +593,6 @@ it.live("session.processor effect tests mark pending tools as aborted on cleanup
yield* Fiber.interrupt(run) yield* Fiber.interrupt(run)
const exit = yield* Fiber.await(run) const exit = yield* Fiber.await(run)
if (Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause)) {
yield* handle.abort()
}
const parts = MessageV2.parts(msg.id) const parts = MessageV2.parts(msg.id)
const call = parts.find((part): part is MessageV2.ToolPart => part.type === "tool") const call = parts.find((part): part is MessageV2.ToolPart => part.type === "tool")
@@ -665,9 +662,6 @@ it.live("session.processor effect tests record aborted errors and idle state", (
yield* Fiber.interrupt(run) yield* Fiber.interrupt(run)
const exit = yield* Fiber.await(run) const exit = yield* Fiber.await(run)
if (Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause)) {
yield* handle.abort()
}
yield* Effect.promise(() => seen.promise) yield* Effect.promise(() => seen.promise)
const stored = MessageV2.get({ sessionID: chat.id, messageID: msg.id }) const stored = MessageV2.get({ sessionID: chat.id, messageID: msg.id })
const state = yield* sts.get(chat.id) const state = yield* sts.get(chat.id)