Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions apps/desktop/src/features/chat/transcript/ActivityGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,11 @@ export function RunActivityIndicator({ activity }: { activity: AgentActivity })
<strong>{retryErrorSummary}</strong>
<code>
{retryError.code}
{/* The transport errno names the failing layer (ENOTFOUND, a
TLS code, a dropped socket) while the localized summary
cannot; it is a technical token in the same style as the
code beside it, so it needs no translation (issue #234). */}
{retryError.networkCode ? ` · ${retryError.networkCode}` : ""}
{retryError.providerStatus !== undefined
? ` · HTTP ${retryError.providerStatus}`
: ""}
Expand Down
14 changes: 13 additions & 1 deletion apps/desktop/src/features/chat/transcript/shared.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,15 @@ export function AssistantErrorMessage({ message }: { message: UiMessage }) {
"PROVIDER_SECRET_MISSING",
"PROVIDER_UNAUTHORIZED",
].includes(error.code);
// The transport errno is what separates "DNS did not resolve" from "TLS was
// rejected" from "the socket died" for the user; the localized summary can
// only say "can't reach the provider" (issue #234).
const networkCode = (() => {
const details = error.details;
if (!details || typeof details !== "object") return undefined;
const value = (details as { networkCode?: unknown }).networkCode;
return typeof value === "string" ? value : undefined;
})();

