@sema-agent/sdk 0.1.9 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -0
- package/dist/client.d.ts +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +1 -1
- package/dist/client.js.map +1 -1
- package/dist/errors.d.ts +97 -28
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +249 -117
- package/dist/errors.js.map +1 -1
- package/dist/events.d.ts +11 -22
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -1
- package/dist/index.js.map +1 -1
- package/dist/normalize.d.ts +17 -4
- package/dist/normalize.d.ts.map +1 -1
- package/dist/normalize.js +16 -9
- package/dist/normalize.js.map +1 -1
- package/dist/resources/approvals.d.ts +2 -7
- package/dist/resources/approvals.d.ts.map +1 -1
- package/dist/resources/approvals.js +3 -3
- package/dist/resources/approvals.js.map +1 -1
- package/dist/resources/assistant.d.ts +1 -1
- package/dist/resources/assistant.js +1 -1
- package/dist/resources/attachments.d.ts.map +1 -1
- package/dist/resources/attachments.js +3 -4
- package/dist/resources/attachments.js.map +1 -1
- package/dist/resources/fleet.d.ts +4 -4
- package/dist/resources/fleet.d.ts.map +1 -1
- package/dist/resources/fleet.js +2 -3
- package/dist/resources/fleet.js.map +1 -1
- package/dist/resources/images.d.ts +4 -4
- package/dist/resources/images.d.ts.map +1 -1
- package/dist/resources/images.js +7 -6
- package/dist/resources/images.js.map +1 -1
- package/dist/resources/leader.d.ts +2 -3
- package/dist/resources/leader.d.ts.map +1 -1
- package/dist/resources/leader.js +2 -3
- package/dist/resources/leader.js.map +1 -1
- package/dist/resources/memory.d.ts +11 -53
- package/dist/resources/memory.d.ts.map +1 -1
- package/dist/resources/memory.js +2 -51
- package/dist/resources/memory.js.map +1 -1
- package/dist/resources/models.d.ts +4 -4
- package/dist/resources/models.js +1 -1
- package/dist/resources/ops.d.ts +3 -3
- package/dist/resources/ops.d.ts.map +1 -1
- package/dist/resources/ops.js +3 -1
- package/dist/resources/ops.js.map +1 -1
- package/dist/resources/runs.d.ts.map +1 -1
- package/dist/resources/runs.js +1 -2
- package/dist/resources/runs.js.map +1 -1
- package/dist/resources/session-sync.d.ts +1 -2
- package/dist/resources/session-sync.d.ts.map +1 -1
- package/dist/resources/session-sync.js +4 -13
- package/dist/resources/session-sync.js.map +1 -1
- package/dist/resources/sessions.d.ts +4 -4
- package/dist/resources/sessions.d.ts.map +1 -1
- package/dist/resources/sessions.js +9 -5
- package/dist/resources/sessions.js.map +1 -1
- package/dist/resources/tasks.d.ts.map +1 -1
- package/dist/resources/tasks.js +1 -2
- package/dist/resources/tasks.js.map +1 -1
- package/dist/resources/workflows.d.ts.map +1 -1
- package/dist/resources/workflows.js +1 -2
- package/dist/resources/workflows.js.map +1 -1
- package/dist/resources/workspace.d.ts.map +1 -1
- package/dist/resources/workspace.js +8 -9
- package/dist/resources/workspace.js.map +1 -1
- package/dist/sse.d.ts.map +1 -1
- package/dist/sse.js +7 -5
- package/dist/sse.js.map +1 -1
- package/dist/transport.d.ts +25 -0
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +37 -2
- package/dist/transport.js.map +1 -1
- package/dist/types.d.ts +108 -55
- package/dist/types.d.ts.map +1 -1
- package/openapi.yaml +184 -219
- package/package.json +2 -2
package/dist/errors.js
CHANGED
|
@@ -5,36 +5,96 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export class APIError extends Error {
|
|
7
7
|
status;
|
|
8
|
-
|
|
8
|
+
errorCode;
|
|
9
9
|
/** `true` = the server said this failure is RETRIABLE: the durable park is still pending, so re-fetching and
|
|
10
10
|
* deciding/resuming again is meaningful (server sets `retriable: true` on the /decide + resume legs).
|
|
11
11
|
*
|
|
12
12
|
* 🔴 `undefined` means "the server did not say", NOT "not retriable" — it is a CONDITIONAL field server-side
|
|
13
13
|
* (absent ⇒ key omitted), so reading its absence as an explicit `false` over-reads the wire.
|
|
14
14
|
*
|
|
15
|
-
* ⚠️ 为什么它不是 `readonly` / 不是构造参数:`retriable`
|
|
15
|
+
* ⚠️ 为什么它不是 `readonly` / 不是构造参数:`retriable` 可以伴随**任何**分支的错误一起回来,而这里已有三十多个
|
|
16
16
|
* 子类、其中好几个有自己的位置化构造签名(retryAfterMs / allowlist / relation …)。做成第 4 个构造参数就得逐个
|
|
17
17
|
* 子类穿线,漏一个就是静默丢信号;做成 mapper 末尾的**单点盖章**则天然覆盖全部返回路径。这是刻意取舍。 */
|
|
18
18
|
retriable;
|
|
19
|
-
constructor(status,
|
|
19
|
+
constructor(status, errorCode, message) {
|
|
20
20
|
super(message);
|
|
21
21
|
this.status = status;
|
|
22
|
-
this.
|
|
22
|
+
this.errorCode = errorCode;
|
|
23
23
|
this.name = new.target.name;
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
-
/** 401
|
|
26
|
+
/** `auth.*` 前缀族 + 401/503 状态兜底 —— fail-closed auth(缺/错 SERVICE_AUTH_TOKEN、非属主、
|
|
27
|
+
* operator-only、bake-runner-only、principal 头缺席…)。
|
|
28
|
+
*
|
|
29
|
+
* 🔴 键位优先于状态(SDK 1.1.0):此前这个类**只**按 `status === 401 || status === 503` 分,于是
|
|
30
|
+
* `auth.forbidden` / `auth.operator_only` / `auth.bake_runner_only` / `auth.ingest_secret_invalid`
|
|
31
|
+
* 四个 403 全塌成裸 APIError,而同一条 503 分支还错吞了 `state.*`(见 {@link ServiceStateError})。
|
|
32
|
+
* 现在先按 `auth.` 前缀分,状态兜底留作**没有机器码时**的退路。 */
|
|
27
33
|
export class AuthError extends APIError {
|
|
28
34
|
}
|
|
35
|
+
// ── 附录 A 判据三:9 个稳定前缀族的兜底 typed 面(SDK 1.1.0 × server 3.0.0)────────────────────────
|
|
36
|
+
// 服务端契约原文(ASSISTANT-WIRE-CONTRACT 附录 A 判据三):「前缀族(`auth.` / `request.` /
|
|
37
|
+
// `not_found.` / `conflict.` / `limit.` / `capability.` / `feature.` / `internal.` / `state.`)是稳定的
|
|
38
|
+
// **粗分支面** —— 按前缀兜底(未知码 → 看前缀 → 再不行看状态码)永远安全。」
|
|
39
|
+
//
|
|
40
|
+
// 🔴 为什么这不是「把码抄进 SDK」这么轻:补齐之前,`capability.run_store_required`(**换部署形态**才可能有)
|
|
41
|
+
// 与 `feature.elicitation_disabled`(**开个开关**就行)都是 501、都塌成裸 `APIError` —— 消费端拿不到任何
|
|
42
|
+
// 可分支的东西,而这两者给用户的行动建议正好相反。契约套件第一批把这件事做成了可执行断言
|
|
43
|
+
// (`COLLAPSED_BASELINE` 棘轮 = 25 行塌型),本批销账。
|
|
44
|
+
//
|
|
45
|
+
// 两族**复用已有类,不新铸**:`auth.*` → {@link AuthError};`conflict.*` → {@link ConflictError}
|
|
46
|
+
// (它已经携带 `activeTaskId`,正是 `conflict.session_active_run` 要的那个字段)。
|
|
47
|
+
//
|
|
48
|
+
// ⚠️ 前缀兜底是**开集**的:server 明天新增一个 `capability.xxx_required`,今天这份 SDK 就已经把它
|
|
49
|
+
// 铸成 `CapabilityUnavailableError` —— 未知码的降级仍然是有信息的(与 `stop.*` / `decide.*` 同款设计)。
|
|
50
|
+
/** `request.*` 前缀族 —— **请求本身有问题,改请求即可**(400 为主,另有 413 / 422 / 4xx 兜底粗码)。
|
|
51
|
+
* 一律 NOT retryable verbatim:同一个 body 重发永远同一个结果。子类 {@link PayloadTooLargeError} 专指
|
|
52
|
+
* 「某个字段/整体太大」这一支(处置更具体:缩短后重发)。 */
|
|
53
|
+
export class BadRequestError extends APIError {
|
|
54
|
+
}
|
|
55
|
+
/** `not_found.*` 前缀族 —— 资源不存在。⚠️ 也包含**属主门**:server 对非属主一律回 404 同码同文
|
|
56
|
+
* (不给存在性 oracle),所以「404」不等于「东西真不在」,只等于「你看不到它」。 */
|
|
57
|
+
export class NotFoundError extends APIError {
|
|
58
|
+
}
|
|
59
|
+
/** `limit.*` 前缀族 —— 配额 / 限流 / 保留窗(429 限流与成本配额、416 续读点过保留窗)。
|
|
60
|
+
* `retryAfterMs` 来自 `Retry-After` 头(由 transport 喂入)。
|
|
61
|
+
* ⚠️ 429 那两个成员码({@link RateLimitedError} 起)走更具体的子类,见下。 */
|
|
62
|
+
export class LimitExceededError extends APIError {
|
|
63
|
+
retryAfterMs;
|
|
64
|
+
constructor(status, errorCode, message, retryAfterMs) {
|
|
65
|
+
super(status, errorCode, message);
|
|
66
|
+
this.retryAfterMs = retryAfterMs;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/** `capability.*` 前缀族 —— **本部署没接这个面**(501)。重试无意义、开开关也没用:要么换部署形态
|
|
70
|
+
* (接上 store / 打开 fleet 接线),要么在 UI 上**别提供这个入口**。
|
|
71
|
+
* 🔴 与 {@link FeatureDisabledError} 的区别正是 P0-1 的全部后果:同为 501,处置相反。 */
|
|
72
|
+
export class CapabilityUnavailableError extends APIError {
|
|
73
|
+
}
|
|
74
|
+
/** `feature.*` 前缀族 —— **面存在,但开关是关的**(501;`feature.metrics_disabled` 是 404)。
|
|
75
|
+
* 处置:提示管理员打开对应开关(`RESOURCE_SUSPEND` / 三个 LIVE HITL 开关 / metrics)——不需要换部署形态。 */
|
|
76
|
+
export class FeatureDisabledError extends APIError {
|
|
77
|
+
}
|
|
78
|
+
/** `internal.*` 前缀族 —— 服务端自己出错(500 / 502)。细节不上 wire。
|
|
79
|
+
* 部分成员**明确可重试**(`internal.cancel_not_terminalized` = 重试 cancel;
|
|
80
|
+
* `internal.snapshot_blobs_missing` = 重试或换快照),按 `.errorCode` 分。 */
|
|
81
|
+
export class InternalServerError extends APIError {
|
|
82
|
+
}
|
|
83
|
+
/** `state.*` 前缀族 —— 服务端**当前状态**不接受这个请求(503):roster 还没拉到、没有可用的
|
|
84
|
+
* sandbox-base。**稍后重试**是主处置。
|
|
85
|
+
* 🔴 修的是一条真缝:这一族此前撞进 `status === 503` 那条分支,被铸成了 {@link AuthError} —— 一个
|
|
86
|
+
* 「稍后重试」的信号被读成了「你没有权限」。 */
|
|
87
|
+
export class ServiceStateError extends APIError {
|
|
88
|
+
}
|
|
29
89
|
/** 413 `workspace_file_too_large`(#3 workspace file 面,[1894]①)——结构化超限信封的 typed 形:
|
|
30
90
|
* `sizeBytes`=文件真实大小、`limit`=部署帽(capabilities.workspace.maxFileBytes)。web 用它渲
|
|
31
91
|
* 「文件过大({sizeBytes}),下载查看」卡;archive 端点是退路。由 WorkspaceResource.file 铸
|
|
32
|
-
* (面局部判别,不进全局 classify——
|
|
92
|
+
* (面局部判别,不进全局 classify——errorCode 是这个面独有的)。 */
|
|
33
93
|
export class WorkspaceFileTooLargeError extends APIError {
|
|
34
94
|
sizeBytes;
|
|
35
95
|
limit;
|
|
36
|
-
constructor(status,
|
|
37
|
-
super(status,
|
|
96
|
+
constructor(status, errorCode, message, sizeBytes, limit) {
|
|
97
|
+
super(status, errorCode, message);
|
|
38
98
|
this.sizeBytes = sizeBytes;
|
|
39
99
|
this.limit = limit;
|
|
40
100
|
}
|
|
@@ -46,40 +106,39 @@ export class WorkspaceFileTooLargeError extends APIError {
|
|
|
46
106
|
* dead).**Retryable against the replacement instance**; honor `retryAfterMs` (from `retry-after`). */
|
|
47
107
|
export class DrainingError extends APIError {
|
|
48
108
|
retryAfterMs;
|
|
49
|
-
constructor(status,
|
|
50
|
-
super(status,
|
|
109
|
+
constructor(status, errorCode, message, retryAfterMs) {
|
|
110
|
+
super(status, errorCode, message);
|
|
51
111
|
this.retryAfterMs = retryAfterMs;
|
|
52
112
|
}
|
|
53
113
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
114
|
+
/** 429 —— per-principal 限流。`retryAfterMs` 来自 `Retry-After`(SDK 重试时遵守它)。
|
|
115
|
+
* 自 SDK 1.1.0 起继承 {@link LimitExceededError}(`limit.*` 族的成员码 `limit.rate_exceeded` /
|
|
116
|
+
* `limit.cost_quota_exceeded` 就是从这里下来的)——**additive**:既有 `instanceof RateLimitedError`
|
|
117
|
+
* 一字不改,同时 `catch (e instanceof LimitExceededError)` 也能一把接住整族。 */
|
|
118
|
+
export class RateLimitedError extends LimitExceededError {
|
|
60
119
|
}
|
|
61
|
-
/** 429 `
|
|
120
|
+
/** 429 `errorCode:"quota_exhausted"` — the principal's usage quota (token windows / budget) is exhausted
|
|
62
121
|
* (E4 double-rail, service ≥1.150.0). Extends RateLimitedError so existing 429 handling still matches;
|
|
63
122
|
* carries the structured deny detail so the RENDER layer can produce a human message (人话归壳 — the wire
|
|
64
123
|
* stays structured, no display strings here). NOT usefully retryable inside the deny window. */
|
|
65
124
|
export class QuotaExhaustedError extends RateLimitedError {
|
|
66
125
|
detail;
|
|
67
|
-
constructor(status,
|
|
68
|
-
super(status,
|
|
126
|
+
constructor(status, errorCode, message, retryAfterMs, detail) {
|
|
127
|
+
super(status, errorCode, message, retryAfterMs);
|
|
69
128
|
this.detail = detail;
|
|
70
129
|
}
|
|
71
130
|
}
|
|
72
|
-
/** 400 `
|
|
131
|
+
/** 400 `errorCode:"scenario_not_allowed"` — the request's explicit `body.scenario` is outside the principal's
|
|
73
132
|
* assigned allowlist (scenario governance, service ≥1.152.0). Carries the service's
|
|
74
133
|
* `allowlist` VERBATIM (pure scenario names) so the RENDER layer can offer the valid choices (人话归壳 —
|
|
75
134
|
* the wire stays structured, no display strings here). NOT retryable verbatim: pick a scenario from
|
|
76
135
|
* `allowlist`, or omit `scenario` entirely to run on the assigned default. */
|
|
77
136
|
export class ScenarioNotAllowedError extends APIError {
|
|
78
137
|
allowlist;
|
|
79
|
-
constructor(status,
|
|
138
|
+
constructor(status, errorCode, message,
|
|
80
139
|
/** The principal's allowed scenario names (may be empty on an older service / defensive drop). */
|
|
81
140
|
allowlist) {
|
|
82
|
-
super(status,
|
|
141
|
+
super(status, errorCode, message);
|
|
83
142
|
this.allowlist = allowlist;
|
|
84
143
|
}
|
|
85
144
|
}
|
|
@@ -89,8 +148,8 @@ export class BudgetExceededError extends APIError {
|
|
|
89
148
|
/** 409 — session already has an active run, or an idempotency conflict / session CAS conflict. */
|
|
90
149
|
export class ConflictError extends APIError {
|
|
91
150
|
activeTaskId;
|
|
92
|
-
constructor(status,
|
|
93
|
-
super(status,
|
|
151
|
+
constructor(status, errorCode, message, activeTaskId) {
|
|
152
|
+
super(status, errorCode, message);
|
|
94
153
|
this.activeTaskId = activeTaskId;
|
|
95
154
|
}
|
|
96
155
|
}
|
|
@@ -98,15 +157,15 @@ export class ConflictError extends APIError {
|
|
|
98
157
|
* lose destination history: the relation classified to `fork` or `stale` and the caller did NOT pass
|
|
99
158
|
* `{ resolution: "overwrite-dst" }`. 409, errorCode `conflict`, with the classified `relation` carrying the
|
|
100
159
|
* exclusive entry sets (mirrors the service SyncConflictError). DISTINCT from the session-CAS ConflictError
|
|
101
|
-
* (same status+
|
|
160
|
+
* (same status+errorCode, but THIS one carries a `relation` — that's how the mapper tells them apart). Client action:
|
|
102
161
|
* surface the keep-local / keep-cloud divergence to the user; to override, re-push with `resolution:"overwrite-dst"`.
|
|
103
162
|
* NEVER auto-overwrite (a silent clobber is exactly the data loss the classifier exists to prevent). */
|
|
104
163
|
export class SyncConflictError extends ConflictError {
|
|
105
164
|
relation;
|
|
106
|
-
constructor(status,
|
|
165
|
+
constructor(status, errorCode, message,
|
|
107
166
|
/** The §7 `fork`/`stale` relation with the exclusive entry sets (untyped at the JSON boundary). */
|
|
108
167
|
relation) {
|
|
109
|
-
super(status,
|
|
168
|
+
super(status, errorCode, message);
|
|
110
169
|
this.relation = relation;
|
|
111
170
|
}
|
|
112
171
|
}
|
|
@@ -115,8 +174,8 @@ export class SyncConflictError extends ConflictError {
|
|
|
115
174
|
* and from session-CAS 409. Client action: this pending is gone → REFETCH the inbox; do not retry the decide. */
|
|
116
175
|
export class ApprovalStaleError extends APIError {
|
|
117
176
|
terminal;
|
|
118
|
-
constructor(status,
|
|
119
|
-
super(status,
|
|
177
|
+
constructor(status, errorCode, message, terminal) {
|
|
178
|
+
super(status, errorCode, message);
|
|
120
179
|
this.terminal = terminal;
|
|
121
180
|
}
|
|
122
181
|
}
|
|
@@ -125,8 +184,8 @@ export class ApprovalStaleError extends APIError {
|
|
|
125
184
|
* Client action: REFETCH the PendingCheckpoint, RE-PRESENT it to the human, NEVER auto-retry. */
|
|
126
185
|
export class ApprovalBindingMismatchError extends APIError {
|
|
127
186
|
field;
|
|
128
|
-
constructor(status,
|
|
129
|
-
super(status,
|
|
187
|
+
constructor(status, errorCode, message, field) {
|
|
188
|
+
super(status, errorCode, message);
|
|
130
189
|
this.field = field;
|
|
131
190
|
}
|
|
132
191
|
}
|
|
@@ -136,18 +195,43 @@ export class ApprovalBindingMismatchError extends APIError {
|
|
|
136
195
|
* again (retryable). */
|
|
137
196
|
export class ApprovalConcurrentlyReopenedError extends APIError {
|
|
138
197
|
}
|
|
198
|
+
/** `steering.*` 族的基类(SDK 1.1.0 —— 从**逐码枚举**改成**前缀分派**,与 `stop.*` / `decide.*` 同形)。
|
|
199
|
+
*
|
|
200
|
+
* 🔴 改之前是这样的:server 真能发 5 个 `steering.*` 码,SDK 只 `===` 认了 2 个,另 3 个
|
|
201
|
+
* (`ambiguous_label` / `ambiguous_target` / `still_running`)塌成通用 409 `ConflictError` —— 一个**开集**
|
|
202
|
+
* 被当成闭集写。契约套件第一批用「钉版 server 产物扫码 → 逐码喂 mapper」把这 3 条塌型做成了
|
|
203
|
+
* `STEERING_COLLAPSED_BASELINE = 3` 的棘轮读数,本批销账(基线降到 0)。
|
|
204
|
+
*
|
|
205
|
+
* ⚠️ 前缀开集的代价必须说清楚:那 3 个码此前偶然是 `ConflictError`(因为它们是 409),现在**不再是**了。
|
|
206
|
+
* 这是有意的 —— 「409」是传输层的巧合,「steer 目标有歧义」才是消费端要分支的东西;按 [clean-cut] 硬切,
|
|
207
|
+
* 不留双继承之类的兼容层。整族一把接:`catch (e instanceof SteeringError)`。 */
|
|
208
|
+
export class SteeringError extends APIError {
|
|
209
|
+
}
|
|
139
210
|
/** design/80 D-A — you steered a run that is NOT running (it is durable-`suspended`, terminal, or unknown).
|
|
140
211
|
* Steer is at-most-once and NOT queued. Client action: surface "task isn't running"; if suspended, the path
|
|
141
212
|
* forward is the approval decide, not a steer. */
|
|
142
|
-
export class SteeringNotRunningError extends
|
|
213
|
+
export class SteeringNotRunningError extends SteeringError {
|
|
143
214
|
}
|
|
144
215
|
/** design/80 D-A — the steer text carried a control-plane escape (e.g. `</system-reminder>`) and core rejected
|
|
145
216
|
* it (runtask.ts:489). Client action: it's an injection attempt or a malformed steer — surface it; don't retry
|
|
146
217
|
* verbatim. (The BFF also fences steer text defensively; this is core's authoritative reject.) */
|
|
147
|
-
export class SteeringInvalidContentError extends
|
|
218
|
+
export class SteeringInvalidContentError extends SteeringError {
|
|
219
|
+
}
|
|
220
|
+
/** `steering.ambiguous_target`(子代 steer/resume 面,server `http/routes/runs.ts`)/
|
|
221
|
+
* `steering.ambiguous_label`(workflow agent steer 面,`http/routes/workflows.ts`)—— **这一个 run 里有
|
|
222
|
+
* 不止一个活体匹配你给的 target/label**,server 拒绝替你猜。409。
|
|
223
|
+
* Client action:两个码同一个处置 —— **换一个唯一的定位方式**(子代面用 `parentToolCallId`;workflow 面
|
|
224
|
+
* 用更具体的 label/句柄),原样重试永远同一个结果。`.errorCode` 区分是哪条轴上歧义。 */
|
|
225
|
+
export class SteeringAmbiguousTargetError extends SteeringError {
|
|
226
|
+
}
|
|
227
|
+
/** `steering.still_running` —— 你对一个**还在飞**的子代调了 `resume`(revive 只在 settle 之后合法;
|
|
228
|
+
* 也可能是上一次 resume 还没落地)。409。
|
|
229
|
+
* Client action:要么改调 steer(给在跑的它递话),要么等它 settle 再 resume。**与
|
|
230
|
+
* {@link SteeringNotRunningError} 正好互为反面**,两个码塌在一起会让 UI 说反话。 */
|
|
231
|
+
export class SteeringStillRunningError extends SteeringError {
|
|
148
232
|
}
|
|
149
233
|
/** `413` + `steer.content_too_large` / `reason_too_large` —— 请求里某个**自由文本**字段超过 server 的字符上限。
|
|
150
|
-
* 两个成员码同族同处置,所以共用一个类型(`err.
|
|
234
|
+
* 两个成员码同族同处置,所以共用一个类型(`err.errorCode` 保留具体是哪一个):
|
|
151
235
|
* · `steer.content_too_large`(server 1.279.2):steer 入参上限 256 KiB。脱敏门是同步的,一次超大 steer 卡住的
|
|
152
236
|
* 是整个副本,所以服务端**拒**而不是先截 —— 先截会让引擎的 `[+N chars]` 披露低报。
|
|
153
237
|
* · `reason_too_large`(server 1.284.0):审批决定的 `reason` 上限 4096 字符,`/decide` 与 plan_review 两道门同值。
|
|
@@ -160,7 +244,7 @@ export class PayloadTooLargeError extends APIError {
|
|
|
160
244
|
* `stop.not_local`(run 在别的副本,无本地进程可 kill)/ `stop.not_landed`(kill 尝试了未落地)/
|
|
161
245
|
* `stop.parked`(行在 parked,无活跃进程)/ `stop.park_resume_won`(stop 与「审批落地→恢复」竞态,
|
|
162
246
|
* 恢复赢了)/ `stop.park_arbiter_unreachable`(仲裁店不可达,真相未知——core 1.397 三分)。
|
|
163
|
-
* 开集:未来新 `stop.*` 码同样落到本类(`
|
|
247
|
+
* 开集:未来新 `stop.*` 码同样落到本类(`errorCode` 原样携带),消费方 switch 要留 default。 */
|
|
164
248
|
export class TaskStopConflictError extends ConflictError {
|
|
165
249
|
}
|
|
166
250
|
/** design/80 D-G (direct-connect door, service [84]) — the signed principal JWT (`X-Approval-Principal-Token`)
|
|
@@ -205,8 +289,8 @@ export class ResumeOutcomeInvalidError extends APIError {
|
|
|
205
289
|
* longer admissible. Not a race: retrying as-is stays blocked until the policy or the request changes. */
|
|
206
290
|
export class ResumeBlockedByPolicyError extends APIError {
|
|
207
291
|
blockedBy;
|
|
208
|
-
constructor(status,
|
|
209
|
-
super(status,
|
|
292
|
+
constructor(status, errorCode, message, blockedBy) {
|
|
293
|
+
super(status, errorCode, message);
|
|
210
294
|
this.blockedBy = blockedBy;
|
|
211
295
|
}
|
|
212
296
|
}
|
|
@@ -220,7 +304,7 @@ export class ResumeBlockedByPolicyError extends APIError {
|
|
|
220
304
|
// "re-check the pending list"). These types are what makes it branchable without string-matching a message.
|
|
221
305
|
/** 409 `resume.*`(dot 前缀;design/122 D2)—— subagent revive 的合同拒绝族:still_running(等 settle)/
|
|
222
306
|
* retain_off(父 run 没开 retainSubagentSessions)/evicted(留存被逐)/cap(留存上限)/
|
|
223
|
-
* session_not_found。前缀开集:core 未来新码至少落到本类(`.
|
|
307
|
+
* session_not_found。前缀开集:core 未来新码至少落到本类(`.errorCode` 判别),不塌匿名 409。
|
|
224
308
|
* ⚠️ 与下划线形的 resume_outcome_invalid / resume_blocked_by_policy 是**两族**(那两个在上面,先判)。 */
|
|
225
309
|
export class SubagentResumeConflictError extends ConflictError {
|
|
226
310
|
}
|
|
@@ -229,7 +313,7 @@ export class SubagentResumeConflictError extends ConflictError {
|
|
|
229
313
|
export class WakeGatePendingError extends ConflictError {
|
|
230
314
|
}
|
|
231
315
|
/** 409 `compact.not_running` / `detach.not_running` —— replica-local live-verb 族:run 已终态,或活在
|
|
232
|
-
* 另一副本(手动 compact/detach 只对本副本的活流有效)。`.
|
|
316
|
+
* 另一副本(手动 compact/detach 只对本副本的活流有效)。`.errorCode` 判别是哪个 verb。 */
|
|
233
317
|
export class RunNotLiveError extends ConflictError {
|
|
234
318
|
}
|
|
235
319
|
/** Base of the `decide.*` family — catch this to handle the whole decide door in one place.
|
|
@@ -239,10 +323,10 @@ export class RunNotLiveError extends ConflictError {
|
|
|
239
323
|
export class DecideError extends APIError {
|
|
240
324
|
taskId;
|
|
241
325
|
rollback;
|
|
242
|
-
constructor(status,
|
|
326
|
+
constructor(status, errorCode, message, taskId,
|
|
243
327
|
/** revive legs only — the claim-rollback disposition the server reported. */
|
|
244
328
|
rollback) {
|
|
245
|
-
super(status,
|
|
329
|
+
super(status, errorCode, message);
|
|
246
330
|
this.taskId = taskId;
|
|
247
331
|
this.rollback = rollback;
|
|
248
332
|
}
|
|
@@ -280,110 +364,124 @@ function classifyApiError(status, body, retryAfterMs) {
|
|
|
280
364
|
// the typed code) instead of crashing scrub() INSIDE the error path (a throw here would mask the real HTTP
|
|
281
365
|
// failure with a TypeError).
|
|
282
366
|
// ⚠️ 纠正(2026-07-25):这段注释原先说「scenario-detail 的 404 body 就是 `{error:{code:…}}`」——已不成立,
|
|
283
|
-
// server [939] 把那一路统一成了「字符串 `error` + 顶层 `errorCode`」(亲读 `http/
|
|
367
|
+
// server [939] 把那一路统一成了「字符串 `error` + 顶层 `errorCode`」(亲读 `src/http/routes/capabilities.ts` 确认)。
|
|
284
368
|
// 本分支**保留**,但定位从「适配某条已知路由」改为「防御任何对象形 error body」:它不该是任何路由的期望形,
|
|
285
369
|
// 而是错误路径上不许崩的兜底。真形走下面 `b.errorCode` 那条(优先级更高)。
|
|
286
370
|
const errField = body?.error;
|
|
287
371
|
const nestedCode = typeof errField === "object" && errField !== null && typeof errField.code === "string"
|
|
288
372
|
? errField.code
|
|
289
373
|
: undefined;
|
|
290
|
-
// 🔴
|
|
291
|
-
//
|
|
292
|
-
// `
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
// `
|
|
296
|
-
|
|
297
|
-
// CheckpointError 族),无一与本兜底冲突;`event: error` 流帧那套(WORKER_DOWN 等)不经本函数。
|
|
298
|
-
const code = b.errorCode ?? nestedCode ?? b.code;
|
|
374
|
+
// 🔴 单键(SDK 1.0.0 / server 3.0.0 [1989][1990]):wire 上**唯一**的机器码键位是 `errorCode`。
|
|
375
|
+
// 历史留档:2026-07-25 这里曾有一条 `?? b.code` 兜底,补的是 server 在 `/decide` 上把 CheckpointError
|
|
376
|
+
// 家族的码发在裸 `code` 字段这个真缺陷(后果:`checkpoint.*`/`resume.*`/`wake.*` 一族的机器码恒为
|
|
377
|
+
// undefined,两条既有 typed 映射在那条路上不可达)。server 3.0.0 已在**发端**根治(`http/send.ts` 的
|
|
378
|
+
// `sendError(res,status,errorCode,message,extra)` + `/decide` 归一臂),兜底随之删除 —— 留着它只会让
|
|
379
|
+
// 「对端还在发老键」这件事被静默吸收、下游测不出来。`event: error` 流帧那套不经本函数(见 sse.ts)。
|
|
380
|
+
const errorCode = b.errorCode ?? nestedCode;
|
|
299
381
|
const rawMsg = b.errorMessage ?? (typeof errField === "string" ? errField : undefined);
|
|
300
382
|
const msg = scrub(rawMsg ?? nestedCode ?? `HTTP ${status}`);
|
|
301
|
-
// scenario governance —
|
|
302
|
-
//
|
|
303
|
-
//
|
|
304
|
-
//
|
|
305
|
-
|
|
306
|
-
// 免得读者以为有两套取码机制在并行。)
|
|
307
|
-
if (code === "scenario_not_allowed") {
|
|
383
|
+
// scenario governance — server 侧 `HttpError(400, msg, { code: "scenario_not_allowed", extra:{allowlist} })`
|
|
384
|
+
// 经 `sendError` 落到 wire 的 `errorCode` 单键。The allowlist passes through with a per-entry type guard
|
|
385
|
+
// (a malformed entry is dropped, never crashes the error path); a bare code with no/bad allowlist still maps
|
|
386
|
+
// to ScenarioNotAllowedError with an empty list (generic-fallback contract).
|
|
387
|
+
if (errorCode === "scenario_not_allowed") {
|
|
308
388
|
const allowlist = Array.isArray(b.allowlist) ? b.allowlist.filter((x) => typeof x === "string" && x.length > 0) : [];
|
|
309
389
|
return new ScenarioNotAllowedError(status, "scenario_not_allowed", msg, allowlist);
|
|
310
390
|
}
|
|
311
|
-
if (
|
|
312
|
-
return new BudgetExceededError(status,
|
|
391
|
+
if (errorCode === "budget.precall" || errorCode === "budget.exceeded")
|
|
392
|
+
return new BudgetExceededError(status, errorCode, msg);
|
|
313
393
|
// server ≥1.309:drain 503 有机器码;旧 server 只发 error:"draining" 字面(冻结契约)——两形同映射,
|
|
314
394
|
// 消费端 instanceof DrainingError 即可,不再抄文案。retryAfterMs 由调用方从 retry-after 头喂入(见 transport)。
|
|
315
|
-
if (
|
|
395
|
+
if (errorCode === "draining" || (status === 503 && errField === "draining"))
|
|
316
396
|
return new DrainingError(status, "draining", msg, retryAfterMs);
|
|
317
397
|
// design/80 D-1 — keyed on errorCode, NOT status (both are independent of the session-CAS 409 ConflictError).
|
|
318
398
|
// [1833] G13:taskStop 409 五形(stop.* 前缀开集)→ typed(壳此前手抄 code 自判,engineTaskHandleWire.ts:186-197)
|
|
319
|
-
if (typeof
|
|
320
|
-
return new TaskStopConflictError(status,
|
|
321
|
-
if (
|
|
322
|
-
return new ApprovalStaleError(status,
|
|
323
|
-
if (
|
|
324
|
-
return new ApprovalConcurrentlyReopenedError(status,
|
|
325
|
-
if (
|
|
326
|
-
return new ApprovalBindingMismatchError(status,
|
|
399
|
+
if (typeof errorCode === "string" && errorCode.startsWith("stop."))
|
|
400
|
+
return new TaskStopConflictError(status, errorCode, msg);
|
|
401
|
+
if (errorCode === "approval_stale" || errorCode === "checkpoint.already_resolved")
|
|
402
|
+
return new ApprovalStaleError(status, errorCode, msg, b.terminal);
|
|
403
|
+
if (errorCode === "checkpoint.reopened_concurrently")
|
|
404
|
+
return new ApprovalConcurrentlyReopenedError(status, errorCode, msg); // D-1 transient (core 1.101.0)
|
|
405
|
+
if (errorCode === "approval_binding_mismatch" || errorCode === "checkpoint.invalid_outcome")
|
|
406
|
+
return new ApprovalBindingMismatchError(status, errorCode, msg, b.field);
|
|
327
407
|
// ASSISTANT-WIRE-CONTRACT §4b/§4c — 错门守卫, 键于 errorCode(都是 409 但语义 ≠ session-CAS ConflictError, 不能塌进通用 409)。
|
|
328
|
-
if (
|
|
329
|
-
return new GateNotResumableError(status,
|
|
330
|
-
if (
|
|
331
|
-
return new GateNotPlanReviewError(status,
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
if (
|
|
335
|
-
|
|
408
|
+
if (errorCode === "gate_not_resumable")
|
|
409
|
+
return new GateNotResumableError(status, errorCode, msg); // §4b resume 撞 human/policy_ask
|
|
410
|
+
if (errorCode === "gate_not_plan_review")
|
|
411
|
+
return new GateNotPlanReviewError(status, errorCode, msg); // §4c plan_review 撞非 plan_review
|
|
412
|
+
// `steering.*` —— 前缀分派(SDK 1.1.0,与 `stop.*` / `decide.*` 同形;见 SteeringError 顶注)。
|
|
413
|
+
// 未来 server 新铸的 steering 码至少落到 SteeringError 基类,不再塌成匿名 409。
|
|
414
|
+
if (errorCode?.startsWith("steering.")) {
|
|
415
|
+
if (errorCode === "steering.not_running")
|
|
416
|
+
return new SteeringNotRunningError(status, errorCode, msg); // design/80 D-A
|
|
417
|
+
if (errorCode === "steering.invalid_content")
|
|
418
|
+
return new SteeringInvalidContentError(status, errorCode, msg); // design/80 D-A (422)
|
|
419
|
+
// 两个「歧义」码同一个处置(换唯一定位方式)⇒ 共用一个类,`.errorCode` 保留是哪条轴。
|
|
420
|
+
if (errorCode === "steering.ambiguous_target" || errorCode === "steering.ambiguous_label") {
|
|
421
|
+
return new SteeringAmbiguousTargetError(status, errorCode, msg);
|
|
422
|
+
}
|
|
423
|
+
if (errorCode === "steering.still_running")
|
|
424
|
+
return new SteeringStillRunningError(status, errorCode, msg);
|
|
425
|
+
return new SteeringError(status, errorCode, msg);
|
|
426
|
+
}
|
|
336
427
|
// 自由文本超上限族(413):两个成员码同处置 —— 缩短后重发,重试原文无意义。见 PayloadTooLargeError 顶注。
|
|
337
|
-
if (
|
|
338
|
-
return new PayloadTooLargeError(status,
|
|
428
|
+
if (errorCode === "steer.content_too_large" || errorCode === "reason_too_large")
|
|
429
|
+
return new PayloadTooLargeError(status, errorCode, msg);
|
|
430
|
+
// 413 `workspace_file_too_large` —— **铸造点从 WorkspaceResource.file 挪到这里**(S2 收编,SDK 1.1.0)。
|
|
431
|
+
// 原先的理由是「errorCode 是这个面独有的,面局部判别就够了」;实况证否了它:面局部的手写分支恰恰是
|
|
432
|
+
// 唯一没有任何 mapper 测试保护的地方,S2 就死在那里(读裸 `code`,typed 类真机零触发)。
|
|
433
|
+
// 两个结构化字段都在才铸(缺一个 ⇒ web 的卡拿到 undefined,不如老实降级成通用 413)。
|
|
434
|
+
if (errorCode === "workspace_file_too_large" && typeof b.sizeBytes === "number" && typeof b.limit === "number") {
|
|
435
|
+
return new WorkspaceFileTooLargeError(status, errorCode, msg, b.sizeBytes, b.limit);
|
|
436
|
+
}
|
|
339
437
|
// design/80 D-G (direct-connect door, service [84]) — service-locked wire codes; forward-draft until M2 BFF signer.
|
|
340
|
-
if (
|
|
341
|
-
return new PrincipalUnverifiedError(status,
|
|
342
|
-
if (
|
|
343
|
-
return new PrincipalUnboundError(status,
|
|
344
|
-
if (
|
|
345
|
-
return new ApprovalMacRequiredError(status,
|
|
346
|
-
if (
|
|
347
|
-
return new ApprovalMacInvalidError(status,
|
|
438
|
+
if (errorCode === "principal_unverified")
|
|
439
|
+
return new PrincipalUnverifiedError(status, errorCode, msg);
|
|
440
|
+
if (errorCode === "principal_unbound")
|
|
441
|
+
return new PrincipalUnboundError(status, errorCode, msg); // cnf.bnd ≠ SHA256(token) = token-lift
|
|
442
|
+
if (errorCode === "approval_mac_required")
|
|
443
|
+
return new ApprovalMacRequiredError(status, errorCode, msg);
|
|
444
|
+
if (errorCode === "approval_mac_invalid")
|
|
445
|
+
return new ApprovalMacInvalidError(status, errorCode, msg);
|
|
348
446
|
// resume 侧两个 server 刻意铸出来的码(见各自类注释)。
|
|
349
|
-
if (
|
|
350
|
-
return new ResumeOutcomeInvalidError(status,
|
|
351
|
-
if (
|
|
352
|
-
return new ResumeBlockedByPolicyError(status,
|
|
447
|
+
if (errorCode === "resume_outcome_invalid")
|
|
448
|
+
return new ResumeOutcomeInvalidError(status, errorCode, msg);
|
|
449
|
+
if (errorCode === "resume_blocked_by_policy") {
|
|
450
|
+
return new ResumeBlockedByPolicyError(status, errorCode, msg, typeof b.blockedBy === "string" ? b.blockedBy : undefined);
|
|
353
451
|
}
|
|
354
452
|
// 2026-07-28 补齐批:三小族(此前裸串)。resume.* 是 dot 前缀开集——必须排在上面两个下划线码之后。
|
|
355
|
-
if (
|
|
356
|
-
return new SubagentResumeConflictError(status,
|
|
357
|
-
if (
|
|
358
|
-
return new WakeGatePendingError(status,
|
|
359
|
-
if (
|
|
360
|
-
return new RunNotLiveError(status,
|
|
453
|
+
if (errorCode?.startsWith("resume."))
|
|
454
|
+
return new SubagentResumeConflictError(status, errorCode, msg);
|
|
455
|
+
if (errorCode === "wake.gate_pending")
|
|
456
|
+
return new WakeGatePendingError(status, errorCode, msg);
|
|
457
|
+
if (errorCode === "compact.not_running" || errorCode === "detach.not_running")
|
|
458
|
+
return new RunNotLiveError(status, errorCode, msg);
|
|
361
459
|
// decide.* 家族 —— 前缀分派,按「调用方该做什么」分组(见 DecideError 上方那段注释)。
|
|
362
460
|
// 前缀式而非逐码穷举:server 新增一个 decide.* 码时,它至少会落到 DecideError 基类(带 taskId,可被
|
|
363
461
|
// `catch (e instanceof DecideError)` 接住),而不是塌成一个匿名 409 —— 未知码的降级仍然是有信息的。
|
|
364
|
-
if (
|
|
462
|
+
if (errorCode?.startsWith("decide.")) {
|
|
365
463
|
const taskId = typeof b.taskId === "string" ? b.taskId : undefined;
|
|
366
464
|
const rollback = typeof b.rollback === "string" ? b.rollback : undefined;
|
|
367
465
|
// 重查 pending 列表后再决定
|
|
368
|
-
if (
|
|
369
|
-
return new DecideParkMovedError(status,
|
|
466
|
+
if (errorCode === "decide.not_parked" || errorCode === "decide.gate_moved" || errorCode === "decide.claim_lost") {
|
|
467
|
+
return new DecideParkMovedError(status, errorCode, msg, taskId, rollback);
|
|
370
468
|
}
|
|
371
469
|
// 换 decide 形(原样重试无意义)
|
|
372
|
-
if (
|
|
373
|
-
return new DecideUnsupportedError(status,
|
|
470
|
+
if (errorCode === "decide.parked_answer_unsupported" || errorCode === "decide.parked_question_unsupported" || errorCode === "decide.parked_remember_unsupported") {
|
|
471
|
+
return new DecideUnsupportedError(status, errorCode, msg, taskId, rollback);
|
|
374
472
|
}
|
|
375
473
|
// 原样重试
|
|
376
|
-
if (
|
|
377
|
-
return new DecideStoreUnreachableError(status,
|
|
474
|
+
if (errorCode === "decide.store_unreachable")
|
|
475
|
+
return new DecideStoreUnreachableError(status, errorCode, msg, taskId, rollback);
|
|
378
476
|
// 其余(row_unrevivable / revive_failed / revive_rejected)= 交给人看
|
|
379
|
-
return new DecideError(status,
|
|
477
|
+
return new DecideError(status, errorCode, msg, taskId, rollback);
|
|
380
478
|
}
|
|
381
479
|
if (status === 429) {
|
|
382
|
-
// E4 quota deny — the SERVICE 429 body keys the typed code on `
|
|
383
|
-
//
|
|
384
|
-
//
|
|
385
|
-
//
|
|
386
|
-
if (
|
|
480
|
+
// E4 quota deny — the SERVICE 429 body keys the typed code on `errorCode` (`sendError(res, 429,
|
|
481
|
+
// "quota_exhausted", …)`, server 3.0 单键). Structured fields pass through VERBATIM with type guards
|
|
482
|
+
// (a malformed field is dropped, never crashes the error path); a bare code with no fields still maps
|
|
483
|
+
// to QuotaExhaustedError with an empty detail (generic-fallback contract).
|
|
484
|
+
if (errorCode === "quota_exhausted") {
|
|
387
485
|
const detail = {
|
|
388
486
|
...(b.windowType === "fiveHour" || b.windowType === "weekly" || b.windowType === "budget" ? { windowType: b.windowType } : {}),
|
|
389
487
|
...(typeof b.remaining === "number" && Number.isFinite(b.remaining) ? { remaining: b.remaining } : {}),
|
|
@@ -392,20 +490,54 @@ function classifyApiError(status, body, retryAfterMs) {
|
|
|
392
490
|
...(typeof b.pool === "string" ? { pool: b.pool } : {}),
|
|
393
491
|
...(typeof b.retryAfterSec === "number" && Number.isFinite(b.retryAfterSec) ? { retryAfterSec: b.retryAfterSec } : {}),
|
|
394
492
|
};
|
|
395
|
-
return new QuotaExhaustedError(status,
|
|
493
|
+
return new QuotaExhaustedError(status, errorCode, msg, retryAfterMs, detail);
|
|
494
|
+
}
|
|
495
|
+
// `limit.rate_exceeded` / `limit.cost_quota_exceeded` 也从这里出去 —— RateLimitedError 是
|
|
496
|
+
// LimitExceededError 的子类,所以走这条更具体的腿**不会**让 `limit.*` 族的兜底落空,反而多带了
|
|
497
|
+
// `retryAfterMs`。⚠️ 正因如此,下面的 `limit.` 前缀分派必须排在 429 之后(排前面会把这条腿吃掉)。
|
|
498
|
+
return new RateLimitedError(status, errorCode, msg, retryAfterMs);
|
|
499
|
+
}
|
|
500
|
+
// ── 附录 A 判据三:9 个稳定前缀族的兜底 ────────────────────────────────────────────────────────
|
|
501
|
+
// 位置:所有**具名码**的精确分派之后(具体优先于粗分支)、状态码兜底之前(键位优先于状态)。
|
|
502
|
+
// 🔴 中间那句不是可有可无的排序偏好:`state.*` 全是 503,若让 `status === 503` 先手,它们会被铸成
|
|
503
|
+
// AuthError —— 「稍后重试」读成「你没有权限」。所以前缀族**必须**排在状态兜底之前。
|
|
504
|
+
if (errorCode !== undefined) {
|
|
505
|
+
if (errorCode.startsWith("auth."))
|
|
506
|
+
return new AuthError(status, errorCode, msg); // 401/403/503 三种状态同一族
|
|
507
|
+
if (errorCode.startsWith("request.")) {
|
|
508
|
+
// 族内唯一分岔:「太大了」的处置更具体(缩短后重发),与 steer/reason 两个扁平码同类。
|
|
509
|
+
if (errorCode === "request.payload_too_large")
|
|
510
|
+
return new PayloadTooLargeError(status, errorCode, msg);
|
|
511
|
+
return new BadRequestError(status, errorCode, msg);
|
|
396
512
|
}
|
|
397
|
-
|
|
513
|
+
if (errorCode.startsWith("not_found."))
|
|
514
|
+
return new NotFoundError(status, errorCode, msg);
|
|
515
|
+
// `conflict.*` 复用 ConflictError —— 它本来就携带 `activeTaskId`,正是 `conflict.session_active_run`
|
|
516
|
+
// 要消费的那个字段。⚠️ 裸码 `conflict`(session-sync 的扁平码)**不**以 `conflict.` 开头,不受影响,
|
|
517
|
+
// 仍走下面 SyncConflictError / 409 那两条腿。
|
|
518
|
+
if (errorCode.startsWith("conflict."))
|
|
519
|
+
return new ConflictError(status, errorCode, msg, b.activeTaskId);
|
|
520
|
+
if (errorCode.startsWith("limit."))
|
|
521
|
+
return new LimitExceededError(status, errorCode, msg, retryAfterMs);
|
|
522
|
+
if (errorCode.startsWith("capability."))
|
|
523
|
+
return new CapabilityUnavailableError(status, errorCode, msg);
|
|
524
|
+
if (errorCode.startsWith("feature."))
|
|
525
|
+
return new FeatureDisabledError(status, errorCode, msg);
|
|
526
|
+
if (errorCode.startsWith("internal."))
|
|
527
|
+
return new InternalServerError(status, errorCode, msg);
|
|
528
|
+
if (errorCode.startsWith("state."))
|
|
529
|
+
return new ServiceStateError(status, errorCode, msg);
|
|
398
530
|
}
|
|
399
531
|
if (status === 401 || status === 503)
|
|
400
|
-
return new AuthError(status,
|
|
532
|
+
return new AuthError(status, errorCode, msg);
|
|
401
533
|
// 2c session-sync: a 409 `conflict` that CARRIES a fork/stale `relation` is the SyncConflictError (distinct from
|
|
402
534
|
// the session-CAS ConflictError, which has no `relation`). Keyed on the relation's presence + tag, not status alone.
|
|
403
|
-
if (status === 409 &&
|
|
404
|
-
return new SyncConflictError(status,
|
|
535
|
+
if (status === 409 && errorCode === "conflict" && (b.relation?.relation === "fork" || b.relation?.relation === "stale")) {
|
|
536
|
+
return new SyncConflictError(status, errorCode, msg, b.relation);
|
|
405
537
|
}
|
|
406
538
|
if (status === 409)
|
|
407
|
-
return new ConflictError(status,
|
|
408
|
-
return new APIError(status,
|
|
539
|
+
return new ConflictError(status, errorCode, msg, b.activeTaskId);
|
|
540
|
+
return new APIError(status, errorCode, msg);
|
|
409
541
|
}
|
|
410
542
|
// ── control plane (sema-registry /api/config/*) typed errors ────────────────────────────────────────────
|
|
411
543
|
// The control plane is a DIFFERENT backend from the worker /v1 plane and uses a DIFFERENT error envelope:
|