@sema-agent/client-core 0.36.0 → 0.38.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.
Files changed (41) hide show
  1. package/CHANGELOG.md +313 -0
  2. package/README.md +2 -2
  3. package/dist/adapt/arms.js +43 -0
  4. package/dist/adapt.d.ts +1 -1
  5. package/dist/adapt.js +2 -0
  6. package/dist/adapter/activeRunSelfHeal.d.ts +35 -2
  7. package/dist/adapter/activeRunSelfHeal.js +157 -4
  8. package/dist/adapter/downstream/eventToSdkMessage.js +99 -0
  9. package/dist/adapter/runStream.d.ts +14 -1
  10. package/dist/adapter/runStream.js +4 -0
  11. package/dist/engineCapsCache.d.ts +81 -3
  12. package/dist/engineCapsCache.js +183 -15
  13. package/dist/engineErrorCodes.d.ts +20 -0
  14. package/dist/engineErrorCodes.js +44 -0
  15. package/dist/fleet/fleetProjection.d.ts +43 -1
  16. package/dist/fleet/fleetProjection.js +53 -3
  17. package/dist/fleetTaskDesc.d.ts +5 -1
  18. package/dist/fleetTaskDesc.js +39 -2
  19. package/dist/hitl/armedGateRegistry.js +11 -3
  20. package/dist/hitl/hitlBridge.d.ts +20 -0
  21. package/dist/hitl/hitlBridge.js +215 -12
  22. package/dist/hitl/parkResolver.d.ts +1 -0
  23. package/dist/hitl/parkResolver.js +22 -2
  24. package/dist/hitl/toolApprovalWire.d.ts +41 -6
  25. package/dist/hitl/toolApprovalWire.js +40 -6
  26. package/dist/index.d.ts +1 -0
  27. package/dist/index.js +4 -0
  28. package/dist/model/modelSupplyRules.d.ts +102 -0
  29. package/dist/model/modelSupplyRules.js +149 -0
  30. package/dist/model/providerPresets.js +68 -12
  31. package/dist/request/taskRequest.js +11 -11
  32. package/dist/retryStatus.d.ts +38 -3
  33. package/dist/retryStatus.js +15 -5
  34. package/dist/seam.d.ts +48 -1
  35. package/dist/seam.js +7 -0
  36. package/dist/subagent/engineSubagentResume.d.ts +18 -0
  37. package/dist/subagent/engineSubagentResume.js +7 -0
  38. package/dist/toolResult.d.ts +8 -0
  39. package/dist/toolResult.js +15 -0
  40. package/docs/INTEGRATION-CLIENTS.md +69 -20
  41. package/package.json +4 -4
