@sema-agent/client-core 0.49.0 → 0.50.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/CHANGELOG.md +130 -0
- package/README.md +2 -1
- package/dist/engineErrorCodes.d.ts +16 -0
- package/dist/engineErrorCodes.js +17 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +7 -0
- package/dist/selfOrchestrationDenial.d.ts +210 -0
- package/dist/selfOrchestrationDenial.js +255 -0
- package/docs/INTEGRATION-CLIENTS.md +282 -14
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -40,6 +40,136 @@
|
|
|
40
40
|
> 门侧窄豁免同批登记(`KNOWN_HEADING_ERRATA` `version: '0.48.0', releasedAt: '4fae01b'`),
|
|
41
41
|
> 与本段互钉,均为永久记录。
|
|
42
42
|
|
|
43
|
+
## 0.50.0(2026-09-03)
|
|
44
|
+
|
|
45
|
+
### 新增:S-81 selfOrchestration 拒绝的三端公共判定(server ≥7.57.0)
|
|
46
|
+
|
|
47
|
+
server 7.57.0 在**半配置的多租户形态**(`REQUIRE_PRINCIPAL=true` + `SELF_ORCHESTRATION_ENABLED=true`
|
|
48
|
+
+ 无中心侧准入解析器)上收窄了三条:`GET /v1/capabilities` 的 `workflows` 由 `true` 变 `false`;
|
|
49
|
+
同一份 caps 上新增 additive 键 `workflowsGate: { engineCan, denial }`;`POST /v1/tasks`(及同闸的
|
|
50
|
+
stream 提交)带 `selfOrchestration:true` 或 `settings.ultracode:true` ⇒ **501
|
|
51
|
+
`capability.self_orchestration_required`**(去掉这两个键则照常受理;非布尔的 `selfOrchestration`
|
|
52
|
+
走 400,不是本码)。单用户 worker / 接了解析器的多租户部署行为**逐字不变**。
|
|
53
|
+
|
|
54
|
+
**新增导出**(5 个运行期符号,additive;公面 795 → **800**)
|
|
55
|
+
- `classifySelfOrchestrationRefusal(e: unknown): SelfOrchestrationRefusal | null` —— 判型:
|
|
56
|
+
**只认** `status === 501` ∧ `errorCode` **恰等**该码,其余一律 `null`(永不抛)
|
|
57
|
+
- `stripSelfOrchestrationIntent(req: TaskRequestLike): TaskRequestLike` —— 去掉顶层
|
|
58
|
+
`selfOrchestration` 与 `settings.ultracode` 两个意图键,返回新对象,幂等
|
|
59
|
+
- `projectWorkflowsGate(caps: unknown): WorkflowsGateProjection | undefined` —— caps 上 workflows
|
|
60
|
+
闸的三位投影(纯函数,永不抛)
|
|
61
|
+
- `SELF_ORCHESTRATION_RETRY_WITHOUT` —— 去键清单单源(`['selfOrchestration', 'settings.ultracode']`)
|
|
62
|
+
- `CAPABILITY_SELF_ORCHESTRATION_REQUIRED` —— 码字面单源(`src/engineErrorCodes.ts`)
|
|
63
|
+
- 类型(type-only,不进运行期基线):`SelfOrchestrationRefusal` / `SelfOrchestrationDenialReason` /
|
|
64
|
+
`WorkflowsGateProjection` / `WorkflowsGateUnknownDenial`
|
|
65
|
+
|
|
66
|
+
**三条判定纪律**(端接线前必读,详见 `docs/INTEGRATION-CLIENTS.md` §13)
|
|
67
|
+
1. **重发判据是合取,且不许放宽成前缀判**:该码是**复用码** —— 与它同前缀的兄弟(别的能力位
|
|
68
|
+
没接线)去掉这两个键**也不会**变成可受理。无码的 501 判不出 ⇒ `null`,绝不挑一个猜。
|
|
69
|
+
2. **去键只减两处**:`settings` 下别的子键与 `deferTools` 一个都不动(连坐删 = 用一次重试静默改掉
|
|
70
|
+
用户的其它设置);只有真删过 `ultracode` 且 `settings` 剩空才整键收口。additive 未知键全保。
|
|
71
|
+
3. **caps 的缺席 ≠ 关着,认不得的 `denial` ≠ 没有拒绝**:`workflows` 读不出 ⇒ 整只 `undefined`
|
|
72
|
+
(端零渲染);闸缺席 ⇒ `engineCan: undefined`;认不得的 `denial` ⇒ `{ unknown: … }`,
|
|
73
|
+
**绝不**折成 `null`(折了就等于向用户断言「没有任何拒绝」)。
|
|
74
|
+
|
|
75
|
+
**端的义务**(本包不做):重发**只一次**、第二次仍 501 按普通失败呈现;重发必须留一行诚实披露
|
|
76
|
+
(用户按的是「用 ultracode 跑」,实际跑的是不带编排的那一发);端若在用幂等键,重发必须换新键
|
|
77
|
+
(body 变了,复用旧键会让 server 回放那条 501)。措辞全归端 —— 本件零文案。
|
|
78
|
+
|
|
79
|
+
**异源对抗复审采纳两条**(均已同批修 + 同批补门,两条变异各自实测翻红):
|
|
80
|
+
- `SELF_ORCHESTRATION_RETRY_WITHOUT` 改为 **`Object.freeze`**(`as const` 只管编译期;判决的
|
|
81
|
+
`retryWithout` 与它是同一只引用 ⇒ 任一消费者 `splice` 一下就污染此后每一次判决,端照它去键
|
|
82
|
+
会删掉别的字段)。
|
|
83
|
+
- 分类器**只在 `501 ∧ 恰码` 两条判据通过之后才读 `denial`**(它不是判据,是判决上的附加位;
|
|
84
|
+
此前无条件先读 = 每一个根本不匹配的抛出物都被跑一次它的 getter)。同批把射程分界写清:
|
|
85
|
+
「getter 零执行」只对读 wire JSON 的 `projectWorkflowsGate` 成立,读抛出物的分类器刻意走普通
|
|
86
|
+
属性读取(抛出物可能是 SDK `APIError` 类实例,判据位坐在原型上),那一侧只承诺「不抛」。
|
|
87
|
+
|
|
88
|
+
**异源对抗复审第二轮再采纳一条**(档面缺陷,同批修 + 同批补门,变异实测翻红):
|
|
89
|
+
- §13b 首版只给了 `try { await submit(req) } catch { … }` 一种接法,而实装 SDK 的
|
|
90
|
+
`tasks.stream()` 是 **async generator** —— 调用它**不发请求**,POST 在 `streamRaw` 里、要等
|
|
91
|
+
**第一次迭代**才跑。照那一段接线的流式端永远进不了 catch ⇒ 分类器不运行、去键重发整条失效,
|
|
92
|
+
半配置部署上用户仍然直接看到 501。现已拆成两段范式(同步腿 / 流腿),并说明 generator 用过
|
|
93
|
+
不能重来、去键后要建新的一条;常驻门用**真 `TasksResource`** + 假传输层离线钉住两条腿的差异
|
|
94
|
+
并与档对账。
|
|
95
|
+
- 同批订正 §13c 的一处自相矛盾:四处不可信读取「accessor 一律当缺席」这句概括与上面的表冲突 ——
|
|
96
|
+
`gate.denial` 的 accessor 落 `{ unknown: '' }` 而**不是** `null`(键不在场才是「没有拒绝」,
|
|
97
|
+
在场却读不出是「拒了但读不出」),端照概括实现正好会犯本节要防的那个错。
|
|
98
|
+
|
|
99
|
+
**异源对抗复审第三轮再采纳三条**(同批修 + 同批补门,三条变异各自实测翻红):
|
|
100
|
+
- **类型面**:`stripSelfOrchestrationIntent` 补 **SDK `TaskRequest` 重载** —— 只留宽形
|
|
101
|
+
(`Record<string, unknown>`)会**擦掉**调用方的类型,一份 `TaskRequest` 进去出来就不再可赋回
|
|
102
|
+
`TaskRequest`,于是 §13b 那句「去键之后直接重发」在 TypeScript 上编不过、端只能靠 `as` 强转。
|
|
103
|
+
本函数删的两位在 `TaskRequest` 里都是**可选位**,所以窄重载是类型面成立的事实而不是宽容。
|
|
104
|
+
- **取消语义**:§13b 的范式补 `signal` 透传与重发前的 `signal?.throwIfAborted()` —— 首发 501 与
|
|
105
|
+
去键重发之间隔着一次分类 + 一次写披露,人完全可能就在这个窗口里按下停止;少了中断闸,端会在
|
|
106
|
+
用户已经喊停之后再发一条会跑工具、会烧 token 的 run,还写一行「已按不带编排的方式继续」。
|
|
107
|
+
- **门的假绿面**:G5 的假传输层原先直接从 `openStream` 抛一只自捏的 `{status, errorCode}`,把 SDK
|
|
108
|
+
真正那一跳(`openStream` 回非 OK `Response` → `errorFrom` 铸 typed error)整个绕过去了。现改为
|
|
109
|
+
回**真的 501 `Response`** 并接 SDK 自己的 `errorFromResponse`,钉调用序;同时把 §13b 两段范式
|
|
110
|
+
**照着实现跑一遍**(恰重发一次 / 第二发真去键 / 第二个 501 直接外溢 / 中断后零重发零披露),
|
|
111
|
+
替掉原先的关键词计数式对账。
|
|
112
|
+
|
|
113
|
+
**异源对抗复审第四轮再采纳三条**(同批修 + 同批补门,两条变异各自实测翻红):
|
|
114
|
+
- **[high] 干净 EOF 不等于跑完**:SDK 的 `tasks.stream()` 只在见到 `done` / `failed` 时 `return`,
|
|
115
|
+
流被中途干净截断(或一帧都没产出)时 `for await` **同样正常结束** ⇒ §13b ② 的 `runOnce` 会把一次
|
|
116
|
+
**被腰斩**的 live task 静默当成功。范式补 `sawTerminal` 判并在缺终帧时响亮抛;门的「成功」假体改发
|
|
117
|
+
真终帧、另加「有帧无终帧 + 干净 EOF」负控。这一条与去键重发**正交**(对每一条流都成立),
|
|
118
|
+
同批写进 §13d 端必读第 5 条。
|
|
119
|
+
- §13b 的 `{ transientOk: true, signal }` 在开了 `exactOptionalPropertyTypes` 的端上 **TS2379**
|
|
120
|
+
(SDK 声明的是 `signal?: AbortSignal`)⇒ 两处改条件展开;并让 G6c 把**档里那两段范式原样**送去
|
|
121
|
+
真 tsc 编(此前 G6 只编去键重载的赋值,档里编不过的范式照样能留着,而端是照档抄的)。
|
|
122
|
+
- 门的中断用例原先绕开共享 `drive` 另写了一段必然先 abort 再 `throwIfAborted` 的局部流程,
|
|
123
|
+
也没有零披露断言 ⇒ 改走同一条真 `drive`(经测试钩在分类后触发 abort),并把披露账改成
|
|
124
|
+
**调用方持有**的 out-param 使抛出路径上也观察得到,同时断言 `signal` 真的透传进了 generator。
|
|
125
|
+
|
|
126
|
+
**异源对抗复审第五轮再采纳四条**(同批修 + 同批补门,三条变异各自实测翻红):
|
|
127
|
+
- **[high] 取消原因保真**:`readSseFrames` 见到 `signal.aborted` 会**正常** return ⇒ 上一轮新加的
|
|
128
|
+
终帧判会把一次**用户主动取消**改写成 `Error: stream ended without a terminal frame`
|
|
129
|
+
(实测复现)。靠 `AbortError` 抑制报错 / 决定要不要重试的端会把取消渲成故障,甚至照着「故障」
|
|
130
|
+
再提交一次。⇒ 范式改成 `signal?.throwIfAborted()` **先于**缺终帧抛,门加「帧到手后在 handler 里
|
|
131
|
+
取消」的真 SDK 用例(终帧晚到的 `ReadableStream` 假体)并在档面钉住两条抛的先后顺序。
|
|
132
|
+
- 门的 `runOnce` 用 `frames.push(ev)` 顶替了档里的 `handle(ev)` ⇒「断言 handler 收到终帧」是假的;
|
|
133
|
+
`drive` 的 rethrow 臂也是 `return` 而不是 `throw`,与档不同形。⇒ 改跑档同形的 `handle` /
|
|
134
|
+
`transcript.note` 真 spy,`drive` 改成逐字同形(认不得就抛),观察挪进 harness 侧薄封装;
|
|
135
|
+
披露账补**引用同一性**正面断言(否则中断腿那条「零披露」是恒真断言)。
|
|
136
|
+
- G6c 把两段范式**合并**成一个虚拟模块编译,于是第二段自身缺 import 搭上了第一段的便车 ⇒
|
|
137
|
+
「两段范式原样编得过」不成立,端只抄第二段会 TS2304。改为**逐块独立**编译、前缀只声明宿主提供位
|
|
138
|
+
(一个库导出都不注入),两段范式各自补全 import,并加「去掉自带 import 必须红」的第二条负控。
|
|
139
|
+
- §13d 标题写「四条」而实际已列五条 ⇒ 标题订正,并把标题计数与真正列出的条数做成门里的闭合检查。
|
|
140
|
+
|
|
141
|
+
**异源对抗复审第六轮再采纳五条**(同批修 + 同批补门,三条变异各自实测翻红):
|
|
142
|
+
- **[high] 终帧优先于取消**:上一轮把取消判放在缺终帧抛之前,但它是**无条件**的 —— 取消恰好落在
|
|
143
|
+
**终帧交付之后**时(实测:在 `handle(done)` 里 abort),一条已经有结局、可能已落副作用的 run 会被
|
|
144
|
+
报成取消,而人多半会再跑一遍 ⇒ 重复执行。⇒ 取消判与截断判都关进 `if (!sawTerminal)`,
|
|
145
|
+
门加 `handle(done)` / `handle(failed)` 内 abort 的两向回归。
|
|
146
|
+
- 门的披露路径没有跑档里那句 `transcript.note(disclose(...))`(harness 直接 push 数组)⇒ 删掉档里的
|
|
147
|
+
`disclose` 或换成错文案都不会红。改成两个真 spy 并让 `disclose` 铸哨兵串,断言 `note` 收到的就是它。
|
|
148
|
+
- 顺序判锚在**整节**上,而整节里第一处 `throwIfAborted()` 是**同步腿**的中断闸 ⇒ 把流腿改成反序仍绿。
|
|
149
|
+
改为先切出流腿 `runOnce` 的函数体再比位置,并加反序正控。
|
|
150
|
+
- G6c 的「去掉自带 import 必须红」负控**恒红** —— 红的是前缀自己(`Parameters<typeof buildTaskRequest>[0]`),
|
|
151
|
+
证不出正文缺 import。前缀改用内联 import 类型与范式解耦,负控加**归因**(诊断必须点名正文里的库符号)
|
|
152
|
+
与「前缀单独编零诊断」前提;提取范围同批扩到 **§13 全节**(§13c 那段端也会单独抄走)。
|
|
153
|
+
- §0a 未发导出说明写「三个 type-only 形」而实际列了四个 ⇒ 订正并把这个计数也纳入门里的闭合检查。
|
|
154
|
+
|
|
155
|
+
**异源对抗复审第七轮再采纳三条**(同批修 + 同批补门,两条变异各自实测翻红):
|
|
156
|
+
- **[high] 每帧取消闸**:一次 SSE 读取会缓冲**多帧**,SDK 连着把它们 yield 出来(真实时序,不属
|
|
157
|
+
§12e 排除族)。只在整个 `for await` 结束后查 signal ⇒ 人已经喊停,仍会消费同一 chunk 里的后续帧 ——
|
|
158
|
+
实测:同 chunk `turn_start` → `done`,在 `handle(turn_start)` 里 abort 仍吃掉 `done` 并**报成功**;
|
|
159
|
+
后一帧若是坏 JSON,取消还会被一条 `SyntaxError` 盖掉。⇒ 非终帧之后立刻 `throwIfAborted()`,
|
|
160
|
+
终帧那一支先 `break`(「终局优先」在这里也成立)。
|
|
161
|
+
- 门的行为矩阵此前跑的是**脚本内手抄的流腿副本**,同步腿更是只验了「首发 501 可分类」⇒ 新增 **G7**:
|
|
162
|
+
把 §13b 两段范式**抽出来转译成可执行模块**(库 import 重写到本仓 `dist`)注入宿主位后**真跑**,
|
|
163
|
+
两条腿各过一遍完整行为矩阵,披露按 `disclose` 铸的哨兵串与档里那一句机械对账。
|
|
164
|
+
- 顺序判此前逐字符切函数体 + `indexOf` 找表达式 ⇒ 注释/字符串里的同名语句能骗过它(实测反例:注释里
|
|
165
|
+
按正确顺序写诱饵、真代码反序,断言照绿)。改成**锚 TS AST**,并带「注释诱饵 + 真语句反序」负控。
|
|
166
|
+
|
|
167
|
+
**常驻门**:`scripts/run-self-orchestration-denial-test.mjs`(207 checks;条数下限零松量)。
|
|
168
|
+
**棘轮**:`public-export-baseline` 795→800;typeshape `unknownExport` 262→264(两处边界读口入参,
|
|
169
|
+
逐条登记);portability index 闭包 140→141。
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
43
173
|
## 0.49.0(2026-09-03)
|
|
44
174
|
|
|
45
175
|
### 新增:崩溃收敛的孤儿审批读面(L-38;server ≥7.55.0)
|
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ Renamed from **`@sema-agent/wire-cc-adapter`** (0.1.x, deprecated — see *Migra
|
|
|
35
35
|
|
|
36
36
|
## Scope
|
|
37
37
|
|
|
38
|
-
**Version:** 0.
|
|
38
|
+
**Version:** 0.50.0
|
|
39
39
|
|
|
40
40
|
- **Today** — the adapter seam, the whole `adapt()` pipeline (all 14 A-layer arms plus the
|
|
41
41
|
B/D/E tool-card layers), the notification/caps/model families, the adapter kernel (stream driver
|
|
@@ -252,6 +252,7 @@ public-surface guard checks that last one).
|
|
|
252
252
|
| `scripts/run-durable-card-display-keys-test.mjs` | The durable approval row's two display keys survive the row→card recast in `surfaceFsApprovalAndDecide`: `governanceForced` stamps on strict `true` only (absence is "no evidence", never `false`), `ruleSuggestions` passes through the same shape-narrowing reader as the live-frame leg and lands on the **read-only** card key — plus a standing pin that the durable leg never stamps the redeemable `ruleSuggestions` card position (the `/decide` body has no rule slot; offering a "don't ask again" option there would be an affordance nothing can honour) |
|
|
253
253
|
| `scripts/run-session-memory-status-test.mjs` | The session **memory-status** read face (S-53): the two judgements three clients would otherwise each get wrong. First, *same status, different code* — this route's 404 carries two unrelated meanings (`not_found.session` = unknown or non-owned session; `not_found.route` = a pre-7.53 server that has no such route at all), so dispatching on the **status** would report "your deployment lacks this surface" as "your session does not exist". The verdict is anchored on `errorCode`, the two 404s are pinned to **different** verdicts, and — the load-bearing negative control — a 404 carrying **no** code falls to `failed` rather than guessing either way, since a wrong guess in either direction is a false statement a user would act on. 501 is allowed a codeless fallback because both of its arms mean the same thing here, and `capability.*` stays split from `feature.*` because those two share a status while their dispositions are opposite. Second, *absence means something different per key*: `optOutSource` and `lastCaptureAt` are legitimately absent on a **healthy** session (a zero-history session really is `{captureOptedOut:false, committedCount:0, foldedCount:0}` with no degradation at all), so reading absence as "off/none/0" asserts something unprovable. Two combined readers are pinned: capture opt-out is read from **both** its keys (a record-store fault yields `indeterminate`, never `active` — the difference between "your conversation is being remembered" and "nobody knows"), and last-capture is a **three-state** read whose discriminator is the *other* key, because `lastCaptureAt`'s absence alone covers both "ledger unreadable" and "genuinely no contributions" and therefore decides nothing; the two shapes are pinned to different verdicts so a single-key read turns red. The thin wrapper is the only IO: it never throws, drops malformed keys to absence rather than trusting them (an unreadable value must answer "don't know", never render as truth), refuses to spend a request on an empty `sessionId`, and passes `signal` through untouched |
|
|
254
254
|
| `scripts/run-crash-converged-projection-test.mjs` | The `crashConverged` read face on `GET /v1/approvals` (L-38): what the *previous life* of a crashed local engine left behind, projected for every client. Three judgements are pinned. First, **absence is not an empty list** — a missing key (an older server, deps not present, or a carrier that is not an array at all) returns `undefined`, and the client renders nothing; an empty array returns a present zero-count object, which is the server actually saying "none". Folding the first into `{total:0}` would have the client assert "nothing was left behind" on a surface a person uses to decide whether it is safe to re-run something — the worst possible direction for a false statement — so the two cases are pinned to different **return shapes** and a test asserts the two verdicts are unequal. Second, bucketing is a **four-term conjunction**: `orphanState === 'pending'` *and* `resumeSafe === true` *and* both approval-evidence keys (`originalDecision`, `decidedAtMs`) absent. A fifth term rejects any row carrying an **accessor**, and accessors are never invoked at all — reading one means synchronously running someone else's code, and `catch` catches throwing, not *never returning*, so a looping getter would pin the startup thread forever (the row cap does nothing against that shape). The same rule covers the three untrusted reads outside the row as well — the envelope's `crashConverged` key, the carrier's `length`, and every numeric index are read as own property *descriptors* and only data descriptors are used, so accessors and prototype entries read as absent and are never invoked. Such a key is treated as absent: if it was a required field the row is counted as dropped, if it was optional or additive the row survives without it. That also closes the ordering attack, since spreading runs getters in property order and an earlier one could `delete` the approval evidence before it is ever copied (measured before the fix: such a row reached the resume-safe bucket), and the check therefore moves ahead of the read, onto the property descriptors — from which the snapshot is then built directly, because checking descriptors and *then* spreading is two independent observations of the same row, and a non-throwing proxy can make the two `ownKeys` calls disagree (first showing `originalDecision: 'approve'` so the row reads as plain data, then omitting that configurable key so the snapshot loses the evidence; measured before the fix: the dangerous row reached the resume-safe bucket after exactly two enumerations, and after it, one). Keys are written with `Object.defineProperty` rather than plain assignment, because `'__proto__'` is a legal own enumerable key and `o['__proto__'] = x` does not store a value — it calls the prototype setter, letting a row whose own properties are all plain data (so the accessor gate never fires) inject a prototype whose `sessionId` getter deletes the approval evidence from the snapshot during validation; `defineProperty` fires no setter, so the key survives as ordinary additive data and the snapshot keeps `Object.prototype`. A row that simply arrives with a custom prototype is treated the same way, since the snapshot only enumerates own properties: approval evidence sitting on the prototype would never reach it, and a perfectly ordinary object with no proxy and no accessors could otherwise be called safe to re-run — real bodies come from `JSON.parse` and always carry `Object.prototype`, so nothing genuine trips it). Validation itself runs on a **null-prototype** dictionary and the bucketing verdict is carried out of that same pass rather than re-read from the delivered row, because every property lookup on an ordinary `{}` reaches `Object.prototype`: a polluted `sessionId` getter there would delete the approval evidence from the snapshot mid-validation and send the row to the safe bucket (measured before the fix). The row handed to the client is still an ordinary object — the null prototype is an implementation detail of the check, not of the value) — real JSON bodies are all data properties, so only a middle-layer-synthesised payload ever trips it, and it too lands in the human bucket rather than being dropped. The `decided` arm means the human had already approved and side effects may be half-landed, so it always goes to the human bucket, as does `resumeSafe === false` and — the last two terms — any row whose own fields contradict each other, since `pending` claims nothing ran while that evidence says somebody pressed approve. Deciding "not safe" costs one extra question (recoverable); deciding "safe" wrongly has somebody re-run work that already partly happened (not). A 2x2 truth table pins that exactly one cell is resume-safe, so reading either key alone turns red, and the contradictory rows are routed to the human bucket rather than dropped — they are real orphans, and the ones most worth showing. Third, unreadable rows are **dropped and counted**, never thrown and never passed through: the product is declared as `CrashConvergedRow`, so letting a row missing a required field — or carrying one of the wrong type — past would be a lie at the type level, and the closed literal discriminators (`decision` / `cause` / `orphanState`) decide family membership rather than being an open vocabulary. The measuring stick stops at the **type** floor, though: degenerate-but-well-typed values (`ts: NaN`, an empty `toolName`) are kept, because swallowing a real orphan over a decorative field is the worse direction, and the one deliberate exception is `approvalId`, which must be non-empty to be a row identity at all. `dropped` is kept separate from `total` so unreadable rows never inflate "N approvals were affected"; each row is a **one-shot snapshot** — every own enumerable key is read exactly once, and validation, bucketing and the handed-back value all read that same snapshot, so additive upstream keys survive while a **non-idempotent** getter (one that never throws, just answers differently on a second read) can no longer erase the approval evidence between the check and the bucketing (measured before the fix: such a row landed in the resume-safe bucket while its checked value was `"approve"`). Hostile carriers are counted rather than allowed to reject: **every** touch of the carrier is guarded — envelope property reads, `Array.isArray` itself (it throws on a revoked proxy), the `length` read, each indexed read and each row's property reads — and a traversal that dies halfway returns absence rather than a half-counted total. A row that cannot be read never takes the batch with it: its own shape check is inside its own guard, so one revoked-proxy row costs a `dropped` tick rather than collapsing the whole projection to absence — which a client would have read as "this deployment does not offer the surface". Traversal goes by **numeric index, never the carrier's own iterator protocol**, because `for...of` hands the carrier the question of which rows exist: an array carrying an overridden `Symbol.iterator` can yield nothing (measured before the fix: a real orphan became `{total:0}`, which a client reads as "the server said there are none") or swap a dangerous `decided` row for a safe-looking one (measured: `fake-safe` was returned in place of `real-danger`). Row count is capped at 100000 and the cap is checked **before** the walk: requiring only a non-negative integer `length` does not stop a proxy trap reporting a billion, and this surface runs on the startup / `--resume` path, where a synchronous spin freezes the thread (measured before the cap: twenty million rows took 18.3 seconds and twenty million index reads; a billion does not come back). The honest boundary is stated rather than overclaimed — a proxy can still lie in its `length` or index traps, which is the same thing as a host injecting a lying transport — and the widening of `ApprovalsResourceLike.list()` is proven **additive** by really running tsc over a legacy `{ pending }` mock *and* over the real `AgentClient` path — the projector takes `unknown` precisely because a parameter shaped as "an object with an optional `crashConverged`" is a TypeScript weak type that the installed SDK's own `list()` return shape shares no property with, which only a real-client compile would have caught — with a known-red control so a clean run means the checker spoke |
|
|
255
|
+
| `scripts/run-self-orchestration-denial-test.mjs` | The three judgements behind a **denied self-orchestration request** (server 7.57.0), each of which all three clients would otherwise get wrong on their own. First, whether to retry at all is a **conjunction that may not be loosened**: HTTP 501 *and* an `errorCode` that is **exactly** `capability.self_orchestration_required`. That code shares its shape with every other `capability.*` 501, so dispatching on the prefix would drag "some other capability is not wired up" into the retry arm — those requests do not become acceptable once the two keys are gone, so the client would spend a request and then tell the user the wrong reason. Negative controls cover all four directions: a sibling `capability.*` code, a truncated or suffixed variant of the right one, a codeless 501 (it decides nothing, so it decides nothing — no guessing), and the right code under 500 / 400 / 503 or a string `"501"`. The classifier reads structurally rather than by `instanceof` (a host may inject its own transport; across realms or duplicate SDK instances an understandable error would read as unreadable), so a class instance, a bare `{status, errorCode}` literal and an error carrying those fields on its **prototype** all reach the same verdict — and a hostile proxy or a throwing getter yields `null` instead of throwing, because this classifier runs inside a `catch` block where anything it throws escapes the caller's own guard. Second, removing the intent is a **structural** operation, not wording: `selfOrchestration` sits at the top level while `ultracode` sits under `settings` — two different stamping legs — and a client hand-writing `delete` will miss the second one, which costs the user the same failure twice. The single stripper is pinned to touch exactly those two: other `settings` sub-keys and their values survive byte for byte, `deferTools` is left alone (pulling `Workflow` out would be a behaviour change, not a removal of intent), additive unknown keys survive at both levels, the input object is never mutated, `settings` is only dropped entirely when `ultracode` was really there and nothing else remains (an already-empty one is left as is), a non-object `settings` is not touched at all, an `ultracode` that only exists on the prototype does not count, and the whole thing is idempotent. The end-to-end leg runs a real `buildTaskRequest` product through it and asserts the stripped body still passes the registration gate key by key. Third, on the capabilities body, **absence is not "switched off"**: a pre-7.57 server has no `workflowsGate` key at all, so reading absence as "the engine says no" asserts something the server never said, and the mirror-image disease is folding an **unrecognised** `denial` into `null`, which would have the client render "nothing was denied" when the truth is "denied, for a reason I do not recognise". Five shapes are pinned — caps unreadable, gate absent, closed-set member, unknown value, accessor — with the unknown arm carrying the raw token (or an empty one when the value is not even a string) and never collapsing to `null`. All four untrusted reads go through own **data descriptors** only, and the guard pins the getter invocation count at zero, since `catch` catches throwing but not *never returning*; a descriptor trap that throws and a revoked proxy both yield honest absence rather than an exception — though *what* absence means differs by field, and the guard pins that split rather than a blanket rule: an accessor on `workflows`, `workflowsGate` or `engineCan` reads as absent, while an accessor on `denial` reads as `{unknown:''}`, because a key that is **not there** is the gate saying "nothing was denied" whereas a key that is there but cannot be read is "denied, and I could not read why" — folding the second into the first is exactly the false statement this face exists to prevent. Two further pins came out of an adversarial review. The exported retry list is **frozen at runtime**, not merely `as const`: the verdict hands out that same reference, so any consumer splicing it once would poison every later verdict in the process — the guard asserts `Object.isFrozen`, that four different mutation attempts leave it byte-identical, and that a verdict issued *after* those attempts still carries the original two entries. And the classifier reads `denial` only **after** both criteria have passed, since it is not a criterion but an extra field on the verdict: the guard pins the getter invocation count at zero for any error that does not match and at most one for an error that does. The scope line is drawn explicitly rather than overclaimed — "no getter ever runs" holds for `projectWorkflowsGate`, which reads **wire JSON** where every field is an own data property by definition, but not for the classifier, which reads a **thrown value** that may well be an SDK `APIError` class instance carrying `status` and `errorCode` on its prototype; insisting on own data descriptors there would report a perfectly readable error as unreadable, so that side promises only that it never throws. A final pin covers the **integration document's own worked example** rather than the library: the shipped SDK's `tasks.stream()` is an `async` generator, so calling it issues no request at all — the POST happens inside `streamRaw` on the first iteration, and a `try` wrapped around the `stream(...)` call itself can never catch the 501. A client following a submit-shaped recipe on the streaming leg would never run the classifier, and the whole strip-and-retry path would silently do nothing. The guard drives the **real** `TasksResource` against a fake transport, offline, and pins both halves: the synchronous leg is in flight the moment it is called, the streaming leg has issued zero requests after the call and raises on the first `next()` — and it does so through the **real** error path, with `openStream` returning an actual 501 `Response` that the SDK's own `errorFromResponse` turns into the typed error, pinning the `openStream`→`errorFrom` call order so a transport that stops minting `errorCode` cannot pass. The documented recipe is then **executed** rather than keyword-counted: exactly one retry, a second body that really lost both keys while every other setting survives byte for byte, the caller's own request object left untouched, one disclosure and only one, a second 501 propagating with the request count still at two, and — after the first 501 — an abort leaving the count at one with nothing disclosed. A last leg is type-level: `stripSelfOrchestrationIntent` carries an SDK `TaskRequest` overload, because the wide `Record<string, unknown>` form erases the caller's type and the document's "strip and resubmit" line would not compile without an unsafe cast; a real tsc run over a virtual file proves both the narrow and the wide path, with a known-red control — and it compiles the document's two recipes **verbatim**, extracted from the section itself, because a recipe that does not compile is a recipe that was never given: `{ transientOk: true, signal }` is a TS2379 under `exactOptionalPropertyTypes`, which no amount of prose review had caught. The last thing pinned is the one that would have been quietest of all: the SDK's `stream()` returns only on a `done` or `failed` frame, so a stream truncated mid-run — or yielding nothing at all — ends the `for await` just as normally as a completed one. The documented `runOnce` therefore tracks whether it ever saw a terminal frame and raises when it did not, the guard's success fixture emits a real terminal and asserts the handler received it, and a truncated-stream control asserts that shape is reported as a failure with no retry and nothing disclosed. That terminal-frame rule then needed one more turn of its own: the underlying reader returns *normally* when the signal is aborted, so the check as first written rewrote a user's cancellation into a generic stream fault — a client keying off `AbortError` to suppress the error would instead have shown a failure, or resubmitted. Cancellation is therefore checked first, a real-SDK case aborts from inside the handler and asserts the original `AbortError` survives with no retry and nothing disclosed, and the document is checked for that ordering. The harness runs the documented `handle` and `transcript.note` as real spies rather than pushing frames itself, the drive loop rethrows exactly as the document does, and the disclosure ledger is proven to be the caller's own array by a positive identity assertion — without which the cancellation leg's "nothing disclosed" would have been vacuously true. Each recipe is compiled **on its own**, with a preamble that declares only what a host supplies and injects no library symbol, since compiling them together let the second one borrow the first one's imports, and the preamble's own types are decoupled from what the recipes import so the "remove the imports and it must fail" control fails for the right reason — which is checked by attribution, not merely by redness. Ordering is the last thing to get right: the cancellation check must come before the truncation error but **both** must sit behind the terminal-frame test, because a cancellation that lands after the run already reported `done` would otherwise overwrite a real outcome — one that may have already had effects — with "cancelled", and a person reading that will run it again. Aborting from inside `handle(done)` and `handle(failed)` are both pinned to still report success, and the ordering assertion is anchored inside the streaming `runOnce` body rather than the section, since the section's first `throwIfAborted` belongs to the synchronous recipe and would have made a reversed streaming recipe pass — and that ordering check is now anchored on the TypeScript AST rather than on text, since a comment reproducing the two statements in the right order let a genuinely reversed body pass. One more timing fact had to be written into the recipe: a single SSE read buffers several frames and the SDK yields them back to back, so checking the signal only after the loop lets a cancelled run keep consuming the rest of the chunk — measured, an abort inside `handle(turn_start)` still swallowed the `done` that followed and reported success. The recipe therefore re-checks after every non-terminal frame. Finally, the behavioural matrix is no longer run against a copy of the recipe: both recipes are extracted from the document, transpiled, and **executed** with injected host objects, so the disclosure assertion really exercises the document's own `transcript.note(disclose(...))` line, and the synchronous leg gets the same full matrix the streaming one does |
|
|
255
256
|
| `scripts/run-package-hygiene-test.mjs` | Everything `package.json` `files` ships — dist JS/typings and the Markdown docs — is screened line-by-line against a deny-list of strings that must never appear in a published artefact. The guard first proves each pattern still bites on a constructed sample (a screen that cannot fail is worse than none) and honours a per-pattern allow-list for legitimate product vocabulary, so the verdict is "clean surface", not "quiet grep". |
|
|
256
257
|
| `scripts/run-integration-doc-freshness-test.mjs` | The **integration contract** (`docs/INTEGRATION-CLIENTS.md`) and the **changelog** (`CHANGELOG.md`) checked against the code, because a document with no guard rots — this one had a whole nest of drift found on it within a day of being written. Five directions, each a claim a machine can actually evaluate. (1) *Counting discipline*: the version-anchor row for the guard count may no longer carry a hand-copied number at all — it changes every time a guard is added, and writing it down is planting a timer; the export counts that are still hand-copied (the surface total, the test-hook count, the sentence describing the surface's internal composition, the sum of the sixteen domain rows, and the three sub-counts) are each compared against a value **derived** from `public-export-baseline.json`, which is the drift a human reviewer caught last time. (2) *Coordinates alive*: every `src/` `scripts/` `docs/` path the doc quotes must be on disk **and tracked by git** — on disk is not in the repo, and a doc that points readers at a file living only in its author's working tree sends every clone to nothing. A file landing in the same commit takes a named carve-out that **stops applying** the moment the file is really tracked (it can no longer let anything through, and the guard prints a line asking for it to be deleted) — deliberately not a red, since turning red on the very commit that lands the file would just manufacture a break that only a follow-up commit could clear. (3) *Arm tables*: the `hitl_out_of_slice` row and the `not_in_slice` fenced list must equal, name for name and in **both** directions, the case labels that really fall into those two buckets — read through the **TypeScript AST**, since which bucket an arm lands in is decided by the argument to `nothing(...)` and by nothing a comment says. The extractor is anchored to the one production projector: exactly one function named `eventToSdkMessage`, exactly one `switch (ev.type)` inside it, and no repeated case label — anything else is a broken anchor rather than a verdict, because a second same-shaped switch elsewhere in the file would otherwise overwrite the real one's conclusions and leave the doc agreeing with a switch nobody runs. The list is delimited by a machine-readable fence rather than by section headings, because the same section also names the terminal arms as a counter-example and prose boundaries cannot tell a member from a foil. (4) *Released sections are frozen*: an **append-only ledger** carries every version ever published — its number, the commit it was published from, and the sha256 of its section — and each one is checked, not just the current release, since pinning only the latest would set every earlier version free the moment the next one ships. The ledger cannot vouch for itself either: each recorded hash is **re-derived from that release commit** through git, so editing an old section and its constant together no longer passes — and the commit the row names is in turn checked against the `gitHead` npm recorded at publish time, which is the one value this repository cannot rewrite, so pointing an old version at a freshly written commit does not pass either. The *set* of versions that must be frozen comes from the registry too, so deleting an old row together with its section — which would otherwise remove that version from every set the guard looks at — is red rather than invisible. A failed registry call is classified rather than swallowed, and the classification consults the registry's own status code *before* it considers connection-level symptoms, so an auth refusal whose body happens to mention the network is still red rather than a skip. The version set is compared as full SemVer including prereleases — matching only `x.y.z` would silently drop a published `0.30.0-beta.1` and reopen the very hole this direction closes — and section headings are matched on a whole-version boundary so a stable release cannot bind itself to the release-candidate section sitting above it. Publishing itself is a two-phase protocol rather than a paradox: before a release, exactly one row may be marked pending and must name the current `package.json` version, exempt from the checks whose inputs do not exist yet; once the registry has that version the row must be promoted, so the temporary state cannot survive its own release. And because the pending exemption rests entirely on "this version is not out yet," it is refused outright when the registry cannot be reached to confirm that — an unverifiable premise is not a licence. Three reverse directions close the rest: a section claiming to be released but absent from the ledger, a ledger entry whose section has vanished, and a `package.json` version that was never frozen. Publishing appends a row; it never rewrites one. (6) *Sentinels*: the readers §5a hands hosts for "is this port installed" are checked against what the source actually declares it returns — `hasXxx()` is a `boolean`, the card port / HITL surface / wire target return `T | null`, the `installHost` family returns `T | undefined`. Testing a `null`-returning reader for `!== undefined` is *always true*, and a self-check that passes whether or not the port is installed is worse than none, because hosts retire their own fallback on the strength of it. Both directions are red: an implementation that changes its sentinel without the doc following, and a doc that names the wrong one. The roster covers the zero-argument readers and their `*For` variants alike — a multi-session host reads the variants, so leaving them off would let exactly the surface desktop depends on drift unwatched — and the §5a table and the §8-B checklist line are each checked against the source, because hosts tick the checklist, and a guard that only watches the prose table misses the line people actually follow. (5) *Packaging*: the README ships with the package and opens by pointing hosts at the integration doc, and the checklist names two more files as required reading before an upgrade — all three must really appear in the `npm pack` manifest, or an npm consumer follows a relative link that npmjs rewrites onto a private repository. Missing tooling never takes the whole verdict down with it: when git, npm or the registry is unreachable those legs print the `SKIPPED-SECTION` marker and the rest still judges, while a release commit the ledger names but git cannot resolve is red rather than skipped. The guard says in its own header what it does **not** do: it judges counts, coordinates, arm sets, released bytes and the packing list — whether a sentence is *right* is still for review and for the hosts to report |
|
|
257
258
|
| `scripts/run-type-superset-ledger-test.mjs` | The type/wire **superset ledger** (`docs/type-superset.json`): positions this package adds on top of a CC-shaped contract, each carrying the evidence for what CC's own type surface does or does not have there. Completeness is deliberately uneven and the ledger says so. The `_sema_*` private-key class is checked in **both** directions (a key in the source that never entered the ledger is red, naming key and file; a ledger row whose key left the source is red) — but only for keys written as literals, which is the convention the ledger mandates. A key assembled by string arithmetic is beyond what any static rule can enumerate, so the guard fails closed on every shape it *can* decide (a bare `_sema_` prefix is red wherever it appears, save one pinned guard site) and leaves the rest as a convention violation for review to catch, rather than claiming a completeness it does not have. The two hand-surveyed classes are only checked for coordinate and evidence integrity, never discovered. Both directions read the source through the **TypeScript AST**, not a text scan, and they read two different sets out of it. A *key site* is an identifier, or a string whose whole value is the key — so `'_sema_decision-v2'` is carried whole rather than truncated at the first non-identifier character into some *other* key that happens to be registered. A *mention* is the key appearing inside a longer string, which is prose, not usage. The staleness direction counts key sites only: a comment or a doc sentence left behind after the last real mint site is deleted must not keep the row alive (mutation-proven — with both the comment and the prose string untouched, removing the one real site turns the guard red). And because a prefix can be concatenated or interpolated into a key no static set will ever see, the bare `_sema_` literal is refused outright rather than traced: every occurrence is red except the single inline `startsWith` guard the sanitizer needs, because the set of expressions a bare prefix can travel through on its way to a concatenation is open-ended and enumerating it is always one form behind. Every row's `host` must still resolve, with the key being a real **member of that declaration** rather than a string occurring somewhere in the same file — `governanceForced`/`delegation` each live on two different shapes in one file, and a member commented out is a member deleted, which a text-shaped check happily reads as still present. And the direction worth the most: each machine-form `ccAbsenceEvidence` is re-derived from the row's own `key` — the ledger's recorded string must match that derivation verbatim, since a row quietly witnessing `\bnever_present\b` is green forever while watching nothing (mutation-proven: the same edit passes the unbound form and is caught by the bound one) — and the check runs against the names the installed `@sema-agent/agent-types` `.d.ts` set actually declares, parsed with the TypeScript AST rather than grepped, so a name CC merely mentions in a comment cannot force the row into the manual escape hatch and thereby retire the very witness that was supposed to fire the day CC declares that name for real. That escape hatch is gated by an allowlist living **in the guard**, not the ledger, so claiming it costs a reviewed diff. Missing material never reads as a pass, and the verdict splits by *why* it is missing: no TypeScript parser skips the suite before it starts; a missing `agent-types` still runs and prints the first three directions, then exits **1** when `package.json` declares the mirror but it is not installed — a broken install must not retire the repository's only "the day CC declares this name" alarm, and reporting it as a skip would leave "never evaluated" and "evaluated, no drift" indistinguishable to the runner — and exits 3 only when nothing declares the mirror at all, which is the one case where the direction genuinely does not apply. Either way a run that evaluated no witness is never counted as one that did. When the mirror *is* present its **installed version** is witnessed too (the two declared floors must agree with each other and the installed copy must meet them), since four preflight probes are satisfied by an arbitrarily stale mirror — they prove the extractor speaks, not that it is current. Every direction carries a positive control — known-present CC symbols, a comment-only sample proving the extractor distinguishes declaration from mention, and synthetic corpora fed through the **same** discriminator function the real verdict uses, so a verdict quietly rewritten to return nothing takes its own control down with it |
|
|
@@ -129,6 +129,22 @@ export declare const TOOL_END_INTERRUPTED_CODES: ReadonlySet<string>;
|
|
|
129
129
|
/** 这条 `tool_end` 是不是「中断留下的合成收口帧」。未知码 ⇒ false(开集:不认得就不认得,
|
|
130
130
|
* 绝不猜)。 */
|
|
131
131
|
export declare function isInterruptedToolEndCode(code: string | undefined): boolean;
|
|
132
|
+
/**
|
|
133
|
+
* 提交面的 selfOrchestration 准入拒绝(`POST /v1/tasks` 与同闸的 stream 提交,**501**)。
|
|
134
|
+
*
|
|
135
|
+
* 语义:引擎侧的 workflow 开关是开着的(`SELF_ORCHESTRATION_ENABLED=true`),但这台部署处在
|
|
136
|
+
* **半配置的多租户形态** —— 要求 principal、却没有中心侧的准入解析器,于是它无法判「这一位
|
|
137
|
+
* 调用者能不能用 workflow」,fail-closed 拒。请求带 `selfOrchestration:true` **或**
|
|
138
|
+
* `settings.ultracode:true`(后者由 server 展开成前者)就撞这一条;把这两个键去掉,**同一条
|
|
139
|
+
* 请求照常受理**。
|
|
140
|
+
*
|
|
141
|
+
* 🔴 **这是复用码**:它与其它 `capability.*` 501 共用体形,而处置**不同** —— 别的能力位
|
|
142
|
+
* 缺席时去掉这两个键也不会变成可受理。⇒ 消费点必须按**恰等**判,绝不放宽成 `capability.`
|
|
143
|
+
* 前缀判(那是 `sessionMemoryStatus` 那条读面上的另一件事,两者刻意不合流)。
|
|
144
|
+
* 判型与「去键重发一次」的判定归 `src/selfOrchestrationDenial.ts`(文案归端)。
|
|
145
|
+
* 🔴 非布尔的 `selfOrchestration` 走的是 **400**,不是本码 —— 那是「值不对」不是「不给用」。
|
|
146
|
+
*/
|
|
147
|
+
export declare const CAPABILITY_SELF_ORCHESTRATION_REQUIRED = "capability.self_orchestration_required";
|
|
132
148
|
/** 409 session-busy 的 canonical 码([2377]C-1,server main `049ff2c`,随 5.0.0 发)。 */
|
|
133
149
|
export declare const ACTIVE_RUN_BUSY_ERROR_CODE = "conflict.session_active_run";
|
|
134
150
|
/** 仲裁店不可达 ⇒ **真相未知**(core 1.397 三分的第三形;塌进 `not_landed` 是诚实缺陷)。 */
|
package/dist/engineErrorCodes.js
CHANGED
|
@@ -182,6 +182,23 @@ export const TOOL_END_INTERRUPTED_CODES = new Set([
|
|
|
182
182
|
export function isInterruptedToolEndCode(code) {
|
|
183
183
|
return typeof code === 'string' && TOOL_END_INTERRUPTED_CODES.has(code);
|
|
184
184
|
}
|
|
185
|
+
// ── 能力准入族(server 7.57.0 S-81;`capability.*` 是**开集前缀**,本行只登记其中一个成员)────
|
|
186
|
+
/**
|
|
187
|
+
* 提交面的 selfOrchestration 准入拒绝(`POST /v1/tasks` 与同闸的 stream 提交,**501**)。
|
|
188
|
+
*
|
|
189
|
+
* 语义:引擎侧的 workflow 开关是开着的(`SELF_ORCHESTRATION_ENABLED=true`),但这台部署处在
|
|
190
|
+
* **半配置的多租户形态** —— 要求 principal、却没有中心侧的准入解析器,于是它无法判「这一位
|
|
191
|
+
* 调用者能不能用 workflow」,fail-closed 拒。请求带 `selfOrchestration:true` **或**
|
|
192
|
+
* `settings.ultracode:true`(后者由 server 展开成前者)就撞这一条;把这两个键去掉,**同一条
|
|
193
|
+
* 请求照常受理**。
|
|
194
|
+
*
|
|
195
|
+
* 🔴 **这是复用码**:它与其它 `capability.*` 501 共用体形,而处置**不同** —— 别的能力位
|
|
196
|
+
* 缺席时去掉这两个键也不会变成可受理。⇒ 消费点必须按**恰等**判,绝不放宽成 `capability.`
|
|
197
|
+
* 前缀判(那是 `sessionMemoryStatus` 那条读面上的另一件事,两者刻意不合流)。
|
|
198
|
+
* 判型与「去键重发一次」的判定归 `src/selfOrchestrationDenial.ts`(文案归端)。
|
|
199
|
+
* 🔴 非布尔的 `selfOrchestration` 走的是 **400**,不是本码 —— 那是「值不对」不是「不给用」。
|
|
200
|
+
*/
|
|
201
|
+
export const CAPABILITY_SELF_ORCHESTRATION_REQUIRED = 'capability.self_orchestration_required';
|
|
185
202
|
// ── 会话冲突族 ────────────────────────────────────────────────────────────────────────────────
|
|
186
203
|
/** 409 session-busy 的 canonical 码([2377]C-1,server main `049ff2c`,随 5.0.0 发)。 */
|
|
187
204
|
export const ACTIVE_RUN_BUSY_ERROR_CODE = 'conflict.session_active_run';
|
package/dist/index.d.ts
CHANGED
|
@@ -173,6 +173,7 @@ export * from './rewindWireCaps.js';
|
|
|
173
173
|
export * from './selfOrchestrationWireCaps.js';
|
|
174
174
|
export * from './skillsWireCaps.js';
|
|
175
175
|
export * from './ultracodeWireCaps.js';
|
|
176
|
+
export * from './selfOrchestrationDenial.js';
|
|
176
177
|
export * from './webSearchWireCaps.js';
|
|
177
178
|
export * from './liveModelCatalog.js';
|
|
178
179
|
export * from './modelBudgetRule.js';
|
package/dist/index.js
CHANGED
|
@@ -199,6 +199,13 @@ export * from './rewindWireCaps.js';
|
|
|
199
199
|
export * from './selfOrchestrationWireCaps.js';
|
|
200
200
|
export * from './skillsWireCaps.js';
|
|
201
201
|
export * from './ultracodeWireCaps.js';
|
|
202
|
+
// ── S-81(server 7.57.0):selfOrchestration **被拒**的三端公共判定 ────────────────────────────
|
|
203
|
+
// 上面两条 stamp 腿(selfOrchestrationWireCaps / ultracodeWireCaps)只管「怎么把意图发出去」;
|
|
204
|
+
// 半配置多租户形态下 server 会把带这两个键的提交 501 拒掉,而「这一发是不是被那两个键拒的 /
|
|
205
|
+
// 要去掉哪两个键 / caps 上的闸怎么读」三处都是判定不是文案 —— 三端各写一遍必然各错一遍
|
|
206
|
+
// (按 status 分诊会把别的 `capability.*` 501 拖进重发臂;手写 delete 必漏掉 `settings.ultracode`
|
|
207
|
+
// 那一处;把 `workflowsGate` 的缺席读成「引擎说不行」= 替一台什么都没说的 server 下断言)。
|
|
208
|
+
export * from './selfOrchestrationDenial.js';
|
|
202
209
|
export * from './webSearchWireCaps.js';
|
|
203
210
|
// 模型面纯逻辑族
|
|
204
211
|
export * from './liveModelCatalog.js';
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* selfOrchestrationDenial.ts — **「这台部署不给 selfOrchestration」的三端公共判定**
|
|
3
|
+
* (S-81;server 7.57.0 起在场)。
|
|
4
|
+
*
|
|
5
|
+
* ## 这一格是什么(先把上游事实说准,判据才有得写)
|
|
6
|
+
* server 7.57.0 在**半配置的多租户形态**(`REQUIRE_PRINCIPAL=true` + `SELF_ORCHESTRATION_ENABLED=true`
|
|
7
|
+
* + 没有中心侧的准入解析器)上收窄了三条:
|
|
8
|
+
* ① `GET /v1/capabilities` 的 `workflows` 由 `true` 变 **`false`** —— 之前它只回答「这个部署开没开
|
|
9
|
+
* workflow 引擎」,现在它同时把「本 principal 能不能真用上」算进去了;
|
|
10
|
+
* ② 同一份 caps 上新增 additive 键 `workflowsGate: { engineCan, denial }` —— 把上面那个合流的判断
|
|
11
|
+
* **拆回两根轴**:`engineCan` = 引擎侧开没开,`denial` = 为什么这位调用者仍然用不上
|
|
12
|
+
* (闭集,今天单成员 `entitlement_resolver_absent`);
|
|
13
|
+
* ③ `POST /v1/tasks`(及同闸的 stream 提交)带 `selfOrchestration:true` 或 `settings.ultracode:true`
|
|
14
|
+
* 在该形态下 ⇒ **501 `capability.self_orchestration_required`**;把这两个键去掉,同一条请求照常受理。
|
|
15
|
+
*
|
|
16
|
+
* ## 为什么这件在库里而不是在三端各写一遍
|
|
17
|
+
* 三处判定,三端各写一遍必然各错一遍:
|
|
18
|
+
* · **「501 要不要重发」是判定不是文案**:重发的前提是「这一发的失败原因恰好是这两个键」——
|
|
19
|
+
* 判据只能是 `status===501` **且** `errorCode` 恰等那一个码。按 status 分诊会把别的 501
|
|
20
|
+
* (别的能力位没接线)也拖进「去掉两个键再来一次」,那是替 server 编了一个它没说的原因。
|
|
21
|
+
* · **「去掉哪两个键」是结构操作不是措辞**:`selfOrchestration` 在**顶层**、`ultracode` 在
|
|
22
|
+
* `settings` 下(两条不同的 stamp 腿,见 `src/selfOrchestrationWireCaps.ts` /
|
|
23
|
+
* `src/ultracodeWireCaps.ts`),端各自手写 `delete` 必然有人漏掉第二个,而漏掉的后果是
|
|
24
|
+
* 「重发一次、又被 501 拒一次」——用户看到的是功能坏了两遍。
|
|
25
|
+
* · **caps 的「缺席」与「关着」是两件事**:老 server 压根没有 `workflowsGate` 这个键,把它读成
|
|
26
|
+
* 「引擎说不行」就是替一台什么都没说的 server 下断言([honest-absence-not-fabricated-zero])。
|
|
27
|
+
*
|
|
28
|
+
* ## 分工(与仓内既有形同款)
|
|
29
|
+
* **纯判定 + 纯结构操作,零 IO、零 module 级可变态、零文案**:本件不构造 client、不认 baseUrl、
|
|
30
|
+
* 不碰凭据,也**一句面向用户的话都不铸** —— 措辞、是否上屏、要不要给入口全归端(接线姿势见
|
|
31
|
+
* `docs/INTEGRATION-CLIENTS.md` §13)。
|
|
32
|
+
*
|
|
33
|
+
* ## 🔴 射程边界(与 `src/hitl/crashConverged.ts` §12e 同一条边界,别把它读成更强的话)
|
|
34
|
+
* 真供给 = server JSON → SDK `JSON.parse` → 端:每一位都是**自有数据属性**,无代理、无 accessor。
|
|
35
|
+
* 被中间层合成的非 JSON 载荷 / 敌意 `Proxy` / 原型注射这一族**不在射程内**:本件对它们只承诺
|
|
36
|
+
* **不抛**、**绝不把一个说不清的 `denial` 折成「没有拒绝」**;**不承诺**还原出「真实内容到底是什么」。
|
|
37
|
+
*
|
|
38
|
+
* 🔴 **「getter 零执行」这一条只对 {@link projectWorkflowsGate} 成立,对
|
|
39
|
+
* {@link classifySelfOrchestrationRefusal} 不成立**(异源对抗复审 [medium] 采纳的订正 ——
|
|
40
|
+
* 上一版把它写成整模块承诺,与实现不符,而一句做不到的承诺比没有承诺更坏):
|
|
41
|
+
* · `projectWorkflowsGate` 读的是 **wire JSON**(`/v1/capabilities` 回体),那里每一位按定义
|
|
42
|
+
* 都是自有数据位 ⇒ 只认自有数据描述符不损失任何真供给,accessor 一次都不执行;
|
|
43
|
+
* · `classifySelfOrchestrationRefusal` 读的是**抛出物**,而抛出物按设计可能是 SDK 的
|
|
44
|
+
* `APIError` **类实例** —— `status` / `errorCode` 完全可能坐在**原型**上、甚至是原型上的
|
|
45
|
+
* getter(传输层的写法本包不拥有)。只认自有数据描述符会把一个读得懂的错判成读不懂,
|
|
46
|
+
* 那正是它必须走**普通属性读取**的原因(与 `classifyMemoryStatusFailure` /
|
|
47
|
+
* `classifyRulesFailure` 逐字同款)。⇒ 它对**抛出物**只承诺「不抛」;一只**挂死**的 getter
|
|
48
|
+
* 长在抛出物上时本包挡不住,那与「宿主注入了一个会撒谎的传输层」是同一件事。
|
|
49
|
+
* · 能收窄的那一半已经收窄:`denial` 这一位**只在两条判据都通过之后**才读(此前无条件先读,
|
|
50
|
+
* 于是一个**根本不匹配**的错误也会被跑一次 getter)。
|
|
51
|
+
*/
|
|
52
|
+
import type { TaskRequest } from '@sema-agent/sdk';
|
|
53
|
+
import type { TaskRequestLike } from './request/taskRequest.js';
|
|
54
|
+
/**
|
|
55
|
+
* 一次 501 拒绝上**读得出来的**机器原因。
|
|
56
|
+
*
|
|
57
|
+
* 🔴 `'unknown'` 是**诚实缺席**,不是「没有原因」:今天 server 的 501 体形与其它 `capability.*`
|
|
58
|
+
* 501 同款(`{ error, errorCode, message }`),**并不携带**机器可读的 `denial` 位 ⇒ 真 wire 上
|
|
59
|
+
* 这一位恒是 `'unknown'`。闭集那一臂是**防御臂**(server 补了这一位的当天自动点亮),留着的理由
|
|
60
|
+
* 与 `sessionMemoryStatus` 的 `feature` 臂逐字同款:今天不可达 ≠ 缺口,而是「不合流」的登记。
|
|
61
|
+
* 端要给出具体原因时,材料在 **caps 侧**({@link projectWorkflowsGate} 的 `denial`),不在这条错误上。
|
|
62
|
+
*/
|
|
63
|
+
export type SelfOrchestrationDenialReason = 'entitlement_resolver_absent' | 'unknown';
|
|
64
|
+
/**
|
|
65
|
+
* 重发前必须**同时**去掉的两个意图键(单源;两条 stamp 腿各在一个文件里,端手抄必漏第二个)。
|
|
66
|
+
* 顺序即书写顺序,`settings.ultracode` 用点分路径表达「它在 `settings` 下,不是顶层键」。
|
|
67
|
+
* 真正执行删除的是 {@link stripSelfOrchestrationIntent} —— 本常量是给端做披露文案/日志用的**清单**,
|
|
68
|
+
* 不是让端自己照着 `delete` 一遍(那正是本模块要根除的散抄病)。
|
|
69
|
+
*
|
|
70
|
+
* 🔴 **运行期冻结,不只是 `as const`**(异源对抗复审 [medium] 采纳):`as const` 只给**编译期**
|
|
71
|
+
* 只读性,运行期这只数组照样可写 —— 而 {@link classifySelfOrchestrationRefusal} 把**同一只
|
|
72
|
+
* 引用**当作判决的 `retryWithout` 带出去(单源的代价)。于是任意一个 JS 消费者(或一次
|
|
73
|
+
* `as unknown as string[]` 强转)`splice` 它一下,**此后同进程内每一次判决**都会带着被改写的
|
|
74
|
+
* 清单 —— 实测复现:`m.SELF_ORCHESTRATION_RETRY_WITHOUT.splice(0, 2, 'objective')` 之后,
|
|
75
|
+
* 判决的 `retryWithout` 变成 `['objective']`,端照它去键就会删掉 `objective`。
|
|
76
|
+
* `Object.freeze` 让写入在严格模式(ESM 恒是)下当场抛、在非严格下静默失败,两条路都改不动它。
|
|
77
|
+
* 与 `RUN_LEVEL_STOP_ERROR_CODES` 同款写法(那一处也是「导出的闭集表必须真冻」)。
|
|
78
|
+
*/
|
|
79
|
+
export declare const SELF_ORCHESTRATION_RETRY_WITHOUT: readonly ['selfOrchestration', 'settings.ultracode'];
|
|
80
|
+
/**
|
|
81
|
+
* {@link classifySelfOrchestrationRefusal} 的判决。**只有一种 kind** —— 本判定回答的是一个是非题
|
|
82
|
+
* (「这一发是不是被那两个键拒的」),不是分类题;认不出来一律 `null`,不给第二个 kind 去承接
|
|
83
|
+
* 「大概是吧」。
|
|
84
|
+
*/
|
|
85
|
+
export interface SelfOrchestrationRefusal {
|
|
86
|
+
readonly kind: 'self-orchestration-denied';
|
|
87
|
+
/** 端重发前要去掉的键清单(= {@link SELF_ORCHESTRATION_RETRY_WITHOUT},随判决带出便于披露)。 */
|
|
88
|
+
readonly retryWithout: typeof SELF_ORCHESTRATION_RETRY_WITHOUT;
|
|
89
|
+
/** 这次拒绝上读得出来的机器原因;读不出 ⇒ `'unknown'`(见 {@link SelfOrchestrationDenialReason})。 */
|
|
90
|
+
readonly reason: SelfOrchestrationDenialReason;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* 一次任务提交失败(任意抛出物)→ 「这是不是 selfOrchestration 准入拒绝」。**永不抛**。
|
|
94
|
+
*
|
|
95
|
+
* 🔴 **两条判据是合取,且都不许放宽**:
|
|
96
|
+
* · `status === 501` —— 别的状态码一律 `null`(400 是「键的值不对」、403 是「越权」,
|
|
97
|
+
* 两者都不该靠去掉键来重试);
|
|
98
|
+
* · `errorCode` **恰等** {@link CAPABILITY_SELF_ORCHESTRATION_REQUIRED} —— 不是 `capability.`
|
|
99
|
+
* 前缀判。这个码是**复用码**,与它同前缀的兄弟(别的能力位没接线)去掉这两个键**也不会**变成
|
|
100
|
+
* 可受理,把它们一起拖进重发臂 = 白发一次请求 + 给用户一句错误的原因。
|
|
101
|
+
* · 两者**都**要在场:无码的 501 判不出(如实 `null`,绝不挑一个猜);码对但状态码不是 501
|
|
102
|
+
* 同样 `null`(那不是本闸说的话)。
|
|
103
|
+
*
|
|
104
|
+
* 🔴 **结构视图读,不 `instanceof`**(与 `classifyMemoryStatusFailure` / `classifyRulesFailure`
|
|
105
|
+
* 逐字同因):抛出来的是不是 SDK 的 `APIError` 由**宿主**决定 —— 跨 realm / 双 SDK 实例下
|
|
106
|
+
* `instanceof` 会把一个读得懂的错判成读不懂。SDK 的 `APIError` 形与端自己合成的
|
|
107
|
+
* `{ status, errorCode }` 裸形因此走**同一条**读法。
|
|
108
|
+
*
|
|
109
|
+
* 🔴 **取属性本身要设防**:`e` 是任意抛出物,可以是带抛错 getter 的对象或敌意 `Proxy`,
|
|
110
|
+
* 那样连 `e.status` 这一下都会抛。本函数常常在 `catch` 块里被调用,而 `catch` 内抛出的异常
|
|
111
|
+
* **不会**再被同一个 `try` 接住 —— 分类器一抛,调用方那句「永不抛」当场破功。
|
|
112
|
+
*
|
|
113
|
+
* 🔴 **本函数走普通属性读取(会沿原型、会执行 getter),这是刻意的**,理由与边界见文件头
|
|
114
|
+
* 「getter 零执行只对 `projectWorkflowsGate` 成立」那一段:抛出物可能是类实例,判据位坐在原型上。
|
|
115
|
+
* 能收窄的那一半已经收窄 —— `denial` **只在 501 ∧ 恰码两条判据都通过之后**才读(异源对抗复审
|
|
116
|
+
* [medium] 采纳):一个根本不匹配的错误不该被本函数跑一次它的 getter,而 `status`/`errorCode`
|
|
117
|
+
* 两位是判据本身,没有更早的地方可以躲。
|
|
118
|
+
*
|
|
119
|
+
* @returns 认得 ⇒ 判决(端据此去键重发**一次**,见档 §13);认不得 ⇒ `null`(按普通失败呈现)。
|
|
120
|
+
*/
|
|
121
|
+
export declare function classifySelfOrchestrationRefusal(e: unknown): SelfOrchestrationRefusal | null;
|
|
122
|
+
/**
|
|
123
|
+
* 把一份已装配好的请求体里**两个 selfOrchestration 意图键**去掉,返回**新对象**。
|
|
124
|
+
*
|
|
125
|
+
* 删的**恰好**是这两处,一个字节都不多动:
|
|
126
|
+
* · 顶层 `selfOrchestration`(`selfOrchestrationFromEnv()` 的 stamp 位);
|
|
127
|
+
* · `settings.ultracode`(`ultracodeForRequest()` 的 stamp 位)。
|
|
128
|
+
* 🔴 `settings` 下的**其它键一个都不碰**(`webSearch` / `hooks` / `outputStyle` / `resolved`
|
|
129
|
+
* 摊开的那一片子键都是别的轴,连坐删掉 = 用一次重试静默改掉用户的其它设置);
|
|
130
|
+
* 只有当 `ultracode` **本来在场**、删完之后 `settings` 里**一个自有可枚举键都不剩**时,才把
|
|
131
|
+
* `settings` 整键删掉(空 `settings` 上 wire 是多余字节,与 `buildTaskRequest`「全缺则整个
|
|
132
|
+
* `settings` 键都不出现」的既有语义一致)。
|
|
133
|
+
*
|
|
134
|
+
* 🔴 **幂等**:再调一次得到同形结果(第一次之后两个键都不在了,两条分支都成了空操作)。
|
|
135
|
+
* 🔴 **不动 `deferTools`**:`workflowDeferForRequest` 的前置门是「stamp 在场」,而本函数的调用点
|
|
136
|
+
* 是**重发**——把 `Workflow` 从 deferTools 里拿掉或塞进去都是行为改动,不是「去掉意图」。
|
|
137
|
+
* 这一条刻意留给端与 `buildTaskRequest`,本函数只做减法、且只减这两处。
|
|
138
|
+
* 🔴 **additive 键全保**:顶层与 `settings` 都按「拷全部自有可枚举键、再删点名的那一个」做,
|
|
139
|
+
* 上游明天加的键照样过境(与 `run-additive-key-passthrough-test.mjs` 同一条纪律)。
|
|
140
|
+
* 🔴 `settings` **没被改动时原样带出**(同一只对象):没有改动就不该产生新的字节,
|
|
141
|
+
* 也让「我到底改了什么」在端侧一眼可判。
|
|
142
|
+
*
|
|
143
|
+
* 🔴 **两个重载,不是一个**(异源对抗复审 R3 [medium] 采纳):只留宽形
|
|
144
|
+
* (`TaskRequestLike` = `Record<string, unknown>`)会**擦掉**调用方的类型 —— 一份 SDK `TaskRequest`
|
|
145
|
+
* 进去、出来就不再可赋回 `TaskRequest`(索引签名下 `objective` 是 `unknown`,必填位的保证没了),
|
|
146
|
+
* 于是档里那句「去键之后直接重发」在 TypeScript 上根本编不过,端只能靠 `as` 强转把类型面绕开。
|
|
147
|
+
* ⇒ 第一重载**收窄到 SDK 的 `TaskRequest`**:本函数删的两位(`selfOrchestration` 顶层键、
|
|
148
|
+
* `settings.ultracode`)在那个型里**都是可选位**,所以「删完仍是一份合法 `TaskRequest`」是
|
|
149
|
+
* 类型面上成立的事实,不是宽容。第二重载保留给本包自己的 `buildTaskRequest` 产物(宽形)。
|
|
150
|
+
* 🔴 刻意**不**写成 `<T extends TaskRequestLike>(req: T): T`:那对一个把
|
|
151
|
+
* `selfOrchestration` 推断成**必填**的对象字面量就是类型面撒谎(键真的被删了)。
|
|
152
|
+
*
|
|
153
|
+
* @param req 已装配好的请求体(SDK `TaskRequest`,或 `buildTaskRequest` 的产物形)。本函数不校验
|
|
154
|
+
* 它的形状 —— 它是调用方手上已经成形的请求,不是不可信供给。
|
|
155
|
+
*/
|
|
156
|
+
export declare function stripSelfOrchestrationIntent(req: TaskRequest): TaskRequest;
|
|
157
|
+
export declare function stripSelfOrchestrationIntent(req: TaskRequestLike): TaskRequestLike;
|
|
158
|
+
/**
|
|
159
|
+
* `workflowsGate.denial` 上一个**本包不认得**的值。带出原串是为了让端能把它记进日志/诊断面 ——
|
|
160
|
+
* 端**不许**拿它当文案直接上屏(那是 server 的内部词,不是给人看的话),更不许因为「认不得」
|
|
161
|
+
* 就当作没有拒绝。
|
|
162
|
+
*
|
|
163
|
+
* 🔴 `unknown` 为空串 = 「闸确实说了点什么,但那个值连一个可读的记号都取不出来」
|
|
164
|
+
* (非串的值 / accessor 位)。它**仍然是拒绝**,与 `null`(闸明说没有拒绝)是两件事。
|
|
165
|
+
*/
|
|
166
|
+
export interface WorkflowsGateUnknownDenial {
|
|
167
|
+
readonly unknown: string;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* {@link projectWorkflowsGate} 的产出。
|
|
171
|
+
*
|
|
172
|
+
* 🔴 三位各答一个不同的问题,端**不许**把它们合流:
|
|
173
|
+
* · `workflows` —— server 合流后的结论:「这位调用者现在能不能用 workflow」(7.57.0 起它已经
|
|
174
|
+
* 把准入算进去了)。这是**唯一**该拿来决定「给不给入口」的位。
|
|
175
|
+
* · `engineCan` —— 引擎侧开没开(`undefined` = 本部署**没说** ⇒ 老 server / 闸读不出,端零渲染)。
|
|
176
|
+
* · `denial` —— 为什么这位调用者仍然用不上。`null` = 闸明说没有拒绝;闭集成员 = 认得的原因;
|
|
177
|
+
* {@link WorkflowsGateUnknownDenial} = 拒了但原因认不得(**绝不**折成 `null`)。
|
|
178
|
+
*/
|
|
179
|
+
export interface WorkflowsGateProjection {
|
|
180
|
+
readonly workflows: boolean;
|
|
181
|
+
readonly engineCan: boolean | undefined;
|
|
182
|
+
readonly denial: 'entitlement_resolver_absent' | WorkflowsGateUnknownDenial | null;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* `GET /v1/capabilities` 回体 → workflows 闸的**三位投影**。纯函数,**永不抛**。
|
|
186
|
+
*
|
|
187
|
+
* ## 🔴 缺席 vs 关着:两件不同的事,判据不许合流
|
|
188
|
+
* · `caps` 非对象 / `workflows` 位不是布尔(缺席、accessor、类型漂了)⇒ 返回 `undefined` ——
|
|
189
|
+
* 「本部署没告诉我这件事」。端此时**零渲染**:绝不渲「workflow 不可用」之类的话,那是替
|
|
190
|
+
* server 下一个它没说过的断言([honest-absence-not-fabricated-zero])。
|
|
191
|
+
* · `workflows` 是布尔而 `workflowsGate` 缺席(或在场却读不出)⇒
|
|
192
|
+
* `{ workflows, engineCan: undefined, denial: null }` —— 老 server 的真形:合流结论有,
|
|
193
|
+
* 两根轴没有。`engineCan === undefined` 就是端判「这台 server 没有这个闸」的那一位。
|
|
194
|
+
* ⇒ 两档的返回形不同(`undefined` vs 对象),端拿 `=== undefined` 一刀分开。
|
|
195
|
+
*
|
|
196
|
+
* ## 🔴 `denial` 的四档(**未知值绝不折成 `null`**)
|
|
197
|
+
* · 键缺席 / 值是 `null` 或 `undefined` ⇒ `null`(闸明说没有拒绝);
|
|
198
|
+
* · 恰等闭集成员 ⇒ 该字面量(端可以据此说人话);
|
|
199
|
+
* · **别的串** ⇒ `{ unknown: <该串> }` —— server 加了第二个成员而本包还没跟车。折成 `null`
|
|
200
|
+
* 会让端渲出「没有任何拒绝」,而真相是「拒了,只是我不认得原因」:那是这条面上最坏方向的
|
|
201
|
+
* 假断言(用户会以为入口该在却不在)。折成闭集成员则是替 server 编原因,同样不许。
|
|
202
|
+
* · **非串的值 / accessor 位** ⇒ `{ unknown: '' }` —— 仍然是拒绝,只是连记号都带不出来。
|
|
203
|
+
*
|
|
204
|
+
* ## 🔴 四处不可信读取只认自有数据描述符
|
|
205
|
+
* `caps.workflows` / `caps.workflowsGate` / `gate.engineCan` / `gate.denial` 四处都走
|
|
206
|
+
* {@link ownRead}:accessor / 只挂在原型上的东西一律不执行(`catch` 接得住「抛」,接不住「不返回」)。
|
|
207
|
+
*
|
|
208
|
+
* @param caps 任意 `/v1/capabilities` 回体(只读上面两键;非对象 / `null` ⇒ `undefined`)。
|
|
209
|
+
*/
|
|
210
|
+
export declare function projectWorkflowsGate(caps: unknown): WorkflowsGateProjection | undefined;
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* selfOrchestrationDenial.ts — **「这台部署不给 selfOrchestration」的三端公共判定**
|
|
3
|
+
* (S-81;server 7.57.0 起在场)。
|
|
4
|
+
*
|
|
5
|
+
* ## 这一格是什么(先把上游事实说准,判据才有得写)
|
|
6
|
+
* server 7.57.0 在**半配置的多租户形态**(`REQUIRE_PRINCIPAL=true` + `SELF_ORCHESTRATION_ENABLED=true`
|
|
7
|
+
* + 没有中心侧的准入解析器)上收窄了三条:
|
|
8
|
+
* ① `GET /v1/capabilities` 的 `workflows` 由 `true` 变 **`false`** —— 之前它只回答「这个部署开没开
|
|
9
|
+
* workflow 引擎」,现在它同时把「本 principal 能不能真用上」算进去了;
|
|
10
|
+
* ② 同一份 caps 上新增 additive 键 `workflowsGate: { engineCan, denial }` —— 把上面那个合流的判断
|
|
11
|
+
* **拆回两根轴**:`engineCan` = 引擎侧开没开,`denial` = 为什么这位调用者仍然用不上
|
|
12
|
+
* (闭集,今天单成员 `entitlement_resolver_absent`);
|
|
13
|
+
* ③ `POST /v1/tasks`(及同闸的 stream 提交)带 `selfOrchestration:true` 或 `settings.ultracode:true`
|
|
14
|
+
* 在该形态下 ⇒ **501 `capability.self_orchestration_required`**;把这两个键去掉,同一条请求照常受理。
|
|
15
|
+
*
|
|
16
|
+
* ## 为什么这件在库里而不是在三端各写一遍
|
|
17
|
+
* 三处判定,三端各写一遍必然各错一遍:
|
|
18
|
+
* · **「501 要不要重发」是判定不是文案**:重发的前提是「这一发的失败原因恰好是这两个键」——
|
|
19
|
+
* 判据只能是 `status===501` **且** `errorCode` 恰等那一个码。按 status 分诊会把别的 501
|
|
20
|
+
* (别的能力位没接线)也拖进「去掉两个键再来一次」,那是替 server 编了一个它没说的原因。
|
|
21
|
+
* · **「去掉哪两个键」是结构操作不是措辞**:`selfOrchestration` 在**顶层**、`ultracode` 在
|
|
22
|
+
* `settings` 下(两条不同的 stamp 腿,见 `src/selfOrchestrationWireCaps.ts` /
|
|
23
|
+
* `src/ultracodeWireCaps.ts`),端各自手写 `delete` 必然有人漏掉第二个,而漏掉的后果是
|
|
24
|
+
* 「重发一次、又被 501 拒一次」——用户看到的是功能坏了两遍。
|
|
25
|
+
* · **caps 的「缺席」与「关着」是两件事**:老 server 压根没有 `workflowsGate` 这个键,把它读成
|
|
26
|
+
* 「引擎说不行」就是替一台什么都没说的 server 下断言([honest-absence-not-fabricated-zero])。
|
|
27
|
+
*
|
|
28
|
+
* ## 分工(与仓内既有形同款)
|
|
29
|
+
* **纯判定 + 纯结构操作,零 IO、零 module 级可变态、零文案**:本件不构造 client、不认 baseUrl、
|
|
30
|
+
* 不碰凭据,也**一句面向用户的话都不铸** —— 措辞、是否上屏、要不要给入口全归端(接线姿势见
|
|
31
|
+
* `docs/INTEGRATION-CLIENTS.md` §13)。
|
|
32
|
+
*
|
|
33
|
+
* ## 🔴 射程边界(与 `src/hitl/crashConverged.ts` §12e 同一条边界,别把它读成更强的话)
|
|
34
|
+
* 真供给 = server JSON → SDK `JSON.parse` → 端:每一位都是**自有数据属性**,无代理、无 accessor。
|
|
35
|
+
* 被中间层合成的非 JSON 载荷 / 敌意 `Proxy` / 原型注射这一族**不在射程内**:本件对它们只承诺
|
|
36
|
+
* **不抛**、**绝不把一个说不清的 `denial` 折成「没有拒绝」**;**不承诺**还原出「真实内容到底是什么」。
|
|
37
|
+
*
|
|
38
|
+
* 🔴 **「getter 零执行」这一条只对 {@link projectWorkflowsGate} 成立,对
|
|
39
|
+
* {@link classifySelfOrchestrationRefusal} 不成立**(异源对抗复审 [medium] 采纳的订正 ——
|
|
40
|
+
* 上一版把它写成整模块承诺,与实现不符,而一句做不到的承诺比没有承诺更坏):
|
|
41
|
+
* · `projectWorkflowsGate` 读的是 **wire JSON**(`/v1/capabilities` 回体),那里每一位按定义
|
|
42
|
+
* 都是自有数据位 ⇒ 只认自有数据描述符不损失任何真供给,accessor 一次都不执行;
|
|
43
|
+
* · `classifySelfOrchestrationRefusal` 读的是**抛出物**,而抛出物按设计可能是 SDK 的
|
|
44
|
+
* `APIError` **类实例** —— `status` / `errorCode` 完全可能坐在**原型**上、甚至是原型上的
|
|
45
|
+
* getter(传输层的写法本包不拥有)。只认自有数据描述符会把一个读得懂的错判成读不懂,
|
|
46
|
+
* 那正是它必须走**普通属性读取**的原因(与 `classifyMemoryStatusFailure` /
|
|
47
|
+
* `classifyRulesFailure` 逐字同款)。⇒ 它对**抛出物**只承诺「不抛」;一只**挂死**的 getter
|
|
48
|
+
* 长在抛出物上时本包挡不住,那与「宿主注入了一个会撒谎的传输层」是同一件事。
|
|
49
|
+
* · 能收窄的那一半已经收窄:`denial` 这一位**只在两条判据都通过之后**才读(此前无条件先读,
|
|
50
|
+
* 于是一个**根本不匹配**的错误也会被跑一次 getter)。
|
|
51
|
+
*/
|
|
52
|
+
import { CAPABILITY_SELF_ORCHESTRATION_REQUIRED } from './engineErrorCodes.js';
|
|
53
|
+
// ── ① 501 拒绝的判型(纯判定)──────────────────────────────────────────────────────────────
|
|
54
|
+
/**
|
|
55
|
+
* server 今天唯一说得出口的拒绝原因:**中心侧的准入解析器不在场**,于是本部署无法为这位
|
|
56
|
+
* principal 判「能不能用 workflow」,fail-closed 拒掉。
|
|
57
|
+
*
|
|
58
|
+
* 🔴 它是**闭集的今日单成员**,不是开集识别表:server 明天加第二个成员时,本包读到的会是
|
|
59
|
+
* {@link SelfOrchestrationDenialReason} 的 `'unknown'` 臂(或 caps 侧的
|
|
60
|
+
* {@link WorkflowsGateUnknownDenial}),**绝不**塌进本成员 —— 塌进去就是替 server 编了一个
|
|
61
|
+
* 它没说过的原因。
|
|
62
|
+
*/
|
|
63
|
+
const ENTITLEMENT_RESOLVER_ABSENT = 'entitlement_resolver_absent';
|
|
64
|
+
/**
|
|
65
|
+
* 重发前必须**同时**去掉的两个意图键(单源;两条 stamp 腿各在一个文件里,端手抄必漏第二个)。
|
|
66
|
+
* 顺序即书写顺序,`settings.ultracode` 用点分路径表达「它在 `settings` 下,不是顶层键」。
|
|
67
|
+
* 真正执行删除的是 {@link stripSelfOrchestrationIntent} —— 本常量是给端做披露文案/日志用的**清单**,
|
|
68
|
+
* 不是让端自己照着 `delete` 一遍(那正是本模块要根除的散抄病)。
|
|
69
|
+
*
|
|
70
|
+
* 🔴 **运行期冻结,不只是 `as const`**(异源对抗复审 [medium] 采纳):`as const` 只给**编译期**
|
|
71
|
+
* 只读性,运行期这只数组照样可写 —— 而 {@link classifySelfOrchestrationRefusal} 把**同一只
|
|
72
|
+
* 引用**当作判决的 `retryWithout` 带出去(单源的代价)。于是任意一个 JS 消费者(或一次
|
|
73
|
+
* `as unknown as string[]` 强转)`splice` 它一下,**此后同进程内每一次判决**都会带着被改写的
|
|
74
|
+
* 清单 —— 实测复现:`m.SELF_ORCHESTRATION_RETRY_WITHOUT.splice(0, 2, 'objective')` 之后,
|
|
75
|
+
* 判决的 `retryWithout` 变成 `['objective']`,端照它去键就会删掉 `objective`。
|
|
76
|
+
* `Object.freeze` 让写入在严格模式(ESM 恒是)下当场抛、在非严格下静默失败,两条路都改不动它。
|
|
77
|
+
* 与 `RUN_LEVEL_STOP_ERROR_CODES` 同款写法(那一处也是「导出的闭集表必须真冻」)。
|
|
78
|
+
*/
|
|
79
|
+
export const SELF_ORCHESTRATION_RETRY_WITHOUT = Object.freeze(['selfOrchestration', 'settings.ultracode']);
|
|
80
|
+
/**
|
|
81
|
+
* 一次任务提交失败(任意抛出物)→ 「这是不是 selfOrchestration 准入拒绝」。**永不抛**。
|
|
82
|
+
*
|
|
83
|
+
* 🔴 **两条判据是合取,且都不许放宽**:
|
|
84
|
+
* · `status === 501` —— 别的状态码一律 `null`(400 是「键的值不对」、403 是「越权」,
|
|
85
|
+
* 两者都不该靠去掉键来重试);
|
|
86
|
+
* · `errorCode` **恰等** {@link CAPABILITY_SELF_ORCHESTRATION_REQUIRED} —— 不是 `capability.`
|
|
87
|
+
* 前缀判。这个码是**复用码**,与它同前缀的兄弟(别的能力位没接线)去掉这两个键**也不会**变成
|
|
88
|
+
* 可受理,把它们一起拖进重发臂 = 白发一次请求 + 给用户一句错误的原因。
|
|
89
|
+
* · 两者**都**要在场:无码的 501 判不出(如实 `null`,绝不挑一个猜);码对但状态码不是 501
|
|
90
|
+
* 同样 `null`(那不是本闸说的话)。
|
|
91
|
+
*
|
|
92
|
+
* 🔴 **结构视图读,不 `instanceof`**(与 `classifyMemoryStatusFailure` / `classifyRulesFailure`
|
|
93
|
+
* 逐字同因):抛出来的是不是 SDK 的 `APIError` 由**宿主**决定 —— 跨 realm / 双 SDK 实例下
|
|
94
|
+
* `instanceof` 会把一个读得懂的错判成读不懂。SDK 的 `APIError` 形与端自己合成的
|
|
95
|
+
* `{ status, errorCode }` 裸形因此走**同一条**读法。
|
|
96
|
+
*
|
|
97
|
+
* 🔴 **取属性本身要设防**:`e` 是任意抛出物,可以是带抛错 getter 的对象或敌意 `Proxy`,
|
|
98
|
+
* 那样连 `e.status` 这一下都会抛。本函数常常在 `catch` 块里被调用,而 `catch` 内抛出的异常
|
|
99
|
+
* **不会**再被同一个 `try` 接住 —— 分类器一抛,调用方那句「永不抛」当场破功。
|
|
100
|
+
*
|
|
101
|
+
* 🔴 **本函数走普通属性读取(会沿原型、会执行 getter),这是刻意的**,理由与边界见文件头
|
|
102
|
+
* 「getter 零执行只对 `projectWorkflowsGate` 成立」那一段:抛出物可能是类实例,判据位坐在原型上。
|
|
103
|
+
* 能收窄的那一半已经收窄 —— `denial` **只在 501 ∧ 恰码两条判据都通过之后**才读(异源对抗复审
|
|
104
|
+
* [medium] 采纳):一个根本不匹配的错误不该被本函数跑一次它的 getter,而 `status`/`errorCode`
|
|
105
|
+
* 两位是判据本身,没有更早的地方可以躲。
|
|
106
|
+
*
|
|
107
|
+
* @returns 认得 ⇒ 判决(端据此去键重发**一次**,见档 §13);认不得 ⇒ `null`(按普通失败呈现)。
|
|
108
|
+
*/
|
|
109
|
+
export function classifySelfOrchestrationRefusal(e) {
|
|
110
|
+
let status;
|
|
111
|
+
let code;
|
|
112
|
+
try {
|
|
113
|
+
const o = e;
|
|
114
|
+
const rawStatus = o?.status;
|
|
115
|
+
const rawCode = o?.errorCode;
|
|
116
|
+
status = typeof rawStatus === 'number' ? rawStatus : undefined;
|
|
117
|
+
code = typeof rawCode === 'string' ? rawCode : undefined;
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// 读不动这个抛出物 ⇒ 判不出它是不是本族的拒绝。不抛,不猜。
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
if (status !== 501)
|
|
124
|
+
return null;
|
|
125
|
+
if (code !== CAPABILITY_SELF_ORCHESTRATION_REQUIRED)
|
|
126
|
+
return null;
|
|
127
|
+
// 🔴 `denial` **只在两条判据都通过之后**才读(见头注):它不是判据,是判决上的一个附加位 ——
|
|
128
|
+
// 无条件先读等于让每一个**根本不匹配**的抛出物都被跑一次它的 getter。这一读同样设防
|
|
129
|
+
// (它可能抛),读不动就当缺席落 `'unknown'`,绝不让本函数向外抛。
|
|
130
|
+
let denial;
|
|
131
|
+
try {
|
|
132
|
+
denial = e?.denial;
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
denial = undefined;
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
kind: 'self-orchestration-denied',
|
|
139
|
+
retryWithout: SELF_ORCHESTRATION_RETRY_WITHOUT,
|
|
140
|
+
// 闭集成员才认;缺席 / 别的值 / 非串一律 `'unknown'`(见 SelfOrchestrationDenialReason 头注)。
|
|
141
|
+
reason: denial === ENTITLEMENT_RESOLVER_ABSENT ? ENTITLEMENT_RESOLVER_ABSENT : 'unknown',
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
// ── ② 去掉两个意图键(纯结构操作)──────────────────────────────────────────────────────────
|
|
145
|
+
/** 自有键判(不查原型:`settings` 是 wire 体上的自有位,原型上的同名东西不是这次要删的那个)。 */
|
|
146
|
+
const hasOwn = (o, k) => Object.prototype.hasOwnProperty.call(o, k);
|
|
147
|
+
export function stripSelfOrchestrationIntent(req) {
|
|
148
|
+
const out = { ...req };
|
|
149
|
+
delete out.selfOrchestration;
|
|
150
|
+
const settings = out.settings;
|
|
151
|
+
if (typeof settings === 'object' && settings !== null && !Array.isArray(settings)) {
|
|
152
|
+
if (hasOwn(settings, 'ultracode')) {
|
|
153
|
+
const rest = { ...settings };
|
|
154
|
+
delete rest.ultracode;
|
|
155
|
+
if (Object.keys(rest).length === 0)
|
|
156
|
+
delete out.settings;
|
|
157
|
+
else
|
|
158
|
+
out.settings = rest;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
163
|
+
const ABSENT = { kind: 'absent' };
|
|
164
|
+
const ACCESSOR = { kind: 'accessor' };
|
|
165
|
+
/**
|
|
166
|
+
* 从一个**不可信**对象上取一位:只认**自有数据描述符**的 `value`。
|
|
167
|
+
*
|
|
168
|
+
* 🔴 为什么不用 `obj[key]`(与 `src/hitl/crashConverged.ts` 的 `ownDataValue` 同一条纪律):
|
|
169
|
+
* 普通属性读取会 ① **执行** accessor,② 一路查到**原型**上去。执行意味着同步跑别人的代码 ——
|
|
170
|
+
* 而 `try/catch` 接得住「抛」,接不住「不返回」:一只忙等 / 死循环的 getter 会把这条读面所在的
|
|
171
|
+
* 线程**永久**钉住。真供给来自 `JSON.parse`,每一位都是自有数据位 ⇒ 这条对真 caps 零影响。
|
|
172
|
+
*
|
|
173
|
+
* 🔴 三态**刻意分开**(不是「读到 / 没读到」两态):`absent` 与 `accessor` 在 `denial` 那一位上
|
|
174
|
+
* 处置相反 —— 键不在场是「闸没说拒绝」,而在场却执行不得是「拒了但读不出」。合成两态就会把
|
|
175
|
+
* 后者折进前者,那正是本模块最不该犯的错。
|
|
176
|
+
*
|
|
177
|
+
* ⚠️ `Object.getOwnPropertyDescriptor` 自己对代理会触发 trap(可能抛)⇒ 抛出时归 `accessor`
|
|
178
|
+
* (「在场但读不出」),不归 `absent`。
|
|
179
|
+
*/
|
|
180
|
+
function ownRead(o, key) {
|
|
181
|
+
try {
|
|
182
|
+
const d = Object.getOwnPropertyDescriptor(o, key);
|
|
183
|
+
if (d === undefined)
|
|
184
|
+
return ABSENT;
|
|
185
|
+
if ('get' in d || 'set' in d)
|
|
186
|
+
return ACCESSOR;
|
|
187
|
+
return { kind: 'data', value: d.value };
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return ACCESSOR;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* `GET /v1/capabilities` 回体 → workflows 闸的**三位投影**。纯函数,**永不抛**。
|
|
195
|
+
*
|
|
196
|
+
* ## 🔴 缺席 vs 关着:两件不同的事,判据不许合流
|
|
197
|
+
* · `caps` 非对象 / `workflows` 位不是布尔(缺席、accessor、类型漂了)⇒ 返回 `undefined` ——
|
|
198
|
+
* 「本部署没告诉我这件事」。端此时**零渲染**:绝不渲「workflow 不可用」之类的话,那是替
|
|
199
|
+
* server 下一个它没说过的断言([honest-absence-not-fabricated-zero])。
|
|
200
|
+
* · `workflows` 是布尔而 `workflowsGate` 缺席(或在场却读不出)⇒
|
|
201
|
+
* `{ workflows, engineCan: undefined, denial: null }` —— 老 server 的真形:合流结论有,
|
|
202
|
+
* 两根轴没有。`engineCan === undefined` 就是端判「这台 server 没有这个闸」的那一位。
|
|
203
|
+
* ⇒ 两档的返回形不同(`undefined` vs 对象),端拿 `=== undefined` 一刀分开。
|
|
204
|
+
*
|
|
205
|
+
* ## 🔴 `denial` 的四档(**未知值绝不折成 `null`**)
|
|
206
|
+
* · 键缺席 / 值是 `null` 或 `undefined` ⇒ `null`(闸明说没有拒绝);
|
|
207
|
+
* · 恰等闭集成员 ⇒ 该字面量(端可以据此说人话);
|
|
208
|
+
* · **别的串** ⇒ `{ unknown: <该串> }` —— server 加了第二个成员而本包还没跟车。折成 `null`
|
|
209
|
+
* 会让端渲出「没有任何拒绝」,而真相是「拒了,只是我不认得原因」:那是这条面上最坏方向的
|
|
210
|
+
* 假断言(用户会以为入口该在却不在)。折成闭集成员则是替 server 编原因,同样不许。
|
|
211
|
+
* · **非串的值 / accessor 位** ⇒ `{ unknown: '' }` —— 仍然是拒绝,只是连记号都带不出来。
|
|
212
|
+
*
|
|
213
|
+
* ## 🔴 四处不可信读取只认自有数据描述符
|
|
214
|
+
* `caps.workflows` / `caps.workflowsGate` / `gate.engineCan` / `gate.denial` 四处都走
|
|
215
|
+
* {@link ownRead}:accessor / 只挂在原型上的东西一律不执行(`catch` 接得住「抛」,接不住「不返回」)。
|
|
216
|
+
*
|
|
217
|
+
* @param caps 任意 `/v1/capabilities` 回体(只读上面两键;非对象 / `null` ⇒ `undefined`)。
|
|
218
|
+
*/
|
|
219
|
+
export function projectWorkflowsGate(caps) {
|
|
220
|
+
try {
|
|
221
|
+
if (typeof caps !== 'object' || caps === null)
|
|
222
|
+
return undefined;
|
|
223
|
+
const w = ownRead(caps, 'workflows');
|
|
224
|
+
if (w.kind !== 'data' || typeof w.value !== 'boolean')
|
|
225
|
+
return undefined;
|
|
226
|
+
const workflows = w.value;
|
|
227
|
+
const g = ownRead(caps, 'workflowsGate');
|
|
228
|
+
if (g.kind !== 'data' || typeof g.value !== 'object' || g.value === null || Array.isArray(g.value)) {
|
|
229
|
+
// 闸缺席,或在场却读不出(accessor / 形漂了)——两档都是「这台 server 没给我两根轴」,
|
|
230
|
+
// `engineCan: undefined` 就是端判这一档的那一位;此时 `denial: null` 不是「没拒绝」这个
|
|
231
|
+
// 断言,而是「本档没有拒绝信息」,端按 `engineCan === undefined` 一并读。
|
|
232
|
+
return { workflows, engineCan: undefined, denial: null };
|
|
233
|
+
}
|
|
234
|
+
const gate = g.value;
|
|
235
|
+
const ec = ownRead(gate, 'engineCan');
|
|
236
|
+
const engineCan = ec.kind === 'data' && typeof ec.value === 'boolean' ? ec.value : undefined;
|
|
237
|
+
const d = ownRead(gate, 'denial');
|
|
238
|
+
let denial;
|
|
239
|
+
if (d.kind === 'absent')
|
|
240
|
+
denial = null;
|
|
241
|
+
else if (d.kind === 'accessor')
|
|
242
|
+
denial = { unknown: '' };
|
|
243
|
+
else if (d.value === null || d.value === undefined)
|
|
244
|
+
denial = null;
|
|
245
|
+
else if (d.value === ENTITLEMENT_RESOLVER_ABSENT)
|
|
246
|
+
denial = ENTITLEMENT_RESOLVER_ABSENT;
|
|
247
|
+
else
|
|
248
|
+
denial = { unknown: typeof d.value === 'string' ? d.value : '' };
|
|
249
|
+
return { workflows, engineCan, denial };
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
// 敌意 / 坏载体:这次供给读不出 ⇒ 诚实缺席,不向外抛。
|
|
253
|
+
return undefined;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
@@ -15,25 +15,29 @@
|
|
|
15
15
|
|
|
16
16
|
## §0 版本锚与重扫纪律
|
|
17
17
|
|
|
18
|
-
### 0a. 版本锚(2026-09-
|
|
18
|
+
### 0a. 版本锚(2026-09-03)
|
|
19
19
|
|
|
20
20
|
| 项 | 值 | 真源 |
|
|
21
21
|
|---|---|---|
|
|
22
|
-
| 本包 | `@sema-agent/client-core` **0.
|
|
22
|
+
| 本包 | `@sema-agent/client-core` **0.49.0**(工作树 = npm 最新;S-81 那一批未发,进 `CHANGELOG.md` 的 `## 0.50.0(未发布)` 段) | `package.json` `version` |
|
|
23
23
|
| peer:wire 契约 | `@sema-agent/sdk` **>=7.4.0**(value-level,非 type-only;0.48.0 抬版,四条硬理由见 `CHANGELOG.md` 0.48.0 段末的地板影响面账) | `package.json` `peerDependencies` |
|
|
24
24
|
| peer:会话词汇表 | `@sema-agent/agent-types` **>=0.2.0**(type-only,零运行时) | 同上 |
|
|
25
25
|
| runtime dep | `diff` ^9.0.0(**唯一**一条;portability 门按**等值**钉死) | `package.json` `dependencies` |
|
|
26
|
-
| 公开导出面 | **
|
|
26
|
+
| 公开导出面 | **800** 个运行期符号(+ 41 个测试钩;= 工作树当下的值 —— 已发的 `0.49.0` 是 **795**,再加 S-81 五件未发 additive 导出;`0.48.0` 是 **794**,npm `0.47.0` 是 **790**,`0.46.0` 是 **787**,`0.44.0` 是 **783**,`0.43.1`/`0.43.0` 是 **776**,`0.42.0` 是 **771**,`0.41.0` 是 **767**,`0.39.0` 是 **766**,`0.38.0` 是 **764**;`0.37.0` 是 **753**,见 `CHANGELOG.md`) | `scripts/public-export-baseline.json` 的 `count` / `testHookCount` —— **别手抄进别处,以该文件为准** |
|
|
27
27
|
| 常驻门 | 以 `scripts/gates-manifest.json` 的 `suites` 长度为准(**本档不抄这个数**) | `scripts/gates-manifest.json`;`npm test` 的名单等值门与它逐名对账 |
|
|
28
28
|
| 沿革档 | 0.29.0 起建 `CHANGELOG.md`;更早批次记账在 `src/index.ts` 文件头 + `docs/REFACTOR-LEDGER.md` | — |
|
|
29
29
|
|
|
30
|
-
⚠️
|
|
31
|
-
`pending` 行)。装 ≤0.47.0 的端注意:0.48.0 新增的 **4 个 additive 导出**
|
|
30
|
+
⚠️ 装 ≤`0.47.0` 的端注意:`0.48.0` 新增的 **4 个 additive 导出**
|
|
32
31
|
(`readSessionMemoryStatus` / `classifyMemoryStatusFailure` / `readCaptureOptOut` / `readLastCapture`)
|
|
33
32
|
在旧版上按名 import 会**在 ESM 实例化当场炸**(具名导出不存在)—— 提货前先抬依赖。
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
装 ≤`0.48.0` 的端同理:`0.49.0` 的 `projectCrashConverged`(L-38,§12)在旧版上不存在。
|
|
34
|
+
⚠️ **工作树里还有一批比 `0.49.0` 更晚的未发 additive 导出**(S-81,见 §13):
|
|
35
|
+
`classifySelfOrchestrationRefusal` / `stripSelfOrchestrationIntent` / `projectWorkflowsGate` /
|
|
36
|
+
`SELF_ORCHESTRATION_RETRY_WITHOUT` / `CAPABILITY_SELF_ORCHESTRATION_REQUIRED`
|
|
37
|
+
(+ **四个** type-only 形 `SelfOrchestrationRefusal` / `SelfOrchestrationDenialReason` /
|
|
38
|
+
`WorkflowsGateProjection` / `WorkflowsGateUnknownDenial`)。它们随下一个版本段发出;
|
|
39
|
+
在此之前按名 import 会在 ESM 实例化当场炸 —— 提货前先抬依赖。
|
|
40
|
+
(L-38 的 `projectCrashConverged` 已随 `0.49.0` 发出,不再是未发件。)
|
|
37
41
|
🔴 **0.48.0 还抬了 peer 地板**(`@sema-agent/sdk >=7.4.0`),这是本版**唯一**的非 additive 面:
|
|
38
42
|
端装 <7.4.0 的 SDK 会看到 peer 警告(运行期不因此变化)。同一条对 0.47.0 那 **3 个 additive 导出**
|
|
39
43
|
成立(`planInteractiveHalt` / `RUN_LEVEL_STOP_ERROR_CODES` / `readDecideCurrentPending`)。同一条对 0.38.0 那 11 个
|
|
@@ -109,7 +113,7 @@ SDK 核对「本包 import 的每个值级符号仍然导出」「`TaskStats.cos
|
|
|
109
113
|
|
|
110
114
|
## §2 公共导出面地图(按域)
|
|
111
115
|
|
|
112
|
-
> 全集真源 = `scripts/public-export-baseline.json` 的 `names`(**
|
|
116
|
+
> 全集真源 = `scripts/public-export-baseline.json` 的 `names`(**800** 项)。
|
|
113
117
|
> 本节**不逐名抄**,只给「域 → 承重导出 → 用途 → 实现锚」。承重导出 = 一个端为了让这个域干活
|
|
114
118
|
> **必须**直接调到的那几个符号;其余是它们的类型、变体与辅助位。
|
|
115
119
|
> 单一入口:`import { … } from '@sema-agent/client-core'`(`exports` 只有 `.` 一个;
|
|
@@ -119,7 +123,7 @@ SDK 核对「本包 import 的每个值级符号仍然导出」「`TaskStats.cos
|
|
|
119
123
|
|
|
120
124
|
`public-export-baseline.json` 由 **`dist/index.js` 的运行期导出**生成(生成口径自述见
|
|
121
125
|
`scripts/run-client-core-typeshape-test.mjs`,双向精确集合门在 `scripts/run-public-surface-test.mjs`)。
|
|
122
|
-
实测:
|
|
126
|
+
实测:800 项 **100% 是运行期导出,零 type-only**。
|
|
123
127
|
|
|
124
128
|
**推论(端必须知道)**:
|
|
125
129
|
- barrel 导出的**类型**面比 707 大得多,且**不被这道门看守** —— `AdapterContext` / `SeamEvent` /
|
|
@@ -129,13 +133,16 @@ SDK 核对「本包 import 的每个值级符号仍然导出」「`TaskStats.cos
|
|
|
129
133
|
- `src/agentSession/contract.ts` 对基线贡献 **0** 项(纯类型模块,`export *` 在 dist 里是空转发)。
|
|
130
134
|
- 同理:L-38 的 `CrashConvergedRow` / `ApprovalsListEnvelope` / `CrashConvergedProjection` 三个形也
|
|
131
135
|
**不在**基线里(纯类型),`src/hitl/crashConverged.ts` 对基线只贡献 `projectCrashConverged` 一项。
|
|
136
|
+
- S-81 同款:`SelfOrchestrationRefusal` / `SelfOrchestrationDenialReason` / `WorkflowsGateProjection` /
|
|
137
|
+
`WorkflowsGateUnknownDenial` 四形**不在**基线里,`src/selfOrchestrationDenial.ts` 对基线贡献
|
|
138
|
+
**4** 项运行期导出(三个函数 + `SELF_ORCHESTRATION_RETRY_WITHOUT`)。
|
|
132
139
|
|
|
133
|
-
|
|
140
|
+
800 项的内部构成(帮助端估读表大小):**235** 项是 `SCREAMING_SNAKE` 常量数据表/词汇表
|
|
134
141
|
(矩阵、键集、env 名、锚串)而非可调用物;**5** 项是 PascalCase 运行期值
|
|
135
142
|
(`ControlRouter` / `ControlSafetyError` / `HitlBridge` / `HitlSafetyError` / `DecideTransportRetryExhaustedError`);
|
|
136
143
|
**41** 项是 `*For(sessionKey, …)` 的 per-session 变体(§6;其中 `engineNamespaceKeyFor` 是命名巧合 —— 参数是 baseUrl 不是 sessionKey,见域 14)。
|
|
137
144
|
|
|
138
|
-
### 2b. 域图(16 域,逐域计数之和 =
|
|
145
|
+
### 2b. 域图(16 域,逐域计数之和 = 800)
|
|
139
146
|
|
|
140
147
|
| # | 域 | 名数 | 承重导出 | 用途 | 实现锚 |
|
|
141
148
|
|---|---|---|---|---|---|
|
|
@@ -147,14 +154,14 @@ SDK 核对「本包 import 的每个值级符号仍然导出」「`TaskStats.cos
|
|
|
147
154
|
| 6 | **请求装配(上行唯一构造口)** | 8 | `buildTaskRequest` · `REQUEST_FIELD_MATRIX` · `unregisteredRequestKeys` · `applyLiveRequestDefaults` · `taskNotificationToPrintFrame` | 两条车道(`interactive`/`print`)出站请求的**唯一**构造器;`unregisteredRequestKeys` 是可执行门 —— 端偷带一个未登记键上 wire 就红 | `src/request/taskRequest.ts`、`src/request/printNotification.ts` |
|
|
148
155
|
| 7 | **通知与 outstanding 台账** | 41 | `installNotificationQueuePort` · `normalizeTaskNotification` · `taskNotificationDedupKeyFromWire` · `registerOutstandingBgTask` / `registerOutstandingWorkflowRun` · `notificationQueuePortMisses` · `subscribeOutstandingWorkflows` · `outstandingDeliverableWorkflowCount` | `task_notification` 归一 + 去重 + 投递进宿主命令队列的**一把闸**;`outstandingDeliverableWorkflowCount()` 是 headless `-p` 的**退出门** | `src/notifications.ts`(11 个 module 台账) |
|
|
149
156
|
| 8 | **工具结果卡** | 25 | `structuredToToolUseResult` · `readAsyncLaunchedAgentReceipt` · `wireOutputToBody` · `parseModelFacingBash` · `getPatchFromContents` · `toolEndResultToUserFrame` · `flattenToolOutput` | 铸端要渲的 `tool_result` 卡体,含客户端 diff hunk(唯一 runtime dep 的用处) | `src/toolResult.ts`、`src/printToolResultFrame.ts`、`src/diff/patch.ts` |
|
|
150
|
-
| 9 | **能力/旋钮 wire 门族** |
|
|
157
|
+
| 9 | **能力/旋钮 wire 门族** | 91 | `projectAgentsForWire` / `awaitTaskAgentsWire` · `hooksForWire` · `mcpConfigsToSpecs` · `ultracodeForRequest` · `resolveWebSearch` + `buildWebSearchEnv` · `attachmentsForRequest` · `projectEffectiveBody` · `classifySelfOrchestrationRefusal` / `stripSelfOrchestrationIntent` / `projectWorkflowsGate` / `SELF_ORCHESTRATION_RETRY_WITHOUT`(S-81,server 7.57.0:上面两条 stamp 腿的**背面** —— 半配置多租户形态下 server 把带 `selfOrchestration` / `settings.ultracode` 的提交 501 拒掉,判型/去键/caps 闸三处都是判定不是文案,详见 §13) | 逐功能的「引擎有没有这个能力 + 这个字段怎么上 wire」投影器,由 `buildTaskRequest` 组装。🔴 `hooksForWire()` 是 **fail-closed**:无 `SettingsPort` ⇒ 返回 `undefined`(信任门,不是取值) | 17 个 `src/*WireCaps.ts` + `src/scratchpadWireCaps.ts`、`src/hooksWireCaps.ts`、`src/goalStopHook.ts`、`src/websearch/searchProviderPresets.ts`、`src/selfOrchestrationDenial.ts`(S-81 拒绝判定层) |
|
|
151
158
|
| 10 | **headless / 部署旋钮 wire** | 61 | `parseSandboxArgv` / `sandboxRequestFields` · `parseLimitsArgv` / `limitsForPrint` · `resolveHeadlessFinalVerify` · `resolveHeadlessPermissionMode` · `resolveHeadlessInteractiveTools` · `armDetachCancel` + `detachCancelArm` + `isDetachArmed` · `withHeadlessR1Reconnect` | `-p`/headless 车道的 env+argv 旋钮。🔴 `detachWire` 是**拆**的补偿:判定与 cancel-arm 台账在库,信号路径的裸 fetch 留宿主(`detachCancelArm()` 是取件口) | `src/sandboxWire.ts`、`scenarioWire.ts`、`finalVerifyWire.ts`、`limitsWire.ts`、`interactiveToolsWire.ts`、`headlessPermissionModeWire.ts`、`headlessReconnectWire.ts`、`detachWire.ts` |
|
|
152
159
|
| 11 | **模型目录与预算** | 67 | `resolveModelCatalog` · `loadCatalogWithSources` · `PROVIDER_PRESETS` / `MODEL_FAMILIES` · `defaultMaxTokensFor` · `getLiveModelCatalog` / `setLiveModelCatalogRefresher` · `providerAuthMethods` / `beginDeviceCodeAuth` · `providerCatalogRows` / `providerCatalogRowDetail` / `providerPresetById`(#244 F4 族D A-028.17:46 家表的规范折表层 —— 全表不重排、诚实缺席「model id typed in」,cli 目录/web 向导同一份折表)· `TIER_ORDER` / `CC_TIER_ALIASES` / `isTier` / `resolveTierBinding`(A-028.18:档位词表+校验+fail-open 降档派生单源;settings 存储归宿主)· `resolveEntryVision` / `computeDeleteBlockers` / `computeDeleteWarnings`(0.38.0 #318 件③ 上收:Model Hub 供给面的三端公共判定 —— vision 生效值+来源三态、删除断链核(拒删+指路)、删除降级后果(照删但必说)。**零 IO**,读盘那半场留各端;`doc === null` 的两义在调用方分流) | 三层 provider 目录解析(线上 URL → 包内预设 → 用户覆盖)+ per-model `maxTokens` 封顶。线上腿需注入 `CatalogFetchJson`,缺席 ⇒ 整条不启用(`online.reason='no-fetch-port'`);缓存落盘经 `CatalogCachePort` | `src/model/{catalog,catalogLoader,providerAuth,providerPresets,providerCatalog,tierVocabulary}.ts`、`src/liveModelCatalog.ts`、`src/modelBudgetRule.ts`、`src/sessionModelLatch.ts`、`src/effortWire.ts` |
|
|
153
160
|
| 12 | **workflow 与后台工作视图** | 19 | `projectWorkflowRun` · `createLiveWorkflowSource` · `ensureWorkflowActivityLedger` · `readWorkflowActivityLedger` · `stopWorkflowActivityLedger` · `resetWorkflowActivityLedgers` · `createBackgroundView` · `projectBackgroundView` · `recordWorkflowAgentTaskId` · `agentDisplayStatus` | 活过一个 turn 的长任务读面:workflow run + 跨 session 后台任务归一表(`assistant.tasks` 与 fleet SSE **两源独立降级**) | `src/workflow.ts`、`src/workflowClient.ts`、`src/workflowMonitor.ts`、`src/agentSession/backgroundView.ts`(+ 纯类型 `src/agentSession/contract.ts`) |
|
|
154
161
|
| 13 | **座位 IPC 契约** | 33 | `LOCAL_SESSIONS_SPEC` · `SEAT_METHOD_NAMES` · `SEAT_EVENT_TYPES` · `isLocalSessionEvent` · `isToolPermissionRequest` · `toolPermissionRequestId` · `SEAT_VALIDATOR_KEY_COVERAGE` | desktop↔web 座位 IPC 契约的**单一真源**(此前两边各一份、名字零重合 ⇒ 编译器永远不会告诉你它们漂了)。🔴 加 verb 忘了加 `LOCAL_SESSIONS_SPEC` **不报错**:preload 不注册 channel、渲染端读到 `undefined` | `src/seatContract.ts`(**零 import**,纯类型 + 常量 + 纯谓词)。🔴 **证据等级标注(0.42.0,test [5087] 的「语料**种类**缺口」/ cli [5088] 认领件)**:该文件里所有以「CC 如何如何」为形的断言(`212 methods` / `854-channel census` / 方法名逐字保留 / `fQe` 逐字段对照 / 一切 `.vite/build/index.chunk-*.js` 坐标)**证据等级 = 桌面 unpack,本地语料库不可复验** —— 本仓手边可复验的参照语料**只覆盖终端 CLI 形态**的静态产物,拿它去 grep 桌面壳里的符号只会零命中,而零命中在这里**既不证真也不证伪**。复核这些断言**不得**拿本仓语料当反证 |
|
|
155
162
|
| 14 | **宿主端口与会话槽** | 26 | `installHost` · `installHostFor` · `hostPortMisses(For)` · `DEFAULT_SESSION_KEY` · `hostEnv` · `unrefTimer` · `parseLocaleTag` / `pickUiLanguage`(#244 F4 族D A-028.20:locale tag 手术单源 + UI 语言判定;与 `resolveRegionHint` 双出口成文 —— 语言偏好域 en/zh ≠ 地址可达域 cn/intl/unknown,`zh-Hant` 前者 zh 后者 intl 是设计)· `engineNamespaceKeyFor` / `mergeSessionMapRecord` / `mergeEngineEntry`(A-028.12:会话 id 映射单一键形 + merge 判定;存储经 `SessionMapStorePort` 归端 —— cli 文件锁/原子写,web localStorage)| 进程/端级装配层(settings/fs/queue/timers/session/log/probe),与 per-turn 的 `AdapterContext` **分层**。头注的判定规则:**这个能力每 turn 都会变吗?** 会 ⇒ `ctx`;不会 ⇒ `installHost` | `src/host.ts`、`src/hostEnv.ts`、`src/sessionSlot.ts`、`src/unrefTimer.ts`、`src/env/{localeGeo,localeTag,uiLanguage}.ts`、`src/sessionMap.ts` |
|
|
156
163
|
| 15 | **控制面与传输** | 82 | `ControlRouter`(+ `ControlSafetyError`)· `makeEngineWireClient` + `resolveWireAuth` · `installEngineWireTarget` · `diagnoseSseIdleTear` / `isSseIdleError` · `attemptActiveRunSelfHeal` + `activeRunBusySignal` + `activeRunSelfHealRow` / `activeRunBusyHeadlessRow` · `kickEngineCapsProbe` / `engineCapTrue` / `invalidateEngineCaps(baseUrl, probe?)`(#307 S25:引擎温切后的 caps 生产失效口 —— kick 自带幂等闸,同 baseUrl 重启后不显式失效就永远读到旧引擎那一版能力位;调用方 = 壳的 respawn/restartEngine。🔴 **推荐两参形**:第二参给替代探测则「推进代际 + 注册新探测」在同一同步块内完成,失效与下一次 kick 之间那个「等待者读到未判」的窗按构造不存在;单参形保留给「只丢缓存、这一刻没有替代探测」的调用方,那种情形下读到未判是诚实结局) · `mapBrainStatusToRetry` · `waitForClaimRelease` + `CLAIM_RELEASED_STATES` / `CLAIM_HELD_STATES` · `atMostOnceFailureClass` / `readSteerDelivery` · `clearRunningChoiceOffer`(Inkglow-1085 P0b①:「Do nothing」登记的清口 —— 端的「重新打开操作菜单」入口;登记在场时 attemptActiveRunSelfHeal 不整卡重弹,not-parked 结局带 `alreadyOffered: true` 判别位,端据此降级渲一行)· `INTERACTIVE_WAY_OUT`(默认出路串单源)· `engineSessionParamFor`(design/285 批 0:`?session=` 派生的 **per-key** 形 —— `hostSessionFor(sessionKey)?.currentSessionId()` + [1501] 空串归一;零参 `engineSessionParam()` = 默认槽兼容层,取值链逐字等价)· `normalizeWirePrincipal`(A-028.10:principal 在场性 trim 原语 —— 全空白=缺席不发头,engineWireTarget 两臂/makeEngineWireClient/壳 livePrincipal 同尺)· `readSessionMemoryStatus` / `classifyMemoryStatusFailure` / `readCaptureOptOut` / `readLastCapture`(S-53 会话记忆姿态读面,0.48.0:失败分诊**码优先**——两个 404 分道 `not_found.session` / `not_found.route`,无码 404 不猜落 failed;五键逐键缺席语义两个合读器,`lastCapture` 三态的判别材料是 `committedCount` 不是本键;IO 归宿主注入 `MemoryStatusClientLike`,详见 §11) · `classifyTurnWireError` / `isWireTransportError` / `isPreStreamDrainingReject` / `isResumeAtRejection` / `drainingRetryDelayMs` / `scenarioDenyFromError` + `WIRE_NETWORK_ERROR_PATTERN`(A-028.11/.13:turn 错误分型判定半场,人话文案与渲染归端) | 上行通道的**监管**半场(submit / steer / kill / 队列命令定序)+ 传输构造、caps 探测、SSE 断流分诊、**409 active-run 自愈** | `src/controlRouter.ts`、`steering.ts`、`sseIdleTriage.ts`、`retryStatus.ts`、`diagnostics.ts`、`engineWireSdk.ts`、`engineWireTarget.ts`、`src/principalWire.ts`、`src/wireErrorTriage.ts`、`engineSessionParam.ts`、`engineCapsCache.ts`、`liveInitToolFace.ts`、`adapter/activeRunSelfHeal.ts`、`src/sessionMemoryStatus.ts` |
|
|
157
|
-
| 16 | **引擎词汇表与包自检** |
|
|
164
|
+
| 16 | **引擎词汇表与包自检** | 49 | `CONFIG_REFUSAL_CODES` / `isConfigRefusalCode` · `STOP_CONFLICT_CODES` · `isInterruptedToolEndCode` · `isRewindFamilyCode` · `CLIENT_VERBS` · `compensationSplitViolations` · `DELEGATION_CAP_CODES` / `isDelegationCapCode` / `DELEGATION_CONCURRENCY_CAP` / `DELEGATION_SESSION_CAP`(0.38.0 #318 件④:core 5.48.0 design/323 委派席位到限**两码,处置不对称禁合并** —— 并发帽=**可等**(兄弟结束即有位)/ 会话累计帽=**等也没用**(这条会话的配额用尽))· `CONFIG_DELEGATION_ENTRY_CAPS`(同批入 `CONFIG_REFUSAL_CODES` 识别表)· `delegationCapDispositionOf` / `MCP_SERVER_REVOKED`(0.39.0 载体到货消费件:core 5.50.0 补 `{ error: code, code }` 孪生拼法后两码真上 `tool_end.errorCode`,0.38.0「先立词不落消费分支」的已知局限自此解除;处置轴 `wait-for-slot` / `reuse-existing-or-await-reap` 机器可读(累计帽=retained-window 帐,行回收配额即回,处置=SendMessage 复用,**非**「换会话/永久耗尽」——0.38.0 段该句系勘误),未知 `delegation.*` 码 ⇒ `undefined`;`mcp.server_revoked` = 操作员 mid-session 吊销 server 后的工具面本地闸(被吊销的 server **名**今天不过 wire 境:detail.server 是进程内位,抬升腿只 lift code——归因渲染候 core 补 typed detail,已点名);载体门 = engine-vocab G3 腿锚 core dist 铸点)· `CAPABILITY_SELF_ORCHESTRATION_REQUIRED`(S-81,server 7.57.0:提交面的 selfOrchestration 准入拒绝码。🔴 **复用码** —— 与其它 `capability.*` 501 同体形而处置不同,消费点必须按**恰等**判、绝不放宽成前缀判;判型与「去键重发一次」归 `src/selfOrchestrationDenial.ts`,详见 §13) | 三端分臂共用的**去字面化** `errorCode` 词表(病根正是三端各抄一份字面);编译期 verb 闭合门;搬迁补偿登记表 | `src/engineErrorCodes.ts`(计数以 `scripts/public-export-baseline.json` 为准,别手抄;A-028.11/.13 补 `DRAINING_ERROR_CODE`/`SCENARIO_NOT_ALLOWED_ERROR_CODE`/`RESUME_AT_ERROR_CODE_PREFIX`;#318 件④ 补 `delegation.*` 族四位 + `config.delegation_entry_caps`;0.39.0 补三新码消费件三位);S-81 补 `capability.self_orchestration_required` 一位)、`src/classifierVerdictWire.ts`、`src/compensations.ts`、`src/clientSlice.ts` |
|
|
158
165
|
|
|
159
166
|
🔴 **`engineErrorCodes` 的开集纪律**(该文件头注逐字):这些 `ReadonlySet` / 前缀谓词一律是**识别表**,
|
|
160
167
|
回答的是「我认不认得这个码」,**绝不是**「合法码只有这些」。消费点 `switch` **必须留 `default`**,
|
|
@@ -1996,3 +2003,264 @@ if (orphans === undefined) {
|
|
|
1996
2003
|
**cli 认领**:壳侧接点(`--resume` 后的一行披露)在下一批。
|
|
1997
2004
|
**实现锚**:`src/hitl/crashConverged.ts`(信封形放宽在 `src/hitl/hitlBridge.ts` 的 `ApprovalsResourceLike`)。
|
|
1998
2005
|
**常驻门**:`scripts/run-crash-converged-projection-test.mjs`。
|
|
2006
|
+
|
|
2007
|
+
---
|
|
2008
|
+
|
|
2009
|
+
## §13 🆕 S-81 selfOrchestration 拒绝的三端同形(server ≥7.57.0;工作树未发)
|
|
2010
|
+
|
|
2011
|
+
server 7.57.0 在**半配置的多租户形态**上收窄了三条。判据只在这一种形态上成立,先把它说准:
|
|
2012
|
+
`REQUIRE_PRINCIPAL=true`(要求 principal)+ `SELF_ORCHESTRATION_ENABLED=true`(引擎侧开着)
|
|
2013
|
+
+ **没有中心侧的准入解析器** ⇒ server 无法为这位 principal 判「能不能用 workflow」,fail-closed。
|
|
2014
|
+
单用户 worker / 接了解析器的多租户部署都**不**走这条路,行为逐字不变。
|
|
2015
|
+
|
|
2016
|
+
| 面 | 7.56 及以前 | 7.57.0 起 |
|
|
2017
|
+
|---|---|---|
|
|
2018
|
+
| `GET /v1/capabilities` 的 `workflows` | `true` | **`false`**(它现在把「本 principal 能不能真用上」也算进去了) |
|
|
2019
|
+
| 同一份 caps | — | 新 additive 键 `workflowsGate: { engineCan: boolean, denial: "entitlement_resolver_absent" \| null }` |
|
|
2020
|
+
| `POST /v1/tasks`(及同闸的 stream 提交)带 `selfOrchestration:true` **或** `settings.ultracode:true` | 受理 | **501 `capability.self_orchestration_required`**(体形与其它 `capability.*` 501 同);去掉这两个键 ⇒ **照常受理** |
|
|
2021
|
+
| 非布尔的 `selfOrchestration` | — | **400**(值不对 ≠ 不给用,**不是**本码) |
|
|
2022
|
+
|
|
2023
|
+
### 13a. 为什么这三处在库里(三端各写一遍必然各错一遍)
|
|
2024
|
+
|
|
2025
|
+
1. **「要不要去键重发」是判定不是文案**。判据是**合取**:`status === 501` **∧** `errorCode`
|
|
2026
|
+
**恰等** `capability.self_orchestration_required`。🔴 这个码是**复用码** —— 按 `capability.`
|
|
2027
|
+
**前缀**分诊会把「别的能力位没接线」的 501 也拖进重发臂,而那些请求去掉这两个键**也不会**
|
|
2028
|
+
变成可受理:白发一次请求 + 给用户一个错误的原因。**无码的 501 判不出**(落 `null`,如实说),
|
|
2029
|
+
码对但状态码不是 501 同样 `null`。
|
|
2030
|
+
2. **「去掉哪两个键」是结构操作不是措辞**。`selfOrchestration` 在**顶层**,`ultracode` 在
|
|
2031
|
+
`settings` 下 —— 两条不同的 stamp 腿(`selfOrchestrationWireCaps.ts` / `ultracodeWireCaps.ts`),
|
|
2032
|
+
端各自手写 `delete` 必然有人漏掉第二处,而漏掉的后果是「重发一次、又被拒一次」:
|
|
2033
|
+
用户看到的是同一个功能坏了两遍。
|
|
2034
|
+
3. **caps 的「缺席」与「关着」是两件事**。老 server 压根没有 `workflowsGate` 这个键;把缺席读成
|
|
2035
|
+
「引擎说不行」是替一台**什么都没说**的 server 下断言([honest-absence-not-fabricated-zero])。
|
|
2036
|
+
反方向的同一种病:把一个**认不得的** `denial` 折成 `null` ⇒ 端渲出「没有任何拒绝」,
|
|
2037
|
+
而真相是「拒了,只是我不认得原因」。
|
|
2038
|
+
|
|
2039
|
+
### 13b. 端怎么接(提交失败 → 去键重发**一次**)
|
|
2040
|
+
|
|
2041
|
+
🔴 **两条提交腿的接法不同,别只抄第一段**(异源对抗复审 R2 采纳)。`sema` 的两条腿在**什么时候
|
|
2042
|
+
把 501 抛出来**这一点上不一样,而判定层要能跑到才有用:
|
|
2043
|
+
|
|
2044
|
+
| 腿 | SDK 形 | 501 什么时候抛 |
|
|
2045
|
+
|---|---|---|
|
|
2046
|
+
| `tasks.submit(req)` / `tasks.streamRaw(req)` | `async` 方法 | **调用即在飞**,在 `await` 处抛 |
|
|
2047
|
+
| `tasks.stream(req, {transientOk:true})` | **async generator**(`async *stream`) | 调用**不发请求**;POST 在 `streamRaw` 里,要等**第一次迭代**(`next()` / `for await` 的第一拍)才跑,501 也在那时才抛 |
|
|
2048
|
+
|
|
2049
|
+
**① 同步腿(`submit` / `streamRaw`)**
|
|
2050
|
+
|
|
2051
|
+
```ts
|
|
2052
|
+
import type { TaskRequest } from '@sema-agent/sdk'
|
|
2053
|
+
import {
|
|
2054
|
+
buildTaskRequest,
|
|
2055
|
+
classifySelfOrchestrationRefusal,
|
|
2056
|
+
stripSelfOrchestrationIntent,
|
|
2057
|
+
} from '@sema-agent/client-core'
|
|
2058
|
+
|
|
2059
|
+
let req: TaskRequest = buildTaskRequest(input, 'interactive') as TaskRequest
|
|
2060
|
+
// 🔴 `...(signal ? { signal } : {})` 而不是 `{ signal }`:SDK 声明的是 `signal?: AbortSignal`,
|
|
2061
|
+
// 本仓(以及任何开了 `exactOptionalPropertyTypes` 的端)显式写入一个可能是 undefined 的值会 TS2379。
|
|
2062
|
+
const opts = (): { signal?: AbortSignal } => ({ ...(signal ? { signal } : {}) })
|
|
2063
|
+
try {
|
|
2064
|
+
return await client.tasks.submit(req, opts())
|
|
2065
|
+
} catch (e) {
|
|
2066
|
+
const denied = classifySelfOrchestrationRefusal(e)
|
|
2067
|
+
if (denied === null) throw e // 🔴 认不得就按普通失败呈现,绝不猜
|
|
2068
|
+
signal?.throwIfAborted() // 🔴 人已经喊停了就别再发第二发(见下)
|
|
2069
|
+
req = stripSelfOrchestrationIntent(req) // 两个键**同时**去掉(单源,端不要自己 delete)
|
|
2070
|
+
transcript.note(disclose(denied.retryWithout)) // 🔴 措辞归端;必须留一行诚实披露(见 13d)
|
|
2071
|
+
return await client.tasks.submit(req, opts()) // 🔴 **只重发一次**;它再 501 就直接外溢
|
|
2072
|
+
}
|
|
2073
|
+
```
|
|
2074
|
+
|
|
2075
|
+
`stripSelfOrchestrationIntent` 有**两个重载**:进一份 SDK `TaskRequest` ⇒ 出来仍是 `TaskRequest`
|
|
2076
|
+
(本函数删的两位在那个型里都是**可选位**,所以「删完仍是一份合法 `TaskRequest`」是类型面成立的事实),
|
|
2077
|
+
所以上面那句「去键之后直接重发」**不需要任何 `as` 强转**;进本包 `buildTaskRequest` 的宽形产物则
|
|
2078
|
+
走宽重载,读法不变。常驻门 §G6 用真 tsc 把这两条路各编一遍(带已知会红的负控)。
|
|
2079
|
+
|
|
2080
|
+
**② 流腿(`tasks.stream`)—— 分类必须放在驱动迭代的 try 里,重发要建**新的** generator**
|
|
2081
|
+
|
|
2082
|
+
```ts
|
|
2083
|
+
// 🔴 两段范式**各自自足**:端只抄第二段也必须编得过(所以 import 在这里再写一遍,不是省略)。
|
|
2084
|
+
import type { TaskRequest } from '@sema-agent/sdk'
|
|
2085
|
+
import {
|
|
2086
|
+
buildTaskRequest,
|
|
2087
|
+
classifySelfOrchestrationRefusal,
|
|
2088
|
+
stripSelfOrchestrationIntent,
|
|
2089
|
+
} from '@sema-agent/client-core'
|
|
2090
|
+
|
|
2091
|
+
async function runOnce(req: TaskRequest, signal?: AbortSignal): Promise<void> {
|
|
2092
|
+
// 🔴 `for await` 的第一拍才真正 POST —— 把 try 套在**迭代**上,不是套在 `stream(...)` 调用上。
|
|
2093
|
+
// 🔴 `signal` 必须**贯穿到每一条** generator,否则外部 AbortController 既停不掉读取、
|
|
2094
|
+
// 也取消不了服务端那条 run(而这是一条会真的跑工具、真的烧 token 的 live task)。
|
|
2095
|
+
// 写法是**条件展开**而不是 `{ signal }` —— 见上一段那条 `exactOptionalPropertyTypes` 注。
|
|
2096
|
+
let sawTerminal = false
|
|
2097
|
+
for await (const ev of client.tasks.stream(req, { transientOk: true, ...(signal ? { signal } : {}) })) {
|
|
2098
|
+
handle(ev)
|
|
2099
|
+
if (ev.type === 'done' || ev.type === 'failed') { sawTerminal = true; break }
|
|
2100
|
+
// 🔴 **每一个非终帧之后立刻查取消**:SSE 的一次读取会缓冲**多帧**,SDK 会连着把它们 yield 出来
|
|
2101
|
+
// (真实时序,不是畸形载荷)。只在整个 `for await` 结束后才查 signal 的话,人已经喊停了却还会
|
|
2102
|
+
// 继续消费同一 chunk 里的后续帧 —— 实测:同一 chunk 是 `turn_start` → `done`,在
|
|
2103
|
+
// `handle(turn_start)` 里 abort,仍会吃掉 `done` 并**报成功**;若后一帧是坏 JSON,取消还会被
|
|
2104
|
+
// 一条 `SyntaxError` 盖掉。终帧那一支先 `break`,让「终帧优先」在这里也成立。
|
|
2105
|
+
signal?.throwIfAborted()
|
|
2106
|
+
}
|
|
2107
|
+
// 🔴 **终帧优先于取消,取消又优先于截断** —— 三者的顺序都是有代价的:
|
|
2108
|
+
// · 已经见到终帧 ⇒ 这条 run **已经有结局了**(可能已经落了副作用)。此时哪怕 signal 也已经
|
|
2109
|
+
// aborted(取消恰好发生在终帧交付之后),报「取消」就是把一个真实终局盖掉,而人多半会再跑
|
|
2110
|
+
// 一遍 ⇒ 重复执行。所以下面两条判都**只在 `!sawTerminal` 时**才轮得到。
|
|
2111
|
+
// · 没见到终帧而 signal 已 aborted ⇒ 这是**用户主动取消**:`readSseFrames` 见到
|
|
2112
|
+
// `signal.aborted` 会**正常** return,于是 `for await` 也正常结束。少了这一判,取消会被改写
|
|
2113
|
+
// 成一条普通流故障 —— 靠 `AbortError` 抑制报错 / 决定要不要重试的端会把取消渲成故障,
|
|
2114
|
+
// 甚至照着「故障」再提交一次。取消的原因必须原样保真。
|
|
2115
|
+
// · 都不是 ⇒ 才是真的**被腰斩**:SDK 的 `stream()` 只在 `done` / `failed` 处 `return`,
|
|
2116
|
+
// 流被中途干净截断(或一帧都没产出)时 `for await` 同样正常结束。少了这一判,一次被腰斩的
|
|
2117
|
+
// live task 会被端静默当成功 —— 这条腿上最难发现的假绿。
|
|
2118
|
+
if (!sawTerminal) {
|
|
2119
|
+
if (signal?.aborted) signal.throwIfAborted()
|
|
2120
|
+
throw new Error('stream ended without a terminal frame (done/failed)')
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
let req: TaskRequest = buildTaskRequest(input, 'interactive') as TaskRequest
|
|
2125
|
+
try {
|
|
2126
|
+
return await runOnce(req, signal)
|
|
2127
|
+
} catch (e) {
|
|
2128
|
+
const denied = classifySelfOrchestrationRefusal(e)
|
|
2129
|
+
if (denied === null) throw e
|
|
2130
|
+
signal?.throwIfAborted() // 🔴 中断闸,见下
|
|
2131
|
+
// 🔴 generator 用过就不能重来:去键之后必须 `stream(...)` **建一条新的**(旧的那只已经出局)。
|
|
2132
|
+
req = stripSelfOrchestrationIntent(req)
|
|
2133
|
+
transcript.note(disclose(denied.retryWithout))
|
|
2134
|
+
return await runOnce(req, signal) // 🔴 仍然**只重发一次**
|
|
2135
|
+
}
|
|
2136
|
+
```
|
|
2137
|
+
|
|
2138
|
+
🔴 **中断闸不是可选的**(异源对抗复审 R3 采纳):首发 501 与去键重发之间隔着一次分类 + 一次
|
|
2139
|
+
写披露,人完全可能就在这个窗口里按下停止。少了 `signal?.throwIfAborted()`,端会在用户已经喊停
|
|
2140
|
+
之后**再发一条会跑工具、会烧 token 的 run**,并且还写一行「已按不带编排的方式继续」——
|
|
2141
|
+
那是实打实的取消语义回归。同理 `signal` 要透传进**两次**调用(第二条 generator 也要受它管)。
|
|
2142
|
+
|
|
2143
|
+
🔴 **把 try 套在 `client.tasks.stream(...)` 这一行上是无效的**:`await` 一个 async generator 对象
|
|
2144
|
+
不会执行函数体,那一行永远不抛 ⇒ 分类器根本不运行 ⇒ 去键重发整条失效,用户直接看到 501。
|
|
2145
|
+
实装 SDK 坐标:`node_modules/@sema-agent/sdk/dist/resources/tasks.js` 的 `async *stream`
|
|
2146
|
+
(它 `await this.streamRaw(req, opts)`,而 POST 在 `streamRaw` 里)。
|
|
2147
|
+
本包常驻门 §G5 用**真 `TasksResource`** + 一只假传输层离线钉住这两条腿的差异。
|
|
2148
|
+
|
|
2149
|
+
🔴 **只重发一次,不做重试环**:第二发若仍是 501,按**普通失败**呈现(不再分类、不再去键)。
|
|
2150
|
+
本包**不提供**重试原语也不代端计数 —— 「重发几次」是端的编排,库只给判据。
|
|
2151
|
+
🔴 **重发的是同一条用户意图,但不是同一个请求**:body 少了两个键 ⇒ 若端在用**幂等键**
|
|
2152
|
+
(server 7.56.0 的 idempotency 语义),这一发必须换一个新键,否则 server 会把它当成前一发的重放
|
|
2153
|
+
而回放那条 501。库不碰这一位(它不在 `TaskRequestLike` 的判定面上),端自己在 `submit` 里给。
|
|
2154
|
+
🔴 **不改 `deferTools`**:`stripSelfOrchestrationIntent` 只做减法、且只减那两处。把 `Workflow`
|
|
2155
|
+
从 `deferTools` 里拿掉或塞回去都是**行为改动**,不是「去掉意图」。
|
|
2156
|
+
|
|
2157
|
+
`stripSelfOrchestrationIntent` 的精确语义(常驻门逐条钉):
|
|
2158
|
+
|
|
2159
|
+
| 输入 | 输出 |
|
|
2160
|
+
|---|---|
|
|
2161
|
+
| 顶层 `selfOrchestration` 在场 | 键被删(不是置 `undefined`) |
|
|
2162
|
+
| `settings.ultracode` 在场,`settings` 还有别的键 | 只删 `ultracode`,**其余子键与它们的值逐字保留** |
|
|
2163
|
+
| `settings` 里只有 `ultracode` | **整个 `settings` 键删掉**(与 `buildTaskRequest`「全缺则整键不出现」同语义) |
|
|
2164
|
+
| `settings` 里没有 `ultracode` | `settings` **原样带出**(同一只对象;本来就空的也不删) |
|
|
2165
|
+
| `settings` 不是对象(串/数组/`null`) | **一律不动**(读不懂的东西不去动它) |
|
|
2166
|
+
| 只挂在**原型**上的 `ultracode` | 不算在场(自有键判) |
|
|
2167
|
+
| 任意 additive 未知键(顶层与 `settings` 两层) | **全保**(「上游已发的键不许被静默剥掉」) |
|
|
2168
|
+
| 再调一次 | **幂等**,同形结果 |
|
|
2169
|
+
| 入参本身 | **原样不动**(返回的是新对象;端还能拿原体做诊断/日志) |
|
|
2170
|
+
|
|
2171
|
+
### 13c. caps 侧:`projectWorkflowsGate` 的返回形
|
|
2172
|
+
|
|
2173
|
+
```ts
|
|
2174
|
+
import { projectWorkflowsGate } from '@sema-agent/client-core'
|
|
2175
|
+
|
|
2176
|
+
const gate = projectWorkflowsGate(caps) // caps = GET /v1/capabilities 的回体
|
|
2177
|
+
if (gate === undefined) { /* 🔴 零渲染:本部署没告诉我这件事 */ }
|
|
2178
|
+
else {
|
|
2179
|
+
// gate.workflows —— **唯一**该拿来决定「给不给 /workflows、ultracode 入口」的位
|
|
2180
|
+
// gate.engineCan —— boolean | undefined(undefined = 这台 server 没有这个闸 ⇒ 两根轴都别渲)
|
|
2181
|
+
// gate.denial —— null | 'entitlement_resolver_absent' | { unknown: string }
|
|
2182
|
+
}
|
|
2183
|
+
```
|
|
2184
|
+
|
|
2185
|
+
| caps 形 | 返回 |
|
|
2186
|
+
|---|---|
|
|
2187
|
+
| 非对象 / `null` / `workflows` 不是布尔 / `workflows` 只在原型上 | `undefined`(**诚实缺席**,端零渲染) |
|
|
2188
|
+
| 有 `workflows`、无 `workflowsGate`(老 server) | `{ workflows, engineCan: undefined, denial: null }` |
|
|
2189
|
+
| `workflowsGate` 在场却读不出(非对象 / accessor) | 同上一档(`engineCan === undefined` 就是端判这一档的那一位) |
|
|
2190
|
+
| `workflowsGate: { engineCan: true, denial: 'entitlement_resolver_absent' }` | `denial` 按**字面量**带出(端可以据此说人话) |
|
|
2191
|
+
| `denial` 缺席 / `null` | `null`(闸明说没有拒绝) |
|
|
2192
|
+
| `denial` 是**别的串** | `{ unknown: <原串> }` —— 🔴 **绝不** `null` |
|
|
2193
|
+
| `denial` 非串 / 是 accessor | `{ unknown: '' }` —— 仍然是拒绝,只是连记号都带不出来 |
|
|
2194
|
+
|
|
2195
|
+
🔴 四处不可信读取(`caps.workflows` / `caps.workflowsGate` / `gate.engineCan` / `gate.denial`)
|
|
2196
|
+
只认**自有数据描述符**,accessor 与只挂在原型上的位**一次都不执行**
|
|
2197
|
+
(`catch` 接得住「抛」,接不住「不返回」——一只死循环的 getter 会把这条读面所在的线程永久钉住)。
|
|
2198
|
+
🔴 **但「不执行」之后落哪一档,四处并不相同**(异源对抗复审 R2 采纳的订正 —— 上一版这里写成
|
|
2199
|
+
一句「一律当缺席」,与上面那张表自相矛盾,端照概括实现就会把「读不出的拒绝」折成「没有拒绝」,
|
|
2200
|
+
正好违反本节的核心不变量):
|
|
2201
|
+
- `caps.workflows` / `caps.workflowsGate` / `gate.engineCan` 三处:accessor **按缺席处理**
|
|
2202
|
+
(依次 ⇒ 整只 `undefined` / 闸缺席档 / `engineCan: undefined`);
|
|
2203
|
+
- `gate.denial` **一处例外**:accessor ⇒ `{ unknown: '' }`,**不是** `null`。键**不在场**才是
|
|
2204
|
+
「闸明说没有拒绝」;在场却执行不得是「拒了,但读不出」——两者处置相反,所以本包的读口刻意是
|
|
2205
|
+
**三态**(缺席 / accessor / 数据)而不是两态。
|
|
2206
|
+
- 只挂在**原型**上的位(不是自有位)四处**都**按缺席处理 —— 那不是这份供给自己说的话。
|
|
2207
|
+
|
|
2208
|
+
真供给来自 `JSON.parse`,每一位都是自有数据位 ⇒ 以上对真 caps 零影响;accessor 载体属 §12e
|
|
2209
|
+
明确排除的「非 JSON 合成载荷」族,这里成文只是为了让公开契约与实现**逐字**对得上。
|
|
2210
|
+
🔴 `{ unknown: … }` 里的串是 **server 的内部词,不是给人看的话**:端可以记进日志/诊断面,
|
|
2211
|
+
**不要**当文案直接上屏。
|
|
2212
|
+
|
|
2213
|
+
### 13d. 端必读的五条(含负控)
|
|
2214
|
+
|
|
2215
|
+
1. 🔴 **重发必须留一行诚实披露**。用户按的是「用 ultracode 跑」,而实际跑的是**没有** workflow
|
|
2216
|
+
编排的那一发 —— 不说 = 让人以为自己要的东西生效了。措辞归端(库零文案),内容至少要说清
|
|
2217
|
+
「这台部署不提供 workflow 编排,已按不带编排的方式继续」+ 去掉了哪两个意图
|
|
2218
|
+
(`SELF_ORCHESTRATION_RETRY_WITHOUT` 就是那份清单)。
|
|
2219
|
+
⚠️ 别把 `reason` 直接翻给用户:今天 server 的 501 体形**不带**机器可读的 `denial` 位 ⇒
|
|
2220
|
+
`reason` 在真 wire 上恒是 `'unknown'`(闭集那一臂是**防御臂**,server 补这一位的当天自动点亮)。
|
|
2221
|
+
要给具体原因,材料在 **caps 侧**的 `projectWorkflowsGate(...).denial`,不在这条错误上。
|
|
2222
|
+
2. 🔴 **负控一:老 server 零渲染**。`projectWorkflowsGate` 返回 `undefined` 时,`/workflows`
|
|
2223
|
+
与 ultracode 入口**照旧**(别渲「不可用」、也别渲「可用」)—— 那台 server 什么都没说。
|
|
2224
|
+
3. 🔴 **负控二:非 501 不重发**。`classifySelfOrchestrationRefusal` 返回 `null` 的一切情形
|
|
2225
|
+
(别的 501 码、无码 501、500/400/503、传输错、非对象抛出物)一律按普通失败呈现。
|
|
2226
|
+
多发一次请求的代价不只是延迟:它会让一个**已经落过副作用**的失败被重放。
|
|
2227
|
+
4. 🔴 **负控三:未知 `denial` 值不崩、也不静默**。`{ unknown: … }` 是一个端必须有分支的形;
|
|
2228
|
+
把它 `?? null` 掉就回到了本节要根除的那句假断言。渲染上的保守做法 = 与闭集成员同档处理
|
|
2229
|
+
(「本部署不提供 workflow 编排」),只是不说具体原因。
|
|
2230
|
+
5. 🔴 **流腿:干净 EOF 不等于跑完**。SDK 的 `tasks.stream()` 只在见到 `done` / `failed` 时 `return`;
|
|
2231
|
+
流被中途干净截断、或一帧都没产出时,`for await` **同样正常结束**。所以 §13b ② 的 `runOnce`
|
|
2232
|
+
带一个 `sawTerminal` 判 —— 少了它,一次**被腰斩**的 live task 会被端静默当成功,而这条腿上
|
|
2233
|
+
恰恰跑着会真的执行工具、真的烧 token 的任务。这一条与本节的去键重发**正交**(它对每一条流都
|
|
2234
|
+
成立),写在这里是因为 §13b 的范式是端照抄的那一份。
|
|
2235
|
+
|
|
2236
|
+
### 13e. 射程边界
|
|
2237
|
+
|
|
2238
|
+
与 §12e 同款:真供给 = server JSON → SDK `JSON.parse` → 端,每一位都是自有数据属性。
|
|
2239
|
+
**被中间层合成的非 JSON 载荷 / 敌意 `Proxy` / 原型注射**这一族**不在射程内** —— 本件对它们只承诺
|
|
2240
|
+
**不抛**、**绝不把一个说不清的 `denial` 折成「没有拒绝」**;**不承诺**还原出「真实内容到底是什么」。
|
|
2241
|
+
后续复审若再命中这一族,处置是**照 §12e 引用、不再迭代**。
|
|
2242
|
+
|
|
2243
|
+
🔴 **但「getter 零执行」这一条只对 `projectWorkflowsGate` 成立,对
|
|
2244
|
+
`classifySelfOrchestrationRefusal` 不成立**(异源对抗复审采纳的订正 —— 一句做不到的承诺比没有承诺更坏):
|
|
2245
|
+
|
|
2246
|
+
| 读的是什么 | 读法 | 承诺 |
|
|
2247
|
+
|---|---|---|
|
|
2248
|
+
| `projectWorkflowsGate(caps)` —— **wire JSON** | 只认**自有数据描述符**(四处) | accessor 与原型位一次都不执行(门钉调用数恒 0) |
|
|
2249
|
+
| `classifySelfOrchestrationRefusal(e)` —— **抛出物** | **普通属性读取**(沿原型、会执行 getter) | 只承诺**不抛** |
|
|
2250
|
+
|
|
2251
|
+
抛出物按设计可能是 SDK 的 `APIError` **类实例**,`status` / `errorCode` 完全可能坐在**原型**上、
|
|
2252
|
+
甚至是原型上的 getter(传输层的写法本包不拥有)。只认自有数据描述符会把一个**读得懂**的错判成
|
|
2253
|
+
读不懂 —— 那正是它必须走普通读取的原因(与 `classifyMemoryStatusFailure` / `classifyRulesFailure`
|
|
2254
|
+
逐字同款)。一只**挂死**的 getter 长在抛出物上时本包挡不住:那与「宿主注入了一个会撒谎的传输层」
|
|
2255
|
+
是同一件事。**能收窄的那一半已经收窄**:`denial` 只在 `501 ∧ 恰码` 两条判据都通过之后才读,
|
|
2256
|
+
所以一个根本不匹配的错误**不会**被跑一次它的 getter(门钉调用数恒 0)。
|
|
2257
|
+
|
|
2258
|
+
🔴 **`SELF_ORCHESTRATION_RETRY_WITHOUT` 运行期是冻结的**(`Object.freeze`,不只是 `as const`)。
|
|
2259
|
+
判决的 `retryWithout` 与它是**同一只引用**(单源的代价),所以它必须真冻:否则任意一个 JS 消费者
|
|
2260
|
+
`splice` 它一下,此后同进程内每一次判决都带着被改写的清单,端照它去键就会删掉别的字段。
|
|
2261
|
+
端**不要**尝试写它(严格模式下当场抛)。
|
|
2262
|
+
|
|
2263
|
+
**cli / web / desktop 认领**:壳侧与两端的接点在各自下一批(表态制)。
|
|
2264
|
+
**实现锚**:`src/selfOrchestrationDenial.ts`(码常量单源在 `src/engineErrorCodes.ts`;
|
|
2265
|
+
两条 stamp 腿仍在 `src/selfOrchestrationWireCaps.ts` / `src/ultracodeWireCaps.ts`)。
|
|
2266
|
+
**常驻门**:`scripts/run-self-orchestration-denial-test.mjs`。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/client-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.50.0",
|
|
4
4
|
"description": "Client-side session runtime shared by every sema human client (TUI / web / desktop): sema wire frames (AgentEvent) -> CC session vocabulary (SDKMessage) with dual-plane output (transcript/chrome), deterministic transcript ids, lane discipline as a type, and the notification/dedup ledgers. Every CC-skin shape is collected here so the wire itself stays neutral. Renamed from @sema-agent/wire-cc-adapter (0.1.x).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|