@sema-agent/client-core 0.37.0 → 0.38.1

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 (42) hide show
  1. package/CHANGELOG.md +159 -14
  2. package/README.md +3 -2
  3. package/dist/adapt/arms.js +40 -0
  4. package/dist/adapt.d.ts +1 -1
  5. package/dist/adapt.js +2 -0
  6. package/dist/adapter/activeRunSelfHeal.d.ts +1 -1
  7. package/dist/adapter/activeRunSelfHeal.js +6 -6
  8. package/dist/adapter/downstream/eventToSdkMessage.js +94 -0
  9. package/dist/agentSession/backgroundView.d.ts +1 -1
  10. package/dist/agentSession/backgroundView.js +1 -1
  11. package/dist/engineCapsCache.d.ts +33 -1
  12. package/dist/engineCapsCache.js +26 -4
  13. package/dist/engineErrorCodes.d.ts +20 -0
  14. package/dist/engineErrorCodes.js +44 -0
  15. package/dist/fleet/fleetLedger.d.ts +1 -1
  16. package/dist/fleet/fleetLedger.js +3 -3
  17. package/dist/fleet/fleetProjection.d.ts +34 -0
  18. package/dist/fleet/fleetProjection.js +40 -0
  19. package/dist/hitl/armedGateRegistry.d.ts +1 -1
  20. package/dist/hitl/armedGateRegistry.js +2 -2
  21. package/dist/hitl/hitlBridge.js +77 -18
  22. package/dist/hitl/persistedRulesWire.d.ts +1 -1
  23. package/dist/hitl/persistedRulesWire.js +4 -4
  24. package/dist/index.d.ts +1 -0
  25. package/dist/index.js +4 -0
  26. package/dist/model/modelSupplyRules.d.ts +102 -0
  27. package/dist/model/modelSupplyRules.js +149 -0
  28. package/dist/model/providerPresets.js +68 -12
  29. package/dist/request/taskRequest.d.ts +1 -1
  30. package/dist/request/taskRequest.js +8 -8
  31. package/dist/seam.d.ts +48 -1
  32. package/dist/seam.js +7 -0
  33. package/dist/subagent/engineSubagentResume.d.ts +20 -2
  34. package/dist/subagent/engineSubagentResume.js +8 -1
  35. package/dist/subagent/engineSubagentSteer.d.ts +1 -1
  36. package/dist/subagent/engineSubagentSteer.js +1 -1
  37. package/dist/subagent/subagentOwnerAbsence.js +1 -1
  38. package/dist/subagentContentStore.js +1 -1
  39. package/dist/wireErrorTriage.js +1 -1
  40. package/docs/INTEGRATION-CLIENTS.md +70 -20
  41. package/docs/REFACTOR-LEDGER.md +2 -2
  42. package/package.json +3 -3
