Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
bbc3ae4
dzianisv:issue-17717-retry-unexpected-aborts
SoShymKing May 26, 2026
e76082c
Merge branch 'agent_issue-17717' into ws
SoShymKing May 26, 2026
ee4e063
no response handling
SoShymKing May 26, 2026
da395f2
fix empty SSE stream
SoShymKing May 27, 2026
2a3abb3
change main retry to 10
SoShymKing May 27, 2026
7fdf340
Merge branch 'dev' into robust-no-response-handling
SoShymKing May 30, 2026
e4f5e3c
Merge branch 'dev' into robust-no-response-handling
SoShymKing May 30, 2026
3029d15
Merge remote-tracking branch 'upstream/dev' into agent_robust-no-resp…
SoShymKing Jun 5, 2026
38e4648
Merge branch 'robust-no-response-handling' of 'dev'
SoShymKing Jun 5, 2026
5ed52d5
Merge branch 'dev' into robust-no-response-handling
SoShymKing Jun 5, 2026
760fe35
Merge branch 'anomalyco:dev' into robust-no-response-handling
SoShymKing Jun 6, 2026
97a7a5c
Merge remote-tracking branch 'upstream/dev' into robust-no-response-h…
SoShymKing Jun 10, 2026
4a5b031
fix merge conflict
SoShymKing Jun 10, 2026
6f8213d
fix merge conflict
SoShymKing Jun 10, 2026
33b4445
Merge branch 'robust-no-response-handling' of https://github.com/SoSh…
SoShymKing Jun 10, 2026
45c2a2a
Merge commit '10b6672be1dae17267e85bffb1c14992a60c19c8' into robust-n…
SoShymKing Jun 18, 2026
72834a5
Merge branch 'dev' into robust-no-response-handling
SoShymKing Jun 18, 2026
5d375cf
Merge remote-tracking branch 'origin/dev' into robust-no-response-han…
SoShymKing Jun 21, 2026
e3bec98
Merge remote-tracking branch 'upstream/dev' into robust-no-response-h…
SoShymKing Jun 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 68 additions & 1 deletion packages/core/src/v1/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,70 @@ export const AuthError = NamedError.create("ProviderAuthError", {
message: Schema.String,
})

export const AbortedError = NamedError.create("MessageAbortedError", { message: Schema.String })
export const AbortSource = Schema.Literals([
"user_cancel",
"session_cancel",
"provider_abort",
"network_abort",
"first_byte_timeout",
"stream_idle_timeout",
"post_tool_first_event_timeout",
"no_visible_part_timeout",
"server_restart",
"client_disconnect",
"unknown",
])
export type AbortSource = Schema.Schema.Type<typeof AbortSource>

export const RequestPhase = Schema.Literals([
"model_stream",
"post_tool_continuation",
"message_finalization",
"unknown",
])
export type RequestPhase = Schema.Schema.Type<typeof RequestPhase>

export const NoResponseDiagnostics = Schema.Struct({
providerID: Schema.optional(ProviderV2.ID),
modelID: Schema.optional(ModelV2.ID),
sessionID: Schema.optional(SessionSchema.ID),
messageID: Schema.optional(MessageID),
elapsedMs: Schema.optional(NonNegativeInt),
isPostToolContinuation: Schema.optional(Schema.Boolean),
retryAttempt: Schema.optional(NonNegativeInt),
firstStreamEventAt: Schema.optional(NonNegativeInt),
lastStreamEventAt: Schema.optional(NonNegativeInt),
firstVisiblePartAt: Schema.optional(NonNegativeInt),
lastVisiblePartAt: Schema.optional(NonNegativeInt),
partCount: Schema.optional(NonNegativeInt),
tokenCount: Schema.optional(NonNegativeInt),
})
export type NoResponseDiagnostics = Schema.Schema.Type<typeof NoResponseDiagnostics>

const NoResponseErrorData = {
message: Schema.String,
abortSource: AbortSource,
phase: RequestPhase,
retryable: Schema.Boolean,
diagnostics: Schema.optional(NoResponseDiagnostics),
}