@@ -0,0 +1,149 @@
1
+ /**
2
+ * modelSupplyRules.ts — **Model Hub 供给面的三端公共判定**(#318 件③ 上收,cli [4752] 预告的
3
+ * 「resolveEntryVision / computeDeleteBlockers / computeDeleteWarnings 三纯函数」)。
4
+ *
5
+ * ── 为什么在这里 ────────────────────────────────────────────────────────────────────────────
6
+ * 三端(TUI / web / desktop)的 Model Hub 要回答同样的三个问题:
7
+ * ① 这条模型档的 `vision` **生效值**是什么、**出处**是哪(自己表态 / 家族缺省 / 没人说过);
8
+ * ② 删这条档会不会**断链**(该拒删,并指路);
9
+ * ③ 删这条档会不会**悄悄改语义**(该照删,但确认屏必须逐条说出后果)。
10
+ * 判定放在库里、装配与呈现留端 —— 三端各写一遍的话,三个 Hub 会对「同一条档能不能删」给出三个
11
+ * 答案,而其中两个是在用户按下 y 之后才被发现的。
12
+ *
13
+ * ── 🔴 零 IO(本模块的立身之本)──────────────────────────────────────────────────────────────
14
+ * 三个函数**只吃已读好的数据**,不碰文件系统、不认识 `models.json` 在哪、不持锁。读盘那半场留在
15
+ * 各端(壳 = `modelChannels.poolDeleteBlockers` / `poolDeleteWarnings`,它们读完再调这里)。
16
+ * 分层的判据在 cli 侧成文:`doc === null` 的两义**在调用方分流** —— 本模块只认「已读到的文档」,
17
+ * 读不出来时由调用方自己产 `unreadable` 阻断(那一刻并不知道谁指着它,fail-closed 拒删比
18
+ * 「猜没人指着」安全)。
19
+ *
20
+ * ── ⚠️ 上收差分(逐条,行为零改动)────────────────────────────────────────────────────────
21
+ * · `resolveEntryVision` 的返回型从**内联匿名对象**改为具名 {@link EntryVisionResolution}
22
+ * —— 本仓 typeshape 门对导出面的内联匿名形有棘轮(B4)。**结构逐字相同**,壳侧剪切时零适配。
23
+ * · `inferFamily` 的来源从壳内 re-export 改为同包 `./providerPresets.js` 直取(壳那份本来就是
24
+ * 本包的 re-export)。⚠️ 与 #318 件③ 的**分层遮蔽修同批**:修前 `inferFamily(deepseek-*)?.vision`
25
+ * 结构性恒 `undefined`,所以 `source:'family'` 这一档对 deepseek 族**从来没走到过**;修后才真的
26
+ * 走得到。上收与修同批落地是刻意的 —— 只上收不修,等于把一条死分支原样搬进三端。
27
+ * · `isPlainObject` 在壳里是文件级私有,这里同形自持(不新增公面导出)。
28
+ */
29
+ import { inferFamily } from './providerPresets.js';
30
+ /** 局部判据:普通对象(非 null、非数组)。与壳侧 `modelChannels.isPlainObject` 同形。 */
31
+ function isPlainObject(v) {
32
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
33
+ }
34
+ /**
35
+ * 解析一条 entry 的 vision **生效值 + 来源**。纯函数(只吃 modelId/vision 两位,零 IO)。
36
+ *
37
+ * 🔴 [honest-absence]:家族与 entry 都不表态时给 `undefined`(未知),**不**给 `false`。
38
+ * ⚠️ 「未知」只是**客户端这一层**诚实:引擎侧 registry-core `ModelEntry.vision` 是
39
+ * `z.boolean().default(false)`,键缺席在那边就等于 false。**呈现层必须把这个后果一起说出来**,
40
+ * 别让用户以为「未知」= 引擎会自己去问供应商。
41
+ */
42
+ export function resolveEntryVision(entry) {
43
+ if (typeof entry.vision === 'boolean')
44
+ return { value: entry.vision, source: 'entry' };
45
+ const fam = inferFamily(entry.modelId)?.vision;
46
+ if (typeof fam === 'boolean')
47
+ return { value: fam, source: 'family' };
48
+ return { value: undefined, source: 'unknown' };
49
+ }
50
+ /**
51
+ * 纯判定:删这条 entry 会带来哪些**语义变化**(不阻断)。
52
+ *
53
+ * 两格,都是「链不断但意思变了」:
54
+ * ① `atModelAllowlist` 的**有效名单**会被删空 ⇒ 按 registry-core 语义**全部启用模型都可 @**
55
+ * —— 一次静默的权限放宽,必须说;名单里还有别人 ⇒ 只是缩小名单,不用说。
56
+ * ② `semaTierGroups` 里有绑定指着它 ⇒ 删完那条绑定悬空 ⇒ 下次按档位解析会落到**另一个模型**上
57
+ * (而不是报错),用户会以为「我明明配了 pro 档」。
58
+ */
59
+ export function computeDeleteWarnings(input) {
60
+ const { id, doc, tierGroups, survivingIds } = input;
61
+ const out = [];
62
+ const allow = doc?.atModelAllowlist;
63
+ if (Array.isArray(allow)) {
64
+ // 🔴 判据锚在**有效名单**上,不锚「字面上是不是只有它一个」:落盘闸口会把**所有悬空名**一起
65
+ // 滤掉,所以 `['victim','already-dangling']` 这种名单删掉 victim 之后有效名单同样变空 = 同样
66
+ // 放宽到「全部启用模型都可 @」,而 `every(n => n === id)` 那条旧判据对它一声不吭 = 承诺给用户
67
+ // 的权限提示上的**假阴**。
68
+ const declared = allow.filter((x) => typeof x === 'string');
69
+ const effectiveBefore = declared.filter(n => n === id || survivingIds.includes(n));
70
+ const effectiveAfter = declared.filter(n => n !== id && survivingIds.includes(n));
71
+ if (effectiveBefore.length > 0 && effectiveAfter.length === 0) {
72
+ out.push({
73
+ kind: 'allowlist-empties',
74
+ consequence: 'it is the last usable model on your @-mention allowlist — deleting it empties the list, which means EVERY enabled model becomes @-mentionable',
75
+ });
76
+ }
77
+ }
78
+ // 档位绑定的取值序与写路径同源:settings 现值优先,settings 没有档位组时回落 models.json 文件里
79
+ // 那份投影(只扫 settings 会漏掉「settings 无档位组、models.json 自带 tierGroups」那一格 ——
80
+ // 那份绑定同样会在写路径被 byId 静默滤掉)。
81
+ const groups = tierGroups
82
+ ? Object.entries(tierGroups.groups).map(([g, def]) => [g, def?.tiers ?? {}])
83
+ : Array.isArray(doc?.tierGroups)
84
+ ? doc.tierGroups.flatMap(g => isPlainObject(g) && typeof g.name === 'string' && isPlainObject(g.tiers)
85
+ ? [[g.name, g.tiers]]
86
+ : [])
87
+ : [];
88
+ for (const [group, tiers] of groups) {
89
+ for (const [tier, ref] of Object.entries(tiers)) {
90
+ if (ref !== id)
91
+ continue;
92
+ out.push({
93
+ kind: 'tier-binding',
94
+ consequence: `tier group "${group}" binds the ${tier} tier to it — that binding goes dangling and the ${tier} tier will resolve to a different model`,
95
+ });
96
+ }
97
+ }
98
+ return out;
99
+ }
100
+ /**
101
+ * 纯判定:这条 entry 现在被哪些**断链类**引用挡着。空数组 = 可以删。
102
+ *
103
+ * 🔴 `doc === null` 的两义**在调用方分流**:这里只认「已读到的文档」;读不出来时调用方自己产
104
+ * `unreadable` 阻断 —— 那一刻我们并不知道谁指着它,fail-closed 拒删比「猜没人指着」安全。
105
+ */
106
+ export function computeDeleteBlockers(input) {
107
+ const { id, doc, rosters } = input;
108
+ const out = [];
109
+ if (doc && typeof doc.default === 'string' && doc.default === id) {
110
+ out.push({
111
+ kind: 'default',
112
+ where: 'models.json `default`',
113
+ fix: 'point another model at the default first (Model Hub: select it and press u), then delete this one',
114
+ });
115
+ }
116
+ if (doc && isPlainObject(doc.roles)) {
117
+ for (const [role, target] of Object.entries(doc.roles)) {
118
+ // `{ select }` 形是**按 tier/needs 现选**,不是 catalog 引用 —— 删模型影响不到它,不拦。
119
+ if (!isPlainObject(target) || typeof target.model !== 'string')
120
+ continue;
121
+ if (target.model !== id)
122
+ continue;
123
+ out.push({
124
+ kind: 'roles',
125
+ where: `models.json roles.${role}`,
126
+ fix: `reassign the "${role}" role to another model first, then delete this one`,
127
+ });
128
+ }
129
+ }
130
+ if (rosters) {
131
+ for (const [name, def] of Object.entries(rosters.rosters)) {
132
+ if (!def || typeof def !== 'object' || !def.slots)
133
+ continue;
134
+ for (const [slot, ref] of Object.entries(def.slots)) {
135
+ if (ref !== id)
136
+ continue;
137
+ // 分阶段(未激活)组同样是真引用:它一被 switchRoster 激活就要按 slot 解析池,指着已删的
138
+ // entry = 激活当场断链。只看 active.slots 的实现会把分阶段组整个漏掉。
139
+ const isActive = name === rosters.active;
140
+ out.push({
141
+ kind: 'roster',
142
+ where: `roster "${name}" slot ${slot}${isActive ? ' (active)' : ' (staged)'}`,
143
+ fix: `point that slot at another model first (/config → Models → rosters), then delete this one`,
144
+ });
145
+ }
146
+ }
147
+ }
148
+ return out;
149
+ }
@@ -64,6 +64,47 @@ function presetIndex() {
64
64
  presetIndexCache = { exact, stems };
65
65
  return presetIndexCache;
66
66
  }