return (
<section className="message-error" aria-label={t("chat.responseError")}>
Expand All @@ -158,7 +167,10 @@ export function AssistantErrorMessage({ message }: { message: UiMessage }) {
</span>
<div className="message-error-copy">
<strong>{summary}</strong>
<code>{error.code}</code>
<code>
{error.code}
{networkCode ? ` · ${networkCode}` : ""}
</code>
</div>
<div className="message-error-actions">
<button
Expand Down
18 changes: 18 additions & 0 deletions apps/desktop/test/chat-error-message.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,22 @@ test("assistant error messages expose readable provider details and one Continue
assert.match(component, /errors\.action\.continue/);
assert.match(component, /chat\.continueCurrentTaskPrompt/);
assert.match(component, /setSettingsTab\("agent"\)/);

// Issue #234: the localized NETWORK_ERROR summary cannot tell DNS from TLS from
// a dropped socket, so both failure surfaces render the transport errno next to
// the stable code.
test("network failures show the transport errno beside the error code", async () => {
const [transcript, activityGroup] = await Promise.all([
readTranscriptSource(),
read("src/features/chat/transcript/ActivityGroup.tsx"),
]);
const card = transcript.slice(
transcript.indexOf("function AssistantErrorMessage"),
transcript.indexOf("const TOOL_ACTION_KEYS"),
);

assert.match(card, /error\.details/);
assert.match(card, /networkCode/);
assert.match(activityGroup, /retryError\.networkCode/);
});
});
10 changes: 7 additions & 3 deletions docs/spec/03-runtime/01-ipc-protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -734,9 +734,13 @@ context. Manual compaction never silently falls back.

Provider `error` events may include bounded diagnostic fields in
`AppError.details`: `phase` (`request` or `stream`), `providerStatus`,
`providerCode`, `providerWaitMs`, `streamMs`, and `retryAttempt`. These fields
are additive and redacted; they never carry credentials or an unrestricted
provider response. A transient stream failure may be replayed once inside the
`providerCode`, `providerWaitMs`, `streamMs`, `retryAttempt`, and, for a
network failure, `networkCategory`, `networkCode`, `networkSyscall` and
`networkHost` plus the request correlation fields `requestMessages`,
`requestBytes` and `compactionGeneration`. These fields are additive and
redacted; they never carry credentials or an unrestricted provider response,
and the request fields are counts and byte sizes only. A transient stream
failure may be replayed once inside the
same turn without a terminal `error` event or a duplicate assistant message.
The second failure emits the terminal normalized `STREAM_FAILED` error.

Expand Down
7 changes: 5 additions & 2 deletions docs/spec/03-runtime/02-agent-runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,8 +227,11 @@ codes, budget size, and precedence.
When the retry budget is exhausted, the final assistant error and lifecycle
`error` are emitted once. Provider failures carry bounded diagnostics in
`AppError.details` when available: `phase` (`request` or `stream`),
`providerStatus`, `providerCode`, `providerWaitMs`, `streamMs`, and
`retryAttempt`. For a persistent 429 or non-429 transient failure,
`providerStatus`, `providerCode`, `providerWaitMs`, `streamMs`,
`retryAttempt`, the network diagnosis (`networkCategory`, `networkCode`,
`networkSyscall`, `networkHost`) and the request correlation fields
(`requestMessages`, `requestBytes`, `compactionGeneration`). For a persistent
429 or non-429 transient failure,
`retryAttempt` is `10`. Credentials and unrestricted response bodies never
enter the event or log. The active-turn status shows the remaining backoff and
the retry budget as `Retrying in 0s · attempt 9/10` in English.
Expand Down
23 changes: 21 additions & 2 deletions docs/spec/03-runtime/08-error-codes.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,17 @@ request is replayed; the session and its tool state are untouched. A
non-retryable `PROVIDER_ERROR` from a
malformed 400/422 request never enters either budget.

A `NETWORK_ERROR` carries the failing transport layer as bounded `details`:
`networkCategory` (`dns`, `tls`, `timeout`, `refused`, `unreachable`, `reset`,
`proxy`, or `unknown` when nothing survived), `networkCode` (the errno, e.g.
`ENOTFOUND`, `ECONNRESET`, `EPROTO`, `UND_ERR_SOCKET`), and, when the transport
reported them, `networkSyscall` and `networkHost`. Only the bare hostname is
kept — never a URL, port, path, query, or credential — and `providerCode` is
omitted when it would repeat `networkCode`. Per-layer codes (`DNS_ERROR`,
`TLS_ERROR`, `SOCKET_RESET`, …) are deliberately not introduced: the category
splits the layers without adding user-visible codes and locale strings for
each of them.

### Permission timeout
UI/host timeout emits `PERMISSION_TIMEOUT` internally, tool result presented as denied (`TOOL_DENIED`) to agent.

Expand Down Expand Up @@ -328,8 +339,16 @@ with an accessible details disclosure containing the redacted provider response,
provider ID, and model ID. Provider detail is capped at 600 characters and
common credential/header values are redacted before event emission or
persistence. When available, the details disclosure may also show bounded
`phase`, `providerStatus`, `providerCode`, `providerWaitMs`, `streamMs`, and
`retryAttempt` fields. The assistant error card offers a localized
`phase`, `providerStatus`, `providerCode`, `providerWaitMs`, `streamMs`,
`retryAttempt`, `networkCategory`, `networkCode`, `networkSyscall`,
`networkHost`, `requestMessages`, `requestBytes`, and `compactionGeneration`
fields. The request fields are counts and byte sizes only and the compaction
field is the checkpoint generation counter; none of them carries message
content. While a transient provider failure retries, the activity indicator's
reason popover shows the localized summary, the stable code, and — for a
network failure — the transport errno (`NETWORK_ERROR · ENOTFOUND`), so the
failing layer is visible during the retry loop as well as in the log record.
The assistant error card offers a localized
Continue action that resends the continuation prompt (`继续当前任务` /
`Continue the current task`) in the same session without truncating the failed
turn. The session-scoped failed-turn recovery card is used only when no
Expand Down
8 changes: 5 additions & 3 deletions docs/zh-CN/spec/03-runtime/01-ipc-protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -616,9 +616,11 @@ type AgentEvent =

提供程序 `error` 事件可能包括以下中的有限诊断字段:
`AppError.details`:`phase`(`request` 或 `stream`)、`providerStatus`、
`providerCode`、`providerWaitMs`、`streamMs` 和 `retryAttempt`。这些领域
是添加和编辑的;他们从不携带凭证或不受限制的
提供商响应。瞬时流故障可能会在内部重播
`providerCode`、`providerWaitMs`、`streamMs`、`retryAttempt`,以及网络故障
时的 `networkCategory`、`networkCode`、`networkSyscall`、`networkHost` 和请求
关联字段 `requestMessages`、`requestBytes`、`compactionGeneration`。这些字段
都是新增且经过编辑的;它们从不携带凭据或不受限制的提供商响应,请求字段
只有计数与字节大小。瞬时流故障可能会在内部重播
同一回合,没有终端 `error` 事件或重复的辅助消息。
第二次失败会发出终端标准化 `STREAM_FAILED` 错误。

Expand Down
5 changes: 4 additions & 1 deletion docs/zh-CN/spec/03-runtime/02-agent-runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,10 @@ HTTP 429 处理是一个逻辑回合策略。此路径禁用了 pi-ai 的嵌套
当 429 预算耗尽时,最终的助手错误和生命周期 `error` 只发出一次。
提供程序故障在可用时于 `AppError.details` 中携带有界诊断:
`phase`(`request` 或 `stream`)、`providerStatus`、`providerCode`、
`providerWaitMs`、`streamMs` 和 `retryAttempt`。对于持续的 429,
`providerWaitMs`、`streamMs`、`retryAttempt`、网络诊断
(`networkCategory`、`networkCode`、`networkSyscall`、`networkHost`)以及
请求关联字段(`requestMessages`、`requestBytes`、`compactionGeneration`)。
对于持续的 429,
`retryAttempt` 为 `5`;对于持续的非 429 瞬时故障,它为 `4`。凭据与不受限制的
响应正文永远不会进入事件或日志。

Expand Down
20 changes: 18 additions & 2 deletions docs/zh-CN/spec/03-runtime/08-error-codes.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,16 @@ Node sidecar 将提供商 SDK 错误映射到:
不可重试 `PROVIDER_ERROR` 永远不会进入任何预算。预算耗尽后的失败仍然是
致命的。

`NETWORK_ERROR` 以有界的 `details` 携带真正失败的传输层:
`networkCategory`(`dns`、`tls`、`timeout`、`refused`、`unreachable`、
`reset`、`proxy`,或在没有留下任何线索时为 `unknown`)、`networkCode`
(errno,例如 `ENOTFOUND`、`ECONNRESET`、`EPROTO`、`UND_ERR_SOCKET`),
以及传输层提供时的 `networkSyscall` 和 `networkHost`。只保留裸主机名——
绝不包含 URL、端口、路径、查询串或凭据——当 `providerCode` 会重复
`networkCode` 时省略它。刻意不引入按层划分的错误码(`DNS_ERROR`、
`TLS_ERROR`、`SOCKET_RESET` 等):分类字段已能区分这些层,而无需为每一层
增加用户可见的错误码与本地化文案。

### 权限超时
UI/host 超时在内部发出 `PERMISSION_TIMEOUT`,工具结果向代理显示为拒绝 (`TOOL_DENIED`)。

Expand Down Expand Up @@ -317,9 +327,15 @@ UI/host 超时在内部发出 `PERMISSION_TIMEOUT`,工具结果向代理显示
助手错误消息显示本地化摘要和稳定代码,并带有
包含经过编辑的提供商响应的可访问详细信息披露,
提供商 ID 和模型 ID。提供商详细信息上限为 600 个字符,并且
公共 credential/header 值在事件发射之前进行编辑或
公共 credential/header 值在事件发射或持久化之前进行编辑。
详细信息披露也可能显示有界的 `phase`、`providerStatus`、`providerCode`、
`providerWaitMs`、`streamMs` 和 `retryAttempt` 字段。
`providerWaitMs`、`streamMs`、`retryAttempt`、`networkCategory`、
`networkCode`、`networkSyscall`、`networkHost`、`requestMessages`、
`requestBytes` 和 `compactionGeneration` 字段。请求字段只有计数与字节大小,
压缩字段是检查点世代计数器,均不携带消息内容。当瞬时提供商故障正在重试时,
活动指示器的原因气泡会显示本地化摘要、稳定错误码,并在网络故障时显示传输层
errno(`NETWORK_ERROR · ENOTFOUND`),因此失败层级在重试期间与日志记录中
同样可见。

## 6. i18n 按键约定

Expand Down
Loading