diff --git a/packages/core/src/session/runner/llm.ts b/packages/core/src/session/runner/llm.ts
index 233a4aa4d8..7cc9ea2595 100644
--- a/packages/core/src/session/runner/llm.ts
+++ b/packages/core/src/session/runner/llm.ts
@@ -305,14 +305,7 @@ export const layer = Layer.effect(
const llmFailure = failure instanceof LLMError ? failure : undefined
if (llmFailure && !publisher.hasProviderError()) {
yield* withPublication(publisher.failUnsettledTools("Provider did not return a tool result", true))
- yield* withPublication(
- events.publish(SessionEvent.Step.Failed, {
- sessionID: session.id,
- timestamp: yield* DateTime.now,
- assistantMessageID: yield* publisher.startAssistant(),
- error: { type: "unknown", message: llmFailure.reason.message },
- }),
- )
+ yield* withPublication(publisher.failAssistant(llmFailure.reason.message))
}
if (stream._tag === "Failure" && Cause.hasInterrupts(stream.cause)) yield* FiberSet.clear(toolFibers)
const settled = yield* restore(awaitToolFibers(toolFibers)).pipe(Effect.exit)
@@ -327,6 +320,8 @@ export const layer = Layer.effect(
) {
yield* FiberSet.clear(toolFibers)
yield* withPublication(publisher.failUnsettledTools("Tool execution interrupted"))
+ if (publisher.hasAssistantStarted())
+ yield* withPublication(publisher.failAssistant("Provider turn interrupted"))
}
if (settled._tag === "Failure" && !Cause.hasInterrupts(settled.cause)) {
const failure = Cause.squash(settled.cause)
diff --git a/packages/core/src/session/runner/publish-llm-event.ts b/packages/core/src/session/runner/publish-llm-event.ts
index 5390a26e3b..526fb8b4c1 100644
--- a/packages/core/src/session/runner/publish-llm-event.ts
+++ b/packages/core/src/session/runner/publish-llm-event.ts
@@ -66,6 +66,7 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
const timestamp = DateTime.now
let assistantMessageID: SessionMessage.ID | undefined
let providerFailed = false
+ let assistantSettled = false
const startAssistant = Effect.fnUntraced(function* () {
if (assistantMessageID !== undefined) return assistantMessageID
@@ -156,6 +157,18 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
yield* toolInput.flush()
})
+ const failAssistant = Effect.fnUntraced(function* (error: string) {
+ if (assistantSettled) return
+ yield* flushFragments()
+ assistantSettled = true
+ yield* events.publish(SessionEvent.Step.Failed, {
+ sessionID: input.sessionID,
+ timestamp: yield* timestamp,
+ assistantMessageID: yield* startAssistant(),
+ error: { type: "unknown", message: error },
+ })
+ })
+
const startToolInput = Effect.fnUntraced(function* (event: { readonly id: string; readonly name: string }) {
if (tools.has(event.id)) return yield* Effect.die(`Duplicate tool input start: ${event.id}`)
const assistantMessageID = yield* startAssistant()
@@ -375,6 +388,7 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
}
case "step-finish":
yield* flush()
+ assistantSettled = true
yield* events.publish(SessionEvent.Step.Ended, {
sessionID: input.sessionID,
timestamp: yield* timestamp,
@@ -388,13 +402,7 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
return
case "provider-error":
providerFailed = true
- yield* flush()
- yield* events.publish(SessionEvent.Step.Failed, {
- sessionID: input.sessionID,
- timestamp: yield* timestamp,
- assistantMessageID: yield* startAssistant(),
- error: { type: "unknown", message: event.message },
- })
+ yield* failAssistant(event.message)
return
}
})
@@ -402,6 +410,7 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
return {
publish,
flush,
+ failAssistant,
failUnsettledTools,
hasAssistantStarted: () => assistantMessageID !== undefined,
hasProviderError: () => providerFailed,
diff --git a/packages/core/src/session/runner/to-llm-message.ts b/packages/core/src/session/runner/to-llm-message.ts
index ae36f205b1..0a3d030727 100644
--- a/packages/core/src/session/runner/to-llm-message.ts
+++ b/packages/core/src/session/runner/to-llm-message.ts
@@ -70,24 +70,34 @@ const toolResult = (tool: SessionMessage.AssistantTool, providerMetadata: Provid
const assistant = (message: SessionMessage.Assistant, model: Model) => {
const sameModel =
String(message.model.providerID) === String(model.provider) && String(message.model.id) === String(model.id)
- const content = message.content.flatMap((item): ContentPart[] => {
- if (item.type === "text") return [{ type: "text", text: item.text }]
- if (item.type === "reasoning")
- return sameModel
- ? [{ type: "reasoning", text: item.text, providerMetadata: item.providerMetadata }]
- : item.text.length > 0
- ? [{ type: "text", text: item.text }]
- : []
- const call = toolCall(item, sameModel ? item.provider?.metadata : undefined)
- const result = toolResult(item, sameModel ? (item.provider?.resultMetadata ?? item.provider?.metadata) : undefined)
- return item.provider?.executed === true && result ? [call, result] : [call]
- })
+ const content = message.content
+ .flatMap((item): ContentPart[] => {
+ if (item.type === "text") return [{ type: "text", text: item.text }]
+ if (item.type === "reasoning")
+ return sameModel
+ ? [{ type: "reasoning", text: item.text, providerMetadata: item.providerMetadata }]
+ : item.text.trim().length > 0
+ ? [{ type: "text", text: item.text }]
+ : []
+ const call = toolCall(item, sameModel ? item.provider?.metadata : undefined)
+ const result = toolResult(
+ item,
+ sameModel ? (item.provider?.resultMetadata ?? item.provider?.metadata) : undefined,
+ )
+ return item.provider?.executed === true && result ? [call, result] : [call]
+ })
+ .filter(Message.isMeaningfulAssistantPart)
const results = message.content
.filter((item): item is SessionMessage.AssistantTool => item.type === "tool" && item.provider?.executed !== true)
.map((item) => toolResult(item, sameModel ? (item.provider?.resultMetadata ?? item.provider?.metadata) : undefined))
.filter((message) => message !== undefined)
.map(Message.tool)
- return [Message.make({ id: message.id, role: "assistant", content, metadata: message.metadata }), ...results]
+ return [
+ ...(content.length > 0
+ ? [Message.make({ id: message.id, role: "assistant", content, metadata: message.metadata })]
+ : []),
+ ...results,
+ ]
}
function toLLMMessage(message: SessionMessage.Message, model: Model): Message[] {
diff --git a/packages/core/test/session-runner-message.test.ts b/packages/core/test/session-runner-message.test.ts
index 708fd9e7f8..bc26b86744 100644
--- a/packages/core/test/session-runner-message.test.ts
+++ b/packages/core/test/session-runner-message.test.ts
@@ -14,6 +14,36 @@ const id = (value: string) => SessionMessage.ID.make(`msg_${value}`)
const model = Model.make({ id: "model", provider: "provider", route: OpenAIChat.route })
describe("toLLMMessages", () => {
+ test("omits empty assistant turns while preserving meaningful assistant content", () => {
+ const assistant = (value: string, content: SessionMessage.Assistant["content"]) =>
+ new SessionMessage.Assistant({
+ id: id(value),
+ type: "assistant",
+ agent: "build",
+ model: { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") },
+ content,
+ time: { created, completed: created },
+ })
+ const messages = toLLMMessages(
+ [
+ assistant("empty", []),
+ assistant("empty-text", [new SessionMessage.AssistantText({ type: "text", id: "text-empty", text: "" })]),
+ assistant("text", [new SessionMessage.AssistantText({ type: "text", id: "text", text: "Partial" })]),
+ assistant("reasoning", [
+ new SessionMessage.AssistantReasoning({
+ type: "reasoning",
+ id: "reasoning",
+ text: "",
+ providerMetadata: { anthropic: { signature: "sig_1" } },
+ }),
+ ]),
+ ],
+ model,
+ )
+
+ expect(messages.map((message) => message.id)).toEqual([id("text"), id("reasoning")])
+ })
+
test("maps every top-level V2 Session message type", () => {
const file = new FileAttachment({ uri: "data:image/png;base64,aGVsbG8=", mime: "image/png", name: "hello.png" })
const messages = toLLMMessages(
diff --git a/packages/core/test/session-runner.test.ts b/packages/core/test/session-runner.test.ts
index 6ff969fb62..8f72c047d1 100644
--- a/packages/core/test/session-runner.test.ts
+++ b/packages/core/test/session-runner.test.ts
@@ -547,6 +547,8 @@ const verifyPartialFlushOnInterruption = (kind: FragmentKind) =>
{ type: "user", text: prompt },
{
type: "assistant",
+ finish: "error",
+ error: { type: "unknown", message: "Provider turn interrupted" },
content: [
kind === "tool input"
? { type: "tool", id: fragmentID(kind, "interrupted"), state: { status: "error" } }
diff --git a/packages/llm/src/route/client.ts b/packages/llm/src/route/client.ts
index 5b5bc5ab2d..1ae2af4b3c 100644
--- a/packages/llm/src/route/client.ts
+++ b/packages/llm/src/route/client.ts
@@ -16,6 +16,7 @@ import {
HttpOptions,
LLMRequest,
LLMResponse,
+ Message,
Model,
ModelLimits,
LLMError as LLMErrorClass,
@@ -172,6 +173,18 @@ const resolveRequestOptions = (request: LLMRequest) =>
http: mergeHttpOptions(request.model.route.defaults.http, request.http),
})
+const omitEmptyAssistantMessages = (request: LLMRequest) => {
+ const messages = request.messages.flatMap((message) => {
+ if (message.role !== "assistant") return [message]
+ const content = message.content.filter(Message.isMeaningfulAssistantPart)
+ if (content.length === 0) return []
+ if (content.length === message.content.length) return [message]
+ return [Message.make({ ...message, content })]
+ })
+ if (messages.length === request.messages.length) return request
+ return LLMRequest.update(request, { messages })
+}
+
export interface MakeInput
{
/** Route id used in diagnostics and prepared request metadata. */
readonly id: string
@@ -335,7 +348,7 @@ export function make(
// validated provider body plus transport-private prepared data, but does not
// execute transport.
const compile = Effect.fn("LLM.compile")(function* (request: LLMRequest) {
- const resolved = applyCachePolicy(resolveRequestOptions(request))
+ const resolved = applyCachePolicy(omitEmptyAssistantMessages(resolveRequestOptions(request)))
const route = resolved.model.route
const body = yield* route.body
diff --git a/packages/llm/src/schema/messages.ts b/packages/llm/src/schema/messages.ts
index b160f2d4a4..98202b8d88 100644
--- a/packages/llm/src/schema/messages.ts
+++ b/packages/llm/src/schema/messages.ts
@@ -212,6 +212,16 @@ export namespace Message {
export const text = (value: string): ContentPart => ({ type: "text", text: value })
+ export const isMeaningfulAssistantPart = (part: ContentPart) => {
+ if (part.type === "text") return part.text.trim().length > 0
+ if (part.type !== "reasoning") return true
+ return (
+ part.text.trim().length > 0 ||
+ part.encrypted !== undefined ||
+ (part.providerMetadata !== undefined && Object.keys(part.providerMetadata).length > 0)
+ )
+ }
+
export const content = (input: ContentInput) =>
typeof input === "string" ? [text(input)] : Array.isArray(input) ? [...input] : [input]
diff --git a/packages/llm/test/provider/openai-chat.test.ts b/packages/llm/test/provider/openai-chat.test.ts
index 9966b92e3d..8ee1c05bb3 100644
--- a/packages/llm/test/provider/openai-chat.test.ts
+++ b/packages/llm/test/provider/openai-chat.test.ts
@@ -92,6 +92,49 @@ describe("OpenAI Chat route", () => {
}),
)
+ it.effect("omits empty assistant turns before protocol serialization", () =>
+ Effect.gen(function* () {
+ const prepared = yield* LLMClient.prepare(
+ LLM.request({
+ model,
+ messages: [Message.user("Before"), Message.assistant(""), Message.user("After")],
+ }),
+ )
+
+ expect(prepared.body.messages).toEqual([
+ { role: "user", content: "Before" },
+ { role: "user", content: "After" },
+ ])
+ }),
+ )
+
+ it.effect("removes empty assistant parts while preserving tool calls", () =>
+ Effect.gen(function* () {
+ const prepared = yield* LLMClient.prepare(
+ LLM.request({
+ model,
+ messages: [
+ Message.assistant([
+ { type: "text", text: "" },
+ ToolCallPart.make({ id: "call_1", name: "read", input: { path: "README.md" } }),
+ ]),
+ ],
+ }),
+ )
+
+ expect(prepared.body.messages).toEqual([
+ {
+ role: "assistant",
+ content: "",
+ reasoning_content: undefined,
+ tool_calls: [
+ { id: "call_1", type: "function", function: { name: "read", arguments: '{"path":"README.md"}' } },
+ ],
+ },
+ ])
+ }),
+ )
+
it.effect("maps OpenAI provider options to Chat options", () =>
Effect.gen(function* () {
const prepared = yield* LLMClient.prepare(
diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts
index e459380faa..e2be4771fc 100644
--- a/packages/opencode/src/provider/transform.ts
+++ b/packages/opencode/src/provider/transform.ts
@@ -129,62 +129,25 @@ function normalizeMessages(
}
})
- // Anthropic rejects messages with empty content - filter out empty string messages
- // and remove empty text/reasoning parts from array content
- if (model.api.npm === "@ai-sdk/anthropic") {
- msgs = msgs
- .map((msg) => {
- if (typeof msg.content === "string") {
- if (msg.content === "") return undefined
- return msg
- }
- if (!Array.isArray(msg.content)) return msg
- const filtered = msg.content.filter((part) => {
- if (part.type === "text") {
- return part.text !== ""
- }
- if (part.type === "reasoning") {
- return (
- part.text.trim().length > 0 ||
- part.providerOptions?.anthropic?.signature != null ||
- part.providerOptions?.anthropic?.redactedData != null
- )
- }
- return true
- })
- if (filtered.length === 0) return undefined
- return { ...msg, content: filtered }
+ // Empty assistant turns carry no history and are rejected by several provider APIs.
+ msgs = msgs
+ .map((msg) => {
+ if (msg.role !== "assistant") return msg
+ if (typeof msg.content === "string") return msg.content.trim().length > 0 ? msg : undefined
+ if (!Array.isArray(msg.content)) return msg
+ const content = msg.content.filter((part) => {
+ if (part.type === "text") return part.text.trim().length > 0
+ if (part.type !== "reasoning") return true
+ return (
+ part.text.trim().length > 0 ||
+ (part.providerOptions !== undefined && Object.keys(part.providerOptions).length > 0)
+ )
})
- .filter((msg): msg is ModelMessage => msg !== undefined && msg.content !== "")
- }
-
- // Bedrock specific transforms
- if (model.api.npm === "@ai-sdk/amazon-bedrock") {
- msgs = msgs
- .map((msg) => {
- if (typeof msg.content === "string") {
- if (msg.content === "") return undefined
- return msg
- }
- if (!Array.isArray(msg.content)) return msg
- const filtered = msg.content.filter((part) => {
- if (part.type === "text") {
- return part.text !== ""
- }
- if (part.type === "reasoning") {
- return (
- part.text.trim().length > 0 ||
- part.providerOptions?.bedrock?.signature != null ||
- part.providerOptions?.bedrock?.redactedData != null
- )
- }
- return true
- })
- if (filtered.length === 0) return undefined
- return { ...msg, content: filtered }
- })
- .filter((msg): msg is ModelMessage => msg !== undefined && msg.content !== "")
- }
+ if (content.length === 0) return undefined
+ if (content.length === msg.content.length) return msg
+ return { ...msg, content }
+ })
+ .filter((msg): msg is ModelMessage => msg !== undefined)
if (model.api.id.includes("claude")) {
const scrub = (id: string) => id.replace(/[^a-zA-Z0-9_-]/g, "_")
diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts
index 1590e08903..ef244bee17 100644
--- a/packages/opencode/src/session/message-v2.ts
+++ b/packages/opencode/src/session/message-v2.ts
@@ -255,15 +255,18 @@ export const toModelMessagesEffect = Effect.fnUntraced(function* (
if (msg.info.role === "assistant") {
const differentModel = `${model.providerID}/${model.id}` !== `${msg.info.providerID}/${msg.info.modelID}`
const media: Array<{ mime: string; url: string; filename?: string }> = []
+ const hasSignedReasoning = msg.parts.some((part) => {
+ if (part.type !== "reasoning") return false
+ return part.metadata?.anthropic?.signature != null
+ })
- if (
- msg.info.error &&
- !(
- AbortedError.isInstance(msg.info.error) &&
- msg.parts.some((part) => part.type !== "step-start" && part.type !== "reasoning")
- )
- ) {
- continue
+ if (msg.info.error) {
+ const hasOutput = msg.parts.some((part) => {
+ if (part.type === "text") return part.text.trim().length > 0
+ if (part.type === "reasoning") return part.text.trim().length > 0 || hasSignedReasoning
+ return part.type === "tool"
+ })
+ if (!(AbortedError.isInstance(msg.info.error) && hasOutput)) continue
}
const assistantMessage: UIMessage = {
id: msg.info.id,
@@ -281,10 +284,6 @@ export const toModelMessagesEffect = Effect.fnUntraced(function* (
// here is the only safe replay point we have.
// Use a single space so the separator survives replay without changing
// the neighboring signed reasoning blocks.
- const hasSignedReasoning = msg.parts.some((part) => {
- if (part.type !== "reasoning") return false
- return part.metadata?.anthropic?.signature != null
- })
for (const part of msg.parts) {
if (part.type === "text") {
const text = part.text === "" && hasSignedReasoning ? " " : part.text
diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts
index 2554315908..33dede1816 100644
--- a/packages/opencode/src/session/processor.ts
+++ b/packages/opencode/src/session/processor.ts
@@ -950,6 +950,7 @@ export const layer = Layer.effect(
}
}
ctx.assistantMessage.error = error
+ ctx.assistantMessage.finish = "error"
yield* events.publish(Session.Event.Error, {
sessionID: ctx.assistantMessage.sessionID,
error: ctx.assistantMessage.error,
diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts
index dad796c998..8c5a118567 100644
--- a/packages/opencode/src/session/prompt.ts
+++ b/packages/opencode/src/session/prompt.ts
@@ -1259,6 +1259,7 @@ export const layer = Layer.effect(
providerID: msg.providerID,
aborted: true,
})
+ msg.finish = "error"
msg.time.completed = Date.now()
yield* sessions.updateMessage(msg)
})
diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts
index 5fa530155d..160631b22c 100644
--- a/packages/opencode/test/provider/transform.test.ts
+++ b/packages/opencode/test/provider/transform.test.ts
@@ -2018,7 +2018,7 @@ describe("ProviderTransform.message - anthropic empty content filtering", () =>
expect(result[1].content[0]).toEqual({ type: "text", text: "Answer" })
})
- test("does not filter for non-anthropic providers", () => {
+ test("filters empty assistant content for non-anthropic providers", () => {
const openaiModel = {
...anthropicModel,
providerID: "openai",
@@ -2039,9 +2039,32 @@ describe("ProviderTransform.message - anthropic empty content filtering", () =>
const result = ProviderTransform.message(msgs, openaiModel, {})
- expect(result).toHaveLength(2)
- expect(result[0].content).toBe("")
- expect(result[1].content).toHaveLength(1)
+ expect(result).toStrictEqual([])
+ })
+
+ test("preserves metadata-only reasoning for non-anthropic providers", () => {
+ const openaiModel = {
+ ...anthropicModel,
+ providerID: "openai",
+ api: { id: "gpt-5", url: "https://api.openai.com", npm: "@ai-sdk/openai" },
+ }
+ const reasoning = {
+ type: "reasoning",
+ text: "",
+ providerOptions: { openai: { itemId: "rs_1", reasoningEncryptedContent: "encrypted" } },
+ } as const
+
+ expect(
+ ProviderTransform.message([{ role: "assistant", content: [reasoning] }], openaiModel, {})[0]?.content,
+ ).toContainEqual(
+ expect.objectContaining({
+ type: "reasoning",
+ text: "",
+ providerOptions: expect.objectContaining({
+ openai: expect.objectContaining({ reasoningEncryptedContent: "encrypted" }),
+ }),
+ }),
+ )
})
test("leaves valid anthropic assistant tool ordering unchanged", () => {
diff --git a/packages/opencode/test/session/message-v2.test.ts b/packages/opencode/test/session/message-v2.test.ts
index 1de84c9dd9..27dc30df19 100644
--- a/packages/opencode/test/session/message-v2.test.ts
+++ b/packages/opencode/test/session/message-v2.test.ts
@@ -988,7 +988,7 @@ describe("session.message-v2.toModelMessage", () => {
expect(await MessageV2.toModelMessages(input, model)).toStrictEqual([])
})
- test("includes aborted assistant messages only when they have non-step-start/reasoning content", async () => {
+ test("includes aborted assistant messages only when they have meaningful output", async () => {
const assistantID1 = "m-assistant-1"
const assistantID2 = "m-assistant-2"
@@ -1038,6 +1038,67 @@ describe("session.message-v2.toModelMessage", () => {
{ type: "text", text: "partial answer" },
],
},
+ {
+ role: "assistant",
+ content: [{ type: "reasoning", text: "thinking", providerOptions: undefined }],
+ },
+ ])
+ })
+
+ test("filters aborted assistant messages with only empty text", async () => {
+ const assistantID = "m-assistant"
+ const input: SessionV1.WithParts[] = [
+ {
+ info: assistantInfo(
+ assistantID,
+ "m-parent",
+ new SessionV1.AbortedError({ message: "aborted" }).toObject() as SessionV1.Assistant["error"],
+ ),
+ parts: [
+ {
+ ...basePart(assistantID, "a1"),
+ type: "text",
+ text: "",
+ },
+ ] as SessionV1.Part[],
+ },
+ ]
+
+ expect(await MessageV2.toModelMessages(input, model)).toStrictEqual([])
+ })
+
+ test("preserves aborted assistant messages with signed reasoning", async () => {
+ const assistantID = "m-assistant"
+ const input: SessionV1.WithParts[] = [
+ {
+ info: assistantInfo(
+ assistantID,
+ "m-parent",
+ new SessionV1.AbortedError({ message: "aborted" }).toObject() as SessionV1.Assistant["error"],
+ ),
+ parts: [
+ {
+ ...basePart(assistantID, "a1"),
+ type: "reasoning",
+ text: "",
+ time: { start: 0 },
+ metadata: { anthropic: { signature: "sig_1" } },
+ },
+ ] as SessionV1.Part[],
+ },
+ ]
+
+ expect(await MessageV2.toModelMessages(input, model)).toStrictEqual([
+ {
+ role: "assistant",
+ content: [
+ {
+ type: "reasoning",
+ text: "",
+ providerOptions: { anthropic: { signature: "sig_1" } },
+ },
+ ],
+ },
])
})
diff --git a/packages/opencode/test/session/processor-effect.test.ts b/packages/opencode/test/session/processor-effect.test.ts
index c8f40d0de1..4581d291d8 100644
--- a/packages/opencode/test/session/processor-effect.test.ts
+++ b/packages/opencode/test/session/processor-effect.test.ts
@@ -899,9 +899,13 @@ it.live("session.processor effect tests record aborted errors and idle state", (
expect(Cause.hasInterruptsOnly(exit.cause)).toBe(true)
}
expect(handle.message.error?.name).toBe("MessageAbortedError")
+ expect(handle.message.finish).toBe("error")
+ expect(handle.message.time.completed).toBeNumber()
expect(stored.info.role).toBe("assistant")
if (stored.info.role === "assistant") {
expect(stored.info.error?.name).toBe("MessageAbortedError")
+ expect(stored.info.finish).toBe("error")
+ expect(stored.info.time.completed).toBeNumber()
}
expect(state).toMatchObject({ type: "idle" })
expect(errs).toContain("MessageAbortedError")
@@ -957,9 +961,13 @@ it.live("session.processor effect tests mark interruptions aborted without manua
expect(Exit.isFailure(exit)).toBe(true)
expect(handle.message.error?.name).toBe("MessageAbortedError")
+ expect(handle.message.finish).toBe("error")
+ expect(handle.message.time.completed).toBeNumber()
expect(stored.info.role).toBe("assistant")
if (stored.info.role === "assistant") {
expect(stored.info.error?.name).toBe("MessageAbortedError")
+ expect(stored.info.finish).toBe("error")
+ expect(stored.info.time.completed).toBeNumber()
}
expect(state).toMatchObject({ type: "idle" })
}),
diff --git a/packages/opencode/test/session/prompt.test.ts b/packages/opencode/test/session/prompt.test.ts
index 5cd97f78e8..7e7e63cfdc 100644
--- a/packages/opencode/test/session/prompt.test.ts
+++ b/packages/opencode/test/session/prompt.test.ts
@@ -1082,7 +1082,7 @@ raceNoLLMServer.instance(
expect(firstInterrupted?.info.role).toBe("assistant")
expect(firstInterrupted?.parts).toHaveLength(0)
if (firstInterrupted?.info.role === "assistant") {
- expect(firstInterrupted.info.finish).toBeUndefined()
+ expect(firstInterrupted.info.finish).toBe("error")
expect(firstInterrupted.info.time.completed).toBeNumber()
expect(firstInterrupted.info.error?.name).toBe("MessageAbortedError")
}