67
+ /**
68
+ * family 表(第④层)的**唯一**匹配器 —— anchored 优先、再 unanchored 兜底。
69
+ *
70
+ * 提出来的理由不是整洁,是**单源**:`vision` 位只住在这张表上,而第①/②/③ 层命中时也要能读到它
71
+ * (见 {@link visionFromFamilyTable})。若两处各写一遍两趟正则,「这个 id 属哪个家族」在同一个函数
72
+ * 里就会有两个答案。
73
+ */
74
+ function familyFromTable(id) {
75
+ for (const fam of MODEL_FAMILIES) {
76
+ if (fam.match && new RegExp(fam.match, 'i').test(id))
77
+ return fam;
78
+ }
79
+ for (const fam of MODEL_FAMILIES) {
80
+ if (fam.match && new RegExp(fam.match.replace(/\^/g, ''), 'i').test(id))
81
+ return fam;
82
+ }
83
+ return undefined;
84
+ }
85
+ /**
86
+ * 🔴 **#318 件③(cli [4752] 自领缺口1)—— `vision` 的分层遮蔽修**。
87
+ *
88
+ * 病(修前的真行为,不是假设):`vision` 位只写在 `modelFamilies.json` 上,而那张表只有**第④层**
89
+ * 读。可是任何真实的 `deepseek-*` id 在**第①层**(preset 大表精确)或**第③层**(家族主干包含)
90
+ * 就已经命中并 return 了,而这两层的构造器 `hitOf` **不带 vision 位** ⇒ 第④层对它们**结构性不可达**
91
+ * ⇒ `deepseek4` 行的 `vision: false` **永远读不出来**,壳的 `MODEL_VISION=false` stamp 恒不发生
92
+ * (下游两处注释自述的行为是死的)。
93
+ *
94
+ * 修的形:把 `vision` 从「第④层的一个字段」提成**与 ctx/maxTokens 证据层正交的一次独立查表**。
95
+ * 判据分工从此是两条轴,各自单源:
96
+ * · **容量轴**(contextWindow / maxTokens / perModelCap)= 四层证据,强者先赢 —— 一字不动;
97
+ * · **vision 轴** = 恒查 family 表(本函数),与哪一层命中**无关**。
98
+ * 为什么正交是对的:preset 大表(第①层)根本没有 vision 列,主干匹配(第③层)拿的是**别的模型**
99
+ * 的行 —— 两者都不是「这个 id 能不能看图」的证据。唯一有这个事实的地方就是 family 表。
100
+ *
101
+ * 🔴 **诚实缺席**:表上没标(键缺席)⇒ 返回 `undefined` ⇒ 调用方**不 stamp 这个键**。
102
+ * 「没人说过这个模型能不能看图」与「确认它不能看图」是两件事,后者会永久封死一个真能力。
103
+ */
104
+ function visionFromFamilyTable(id) {
105
+ const fam = familyFromTable(id);
106
+ return typeof fam?.vision === 'boolean' ? fam.vision : undefined;
107
+ }
67
108
  function normalizeId(raw) {
68
109
  let id = raw.trim().toLowerCase();
69
110
  const slash = id.lastIndexOf('/');
@@ -98,9 +139,23 @@ export function inferFamily(modelId) {
98
139
  };
99
140
  }
100
141
  const id = normalizeId(raw);
142
+ // vision 轴:与容量证据层**正交**的一次独立查表(#318 件③ 分层遮蔽修,推导见 visionFromFamilyTable)。
143
+ // 恒按**完整归一 id** 查(不按主干):family 的 match 是锚在 id 上的前缀正则,拿主干去查会把
144
+ // `deepseek-chat` 的证据换成 `deepseek` 的证据 —— 同族时同解,不同族时是另一个答案。
145
+ const vision = visionFromFamilyTable(id);
101
146
  // ① preset 大表精确(含剥变体尾巴后再试)——精确命中才带 perModelCap(F2 裁决封顶① 证据位;
102
147
  // ③ 层主干匹配拿的是家族参考行,不是该模型的确证 cap)
103
- const hitOf = (m, exact = false) => ({ id: m.famId, name: m.famId, match: '', contextWindow: m.contextWindow, maxTokens: m.maxTokens, ...(exact ? { perModelCap: m.maxTokens } : {}) });
148
+ const hitOf = (m, exact = false) => ({
149
+ id: m.famId,
150
+ name: m.famId,
151
+ match: '',
152
+ contextWindow: m.contextWindow,
153
+ maxTokens: m.maxTokens,
154
+ // 🔴 诚实缺席:表上没标就**不 stamp 键**(`exactOptionalPropertyTypes` 下 `vision: undefined`
155
+ // 与键缺席不是一回事,消费方读的正是「键在不在」这个三态)。
156
+ ...(vision !== undefined ? { vision } : {}),
157
+ ...(exact ? { perModelCap: m.maxTokens } : {}),
158
+ });
104
159
  const exact = idx.exact.get(id);
105
160
  if (exact)
106
161
  return hitOf(exact, true);
@@ -116,7 +171,16 @@ export function inferFamily(modelId) {
116
171
  const n = Number(inline[1]);
117
172
  if ((inline[2] === 'k' && n >= 8) || inline[2] === 'm') {
118
173
  const ctx = inline[2] === 'm' ? n * 1000000 : n * 1024;
119
- return { id: 'inline-cap', name: 'capacity in id', match: '', contextWindow: ctx, maxTokens: Math.min(ctx, 65536) };
174
+ // vision 轴同样正交透出(`deepseek-v3-128k` 这类 id 在这一层就 return,不透 = 同一个遮蔽病
175
+ // 换个层复发)。
176
+ return {
177
+ id: 'inline-cap',
178
+ name: 'capacity in id',
179
+ match: '',
180
+ contextWindow: ctx,
181
+ maxTokens: Math.min(ctx, 65536),
182
+ ...(vision !== undefined ? { vision } : {}),
183
+ };
120
184
  }
121
185
  }
122
186
  // ③ 家族主干:候选主干=preset model id 剥变体尾;主干出现在待匹配 id 中,取最长主干
@@ -135,16 +199,8 @@ export function inferFamily(modelId) {
135
199
  }
136
200
  if (best)
137
201
  return hitOf(best);
138
- // ④ family 表前缀(anchored → unanchored)
139
- for (const fam of MODEL_FAMILIES) {
140
- if (fam.match && new RegExp(fam.match, 'i').test(id))
141
- return fam;
142
- }
143
- for (const fam of MODEL_FAMILIES) {
144
- if (fam.match && new RegExp(fam.match.replace(/\^/g, ''), 'i').test(id))
145
- return fam;
146
- }
147
- return undefined;
202
+ // ④ family 表前缀(anchored → unanchored)—— 与 vision 轴共用同一个匹配器(单源,见 familyFromTable)
203
+ return familyFromTable(id);
148
204
  }
149
205
  /** Format token counts like "1m" / "384k". */
