fix!: rename chat stream silence timeout error (#25973)

Renames the Agents chat stream-silence error from `startup_timeout` to
`stream_silence_timeout` now that the timeout applies to any gap between
provider stream parts, not just first-token startup.

Updates the SDK enum, generated API docs/types, chat error copy, and
Agents UI stories/status labels so the user-facing wording describes a
stalled provider response instead of startup delay.

> **Breaking change:** This is a very minor breaking change for the
Coder Agents API: the public chat error kind enum no longer includes
`startup_timeout`, so clients matching that specific value should handle
`stream_silence_timeout` instead.
This commit is contained in:
Ethan
2026-06-04 18:36:02 +10:00
committed by GitHub
parent b7635b5036
commit 3ab1323bc9
15 changed files with 65 additions and 65 deletions
+2 -2
View File
@@ -16723,7 +16723,7 @@ const docTemplate = `{
"overloaded",
"rate_limit",
"timeout",
"startup_timeout",
"stream_silence_timeout",
"auth",
"config",
"usage_limit",
@@ -16735,7 +16735,7 @@ const docTemplate = `{
"ChatErrorKindOverloaded",
"ChatErrorKindRateLimit",
"ChatErrorKindTimeout",
"ChatErrorKindStartupTimeout",
"ChatErrorKindStreamSilenceTimeout",
"ChatErrorKindAuth",
"ChatErrorKindConfig",
"ChatErrorKindUsageLimit",
+2 -2
View File
@@ -15049,7 +15049,7 @@
"overloaded",
"rate_limit",
"timeout",
"startup_timeout",
"stream_silence_timeout",
"auth",
"config",
"usage_limit",
@@ -15061,7 +15061,7 @@
"ChatErrorKindOverloaded",
"ChatErrorKindRateLimit",
"ChatErrorKindTimeout",
"ChatErrorKindStartupTimeout",
"ChatErrorKindStreamSilenceTimeout",
"ChatErrorKindAuth",
"ChatErrorKindConfig",
"ChatErrorKindUsageLimit",
+4 -4
View File
@@ -981,21 +981,21 @@ func TestClassify_StatusCodeBeatsHTTP2Transport(t *testing.T) {
}
}
func TestClassify_StartupTimeoutWrappedClassificationWins(t *testing.T) {
func TestClassify_StreamSilenceTimeoutWrappedClassificationWins(t *testing.T) {
t.Parallel()
wrapped := chaterror.WithClassification(
xerrors.New("context canceled"),
chaterror.ClassifiedError{
Kind: codersdk.ChatErrorKindStartupTimeout,
Kind: codersdk.ChatErrorKindStreamSilenceTimeout,
Provider: "openai",
Retryable: true,
},
)
require.Equal(t, chaterror.ClassifiedError{
Message: "OpenAI did not start responding in time.",
Kind: codersdk.ChatErrorKindStartupTimeout,
Message: "OpenAI did not send response data in time.",
Kind: codersdk.ChatErrorKindStreamSilenceTimeout,
Provider: "openai",
Retryable: true,
StatusCode: 0,
+4 -4
View File
@@ -28,9 +28,9 @@ func terminalMessage(classified ClassifiedError) string {
}
return stringutil.Capitalize(fmt.Sprintf("%s is temporarily unavailable.", subject))
case codersdk.ChatErrorKindStartupTimeout:
case codersdk.ChatErrorKindStreamSilenceTimeout:
return stringutil.Capitalize(fmt.Sprintf(
"%s did not start responding in time.", subject,
"%s did not send response data in time.", subject,
))
case codersdk.ChatErrorKindUsageLimit:
@@ -89,9 +89,9 @@ func retryMessage(classified ClassifiedError) string {
return stringutil.Capitalize(fmt.Sprintf("%s is rate limiting requests.", subject))
case codersdk.ChatErrorKindTimeout:
return stringutil.Capitalize(fmt.Sprintf("%s is temporarily unavailable.", subject))
case codersdk.ChatErrorKindStartupTimeout:
case codersdk.ChatErrorKindStreamSilenceTimeout:
return stringutil.Capitalize(fmt.Sprintf(
"%s did not start responding in time.", subject,
"%s did not send response data in time.", subject,
))
case codersdk.ChatErrorKindAuth:
return fmt.Sprintf(
+7 -7
View File
@@ -11,7 +11,7 @@ import (
)
// TestTerminalMessage covers the per-provider "temporarily
// unavailable" copy, the startup-timeout copy, and the generic
// unavailable" copy, the stream-silence timeout copy, and the generic
// fallback string for its intended (unclassified, non-retryable)
// path.
func TestTerminalMessage(t *testing.T) {
@@ -54,18 +54,18 @@ func TestTerminalMessage(t *testing.T) {
want: "The request timed out before it completed.",
},
{
name: "StartupTimeout_Anthropic",
kind: codersdk.ChatErrorKindStartupTimeout,
name: "StreamSilenceTimeout_Anthropic",
kind: codersdk.ChatErrorKindStreamSilenceTimeout,
provider: "anthropic",
retryable: true,
want: "Anthropic did not start responding in time.",
want: "Anthropic did not send response data in time.",
},
{
name: "StartupTimeout_OpenAI",
kind: codersdk.ChatErrorKindStartupTimeout,
name: "StreamSilenceTimeout_OpenAI",
kind: codersdk.ChatErrorKindStreamSilenceTimeout,
provider: "openai",
retryable: true,
want: "OpenAI did not start responding in time.",
want: "OpenAI did not send response data in time.",
},
{
// Generic fallback reserved for genuinely
+1 -1
View File
@@ -867,7 +867,7 @@ func classifyStreamSilenceTimeout(
err = errStreamSilenceTimeout
}
return chaterror.WithClassification(err, chaterror.ClassifiedError{
Kind: codersdk.ChatErrorKindStartupTimeout,
Kind: codersdk.ChatErrorKindStreamSilenceTimeout,
Provider: provider,
Retryable: true,
})
@@ -700,12 +700,12 @@ func TestRun_RetriesSilenceTimeoutWhileOpeningStream(t *testing.T) {
require.NoError(t, awaitRunResult(ctx, t, done))
require.Equal(t, 2, attempts)
require.Len(t, retries, 1)
require.Equal(t, codersdk.ChatErrorKindStartupTimeout, retries[0].Kind)
require.Equal(t, codersdk.ChatErrorKindStreamSilenceTimeout, retries[0].Kind)
require.True(t, retries[0].Retryable)
require.Equal(t, "openai", retries[0].Provider)
require.Equal(
t,
"OpenAI did not start responding in time.",
"OpenAI did not send response data in time.",
retries[0].Message,
)
select {
@@ -930,12 +930,12 @@ func TestRun_RetriesSilenceTimeoutBeforeFirstPart(t *testing.T) {
require.NoError(t, awaitRunResult(ctx, t, done))
require.Equal(t, 2, attempts)
require.Len(t, retries, 1)
require.Equal(t, codersdk.ChatErrorKindStartupTimeout, retries[0].Kind)
require.Equal(t, codersdk.ChatErrorKindStreamSilenceTimeout, retries[0].Kind)
require.True(t, retries[0].Retryable)
require.Equal(t, "openai", retries[0].Provider)
require.Equal(
t,
"OpenAI did not start responding in time.",
"OpenAI did not send response data in time.",
retries[0].Message,
)
select {
@@ -1161,7 +1161,7 @@ func TestRun_RetriesSilenceTimeoutBetweenParts(t *testing.T) {
require.NoError(t, awaitRunResult(ctx, t, done))
require.Equal(t, 2, attempts)
require.Len(t, retries, 1)
require.Equal(t, codersdk.ChatErrorKindStartupTimeout, retries[0].Kind)
require.Equal(t, codersdk.ChatErrorKindStreamSilenceTimeout, retries[0].Kind)
require.True(t, retries[0].Retryable)
require.Equal(t, "openai", retries[0].Provider)
select {
@@ -1278,12 +1278,12 @@ func TestRun_RetriesSilenceTimeoutWhenStreamStaysSilent(t *testing.T) {
require.NoError(t, awaitRunResult(ctx, t, done))
require.Equal(t, 2, attempts)
require.Len(t, retries, 1)
require.Equal(t, codersdk.ChatErrorKindStartupTimeout, retries[0].Kind)
require.Equal(t, codersdk.ChatErrorKindStreamSilenceTimeout, retries[0].Kind)
require.True(t, retries[0].Retryable)
require.Equal(t, "openai", retries[0].Provider)
require.Equal(
t,
"OpenAI did not start responding in time.",
"OpenAI did not send response data in time.",
retries[0].Message,
)
select {
+1 -1
View File
@@ -293,7 +293,7 @@ func TestRecordStreamRetry(t *testing.T) {
{name: "overloaded", kind: codersdk.ChatErrorKindOverloaded},
{name: "rate_limit", kind: codersdk.ChatErrorKindRateLimit},
{name: "timeout", kind: codersdk.ChatErrorKindTimeout},
{name: "startup_timeout", kind: codersdk.ChatErrorKindStartupTimeout},
{name: "stream_silence_timeout", kind: codersdk.ChatErrorKindStreamSilenceTimeout},
{name: "auth", kind: codersdk.ChatErrorKindAuth},
{name: "config", kind: codersdk.ChatErrorKindConfig},
{name: "missing_key", kind: codersdk.ChatErrorKindMissingKey},
+11 -11
View File
@@ -1525,16 +1525,16 @@ type ChatStreamStatus struct {
type ChatErrorKind string
const (
ChatErrorKindGeneric ChatErrorKind = "generic"
ChatErrorKindOverloaded ChatErrorKind = "overloaded"
ChatErrorKindRateLimit ChatErrorKind = "rate_limit"
ChatErrorKindTimeout ChatErrorKind = "timeout"
ChatErrorKindStartupTimeout ChatErrorKind = "startup_timeout"
ChatErrorKindAuth ChatErrorKind = "auth"
ChatErrorKindConfig ChatErrorKind = "config"
ChatErrorKindUsageLimit ChatErrorKind = "usage_limit"
ChatErrorKindMissingKey ChatErrorKind = "missing_key"
ChatErrorKindProviderDisabled ChatErrorKind = "provider_disabled"
ChatErrorKindGeneric ChatErrorKind = "generic"
ChatErrorKindOverloaded ChatErrorKind = "overloaded"
ChatErrorKindRateLimit ChatErrorKind = "rate_limit"
ChatErrorKindTimeout ChatErrorKind = "timeout"
ChatErrorKindStreamSilenceTimeout ChatErrorKind = "stream_silence_timeout"
ChatErrorKindAuth ChatErrorKind = "auth"
ChatErrorKindConfig ChatErrorKind = "config"
ChatErrorKindUsageLimit ChatErrorKind = "usage_limit"
ChatErrorKindMissingKey ChatErrorKind = "missing_key"
ChatErrorKindProviderDisabled ChatErrorKind = "provider_disabled"
)
// AllChatErrorKinds contains every ChatErrorKind value.
@@ -1544,7 +1544,7 @@ var AllChatErrorKinds = []ChatErrorKind{
ChatErrorKindOverloaded,
ChatErrorKindRateLimit,
ChatErrorKindTimeout,
ChatErrorKindStartupTimeout,
ChatErrorKindStreamSilenceTimeout,
ChatErrorKindAuth,
ChatErrorKindConfig,
ChatErrorKindUsageLimit,
+7 -7
View File
@@ -292,13 +292,13 @@ Status Code **200**
#### Enumerated Values
| Property | Value(s) |
|---------------|------------------------------------------------------------------------------------------------------------------------------------------|
| `client_type` | `api`, `ui` |
| `kind` | `auth`, `config`, `generic`, `missing_key`, `overloaded`, `provider_disabled`, `rate_limit`, `startup_timeout`, `timeout`, `usage_limit` |
| `type` | `context-file`, `file`, `file-reference`, `reasoning`, `skill`, `source`, `text`, `tool-call`, `tool-result` |
| `plan_mode` | `plan` |
| `status` | `completed`, `error`, `paused`, `pending`, `requires_action`, `running`, `waiting` |
| Property | Value(s) |
|---------------|-------------------------------------------------------------------------------------------------------------------------------------------------|
| `client_type` | `api`, `ui` |
| `kind` | `auth`, `config`, `generic`, `missing_key`, `overloaded`, `provider_disabled`, `rate_limit`, `stream_silence_timeout`, `timeout`, `usage_limit` |
| `type` | `context-file`, `file`, `file-reference`, `reasoning`, `skill`, `source`, `text`, `tool-call`, `tool-result` |
| `plan_mode` | `plan` |
| `status` | `completed`, `error`, `paused`, `pending`, `requires_action`, `running`, `waiting` |
To perform this operation, you must be authenticated. [Learn more](authentication.md).
+3 -3
View File
@@ -2703,9 +2703,9 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
#### Enumerated Values
| Value(s) |
|------------------------------------------------------------------------------------------------------------------------------------------|
| `auth`, `config`, `generic`, `missing_key`, `overloaded`, `provider_disabled`, `rate_limit`, `startup_timeout`, `timeout`, `usage_limit` |
| Value(s) |
|-------------------------------------------------------------------------------------------------------------------------------------------------|
| `auth`, `config`, `generic`, `missing_key`, `overloaded`, `provider_disabled`, `rate_limit`, `stream_silence_timeout`, `timeout`, `usage_limit` |
## codersdk.ChatFileMetadata
+2 -2
View File
@@ -1992,7 +1992,7 @@ export type ChatErrorKind =
| "overloaded"
| "provider_disabled"
| "rate_limit"
| "startup_timeout"
| "stream_silence_timeout"
| "timeout"
| "usage_limit";
@@ -2004,7 +2004,7 @@ export const ChatErrorKinds: ChatErrorKind[] = [
"overloaded",
"provider_disabled",
"rate_limit",
"startup_timeout",
"stream_silence_timeout",
"timeout",
"usage_limit",
];
@@ -257,14 +257,14 @@ export const RetryingTimeoutAnthropic: Story = {
},
};
/** Terminal startup timeouts get a specific heading without provider metadata. */
export const TerminalStartupTimeoutError: Story = {
/** Terminal stream-silence timeouts get a specific heading without provider metadata. */
export const TerminalStreamSilenceTimeoutError: Story = {
args: {
...defaultArgs,
liveStatus: buildLiveStatus({
persistedError: {
kind: "startup_timeout",
message: "Anthropic did not start responding in time.",
kind: "stream_silence_timeout",
message: "Anthropic did not send response data in time.",
provider: "anthropic",
retryable: true,
},
@@ -273,10 +273,10 @@ export const TerminalStartupTimeoutError: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(
canvas.getByRole("heading", { name: /startup timed out/i }),
canvas.getByRole("heading", { name: /response stalled/i }),
).toBeVisible();
expect(
canvas.getByText(/anthropic did not start responding in time./i),
canvas.getByText(/anthropic did not send response data in time./i),
).toBeVisible();
expect(canvas.queryByText(/please try again/i)).not.toBeInTheDocument();
expect(canvas.queryByText(/^retryable$/i)).not.toBeInTheDocument();
@@ -225,15 +225,15 @@ export const RetryTimeout: Story = {
},
};
/** Startup timeouts explain the first-token delay before retrying. */
export const RetryStartupTimeout: Story = {
/** Stream-silence timeouts explain the first-token delay before retrying. */
export const RetryStreamSilenceTimeout: Story = {
args: {
streamState: null,
streamTools: [],
liveStatus: buildLiveStatus({
retryState: buildRetryState({
kind: "startup_timeout",
error: "Anthropic did not start responding in time.",
kind: "stream_silence_timeout",
error: "Anthropic did not send response data in time.",
}),
isAwaitingFirstStreamChunk: true,
}),
@@ -241,10 +241,10 @@ export const RetryStartupTimeout: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(
canvas.getByRole("heading", { name: /startup timed out/i }),
canvas.getByRole("heading", { name: /response stalled/i }),
).toBeVisible();
expect(
canvas.getByText(/anthropic did not start responding in time/i),
canvas.getByText(/anthropic did not send response data in time/i),
).toBeVisible();
expect(canvas.queryByText(/please try again/i)).not.toBeInTheDocument();
expect(canvas.queryByText(/provider anthropic/i)).not.toBeInTheDocument();
@@ -34,8 +34,8 @@ export const getErrorTitle = (
return "Rate limited";
case "timeout":
return "Request timed out";
case "startup_timeout":
return "Startup timed out";
case "stream_silence_timeout":
return "Response stalled";
case "auth":
return "Authentication failed";
case "config":