export const AbortedError = NamedError.create("MessageAbortedError", {
message: Schema.String,
abortSource: Schema.optional(AbortSource),
})
export const UnexpectedProviderAbortError = NamedError.create("UnexpectedProviderAbortError", {
...NoResponseErrorData,
})
export const PostToolContinuationTimeoutError = NamedError.create("PostToolContinuationTimeoutError", {
...NoResponseErrorData,
})
export const EmptyAssistantResponseError = NamedError.create("EmptyAssistantResponseError", {
...NoResponseErrorData,
})
export const NoResponseError = NamedError.create("NoResponseError", {
...NoResponseErrorData,
})
export const StructuredOutputError = NamedError.create("StructuredOutputError", {
message: Schema.String,
retries: NonNegativeInt,
Expand Down Expand Up @@ -389,6 +452,10 @@ const AssistantErrorSchema = Schema.Union([
NamedError.Unknown.EffectSchema,
OutputLengthError.EffectSchema,
AbortedError.EffectSchema,
UnexpectedProviderAbortError.EffectSchema,
PostToolContinuationTimeoutError.EffectSchema,
EmptyAssistantResponseError.EffectSchema,
NoResponseError.EffectSchema,
StructuredOutputError.EffectSchema,
ContextOverflowError.EffectSchema,
ContentFilterError.EffectSchema,
Expand Down
4 changes: 4 additions & 0 deletions packages/opencode/src/session/llm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ export type StreamInput = {
tools: Record<string, Tool>
retries?: number
toolChoice?: "auto" | "required" | "none"
internal?: {
postToolContinuation?: boolean
postToolFirstEventTimeoutMs?: number
}
}

export type StreamRequest = StreamInput & {
Expand Down
70 changes: 65 additions & 5 deletions packages/opencode/src/session/message-v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,23 @@ import { SessionV1 } from "@opencode-ai/core/v1/session"
import { ProviderV2 } from "@opencode-ai/core/provider"
import {
APIError,
AbortSource,
AbortedError,
Assistant,
AuthError,
CompactionPart,
ContextOverflowError,
EmptyAssistantResponseError,
Info,
NoResponseDiagnostics,
NoResponseError,
OutputLengthError,
Part,
PostToolContinuationTimeoutError,
RequestPhase,
StructuredOutputError,
SubtaskPart,
UnexpectedProviderAbortError,
User,
WithParts,
type ToolPart,
Expand Down Expand Up @@ -50,6 +57,29 @@ interface FetchDecompressionError extends Error {

export const SYNTHETIC_ATTACHMENT_PROMPT = "Attached media from tool result:"
export { isMedia }
export {
APIError,
AbortSource,
AbortedError,
Assistant,
AuthError,
CompactionPart,
ContextOverflowError,
EmptyAssistantResponseError,
Info,
NoResponseDiagnostics,
NoResponseError,
OutputLengthError,
Part,
PostToolContinuationTimeoutError,
RequestPhase,
StructuredOutputError,
SubtaskPart,
UnexpectedProviderAbortError,
User,
WithParts,
}
export type { ToolPart }

function truncateToolOutput(text: string, maxChars?: number) {
if (!maxChars || text.length <= maxChars) return text
Expand Down Expand Up @@ -262,7 +292,7 @@ export const toModelMessagesEffect = Effect.fnUntraced(function* (
if (
msg.info.error &&
!(
AbortedError.isInstance(msg.info.error) &&
(AbortedError.isInstance(msg.info.error) || UnexpectedProviderAbortError.isInstance(msg.info.error)) &&
msg.parts.some((part) => part.type !== "step-start" && part.type !== "reasoning")
)
) {
Expand Down Expand Up @@ -616,12 +646,39 @@ export function latest(msgs: WithParts[]) {

export function fromError(
e: unknown,
ctx: { providerID: ProviderV2.ID; aborted?: boolean },
ctx: {
providerID: ProviderV2.ID
aborted?: boolean
abortSource?: AbortSource
phase?: RequestPhase
diagnostics?: NoResponseDiagnostics
},
): NonNullable<Assistant["error"]> {
switch (true) {
case UnexpectedProviderAbortError.isInstance(e):
case PostToolContinuationTimeoutError.isInstance(e):
case EmptyAssistantResponseError.isInstance(e):
case NoResponseError.isInstance(e):
return e instanceof NamedError
? (e.toObject() as NonNullable<Assistant["error"]>)
: (e as NonNullable<Assistant["error"]>)
case e instanceof DOMException && e.name === "AbortError":
return new AbortedError(
{ message: e.message },
if (ctx.aborted || ctx.abortSource === "user_cancel" || ctx.abortSource === "session_cancel") {
return new AbortedError(
{ message: e.message, abortSource: ctx.abortSource ?? "user_cancel" },
{
cause: e,
},
).toObject()
}
return new UnexpectedProviderAbortError(
{
message: e.message,
abortSource: ctx.abortSource ?? "provider_abort",
phase: ctx.phase ?? "model_stream",
retryable: true,
diagnostics: ctx.diagnostics,
},
{
cause: e,
},
Expand Down Expand Up @@ -651,7 +708,10 @@ export function fromError(
).toObject()
case e instanceof Error && (e as FetchDecompressionError).code === "ZlibError":
if (ctx.aborted) {
return new AbortedError({ message: e.message }, { cause: e }).toObject()
return new AbortedError(
{ message: e.message, abortSource: ctx.abortSource ?? "user_cancel" },
{ cause: e },
).toObject()
}
return new APIError(
{
Expand Down
Loading
Loading