150
206
  export function fmtTokens(n) {
@@ -59,7 +59,7 @@ export const REQUEST_FIELD_MATRIX = [
59
59
  { field: 'reasoningEffort', lanes: ['interactive'], live: false, why: '`/effort` 拨盘存在 AppState,print 无 AppState', gap: true },
60
60
  { field: 'model', lanes: ['interactive'], live: false, why: '`/model` 中途换模型读 toolUseContext.options.mainLoopModel;print 的模型走 MODEL_ID/--model 另一条路', gap: true },
61
61
  { field: 'images', lanes: ['interactive'], live: false, why: '贴图提交是交互动作,`-p` 的 stdin 没有图片块' },
62
- // ⚠️ 四键一条(0.34.1 补第四键 `resumeAtMode`):它是 `rewindWireCaps` 的 `SeamRewindSpec` 真 wire 键,
62
+ // ⚠️ 四键一条(0.35.0 补第四键 `resumeAtMode`):它是 `rewindWireCaps` 的 `SeamRewindSpec` 真 wire 键,
63
63
  // 且 `rewindSpecForMode` 的 `both` / `conversation` 两臂**恒返**它(排他截语义 —— 回到目标消息
64
64
  // **之前**)。修前合写项只列三键 ⇒ 真 `/rewind` 还原 turn 上 `unregisteredRequestKeys` 会点名
65
65
  // `resumeAtMode`,而那正是「本层不许有未登记键」这条判据的误报,会教端去白名单化真键。
@@ -129,7 +129,7 @@ const shapeTag = (v) => {
129
129
  /**
130
130
  * plain object 判别 —— 按 **prototype** 判,不按 `Object.prototype.toString` 的标签判。
131
131
  *
132
- * 🔴 标签判法是**假的**(0.34.1 codex 对抗复审 [high] 采纳):`[object Object]` 对**普通类实例**同样
132
+ * 🔴 标签判法是**假的**(0.35.0 codex 对抗复审 [high] 采纳):`[object Object]` 对**普通类实例**同样
133
133
  * 成立,而 `Symbol.toStringTag` 还能让任意载体自报这个标签。放它过去之后,下面的摊开用的是
134
134
  * `Object.entries`(**只取自有可枚举键**)—— 于是一个把 `permissions.deny/ask` 挂在**原型 getter**
135
135
  * 上的载体会摊出一个**空快照**,请求照发 = 权限静默变宽,正是本节要堵的那个 fail-open 形。
@@ -139,7 +139,7 @@ const shapeTag = (v) => {
139
139
  * `Map` / `Date` / boxed 包装对象 / 数组一律拒 —— 它们要么内容不在自有键上,要么摊开就是垃圾键;
140
140
  * 「上游产出坏了」比「悄悄发一个更宽的权限面」更该被人看见。
141
141
  *
142
- * 🔴 判据 **realm 无关**(0.34.1 codex 对抗复审第八轮 [medium] 采纳):不拿「**本** realm 的
142
+ * 🔴 判据 **realm 无关**(0.35.0 codex 对抗复审第八轮 [medium] 采纳):不拿「**本** realm 的
143
143
  * `Object.prototype`」做身份比较 —— iframe / `node:vm` / 另一个渲染进程里的对象字面量各有**自己的**
144
144
  * `Object.prototype`,身份比较会把这些**完全合法、JSON 忠实**的载体误判成异形,在 web / desktop 宿主
145
145
  * 上变成提交前的硬失败。
@@ -242,7 +242,7 @@ const materializeJsonFaithful = (v, path, seen = new Set()) => {
242
242
  }
243
243
  return { value: items }; // 重建:原数组(可能是子类/带访问器/带覆盖方法)不出门
244
244
  }
245
- // 🔴 重建成**无原型**记录(0.34.1 codex 对抗复审第七轮 [high] 的可采半场):
245
+ // 🔴 重建成**无原型**记录(0.35.0 codex 对抗复审第七轮 [high] 的可采半场):
246
246
  // ① 忠实 —— 源本来就允许 `Object.create(null)` 字典,重建成 `{}` 等于把载体形换掉了;
247
247
  // ② 少一条改写面 —— 无原型记录不会继承任何**事后**装到 `Object.prototype` 上的 `toJSON`。
248
248
  // ⚠️ 但这只关掉了 record 那一半:重建出来的**数组**必须是真数组(`Array.isArray` / 序列化成
@@ -264,7 +264,7 @@ const materializeJsonFaithful = (v, path, seen = new Set()) => {
264
264
  // `Object.prototype` 的 setter 上:键整个丢掉、还顺手换了目标对象的原型。
265
265
  Object.defineProperty(rec, k, { value: r.value, enumerable: true, writable: true, configurable: true });
266
266
  }
267
- // 🔴 判据的读法**一律排在捕获之后**(0.34.1 codex 对抗复审第十二轮 [high] 采纳):`toJSON` 走属性
267
+ // 🔴 判据的读法**一律排在捕获之后**(0.35.0 codex 对抗复审第十二轮 [high] 采纳):`toJSON` 走属性
268
268
  // 查找、`isPlainRecord` 走 `getPrototypeOf`/`constructor` —— 这三种读法**都可以带副作用**
269
269
  // (`getPrototypeOf` 陷阱在被问的那一刻把 `deny` 清空),而**原生 `JSON.stringify` 从不问原型**。
270
270
  // 校在捕获之前 = 本层自己的读法成了丢内容的那一环(比被动撒谎更糟:那是我们引入的丢失面)。
@@ -281,7 +281,7 @@ const materializeJsonFaithful = (v, path, seen = new Set()) => {
281
281
  /**
282
282
  * 快照 → 摊开成 `settings` 子键前的两道处理:**具名通道让位** + **畸形响亮拒**。
283
283
  *
284
- * ## ① 具名通道管着的键一律让位(0.34.1)
284
+ * ## ① 具名通道管着的键一律让位(0.35.0)
285
285
  *
286
286
  * 快照是**开放集 spread**(子键原样就是 wire 键),所以它天生是一条**第二通道**。0.34.0 只剥了
287
287
  * 「车道异名」键(表里那行的 lanes 不含本车道),于是**两条车道都登记**的具名键剥不到 ——
@@ -296,7 +296,7 @@ const materializeJsonFaithful = (v, path, seen = new Set()) => {
296
296
  * 🔴 剥得**窄**:只剥表里点名的具名子键。快照**表外**的子键(permissions / env / model / …)
297
297
  * 原样摊开 —— 那是开放集的全部意义;`outputStyle` 没有车道行(它是 live 兜底层的位)故不受影响。
298
298
  *
299
- * ## ② 畸形快照响亮拒(0.34.1,fail-closed)
299
+ * ## ② 畸形快照响亮拒(0.35.0,fail-closed)
300
300
  *
301
301
  * `undefined` / `null` = **合法缺席**(端没解析出快照;老写法拿 null 当空快照传),照旧降空对象。
302
302
  * 其余非 plain-object 形(数组 / 原始值 / boxed 包装对象 / Map / **类实例**…)= **上游产出坏了**:
@@ -305,11 +305,11 @@ const materializeJsonFaithful = (v, path, seen = new Set()) => {
305
305
  * `TypeError` 带形状描述(合法载体的判据见 {@link isPlainRecord} —— 「摊得全」是它的全部理由)。
306
306
  * (本包是发出去的 npm 公开面:JS 调用方与版本偏斜的宿主都到得了这里,型面拦不住。)
307
307
  *
308
- * ## ③ 权限面**子树**同样要摊得全(0.34.1 codex 对抗复审第二轮 [high] 采纳)
308
+ * ## ③ 权限面**子树**同样要摊得全(0.35.0 codex 对抗复审第二轮 [high] 采纳)
309
309
  *
310
310
  * 只校最外层是不够的:`{ permissions: new Map([['deny',['Write']]]) }` 的外层是合法字面量,可
311
311
  * `JSON.stringify` 把那只 Map 序列化成 `"permissions":{}` —— deny 规则**静默消失**,与 ② 要堵的
312
- * 是同一个权限变宽形,只是深了一层。所以 `permissions` 子树递归过 {@link jsonFaithfulPath}:
312
+ * 是同一个权限变宽形,只是深了一层。所以 `permissions` 子树递归过 {@link materializeJsonFaithful}:
313
313
  * plain record / 数组 / JSON 原始值才算摊得全,`Map`/`Set`/类实例/boxed/环一律拒。
314
314
  *
315
315
  * 🔴 **射程刻意只到 `permissions`**,不做整份请求体的深净化:那是「包级 wire 载荷 JSON-safe
@@ -335,7 +335,7 @@ const resolvedSnapshotForWire = (resolved) => {
335
335
  // 当场物化成脱钩副本),原件的形状判据留到最后;校不过就整体拒,拒绝面一字未变。
336
336
  const named = namedSettingsSubKeys();
337
337
  const out = {};
338
- // 🔴 **键先捕获、值逐个即读即处理**(0.34.1 codex 对抗复审第六轮 [high]):`Object.entries(resolved)`
338
+ // 🔴 **键先捕获、值逐个即读即处理**(0.35.0 codex 对抗复审第六轮 [high]):`Object.entries(resolved)`
339
339
  // 会把**所有兄弟键**的值先读齐 —— 于是一个 `env`/`model` 位上的 getter 能在 `permissions` 被
340
340
  // 重建**之前**把它的 deny 数组清空(拿到的是同一个引用)。这与内层那条(见
341
341
  // {@link materializeJsonFaithful} 第五条)是同一个病、只是高一层:两层都必须按 stringify 的
@@ -410,7 +410,7 @@ export function buildTaskRequest(input, lane) {
410
410
  // resolved 两车道都进表(#292 P1)——它是**开放集 spread**(子键即 wire 键),所以走
411
411
  // `laneHas + live` 而不是 `on()`:`on()` 判的是「这个键值非空」,而这里要判的是「这份快照要不要
412
412
  // 摊开」。
413
- // 🔴 具名通道**赢过快照**这件事不靠下面的合并序(0.34.1):合并序只在具名通道**有值**时管用,
413
+ // 🔴 具名通道**赢过快照**这件事不靠下面的合并序(0.35.0):合并序只在具名通道**有值**时管用,
414
414
  // 而具名门否决时的表现恰恰是**没值**(端不给这个键)。让位改由 `resolvedSnapshotForWire`
415
415
  // 做**结构剥离** —— 具名通道管着的子键快照一概不产,顺序此后只是可读性,不再是安全依据。
416
416
  const s = input.settings ?? {};
@@ -9,7 +9,8 @@
9
9
  * recovered → null → 覆盖层摘掉(重试**成功**,不是错误 —— 见下)
10
10
  * gave_up → error+terminal → '✻ <detail>'(重试用尽的终态;与 recovered 反向。**打 terminal 位**,
11
11
  * 渲染面据此不得再接「· Retrying in Ns」——已经没有下一次了)
12
- * 绝不捏造 attempt 计数——只用引擎真给的 phase / detail / retryInSec / retryInMs / attempt / maxRetries。
12
+ * 绝不捏造 attempt 计数——只用引擎真给的 phase / detail / retryInSec / retryInMs / attempt /
13
+ * maxRetries / errClass(core 5.43.0 起七键)。
13
14
  *
14
15
  * 🔴 员数与字段补全(2026-08-08,#3004 跟修批)。此前本文件只列 4 相 + 3 字段,而引擎侧
15
16
  * (core `BrainStatusPhase` / `BrainStatus`,dist/core/types.d.ts)是 **6 相 + 6 字段**,server 两腿的
@@ -30,11 +31,15 @@ export type RetryStatus =
30
31
  deadline: number;
31
32
  attempt?: number;
32
33
  maxRetries?: number;
34
+ /** 见 {@link BrainStatusPayload.errClass}(引擎给了才在场;本层只透传,措辞是壳半场)。 */
35
+ errClass?: BrainRetryErrClass | (string & {});
33
36
  } | {
34
37
  kind: 'error';
35
38
  deadline: number;
36
39
  attempt?: number;
37
40
  maxRetries?: number;
41
+ /** 见 {@link BrainStatusPayload.errClass}(引擎给了才在场;本层只透传,措辞是壳半场)。 */
42
+ errClass?: BrainRetryErrClass | (string & {});
38
43
  /**
39
44
  * 🔴 **终态位**(2026-08-08 对抗复审命中):`true` ⇔ 引擎**不会再重试了**(`gave_up` 相)。
40
45
  * 缺席 = 仍在重试循环里(retrying / rate_limited / circuit_open)。
@@ -65,7 +70,23 @@ export type RetryStatus =
65
70
  */
66
71
  export declare const BRAIN_STATUS_PHASES: readonly ["rate_limited", "retrying", "reconnecting", "circuit_open", "recovered", "gave_up"];
67
72
  export type BrainStatusPhase = (typeof BRAIN_STATUS_PHASES)[number];
68
- /** wire 上 `status` 臂的载荷(= core `BrainStatus`;server 两腿白名单原样转发这 6 键)。 */
73
+ /**
74
+ * core `BrainRetryErrClass`(5.43.0)的**类型面镜像** —— 一次重试等待的**原因分桶**,
75
+ * provider 中立(绝不是 HTTP 状态码 / syscall code / provider 分类法;那些不许过这条通道):
76
+ * · `connect_refused` 目标本身给了确定否定(该地址没人 accept / 名字无地址)—— 短梯队服务的那一类;
77
+ * · `transport` 其余传输层失败(connect 超时 / reset / 流中断 / 流停滞)—— 全梯队;
78
+ * · `rate_limit` provider 要求放慢;
79
+ * · `server` provider 自报自己这边出错;
80
+ * · `http` 状态谓词判终态、但 provider 自己的显式重试裁定说重试;
81
+ * · `output_cap` 根本不是连接失败:provider 报上下文超限后按更低 output cap 重发(无退避)。
82
+ *
83
+ * 🔴 **刻意没有运行期值镜像**(与 {@link BRAIN_STATUS_PHASES} 不同):本包对 errClass **零分支**
84
+ * (纯透传),少一个成员没有任何行为后果 —— 开集读会把不认得的桶原样带过去。而 phase 是有 switch
85
+ * 分支的,少一相会落 default 臂被渲成错话,所以那张表才需要运行期镜像 + engine-vocab 等值门。
86
+ * 判据锚在「真正决定结果的量」上:决定结果的是有没有分支,不是有没有一张表。
87
+ */
88
+ export type BrainRetryErrClass = 'connect_refused' | 'transport' | 'rate_limit' | 'server' | 'http' | 'output_cap';
89
+ /** wire 上 `status` 臂的载荷(= core `BrainStatus`;server 两腿白名单原样转发这 7 键)。 */
69
90
  export interface BrainStatusPayload {
70
91
  /** 闭集 + `(string & {})`:未知相仍可携带(开集读),不必先改类型再解析。 */
71
92
  phase: BrainStatusPhase | (string & {});
@@ -79,13 +100,27 @@ export interface BrainStatusPayload {
79
100
  attempt?: number;
80
101
  /** 引擎这一轮的重试上限。与 `attempt` 一起才能渲「2/5」。 */
81
102
  maxRetries?: number;
103
+ /**
104
+ * core 5.43.0(#307 双扫 S44,2026-08-19;ADDITIVE)——**这次等待的原因分桶**
105
+ * (core `BrainRetryErrClass`,provider 中立闭集)。与 `phase`(引擎正在**做什么**)互补:
106
+ * 本键说的是**为什么**。只在**重试等待帧**上在场且引擎分得出类;`recovered`/`gave_up` 两个终态帧
107
+ * 与 `circuit_open`(本地快失败,不是观测到的失败)上刻意缺席。
108
+ *
109
+ * 🔴 **闭集 + `(string & {})`,读时按开集处理**,与 `phase` 同款纪律:一个部署可以在自己的通道上
110
+ * 冒出别的桶,认得的照走、认不得的原样带过去 —— 绝不因为「不认得」就把这次等待的原因抹成缺席
111
+ * (那是把引擎真给的量在本层剥掉,#3004 那批修的正是这一类)。
112
+ * 缺席 = 引擎没分类,**不得**被渲成某个默认桶。
113
+ *
114
+ * 分工:本包只负责让这个事实到得了壳(投影/透传保真),措辞与是否上屏是壳半场。
115
+ */
116
+ errClass?: BrainRetryErrClass | (string & {});
82
117
  }
83
118
  /**
84
119
  * 上面那个 interface 的**运行期键镜像**(照 `TOOL_APPROVAL_FRAME_KEYS_MIRROR` 先例):
85
120
  * engine-vocab 门 G2-c 拿它与 core `BrainStatus` 的键集逐元素比 ⇒ 引擎 additive 增键当天红。
86
121
  * 下面两个类型钉保证镜像与 interface 之间不可能漂移(少键/多键都是编译错)。
87
122
  */
88
- export declare const BRAIN_STATUS_PAYLOAD_KEYS: readonly ["phase", "detail", "retryInSec", "retryInMs", "attempt", "maxRetries"];
123
+ export declare const BRAIN_STATUS_PAYLOAD_KEYS: readonly ["phase", "detail", "retryInSec", "retryInMs", "attempt", "maxRetries", "errClass"];
89
124
  /**
90
125
  * BrainStatus 载荷 → spinner 行状态。
91
126
  *
@@ -9,7 +9,8 @@
9
9
  * recovered → null → 覆盖层摘掉(重试**成功**,不是错误 —— 见下)
10
10
  * gave_up → error+terminal → '✻ <detail>'(重试用尽的终态;与 recovered 反向。**打 terminal 位**,
11
11
  * 渲染面据此不得再接「· Retrying in Ns」——已经没有下一次了)
12
- * 绝不捏造 attempt 计数——只用引擎真给的 phase / detail / retryInSec / retryInMs / attempt / maxRetries。
12
+ * 绝不捏造 attempt 计数——只用引擎真给的 phase / detail / retryInSec / retryInMs / attempt /
13
+ * maxRetries / errClass(core 5.43.0 起七键)。
13
14
  *
14
15
  * 🔴 员数与字段补全(2026-08-08,#3004 跟修批)。此前本文件只列 4 相 + 3 字段,而引擎侧
15
16
  * (core `BrainStatusPhase` / `BrainStatus`,dist/core/types.d.ts)是 **6 相 + 6 字段**,server 两腿的
@@ -46,6 +47,8 @@ export const BRAIN_STATUS_PAYLOAD_KEYS = [
46
47
  'retryInMs',
47
48
  'attempt',
48
49
  'maxRetries',
50
+ // core 5.43.0 跟车一键(#307 双扫 S44,2026-08-19):engine-vocab G2-c 对实装 core 逐键对账。
51
+ 'errClass',
49
52
  ];
50
53
  const _brainStatusKeyPin = [true, true];
51
54
  void _brainStatusKeyPin;
@@ -64,19 +67,26 @@ export function mapBrainStatusToRetry(p, nowMs) {
64
67
  ...(typeof p.attempt === 'number' ? { attempt: p.attempt } : {}),
65
68
  ...(typeof p.maxRetries === 'number' ? { maxRetries: p.maxRetries } : {}),
66
69
  };
70
+ /**
71
+ * 原因桶(#307 S44):引擎真给的**非空串**才落位。缺席 / 空串 / 非串 ⇒ 键不 stamp
72
+ * ——「不知道为什么等」渲成缺席,绝不折成某个默认桶。开集:不认得的桶原样带过去
73
+ * (本层零分支,认不得也没有可落错的臂)。
74
+ */
75
+ const cause = typeof p.errClass === 'string' && p.errClass.length > 0 ? { errClass: p.errClass } : {};
67
76
  switch (p.phase) {
68
77
  // 🔴 引擎直报「恢复」:摘行。绝不落 error 臂 —— 那是把成功渲成失败。
69
78
  case 'recovered':
70
79
  return null;
71
80
  case 'reconnecting':
72
- return { kind: 'stalled', deadline, ...counts };
81
+ return { kind: 'stalled', deadline, ...counts, ...cause };
73
82
  case 'rate_limited':
74
- return { kind: 'error', deadline, ...counts, error: { formatted: '', rateLimits: {} } };
83
+ return { kind: 'error', deadline, ...counts, ...cause, error: { formatted: '', rateLimits: {} } };
75
84
  case 'circuit_open':
76
85
  return {
77
86
  kind: 'error',
78
87
  deadline,
79
88
  ...counts,
89
+ ...cause,
80
90
  error: { formatted: p.detail ?? 'Service temporarily unavailable', isNetworkDown: true },
81
91
  };
82
92
  // 重试用尽:仍是错误行(与 recovered 反向,绝不许一起归成「结束了 ⇒ 清行」),但**打终态位** ——
@@ -84,9 +94,9 @@ export function mapBrainStatusToRetry(p, nowMs) {
84
94
  // retrying'),既没有计数也没有 retryIn*,所以「不会再重试了」这件事**只能**由本位表达;
85
95
  // 靠 attempt===maxRetries 去推是错的(供给方根本不发那两位)。
86
96
  case 'gave_up':
87
- return { kind: 'error', deadline, terminal: true, ...counts, error: { formatted: p.detail ?? '' } };
97
+ return { kind: 'error', deadline, terminal: true, ...counts, ...cause, error: { formatted: p.detail ?? '' } };
88
98
  case 'retrying':
89
99
  default:
90
- return { kind: 'error', deadline, ...counts, error: { formatted: '' } };
100
+ return { kind: 'error', deadline, ...counts, ...cause, error: { formatted: '' } };
91
101
  }
92
102
  }
package/dist/seam.d.ts CHANGED
@@ -389,7 +389,54 @@ export type ChromeEvent = {
389
389
  * `actor.hostAsserted` 是消费端唯一能判「这个署名可信吗」的位:渲署名而不渲这个位 = 把一个
390
390
  * 未经验证的名字渲成可信的(core 自己的 `[from …]` 渲染就是靠它决定加不加 `(unverified)`)。
391
391
  */
392
- | HumanInputChromeEvent;
392
+ | HumanInputChromeEvent | EngineNoticeChromeEvent;
393
+ /**
394
+ * {@link ChromeEvent} 的 `engine_notice` 臂(#310 / #318 件①,server ≥7.36;契约 = server
395
+ * `ASSISTANT-WIRE-CONTRACT` 附录 D)——「**引擎想让这条会话的人知道一件事**」。
396
+ *
397
+ * 为什么**不走 `attachment` 臂**(与 `human_input` 同因):attachment 三条是要在转录流里渲一行的
398
+ * 东西,而通告是**会话级披露**、且 live/durable 两腿都会到(重放会再看到)⇒ 它需要一个带幂等键、
399
+ * 带结构化事实的独立臂,塞进 attachment 会逼消费端从一行文案里反解 code。
400
+ *
401
+ * 🔴 **宿主消费义务**(全部可选、fail-soft;本臂存在的第一价值 = 帧不再落 `unknown_arm` 被丢弃):
402
+ * ① **按 `code` + `detail` 渲染,`message` 只作 fallback**。core 明写
403
+ * `memory.session_polluted` 的 message 随 `memoryProvenance` 模式变文 ⇒ **按 message 文本匹配必碎**。
404
+ * ② **`code` 是开集,认不得也不许丢**:认得的码渲专用呈现,认不得的码用 `message` 兜底展示。
405
+ * 库侧**一个码都不硬编**,所以「认不认得」这张表由渲染面自己持有 —— 但它只能决定**怎么渲**,
406
+ * 不能决定**渲不渲**。
407
+ * ③ **幂等消费**:durable 腿重放会再送同一条(与 `workspace_changed` 同纪律),按 `eventId`
408
+ * (在场时)或 `code+ts` 去重,别按到达次数计数。
409
+ * ④ 🔴 `memory.harvest_quarantined` 的 `moved` 与 `escalated` **不可相减**(就地墓碑同时计入两者,
410
+ * core 顶注)—— 两个数各自读、并列呈现,任何减法都会得出一个不存在的量。
411
+ * ⑤ 🔴 **缺席不可反推**:server 对非白名单码 / 缺 `sessionId` 的通告**如实不投**(宁缺席不串台),
412
+ * 全族那一份只在 server 的运维日志里。所以「没收到通告」**不等于**「没发生」,渲染面不许把
413
+ * 本臂的缺席说成「一切正常」。
414
+ * 缺席(宿主不接本臂)= 引擎的这类披露在该宿主上看不见,**不是**报错。
415
+ */
416
+ export interface EngineNoticeChromeEvent {
417
+ kind: 'engine_notice';
418
+ laneProof: LaneProof;
419
+ /** 稳定机器码,点分命名空间(`memory.session_polluted` …)。**开集**——见臂注 ②。 */
420
+ code: string;
421
+ /** core 铸的人话行。🔴 **仅 fallback 展示,不是匹配键**;wire 上非串时本位是空串。 */
422
+ message: string;
423
+ /** 机器可读事实(逐码不同,开集)。server 已脱敏 + 尺寸 bound,**仍按外部串处理**。 */
424
+ detail: Record<string, unknown>;
425
+ /** 归属会话(server 从 `detail` 提升为顶层)。畸形/缺席 ⇒ 本键不在场。 */
426
+ sessionId?: string;
427
+ /** **server 观察时刻**(ms epoch),**不是**引擎铸造时刻 —— `EngineNotice` 自身不带时间戳。 */
428
+ ts?: number;
429
+ /** durable 重放的稳定身份键 —— **core 铸的事件身份**(uuidv7 形)。wire 今天未必带。 */
430
+ eventId?: string;
431
+ /**
432
+ * durable 重放的**第二层**身份 —— SDK 从 SSE `id:` 字段 stamp 上来的 per-task 序号
433
+ * (= `task_event.seq`)。对「同一条账本行」稳定,重放会带同一个值。
434
+ * 🔴 与 {@link EngineNoticeChromeEvent.eventId} 是**两个命名空间**(全局身份 vs per-task 序号),
435
+ * 绝不互相顶替。消费端幂等序:`eventId` > `eventSeq` > 两者都缺才退 `code+ts`
436
+ * (最后那一档是有损的:`ts` 是 server 观察时刻(ms),同毫秒同码的两条会被折成一条)。
437
+ */
438
+ eventSeq?: string;
439
+ }
393
440
  /**
394
441
  * `human_input` 臂的署名(core `ActorAssertion` 的投影;server 已 redact 后上帧)。
395
442
  * 🔴 `hostAsserted` 是消费端**唯一**能判「这个署名可信吗」的位 —— 渲 `id` 而不渲它,
package/dist/seam.js CHANGED
@@ -41,6 +41,13 @@ const CHROME_ARM_TABLE = {
41
41
  required: false,
42
42
  duty: '可选:记进会话审计/时间线(谁把什么喂进了这条 run)。绝不铸 transcript 行(本帧不带正文);渲署名必须跟渲 actor.hostAsserted 的可信度标注',
43
43
  },
44
+ // #318 件①:引擎结构化通告。`required:false` —— 不接 = 这类披露在该宿主上看不见(一个可选的
45
+ // 披露面),不属「已发生的行为丢失」那一档。🔴 但一旦接,四条硬约束(code+detail 渲 / 未知码
46
+ // 不许丢 / 重放幂等 / moved-escalated 禁相减)在臂注释里是**真源**,duty 这行只是索引。
47
+ engine_notice: {
48
+ required: false,
49
+ duty: '可选:渲引擎通告(按 code+detail,message 仅 fallback;未知 code 也必须渲不许丢;durable 重放按 eventId 幂等;harvest 的 moved/escalated 并列呈现绝不相减)',
50
+ },
44
51
  };
45
52
  /**
46
53
  * chrome 臂**覆盖率断言出口**(§8-1 裁决的一半:client-core 定接口不定实现)。
@@ -37,6 +37,11 @@
37
37
  * `resume.cap` / `resume.session_not_found`(留存失效)/ `steering.still_running`(**它还在跑,
38
38
  * 该 steer 不该 resume**)/ `steering.not_running`(本副本无匹配 / run 已 settle)。
39
39
  * 判据是 `errorCode` 串(机器轴,开集),**不按 HTTP 数字分支**;未知码原样透传落诚实兜底臂。
40
+ * ⚠️ **0.38.0(#318 件④)补第七、八格**:`resume.row_recycling`(行正在被裁决 —— **窗口自清,
41
+ * 过一会儿再发就成**)/ `resume.row_gone`(行已被终态 GC 收走 —— **等也没用,重开新 agent**)。
42
+ * 这两码在 core 侧早已在铸,是 [4743] #323「completed bg 子代跨重启 SendMessage 复活」把复活裁决
43
+ * 腿变成常走路径之后才真正会被用户撞见;此前它们双双落进开集兜底,两个不同的下一步被压成一句
44
+ * 泛泛失败。
40
45
  *
41
46
  * ── UNTRUSTED ───────────────────────────────────────────────────────────────────────────────
42
47
  * 收据 `note` 是 server 铸的文案(引擎会把子代名字拼进去,名字是 spawning model 的自由文本)——
@@ -80,6 +85,19 @@ export type SubagentResumeFailureKind =
80
85
  | 'not-resumable'
81
86
  /** `steering.ambiguous_target` —— 按名取址撞上同名多员(取址口径本身可分,是唯一「换个地址就成」的一格)。 */
82
87
  | 'ambiguous-target'
88
+ /**
89
+ * `resume.row_recycling`(#318 件④ / [4743] SendMessage 复活语义半场)—— registry 行**正在被裁决**
90
+ * (另一个复活 claim 或一次 reap sweep 正持着它)。
91
+ * 🔴 与 `not-resumable` **禁合并**:core 铸文逐字「this clears on its own; send again in a moment」——
92
+ * 窗口自清,**同一条命令过一会儿就成**。渲成「不可复活」会把一个自愈的瞬时窗说成终局。
93
+ */
94
+ | 'row-contended'
95
+ /**
96
+ * `resume.row_gone`(同上)—— registry 行**已经不存在**(终态 GC 收走了)。
97
+ * 🔴 与 `row-contended` **禁合并**:这一格**等也没用**,core 给的下一步是「relaunch a new agent」。
98
+ * 两码合并 = 一半用户白等、另一半白重开。
99
+ */
100
+ | 'row-gone'
83
101
  /** 404 —— 未知 run / 非属主(**无存在性谕示**,两者同形)。 */
84
102
  | 'not-found'
85
103
  /** 400 —— 空 content 等入参问题。 */
@@ -59,6 +59,13 @@ export function classifySubagentResumeFailure(e) {
59
59
  return { reason: 'not-resumable', detail: message };
60
60
  if (errorCode === 'steering.ambiguous_target')
61
61
  return { reason: 'ambiguous-target', detail: message };
62
+ // #318 件④([4743] SendMessage 复活语义)—— 复活裁决腿的两码。此前它们双双落进末尾的开集兜底
63
+ // `error`,于是 core 明明给了**两个不同的下一步**(等一会儿再发 / 重开一个新 agent),到客户端
64
+ // 只剩一句泛泛失败。判定归包、文案归端:这里只分格,不写出路文案。
65
+ if (errorCode === 'resume.row_recycling')
66
+ return { reason: 'row-contended', detail: message };
67
+ if (errorCode === 'resume.row_gone')
68
+ return { reason: 'row-gone', detail: message };
62
69
  if (errorCode === undefined && status === 404)
63
70
  return { reason: 'not-found', detail: message };
64
71
  if (errorCode === undefined && status === 400)
@@ -8,6 +8,14 @@ import { type ModelFallbackReason } from './engineErrorCodes.js';
8
8
  * 反过来,一个 `{type:'something-else'}` 或裸对象**不算** structured 在场 —— 宁可回落到正则,
9
9
  * 也不许把「有个对象」当成「引擎发了结构化」(判据锚在决定结果的量上:决定的是「该不该信正则」)。
10
10
  * ⚠️ 与设计稿的口径差:任务书写「29 项」,[1840] 清单逐条数是 **30 项**(见交接报告「设计稿错漏」)。
11
+ *
12
+ * 🔴 **本表是 core `CC_DETAIL_TYPES` 的镜像,不是本包自铸的词表**(core
13
+ * `dist/core/runner/tool-output-projection.js`)。等值由 `scripts/run-engine-vocab-floor-test.mjs` ⑤ 段
14
+ * 对**实装 core** 双向钉(漏一词 / 多一词都红),对账物 = 本仓 devDep `@sema-agent/core`。
15
+ * ⇒ **core 提货窗必须跟车对表**:每次抬 devDep core 版本,先跑 engine-vocab 门看这张表红不红,
16
+ * 红了按 core 那一版的铸点逐词补/删,别改门去迁就表。历史上两次恒绿(5.10 前停 devDep 5.1、
17
+ * 5.43 前停 devDep 5.20)的病根都不在表本身,在**对账物没跟着抬** —— 表漏一词的实际后果是
18
+ * 那一类卡永远退回正则解模型面文本,而任何一层都不会响。
11
19
  */
12
20
  export declare const STRUCTURED_DETAIL_TYPES: ReadonlySet<string>;
13
21
  /** structured 在场判别:顶层 `type` ∈ 白名单 ⇒ 返回该 type,否则 undefined(= 不在场)。 */