@@ -0,0 +1,102 @@
1
+ /** vision 生效值的**出处**(UI 那一行「当前生效值与来源」的唯一数据源)。 */
2
+ export type VisionSource =
3
+ /** 这条 entry 自己显式表态。 */
4
+ 'entry'
5
+ /** entry 不表态,取 `inferFamily` 的家族缺省。 */
6
+ | 'family'
7
+ /** 两边都不表态 —— 生效值**未知**,绝不折算成 false。 */
8
+ | 'unknown';
9
+ /**
10
+ * {@link resolveEntryVision} 的结果(具名:本仓 typeshape 门对导出面内联匿名形有棘轮)。
11
+ *
12
+ * 🔴 `value: undefined` 与 `value: false` 是**两件事**:前者「没人说过这个模型能不能看图」,
13
+ * 后者「确认它不能看图」。把前者渲成后者会永久封死一个真能力。
14
+ */
15
+ export interface EntryVisionResolution {
16
+ value: boolean | undefined;
17
+ source: VisionSource;
18
+ }
19
+ /**
20
+ * 解析一条 entry 的 vision **生效值 + 来源**。纯函数(只吃 modelId/vision 两位,零 IO)。
21
+ *
22
+ * 🔴 [honest-absence]:家族与 entry 都不表态时给 `undefined`(未知),**不**给 `false`。
23
+ * ⚠️ 「未知」只是**客户端这一层**诚实:引擎侧 registry-core `ModelEntry.vision` 是
24
+ * `z.boolean().default(false)`,键缺席在那边就等于 false。**呈现层必须把这个后果一起说出来**,
25
+ * 别让用户以为「未知」= 引擎会自己去问供应商。
26
+ */
27
+ export declare function resolveEntryVision(entry: {
28
+ modelId: string;
29
+ vision?: boolean;
30
+ }): EntryVisionResolution;
31
+ /** 一处挡住删除的引用。`where` 是坐标(用户照着去改),`fix` 是这一处该怎么解开。 */
32
+ export type PoolDeleteBlocker = {
33
+ kind: 'default' | 'roles' | 'roster' | 'unreadable';
34
+ where: string;
35
+ fix: string;
36
+ };
37
+ /** 一条「照删,但你该知道」的后果。删除**不**被阻断,确认屏必须把它渲出来。 */
38
+ export type PoolDeleteWarning = {
39
+ kind: 'allowlist-empties' | 'tier-binding';
40
+ /** 人话:删了会发生什么(不是坐标,是后果)。 */
41
+ consequence: string;
42
+ };
43
+ /**
44
+ * 档位组的**归一形**(settings `semaTierGroups`)。两处删除判定共用,单独具名以免两边各写一份内联形。
45
+ */
46
+ export interface TierGroupsView {
47
+ active: string;
48
+ groups: Record<string, {
49
+ tiers: Record<string, string | undefined>;
50
+ }>;
51
+ }
52
+ /** {@link computeDeleteWarnings} 的入参(具名:导出面禁内联匿名形,B4 棘轮)。 */
53
+ export interface DeleteWarningsInput {
54
+ id: string;
55
+ /**
56
+ * 已读到的 models.json 文档切片。🔴 两位是 `unknown` 而不是收窄形 —— 它们是**盘上 JSON 的原样**,
57
+ * 属主是引擎的 models 域 schema(开集、可演进)。在这里手抄一份结构就是手抄一份会漂的上游形;
58
+ * 本函数对它们的处理本来就是「只判是不是数组/对象,然后逐项过滤」,窄读没有消费者。
59
+ * `null` = 调用方没读到文档(两义分流在调用方,见 {@link computeDeleteBlockers} 头注)。
60
+ */
61
+ doc: {
62
+ atModelAllowlist?: unknown;
63
+ tierGroups?: unknown;
64
+ } | null;
65
+ /** settings `semaTierGroups` 归一形(缺席 ⇒ 回落 `doc.tierGroups`,与写路径的取值序同源)。 */
66
+ tierGroups: TierGroupsView | null;
67
+ /** 删除**之后**仍在池里的 catalog name 全集(判「有效名单会不会变空」要的就是它)。 */
68
+ survivingIds: readonly string[];
69
+ }
70
+ /** {@link computeDeleteBlockers} 的入参(具名:同上)。 */
71
+ export interface DeleteBlockersInput {
72
+ id: string;
73
+ /** 已读到的 models.json 文档切片;两位 `unknown` 的理由同 {@link DeleteWarningsInput.doc}。 */
74
+ doc: {
75
+ default?: unknown;
76
+ roles?: unknown;
77
+ } | null;
78
+ /** roster 归一形(激活组名 + 全部组的 slot 表;**分阶段组也在里面**,它们是真引用)。 */
79
+ rosters: {
80
+ active: string;
81
+ rosters: Record<string, {
82
+ slots: Record<string, string>;
83
+ }>;
84
+ } | null;
85
+ }
86
+ /**
87
+ * 纯判定:删这条 entry 会带来哪些**语义变化**(不阻断)。
88
+ *
89
+ * 两格,都是「链不断但意思变了」:
90
+ * ① `atModelAllowlist` 的**有效名单**会被删空 ⇒ 按 registry-core 语义**全部启用模型都可 @**
91
+ * —— 一次静默的权限放宽,必须说;名单里还有别人 ⇒ 只是缩小名单,不用说。
92
+ * ② `semaTierGroups` 里有绑定指着它 ⇒ 删完那条绑定悬空 ⇒ 下次按档位解析会落到**另一个模型**上
93
+ * (而不是报错),用户会以为「我明明配了 pro 档」。
94
+ */
95
+ export declare function computeDeleteWarnings(input: DeleteWarningsInput): PoolDeleteWarning[];
96
+ /**
97
+ * 纯判定:这条 entry 现在被哪些**断链类**引用挡着。空数组 = 可以删。
98
+ *
99
+ * 🔴 `doc === null` 的两义**在调用方分流**:这里只认「已读到的文档」;读不出来时调用方自己产
100
+ * `unreadable` 阻断 —— 那一刻我们并不知道谁指着它,fail-closed 拒删比「猜没人指着」安全。
101
+ */
102
+ export declare function computeDeleteBlockers(input: DeleteBlockersInput): PoolDeleteBlocker[];
@@ -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) {
@@ -170,7 +170,7 @@ export type BuiltTaskRequest = TaskRequest;
170
170
  * `settings.<resolved>` 的车道(#292 P1 起 = **两条**)那份 resolver effective 快照是**开放集**,
171
171
  * 所以**表外**子键(permissions/env/model/… 不可枚举)放行。
172
172
  *
173
- * 🔴 开放集**不是**整只 `settings` 免检(#292 P1 codex 复审第二轮 [medium] 采纳):表里点名了、
173
+ * 🔴 开放集**不是**整只 `settings` 免检(#292 P1 对抗复审第二轮 [medium] 采纳):表里点名了、
174
174
  * 而 lanes 不含本车道的子键(如 print 车道的 `settings.ultracode`)照旧**点名** —— 那些键是
175
175
  * **可枚举的已知量**,放过它们等于让「ultracode 仅 interactive」这条登记在真 wire 面失效。
176
176
  * `buildTaskRequest` 内的 `resolvedSnapshotForWire` 只管住构造器自己那一份;端在构造之后仍能往请求体上
@@ -129,7 +129,7 @@ const shapeTag = (v) => {
129
129
  /**
130
130
  * plain object 判别 —— 按 **prototype** 判,不按 `Object.prototype.toString` 的标签判。
131
131
  *
132
- * 🔴 标签判法是**假的**(0.35.0 codex 对抗复审 [high] 采纳):`[object Object]` 对**普通类实例**同样
132
+ * 🔴 标签判法是**假的**(0.35.0 对抗复审 [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.35.0 codex 对抗复审第八轮 [medium] 采纳):不拿「**本** realm 的
142
+ * 🔴 判据 **realm 无关**(0.35.0 对抗复审第八轮 [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.35.0 codex 对抗复审第七轮 [high] 的可采半场):
245
+ // 🔴 重建成**无原型**记录(0.35.0 对抗复审第七轮 [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.35.0 codex 对抗复审第十二轮 [high] 采纳):`toJSON` 走属性
267
+ // 🔴 判据的读法**一律排在捕获之后**(0.35.0 对抗复审第十二轮 [high] 采纳):`toJSON` 走属性
268
268
  // 查找、`isPlainRecord` 走 `getPrototypeOf`/`constructor` —— 这三种读法**都可以带副作用**
269
269
  // (`getPrototypeOf` 陷阱在被问的那一刻把 `deny` 清空),而**原生 `JSON.stringify` 从不问原型**。
270
270
  // 校在捕获之前 = 本层自己的读法成了丢内容的那一环(比被动撒谎更糟:那是我们引入的丢失面)。
@@ -305,7 +305,7 @@ const materializeJsonFaithful = (v, path, seen = new Set()) => {
305
305
  * `TypeError` 带形状描述(合法载体的判据见 {@link isPlainRecord} —— 「摊得全」是它的全部理由)。
306
306
  * (本包是发出去的 npm 公开面:JS 调用方与版本偏斜的宿主都到得了这里,型面拦不住。)
307
307
  *
308
- * ## ③ 权限面**子树**同样要摊得全(0.35.0 codex 对抗复审第二轮 [high] 采纳)
308
+ * ## ③ 权限面**子树**同样要摊得全(0.35.0 对抗复审第二轮 [high] 采纳)
309
309
  *
310
310
  * 只校最外层是不够的:`{ permissions: new Map([['deny',['Write']]]) }` 的外层是合法字面量,可
311
311
  * `JSON.stringify` 把那只 Map 序列化成 `"permissions":{}` —— deny 规则**静默消失**,与 ② 要堵的
@@ -335,7 +335,7 @@ const resolvedSnapshotForWire = (resolved) => {
335
335
  // 当场物化成脱钩副本),原件的形状判据留到最后;校不过就整体拒,拒绝面一字未变。
336
336
  const named = namedSettingsSubKeys();
337
337
  const out = {};
338
- // 🔴 **键先捕获、值逐个即读即处理**(0.35.0 codex 对抗复审第六轮 [high]):`Object.entries(resolved)`
338
+ // 🔴 **键先捕获、值逐个即读即处理**(0.35.0 对抗复审第六轮 [high]):`Object.entries(resolved)`
339
339
  // 会把**所有兄弟键**的值先读齐 —— 于是一个 `env`/`model` 位上的 getter 能在 `permissions` 被
340
340
  // 重建**之前**把它的 deny 数组清空(拿到的是同一个引用)。这与内层那条(见
341
341
  // {@link materializeJsonFaithful} 第五条)是同一个病、只是高一层:两层都必须按 stringify 的
@@ -365,7 +365,7 @@ const resolvedSnapshotForWire = (resolved) => {
365
365
  out[k] = checked.value;
366
366
  continue;
367
367
  }
368
- // 🔴 函数值一律不进(#292 P1 codex 复审第三轮 [medium] 采纳):JSON wire 上没有函数,而一个
368
+ // 🔴 函数值一律不进(#292 P1 对抗复审第三轮 [medium] 采纳):JSON wire 上没有函数,而一个
369
369
  // **自有 `toJSON`** 会在序列化那一刻整只改写 `settings` 的字节 —— 实测
370
370
  // `resolved={permissions:…, toJSON(){return {ultracode:true}}}` 能让 `JSON.stringify(req)` 出
371
371
  // `settings:{ultracode:true}`,把上面那道车道剥离(以及一切按对象查的判据)整体绕过。
@@ -550,7 +550,7 @@ export function applyLiveRequestDefaults(req, host) {
550
550
  * `settings.<resolved>` 的车道(#292 P1 起 = **两条**)那份 resolver effective 快照是**开放集**,
551
551
  * 所以**表外**子键(permissions/env/model/… 不可枚举)放行。
552
552
  *
553
- * 🔴 开放集**不是**整只 `settings` 免检(#292 P1 codex 复审第二轮 [medium] 采纳):表里点名了、
553
+ * 🔴 开放集**不是**整只 `settings` 免检(#292 P1 对抗复审第二轮 [medium] 采纳):表里点名了、
554
554
  * 而 lanes 不含本车道的子键(如 print 车道的 `settings.ultracode`)照旧**点名** —— 那些键是
555
555
  * **可枚举的已知量**,放过它们等于让「ultracode 仅 interactive」这条登记在真 wire 面失效。
556
556
  * `buildTaskRequest` 内的 `resolvedSnapshotForWire` 只管住构造器自己那一份;端在构造之后仍能往请求体上
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 等入参问题。 */
@@ -131,7 +149,7 @@ export interface SubagentResumeContext {
131
149
  * 取运行 → 无则 no-run;makeEngineWireClient)—— 两条腿共用同一套装配,免得 steer 能到达而 resume
132
150
  * 到不了(或反过来),那种不对称在用户面上表现为「同一个 agent 有时能说话有时不能」。
133
151
  *
134
- * 🔴 `session` 的三态(#242 批 2 定口径,codex 复审 medium 收紧):
152
+ * 🔴 `session` 的三态(#242 批 2 定口径,对抗复审 medium 收紧):
135
153
  * · 台账有这一行**且**捕到了它的会话 ⇒ 用**行登记的**会话(resume 是 per-id 读面,配错的
136
154
  * `{runId, session}` 组合在 session-bound run 上 fail-closed 404);
137
155
  * · 台账**有这一行但没捕到会话**(fleet 行帧腿/通知腿今天不传 session)⇒ **不带 `?session=`**,
@@ -155,7 +173,7 @@ export declare function resumeSettledSubagent(facade: SubagentResumeFacade | nul
155
173
  signal?: AbortSignal;
156
174
  }): Promise<SubagentResumeOutcome>;
157
175
  /**
158
- * {@link auditRetainWithoutWake} 的判决(codex 复审 low 收紧的**返回契约**)。
176
+ * {@link auditRetainWithoutWake} 的判决(对抗复审 low 收紧的**返回契约**)。
159
177
  *
160
178
  * 两位分别回答两个不同的问题,**不许合成一个**:
161
179
  * · `message` = 组合成不成立(成立才有这条记录;`undefined` 返回 = 不成立);
@@ -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)
@@ -96,7 +103,7 @@ export function resolveOwnerRunId(childTaskId, activeRunId = activeEngineRunId()
96
103
  * 取运行 → 无则 no-run;makeEngineWireClient)—— 两条腿共用同一套装配,免得 steer 能到达而 resume
97
104
  * 到不了(或反过来),那种不对称在用户面上表现为「同一个 agent 有时能说话有时不能」。
98
105
  *
99
- * 🔴 `session` 的三态(#242 批 2 定口径,codex 复审 medium 收紧):
106
+ * 🔴 `session` 的三态(#242 批 2 定口径,对抗复审 medium 收紧):
100
107
  * · 台账有这一行**且**捕到了它的会话 ⇒ 用**行登记的**会话(resume 是 per-id 读面,配错的
101
108
  * `{runId, session}` 组合在 session-bound run 上 fail-closed 404);
102
109
  * · 台账**有这一行但没捕到会话**(fleet 行帧腿/通知腿今天不传 session)⇒ **不带 `?session=`**,
@@ -20,7 +20,7 @@ export type SubagentSteerOutcome = {
20
20
  * 指名了行而台账缺席 ⇒ `no-run` + `noteBgOwnerAbsence` 留痕(绝不回落在飞 run);
21
21
  * **没指名行**(既有两参调用)⇒ 才回落在飞 run —— 老调用方逐字零行为变化,本参数是 additive。
22
22
  *
23
- * 🔴 会话参数**二态**(codex 复审 medium 采纳:「取了台账的 run 却配现势 session」是确定性 404):
23
+ * 🔴 会话参数**二态**(对抗复审 medium 采纳:「取了台账的 run 却配现势 session」是确定性 404):
24
24
  * · 行登记时**捕到了会话** ⇒ 用**行的会话**(与行的 runId 同源同拍,是唯一自洽的组合);
25
25
  * · 行**没有**随行会话(`recordBgParentRun` 只写 run —— 今天 bg 行多数是这一格)⇒ 退**现势会话**。
26
26
  * 刻意**不**跟 resume 腿的「没捕到就干脆不带」:本腿与 tail/subagentOutput 同族,遵 [1498]③
@@ -45,7 +45,7 @@ import { noteBgOwnerAbsence } from './subagentOwnerAbsence.js';
45
45
  * 指名了行而台账缺席 ⇒ `no-run` + `noteBgOwnerAbsence` 留痕(绝不回落在飞 run);
46
46
  * **没指名行**(既有两参调用)⇒ 才回落在飞 run —— 老调用方逐字零行为变化,本参数是 additive。
47
47
  *
48
- * 🔴 会话参数**二态**(codex 复审 medium 采纳:「取了台账的 run 却配现势 session」是确定性 404):
48
+ * 🔴 会话参数**二态**(对抗复审 medium 采纳:「取了台账的 run 却配现势 session」是确定性 404):
49
49
  * · 行登记时**捕到了会话** ⇒ 用**行的会话**(与行的 runId 同源同拍,是唯一自洽的组合);
50
50
  * · 行**没有**随行会话(`recordBgParentRun` 只写 run —— 今天 bg 行多数是这一格)⇒ 退**现势会话**。
51
51
  * 刻意**不**跟 resume 腿的「没捕到就干脆不带」:本腿与 tail/subagentOutput 同族,遵 [1498]③
@@ -1,5 +1,5 @@
1
1
  /**
2
- * subagentOwnerAbsence — #242 批 3(黑板 [4000] Q3=B 裁定落地):bg 子代宿主 run 台账缺席时的
2
+ * subagentOwnerAbsence — #242 批 3(协作板 [4000] Q3=B 裁定落地):bg 子代宿主 run 台账缺席时的
3
3
  * **响亮留痕**单源。
4
4
  *
5
5
  * 三条读面腿(`engineSubagentTail` / `engineTaskHandleWire` / `engineSubagentOutput`)此前一律
@@ -391,7 +391,7 @@ export function getBgTerminalFacts(taskId) {
391
391
  // 🔴 有界:键来自 wire(不可信),无界表 = 引擎乱发 taskId 就能吃掉宿主内存。上限取**两表
392
392
  // 上限之和**(壳表 256 + 本表原 `MAX_TASKS*2`=64)—— 合表**不许缩窄既有保障**:收编前两条
393
393
  // lane 各有各的池子,能同时记 256 条 tick + 64 条 fleet;若按 256 钉,「tick 池已满 + 再来一条
394
- // fleet 行」当场淘汰一条 tick,那条子代的 resume 入口凭空消失,而收编前它还在(codex 复审
394
+ // fleet 行」当场淘汰一条 tick,那条子代的 resume 入口凭空消失,而收编前它还在(对抗复审
395
395
  // medium 实证形)。两读口的 LRU touch 也会让高频 fleet 行持续挤压 tick 历史,同一条道理。
396
396
  // 满了按 LRU 淘汰,淘汰后果只是那一行退回「不知道宿主 run」= 诚实缺席,不是错值。
397
397
  const MAX_BG_PARENT_RUNS = 320;
@@ -50,7 +50,7 @@ export function classifyTurnWireError(err) {
50
50
  const message = typeof e.message === 'string' && e.message.length > 0 ? e.message : String(err);
51
51
  return { kind: 'http', status: e.status, ...(errorCode !== undefined ? { errorCode } : {}), message };
52
52
  }
53
- // park 判别按 `name` **结构读**,不附加 instanceof Error(codex F3 [medium] 真病修):壳源形的
53
+ // park 判别按 `name` **结构读**,不附加 instanceof Error(复审 F3 [medium] 真病修):壳源形的
54
54
  // `err instanceof Error &&` 合取在单进程壳里恒真无害,但本件是三端共用面 —— desktop IPC 序列化 /
55
55
  // web 跨 bundle 的同名错误是 plain object,instanceof 合取会把「候人决断」误诊成「客户端 bug +
56
56
  // 建议重跑」(重跑=重复 turn,且掩盖真待决态)。壳注释自陈的意图本就是「name 判别(跨包