@sema-agent/client-core 0.36.0 → 0.37.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 CHANGED
@@ -15,6 +15,184 @@
15
15
  >
16
16
  > 🔴 **互链**(web [C166]⑦):各版「已知局限」段只记**该版新增**;接入面已知局限的完整台账在
17
17
  > `docs/INTEGRATION-CLIENTS.md` §6e/§7 —— **只读其一会漏**,两处都过。
18
+ >
19
+ > 🔴 **勘误(#307 双扫,2026-08-19,永久记录 —— 见下方为什么不能退休)**:`## 0.36.0` 段标题的
20
+ > `(未发布)` 是发布批的操作失误 —— 0.36.0 **已经发布**(releasedAt=npm gitHead `b165f3f`,
21
+ > registry 在架),阶段二转正提交 `e56563c` 只补了 `FROZEN` 账(releasedAt/sha256、删
22
+ > pending),漏改标题。该段**已冻结**(`b165f3f` 上的字节被 ④a 逐字锁定,`FROZEN` 账不许改
23
+ > 既有行)⇒ 标题**不回改、且永远不会被改**(`b165f3f` 那个 commit 的字节是历史,任何未来提交
24
+ > 都改不到它)⇒ 这条勘误与门侧窄豁免(`run-integration-doc-freshness-test.mjs` ④b
25
+ > `KNOWN_HEADING_ERRATA`,登记 `version: '0.36.0', releasedAt: 'b165f3f'`)**都是永久的**,不是
26
+ > 「下一版删掉」的临时态(codex 复审 finding①:那样写会让豁免一删,门在**任何**后续版本上
27
+ > 都会重新对这个永久冻结的标题判红,退休条件不可能被满足)。门侧核验两件事把这条勘误钉死、
28
+ > 不许悄悄漂:豁免登记的 `releasedAt` 与 `FROZEN` 账上 0.36.0 那一行逐字相等;本段(点名版本号
29
+ > `0.36.0` + 关键字「勘误」)必须还在这份头注里 —— 删掉本段而不同批把门侧豁免一起处理,门当场红。
30
+
31
+ ## 0.37.0(2026-08-19)
32
+
33
+ **#307 双扫台账余量四条(S43/S44/S47/S25)。行为面**四条**:三条是「上游给了本层没接」的补接
34
+ (两条 additive 词表/键表 + 一条 wire 形收窄),一条是新导出的生产失效口。**
35
+
36
+ > 📌 **本段的公面基线是 750,不是 0.36.0 发布时的 749**:`0.36.0` 发布(2026-08-18)之后、本段之前,
37
+ > #307 呈现面批已经落了一个**未发布**的 additive 导出 `escapeDisplayControlChars`(749→750,当时
38
+ > CHANGELOG 还没有未发段可记)。本段接着它记 750→**751**。⇒ npm 上的 `0.36.0` 是 **749**;
39
+ > 本段与那一批的两个新导出**都还没发布**,端按名 import 会在 ESM 实例化当场炸
40
+ > (同一条口径写进 `docs/INTEGRATION-CLIENTS.md` §0a 的版本锚)。
41
+
42
+ - **structured 白名单补 `read_path_denied`(S43)**:core 5.43.0 起 fs 安全层拒读铸
43
+ `{type:"read_path_denied", …}` 细节卡(`dist/tools/fs/safety.js`),而本包 `STRUCTURED_DETAIL_TYPES`
44
+ 只有 41 项 ⇒ `structuredDetailType()` 对这张卡返 `undefined` ⇒ 该卡**退回正则解模型面文本**,
45
+ 而任何一层都不会响。补词后 42 项,与实装 core 逐词等值。
46
+ 🔴 **病根第三次是同一个:对账物没跟着抬**(5.10 前停 devDep 5.1、5.20 前停 5.10、本次停 5.20)。
47
+ 同批把 devDep `@sema-agent/core` 升 `^5.43.0` 并把 `run-engine-vocab-floor-test.mjs` 的
48
+ `CORE_FLOOR` 提级 5.20 → 5.43,钉住「对账物不得回退」;`STRUCTURED_DETAIL_TYPES` 头注补
49
+ 「core 提货窗必须跟车对表」的成文纪律。
50
+ - **`BrainStatusPayload` 补 `errClass`(S44,ADDITIVE)**:core 5.43.0 给 `BrainStatus` 加了
51
+ `errClass`(`BrainRetryErrClass` 闭集:`connect_refused`/`transport`/`rate_limit`/`server`/`http`/
52
+ `output_cap`)—— 与 `phase`(引擎正在**做什么**)互补,它说的是**为什么**在等。此前本包七键只接
53
+ 六键 ⇒ 引擎真发的量在本层被剥掉,用户看得到「在等」看不到「为什么等」(与 #3004 那批修的
54
+ attempt/maxRetries/retryInMs 同一个形)。
55
+ 接线三处:`BrainStatusPayload` + `BRAIN_STATUS_PAYLOAD_KEYS`(6→**7** 键,engine-vocab G2-c 对账)、
56
+ `RetryStatus` 的 `stalled`/`error` 两臂、两条投影臂(`adapt/arms.ts` 的 `retryStatusArm` 与
57
+ `adapter/downstream/eventToSdkMessage.ts` 的 `status` 臂 —— SDK 的 `status` 臂类型同样还没跟,
58
+ 按 unknown 读 + 串窄化透传)。
59
+ 🔴 **开集读 + 诚实缺席**:表外桶原样带过去(本层零分支);缺席/空串/非串一律**键不 stamp**,
60
+ 绝不折成某个默认桶。`BrainRetryErrClass` 刻意**只出类型不出运行期值镜像** —— 本包对它零分支,
61
+ 少一个成员没有行为后果,而 `phase` 有 switch 分支才需要那张表 + 等值门。
62
+ - **`ToolApprovalFrame.fromSubagent` 形收窄 `boolean` → `true`(S47)**:SDK 7.1.0/7.2.0
63
+ `dist/resources/tool-approvals.d.ts` 逐字是 `fromSubagent?: true`(与本 interface 上
64
+ `governanceForced?: true` / `requiresRealApproval?: true` 同族的**在场即真**键),本包此前写
65
+ `boolean` = 自铸了一个 wire 上不存在的第三态 `false`。
66
+ 🔴 **`isFromSubagent` 的 `=== true` 判别与回落臂逐字节不变,并把理由写成头注**:显式 `false`
67
+ 只能来自注入面/不合契约的实现,承载的是「这个载体不合契约」而不是「引擎判定不是子代」⇒ 与缺席
68
+ 同档,落回 `sourceTaskId` 在场性那条独立证据。方向是刻意的 —— 本判据唯一消费面是**展示归属**
69
+ (`workerBadge`/`delegation`),多一枚徽章只是噪声、少一枚是把事实对用户藏起来;让不合契约的
70
+ `false` 否决一条独立成立的证据,等于给注入面一个「隐藏子代身份」的开关。
71
+ 门侧:`run-approval-frame-keys-test.mjs` 此前只做**键名集合**比较(零类型判据 —— 一个键从
72
+ `?: true` 悄悄放宽成 `?: boolean` 键名一个字不变、门原样绿),本版补**形腿**:三个在场即真键
73
+ 逐键 grep 源文件声明形 + 跨仓核 SDK 锚同形 + 一条反空转探针。
74
+ - **🔴 新导出 `invalidateEngineCaps(baseUrl)`(S25;公面 750→**751**)**:`engineCapsCache` 的
75
+ **生产失效口**。病:`kickEngineCapsProbe` 首行就是 `capsByBase.has(baseUrl) ⇒ return`,而模块头注
76
+ 自述「随每次 `createLiveConversationClient` 构造重 kick(引擎温切重启后新构造自然重探)」——
77
+ **那句话不成立**:引擎温切(respawn/restartEngine)后 baseUrl 常与重启前一模一样,新构造被这条
78
+ 幂等闸原样挡住,缓存里留的永远是**旧引擎**那一版 caps。后果不是报错,是安静地按旧能力位走:
79
+ 新引擎新增的车道被判成「没有」(藏功能),旧引擎有而新引擎撤掉的车道被判成「有」(走死路)。
80
+ 此前除测试钩 `__resetEngineCapsCacheForTests` 外**零生产失效路径**。同批订正那段过时自述。
81
+ 语义:**推进 per-base 代际** + 清 `capsByBase` + `inFlight`;**不 abort** 在途探测,**不清**
82
+ `settleByBase`(`engineCapsSettled` 的语义是「等**当前这一次**探测落地」,抽走在途 promise 会让
83
+ await 方拿到假「已落地」);空串 ⇒ no-op,从没探过也没失效过的 base ⇒ **真 no-op 不留代际条目**;
84
+ 绝不 throw。**per-base**,不连坐别的 base。
85
+ 🔴 **代际计数 `genByBase`(codex 对抗复审 [high] 采纳,同批修)**:失效口清了 `inFlight` 之后同
86
+ base 立刻可以再 kick ⇒ **两次探测并发跑在同一份无版本共享态上**。没有代际时两个真后果,都发生在
87
+ 「重启撞上一次慢 capabilities 请求」这个最该正确的时刻 —— ①旧引擎那次**后**落地会把 caps 覆盖回
88
+ 旧引擎的位(失效等于没做);②旧探测的 `finally` 删掉的是**新 run** 的 `inFlight`/`settleByBase`
89
+ 条目 ⇒ `engineCapsSettled` 提前 resolve(假「已落地」)+ 幂等闸被打开。修 = 每个 run 出生抓一份
90
+ 代号,写缓存/归还在途位之前核对是否仍是当代;不是当代的 run **只许安静退场**。常驻回归腿按
91
+ 落地**顺序**(新先旧后 / 旧先新后)与旧 run **结局**(resolve/reject)两轴全跑。
92
+ 同批 `docs/refactor/p1-scan/singleton-manifest.json` 登记 `genByBase`(dupRisk high,与
93
+ capsByBase/inFlight/settleByBase 同族必须同一份)+ singleton 门 `HIGH_RISK_CEILING` 91→92。
94
+ 🔴 **`engineCapsSettled` 跨代际接力(对抗复审第二轮 [medium] 采纳,同批修)**:代际闸挡住了
95
+ **写**,却没管**等** —— 调用方 W 抓的是旧代际探测 P0 的 promise,invalidate 推进代际并起 P1 之后,
96
+ P0 先落地就把 W 放走,而 P0 已被挡住不许写缓存 ⇒ **W 当场读到空缓存,把新引擎的能力位当成缺席**。
97
+ 同一类假缺席,只是搬到了 settle 面。修 = 每等完一条就重新求值表里的当代条目,换上了更新代际的
98
+ 那条就改等它(代际严格递增 ⇒ 必然终止;每轮真 await 一条 promise ⇒ 不忙等)。
99
+ 🔴 **两参原子形 `invalidateEngineCaps(baseUrl, probe?)`(对抗复审第三轮 [medium] 采纳,同批修;
100
+ ADDITIVE 可选第二参,公面导出数不变)**:单参形与「下一次 kick」之间还有一个**真窗** —— 失效之后、
101
+ 新探测注册之前,旧代际探测正好在这一拍落地时 `settleByBase` 里已无更新代际的条目 ⇒ 等待者被放走,
102
+ 而缓存刚被清空 ⇒ 把**当代引擎**的能力位读成缺席。窗只在调用方于失效与 kick 之间 `await` 了什么时
103
+ 张开(同步块里 JS 单线程,旧探测的续体插不进来),而引擎温切本身就是异步流程 ⇒ 可达。
104
+ 修 = 第二参给替代探测时,「推进代际」与「注册替代探测」落在**同一个同步块**里 ⇒ 窗按构造不存在,
105
+ 等待者被接力到新探测上。**壳的 respawn/restartEngine 应当用两参形**;单参形保留给「只想丢掉缓存、
106
+ 这一刻没有替代探测」的调用方 —— 那种情形下等待者读到**未判**是诚实结局(判据永远是缓存位),
107
+ 硬等一个可能永远不会来的 kick 才是错的。两条腿都有常驻回归钉(单参形的诚实结局也钉住)。
108
+ 🔴 **代际变更信号(对抗复审第四轮 [high] 采纳,同批修)**:接力此前**无条件先 await 旧那条
109
+ promise**,而引擎重启恰恰是最容易把旧探测**吊死**的时刻(旧进程没了,那个 fetch 可能永远不返回)
110
+ ⇒ 两参原子失效已经起了 P1、P1 也已经把新能力位写进缓存,而 invalidate 之前取件的等待者仍**永久**
111
+ 卡在 P0 上:缓存里明明是对的,等待者却永远拿不到。修 = `settleByBase` 每条带一个 `superseded`
112
+ 信号,`invalidateEngineCaps` 推进代际时 resolve 它,等待者 `Promise.race([探测, 代际信号])`
113
+ 被叫醒后**照走接力循环重新求值**。
114
+ ⚠️ **叫醒 ≠ 放行**(有专门负控钉):醒来时表里换上了新代际(两参原子形保证同拍就在)就接着等
115
+ 新探测;没换才按诚实缺席返回。所以既不会漏掉新引擎的位,也不会硬等一个可能永远不来的 kick。
116
+ 同批把既有那条「invalidate 不把在途 settle 抽走」的断言按修正后的语义**改判**(它编码的是修前
117
+ 行为):判据从「等了多久」改锚「落地那一拍读到什么」—— 早落地不是病,落地时交出旧引擎的位或一个
118
+ 假「新位已到」才是;另补一条「被顶掉的旧探测事后落地也不写缓存」。
119
+ 🔴 **settle 位落位顺序(对抗复审第五轮 [medium] 采纳,同批修)**:`probe` 是**调用方给的闭包**,
120
+ 它的同步段完全可以回头调 `invalidateEngineCaps(baseUrl, 替代探测)` —— 两参原子形正是为温切设计,
121
+ 而温切逻辑就住在这种回调里。`kickEngineCapsProbe` 此前**先跑 IIFE(= 先跑调用方代码)、后
122
+ `settleByBase.set`** ⇒ 嵌套 kick 装好的**新代际**条目当场被外层那一行覆盖回旧条目,随后旧 run 的
123
+ finally 又把它删掉 ⇒ `engineCapsSettled` 在替代探测仍在途时返回,能力读口报 false/undefined。
124
+ 修 = settle 位改用**手工兑现**的 promise 并在**调用方代码跑起来之前**落位;重入时外层没有任何机会
125
+ 回头覆盖别人。`probe` 仍是同步调用(不改时序)。
126
+ ⚠️ **消费方在壳、本版不接线**:cli 侧应在 respawn/restartEngine 成功后、重新构造
127
+ `createLiveConversationClient` **之前**调用;库这一层只看得到一个字符串 base,分辨不出对面是不是
128
+ 同一个进程 —— 靠 TTL 或「每次构造清」去猜,是把一个确定事实换成一个定时器。
129
+
130
+ **Inkglow-1085 案 P0a/P0b 的包半场([4660] 立案、[4664] server 定谳对表,2026-08-19;两件都是
131
+ 行为面。公面 751→**753**:新导出 `DecideTransportRetryExhaustedError` / `clearRunningChoiceOffer`,
132
+ 测试钩 37→**39**)。**
133
+
134
+ - **🔴 P0a:decide 出站超时/瞬断不再判死 turn(分诊有界重试 + 重呈臂;[4664] 长调用口径)**。
135
+ 病(测试员真实项目转录,案卷 `INKGLOW-1085-AUDIT.md` 车1 全链):`approvals.decide` 是非 submit
136
+ POST —— SDK transport 对它 `maxAttempts=1`,且每发带显式 `AbortSignal.timeout(timeoutMs)`(缺省
137
+ 60s;per-call opts 只有 `signal`,与之合流**只能收短不能放长**;timeoutMs 是 client 构造级旋钮,
138
+ 包对注入 client 不可配)⇒ 一次超时/网络断 = 单发即死 ⇒ `toolApprovalWire` 拼 `failed` ⇒
139
+ `parkResolver` 合成 `hitl_unanswered` 终帧 ⇒ ask 死局(零重试/零重呈/run 永挂)。
140
+ 🔴 [4664] 定谳真因:legacy 任务级 decide 腿在响应前**同步跑整条 resume**(模型往返,无上限)——
141
+ 60s 帽结构性必炸;server 立件 #316 改快速 ack,过渡期口径 = 壳把这条腿当**长调用**。修三段:
142
+ ① `HitlBridge.decideRaw`(唯一 decide choke point,四条决断腿全过)分诊重试:**超时类**
143
+ (TimeoutError = 60s 帽掐断,server 多半仍在跑)带退避在**长调用总窗**(缺省 10 分钟,
144
+ `__setDecideTimeoutRetryBudgetForTests` 测试钩)内继续重试;**网络断类**(ECONNREFUSED/
145
+ fetch failed 等)重试**恰一次**(引擎真死尽快显形);**带 HTTP status 的语义答复一律零重试**。
146
+ 重试安全两道背书:[4664] 原话「重复 decide 不双跑,server CAS 保证」+ D-1 绑定回显(第一发其实
147
+ 送达时重发只能撞 4xx,绝不可能批掉另一件事;该 4xx 原样上抛,既有「已解决重放救回」判据照认)。
148
+ `HitlSafetyError` 与用户中止恒零重试;remember 回退臂对瞬断耗尽**不再**回退纯 approve(引擎够不着
149
+ 时补发只烧第二轮超时还错标病因)。
150
+ ② 重试耗尽(超时类窗尽 / 网络断类第二发仍断)⇒ 抛 typed `DecideTransportRetryExhaustedError`,
151
+ 两条决断腿(fs 卡 / ask overlay)在 outcome 上 stamp **`retryExhausted: true`**
152
+ (`FsApprovalOutcome`/`GateOutcome` failed 臂的 additive 判别位,在场即真)。
153
+ ③ `resolvePark` 对该位走**重呈臂**:reattach ⇒ durable 流对 suspended run 必然重放 park ⇒ **同一张
154
+ 卡重新交给用户**(与 #110 已解决重放同一条机械路径,hop 预算照吃);不再合成 `hitl_unanswered` ——
155
+ 判死是三条出路里唯一不可逆的,只配给引擎答了且拒了的语义失败。引擎真死时失败尽快显形:下一轮
156
+ approvals.list 当场失败走既有诚实红。门:`run-hitl-gate-honesty-test.mjs` 新 F6 组(红先绿后 +
157
+ 四变异各自抓红);耗尽错 message 有负控钉「不落 isAlreadyResolvedGateReason 词表」。
158
+ ⚠️ **请托 SDK 半场(候黑板)**:decide 的 per-call 面今天无 timeout 旋钮(只有 signal,合流只能
159
+ 收短)—— 若 SDK 给 decide 开 per-call timeoutMs(或对 HITL 面单列长缺省),超时类重试环可整段
160
+ 收敛成一发长等待;#316(快速 ack)落地后总窗亦可回收。
161
+ - **🔴 P0b:Session busy 幽灵 running 的三选卡半场(存活对账 + 已呈现登记)**。病(案卷车2):
162
+ `runningChoiceArm` 的判据 = 409 终帧 `activeTaskStatus`(best-effort 投影,claim/store 错时**每次**
163
+ 回送同一个错的 `running`),无存活对账;且每次 409 无条件整卡重弹(用户选「Do nothing」后每条
164
+ 消息再弹一张)。修两段:
165
+ ① **出卡前存活对账**:wire 直供 `running` 时补一发 `runs.get`(4s 有界,session 逐次透传;status
166
+ 本就是本层刚 get 来的则不打第二发)—— 读回非 running 的已知词 ⇒ 按真态走(park 词进对应重开臂);
167
+ **404 ⇒ 新结局 `running-not-found`**(幽灵 claim:三选卡不呈 —— 对不存在的 run 渲 steer/cancel 是
168
+ 假 affordance;也不自动重发 —— 该 404 与 non-owner 同码刻意不可分辨,文案只说已证事实 + 「再发
169
+ 一次」真出路);**get 网络失败/窗尽 ≠ 非 running,保守维持出卡**。
170
+ ② **已呈现登记**:模块级 (sessionKey\0taskId,`\0` 为字面转义) 台账记「卡真呈现过且用户选了 wait」——
171
+ 同键再撞 409 不再整卡重弹,`not-parked` 结局带 **`alreadyOffered: true`** 判别位(默认文案层给
172
+ 降级行:仍被占用 + 已问过不再重复问 + cancel 端点与 wayOut 两条真出路;端可按该位自渲更轻的形)。
173
+ 选 steer/cancel 清登记;呈卡腿抛错那次不登记(用户没看到卡);宿主「重新打开操作菜单」走新导出
174
+ **`clearRunningChoiceOffer(taskId, sessionId?)`**。门:`run-selfheal-reopen-test.mjs` 新 G10 组
175
+ (红先绿后 + 两变异抓红);G8⑤ 系列夹具的 `runs.get` 改相位形(首发答 running = 对账放行)。
176
+ ③ **[4664] 对表补强**:`ActiveRunBusySignal` 补 additive 键 **`msSinceLastActivity`**(409 体内
177
+ 唯一可消费的活性证据位;只认有限非负数,坏形键不 stamp,缺席 ≠「没有活动」;SDK 7.2.0 类型面
178
+ 尚无此键 = probeCause 同款领先锚姿势)—— not-parked(running 形)默认行在位在场时追加
179
+ 「last recorded activity … ago」一句,缺席一字不加。分诊上界如实登记:SQL 车道死 run 自愈上界
180
+ ≈180s,**LOCAL(file)车道无周期腿 = 无上界**,故本包不做时间性假设,只做 runs.get 一手复核;
181
+ [4664] 四个洞(park 态不在 reapStale 射程 / updated_at 误杀窗 / 409 与 poll 口径短暂相左 /
182
+ LOCAL 无周期腿)在 `ActiveRunBusySignal.activeTaskStatus` 与存活对账腿的注释上逐条引用 ——
183
+ 409 的 `running` 不是绝对真值。
184
+ ④ **codex 对抗复审跟修(同批,真病三件)**:[high] 对账读回**终态词**(completed/failed/blocked/
185
+ timeout)不再落 not-parked(其默认文案对已终结的 run 说「wait for it to finish」= 永远等不到的
186
+ 假话)—— 专属结局 **`running-settled`**(诚实说 claim 陈旧/释放中 + 重发指引);[medium] 对账
187
+ 期间用户 Esc ⇒ 探测归来后零派臂/零出卡/零登记(零动作现状行收口);[medium 轻形] 登记表 FIFO
188
+ 有界(cap 512,驱逐代价 = 卡重弹一次 = 修前行为)。驳回两件:allow_session remember 回退臂的
189
+ catch 宽度是 0.28.0 起的既有设计(本批已把瞬断耗尽从回退剥出 = 严格收窄;按语义收窄到「仅
190
+ unknown-key 400」登记为候件,行为面另立项);超时类总窗刻意只兜「起新发」不掐在飞发(给一发
191
+ 可能已被受理的 decide 塞截止 signal 换不来安全,只制造送达未知;上冲 ≤ 一发 + 退避,注释成文)。
192
+ ⚠️ **端半场(cli/web/desktop 候接)**:①消费 `alreadyOffered: true` 降级渲染(不接=默认降级行,
193
+ 已可用);②「重新打开菜单」入口接 `clearRunningChoiceOffer`;③新结局 kind `running-not-found` /
194
+ `running-settled` 若端有自绘 rowFor 覆盖需补臂(默认文案已给);④`msSinceLastActivity` 判别位
195
+ 可自渲「已 N 分钟无活动」(不接=默认行已带)。
18
196
 
19
197
  ## 0.36.0(未发布)
20
198
 
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.36.0
38
+ **Version:** 0.37.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
@@ -677,6 +677,9 @@ const retryStatusArm = function* (m, { ctx, flags }) {
677
677
  ...(typeof m.retryInMs === 'number' ? { retryInMs: m.retryInMs } : {}),
678
678
  ...(typeof m.attempt === 'number' ? { attempt: m.attempt } : {}),
679
679
  ...(typeof m.maxRetries === 'number' ? { maxRetries: m.maxRetries } : {}),
680
+ // 七键(#307 S44,2026-08-19):core 5.43.0 起 `BrainStatus` 带 `errClass`(等待原因分桶)。
681
+ // 剥掉它 = 用户看得到「在等」看不到「为什么等」,而供给方是发了的。
682
+ ...(typeof m.errClass === 'string' ? { errClass: m.errClass } : {}),
680
683
  }, ctx.now());
681
684
  // `status === null` = 引擎直报 `recovered`(重试成功)。这一拍是**清除腿之一** —— 但间接腿
682
685
  // (TurnFlags 的「主 lane 内容帧到达即清」)可能已经清过了:已安装 core 在内容流跑完的 finally
@@ -234,11 +234,38 @@ export type SelfHealOutcome =
234
234
  decidePath: string | null;
235
235
  }
236
236
  /** 持锁的是**活着的** run(通常是背景化的那一轮),或 wire 指名了两张分诊表之外的 gate
237
- * kind(结局④:不动 + 如实说)⇒ 不动它,如实告知。 */
237
+ * kind(结局④:不动 + 如实说)⇒ 不动它,如实告知。
238
+ * `alreadyOffered`(Inkglow-1085 P0b①,0.37.0,在场即真):同一 (sessionKey,taskId) 的三选卡
239
+ * **已呈现过且用户选了「Do nothing」** —— 本次不再整卡重弹,端据此把整卡降级渲成一行提示
240
+ * (默认文案层已给降级行;登记的清口见 {@link clearRunningChoiceOffer})。缺席 = 首撞或登记
241
+ * 已被 steer/cancel 选择清掉,现状行字节不变。 */
238
242
  | {
239
243
  kind: 'not-parked';
240
244
  taskId: string;
241
245
  status: string;
246
+ alreadyOffered?: true;
247
+ }
248
+ /**
249
+ * 存活对账腿(Inkglow-1085 P0b②,0.37.0):wire 409 终帧说这条 run `running` 占着会话,而出卡前
250
+ * 的 `runs.get` 真查撞 **404** —— claim 指着一条引擎已不认得的 run(幽灵 running)。三选卡不呈
251
+ * (对不存在的 run 渲 steer/cancel 是假 affordance),也**不自动重发**(该 404 与 non-owner 同码
252
+ * 刻意不可分辨,自动重发可能撞回一个仍锁着的会话)——如实说 + 「再发一次」是真出路。
253
+ */
254
+ | {
255
+ kind: 'running-not-found';
256
+ taskId: string;
257
+ }
258
+ /**
259
+ * 存活对账腿(codex 对抗复审 [high] 采纳,2026-08-19):wire 409 说这条 run `running`,而出卡前的
260
+ * `runs.get` 读回**终态词**({@link CLAIM_RELEASED_STATES})—— run 已终结,claim 是陈旧/释放中的
261
+ * 投影([4664] 洞③:409 与 poll 口径短暂相左)。三选卡不呈(steer/cancel 对已终结的 run 全是假
262
+ * affordance),也**不自动重发**(claim 可能尚未真正落地释放)—— 如实说 + 「再发一次」真出路。
263
+ * 修前这一形落 `not-parked`,默认文案对一条已终结的 run 说「wait for it to finish」= 永远等不到。
264
+ */
265
+ | {
266
+ kind: 'running-settled';
267
+ taskId: string;
268
+ status: string;
242
269
  }
243
270
  /** 读不到状态(缺 verb / 404 / 传输错)⇒ 不做破坏性动作,如实告知。 */
244
271
  | {
@@ -347,7 +374,8 @@ export type SelfHealSubmissionDisposition = 'held-for-decision' | 'resending' |
347
374
  * 🔴 判据锚在「**有没有一张现在就能答、答了就放行的卡**」这个决定量上,不是锚在「是不是 park
348
375
  * 态」这个前置条件上([anchor-on-the-deciding-quantity])。所以三张 reopen-failed / not-parked /
349
376
  * state-unknown 全部落 `not-delivered`:它们同样是 park,但卡没能呈到用户面前,把注入件放回队列
350
- * 只会在下一拍再撞一次同样的 409
377
+ * 只会在下一拍再撞一次同样的 409。`running-not-found`(幽灵 claim)同落 `not-delivered`:
378
+ * 「已解锁」证不出,自动重投可能撞回仍锁着的会话。
351
379
  */
352
380
  export declare function selfHealSubmissionDisposition(outcome: SelfHealOutcome): SelfHealSubmissionDisposition;
353
381
  /**
@@ -371,6 +399,11 @@ export declare function readSteerDelivery(receipt: unknown): string | null;
371
399
  export declare function readSteerReceiptStatus(receipt: unknown): string | null;
372
400
  /** cancel 之后**有界**等那条 run 交出会话的缺省窗(`POST …/cancel` 是 202 异步 —— 收下 ≠ 已停)。 */
373
401
  export declare const CANCEL_RELEASE_WAIT_MS = 10000;
402
+ /** 清掉某条 run 的「Do nothing」登记 —— 宿主「重新打开操作菜单」入口(下一次 409 重新整卡呈现)。
403
+ * `sessionId` 与当时喂给 {@link ActiveRunSelfHealDeps.sessionId} 的值同源(缺席 = 默认键)。 */
404
+ export declare function clearRunningChoiceOffer(taskId: string, sessionId?: string): void;
405
+ /** 测试钩:清空整张登记表(套件各组之间隔离用)。 */
406
+ export declare function __resetRunningChoiceLedgerForTests(): void;
374
407
  /** {@link waitForClaimRelease} 的收口:`released` = 那条 run 真交出了会话;`waitedMs` = **真等了
375
408
  * 多久**;`aborted` = 收口原因是调用方中止(用户 Esc)而不是窗口到点。 */
376
409
  export interface ClaimReleaseVerdict {
@@ -125,7 +125,8 @@ export const CLAIM_HELD_STATES = ['running', 'suspended', 'needs_review'];
125
125
  * 🔴 判据锚在「**有没有一张现在就能答、答了就放行的卡**」这个决定量上,不是锚在「是不是 park
126
126
  * 态」这个前置条件上([anchor-on-the-deciding-quantity])。所以三张 reopen-failed / not-parked /
127
127
  * state-unknown 全部落 `not-delivered`:它们同样是 park,但卡没能呈到用户面前,把注入件放回队列
128
- * 只会在下一拍再撞一次同样的 409
128
+ * 只会在下一拍再撞一次同样的 409。`running-not-found`(幽灵 claim)同落 `not-delivered`:
129
+ * 「已解锁」证不出,自动重投可能撞回仍锁着的会话。
129
130
  */
130
131
  export function selfHealSubmissionDisposition(outcome) {
131
132
  switch (outcome.kind) {
@@ -202,6 +203,37 @@ export function readSteerReceiptStatus(receipt) {
202
203
  }
203
204
  /** cancel 之后**有界**等那条 run 交出会话的缺省窗(`POST …/cancel` 是 202 异步 —— 收下 ≠ 已停)。 */
204
205
  export const CANCEL_RELEASE_WAIT_MS = 10_000;
206
+ /** 出卡前存活对账那一发 `runs.get` 的等待上界(Inkglow-1085 P0b②)——与假死锁复核读口同款
207
+ * 「慢网也回得来」的诚实预算;窗尽/传输错 = 读不到 ⇒ 保守维持出卡(见 runningChoiceArm 注)。 */
208
+ const RUNNING_LIVENESS_RECHECK_TIMEOUT_MS = 4_000;
209
+ // ── running 三选卡的「已呈现且用户选了 Do nothing」登记(Inkglow-1085 P0b①,2026-08-19)────────
210
+ //
211
+ // 病形(案卷车2 发现2):同一条幽灵/长跑 run 占着会话时,用户每发一条消息就被整卡打断一次 ——
212
+ // 呈现台账只有 per-call 局部量(offeringByTask / runningChoiceOffered),没有任何跨 turn 的
213
+ // 「用户已对 run X 说过『别动它』」记账。本登记 = 模块级 (sessionKey,taskId) 集合:
214
+ // · 只在**卡真呈现过且用户选了 wait** 时登记(呈卡腿抛错那次不登记 —— 用户没看到卡,压掉后续
215
+ // 等于把没呈过的卡当已呈);
216
+ // · 登记在场 ⇒ 同 (sessionKey,taskId) 再撞 409 不再整卡重弹,结局带 `alreadyOffered: true`
217
+ // 判别位(端据此降级渲一行;默认文案层已给降级行,含 cancel 端点与 wayOut 两条真出路);
218
+ // · 用户选 steer/cancel ⇒ 清登记(他重新对这条 run 表了态);宿主的「重新打开操作菜单」入口
219
+ // 走 {@link clearRunningChoiceOffer}。
220
+ // run 身份变化天然换键(taskId 不同);同 run 状态迁移到 park 态会走别的臂,登记不拦。
221
+ const runningChoiceDeclined = new Set();
222
+ /** 登记表上限(codex 复审轻形采纳):超限 FIFO 驱逐最老条目 —— 驱逐代价 = 卡重弹一次(修前行为)。 */
223
+ const RUNNING_CHOICE_LEDGER_CAP = 512;
224
+ function runningChoiceDeclineKey(sessionId, taskId) {
225
+ // NUL 分隔:两段都是外来串,可打印分隔符在「sessionId 里恰有它」时会串键。
226
+ return `${sessionId ?? ''}\u0000${taskId}`;
227
+ }
228
+ /** 清掉某条 run 的「Do nothing」登记 —— 宿主「重新打开操作菜单」入口(下一次 409 重新整卡呈现)。
229
+ * `sessionId` 与当时喂给 {@link ActiveRunSelfHealDeps.sessionId} 的值同源(缺席 = 默认键)。 */
230
+ export function clearRunningChoiceOffer(taskId, sessionId) {
231
+ runningChoiceDeclined.delete(runningChoiceDeclineKey(sessionId, taskId));
232
+ }
233
+ /** 测试钩:清空整张登记表(套件各组之间隔离用)。 */
234
+ export function __resetRunningChoiceLedgerForTests() {
235
+ runningChoiceDeclined.clear();
236
+ }
205
237
  /** 假死锁复核读口(`deps.listOwnedPendingApprovals`)的等待上界:一发 approvals 列表读,4s 是
206
238
  * 「慢网也回得来」的诚实预算;窗尽=分不出真卡与幽灵 ⇒ 保守 stand down(见调用点注)。 */
207
239
  const OWNED_PENDING_RECHECK_TIMEOUT_MS = 4_000;
@@ -419,8 +451,12 @@ export async function attemptActiveRunSelfHeal(signal, runs, deps) {
419
451
  return askParkArm(taskId, signal, deps);
420
452
  // 真在跑 ⇒ 把引擎给着的两条路(steer / cancel)+ 现状做成三选卡交给用户;表外的其它状态
421
453
  // (含今天还不存在的)照旧「不动它 + 如实说」—— 语义不明的状态不配递把手(见 RUNNING_STATES 注)。
422
- if (RUNNING_STATES.includes(status))
423
- return runningChoiceArm(taskId, status, runs, deps);
454
+ // Inkglow-1085 P0b②:`statusFromWire` = 这个 running 是 409 终帧直供的投影(best-effort,可陈旧
455
+ // ——claim/store 错时恒回送 running 的幽灵读数);出卡前 runningChoiceArm 会对它补一发 runs.get
456
+ // 真查。status 本就是上面刚 get 来的(wire 缺席回退)⇒ 已新鲜,不打第二发。
457
+ if (RUNNING_STATES.includes(status)) {
458
+ return runningChoiceArm(taskId, status, signal, signal.activeTaskStatus !== null, runs, deps);
459
+ }
424
460
  return { kind: 'not-parked', taskId, status };
425
461
  }
426
462
  /** plan_review 重开口的判决(臂与判决分开:steer 的 `queued` 回执要的是**同一份**重开语义的判决值,
@@ -465,7 +501,7 @@ async function askParkArm(taskId, signal, deps) {
465
501
  * ③ 选项按**供给**渲(verb 缺席 = 那条路兑现不了 = 整个不渲),cancel 还额外要 `get` ——
466
502
  * 没有它就确认不了 claim 真的释放,「取消并重发」就成了猜。
467
503
  */
468
- async function runningChoiceArm(taskId, status, runs, deps) {
504
+ async function runningChoiceArm(taskId, status, busy, statusFromWire, runs, deps) {
469
505
  const notParked = { kind: 'not-parked', taskId, status };
470
506
  const offer = deps?.offerRunningChoice;
471
507
  if (typeof offer !== 'function')
@@ -480,13 +516,92 @@ async function runningChoiceArm(taskId, status, runs, deps) {
480
516
  const canCancel = typeof runs?.cancel === 'function' && typeof runs?.get === 'function';
481
517
  if (!canSteer && !canCancel)
482
518
  return notParked;
519
+ // ── 存活对账腿(Inkglow-1085 P0b②):出卡前对 wire 直供的 running 补一发 runs.get 真查 ────────
520
+ // 409 终帧的 `activeTaskStatus` 是引擎 claim/store 的 best-effort 投影 —— claim 指着一条早已终结
521
+ // 甚至已不存在的 run 时,它会**每次都**回送同一个错的 running(幽灵),用户按 wire 读数拿到的
522
+ // steer/cancel 两条路全是死路。
523
+ // 🔴 [4664] server 定谳(2026-08-19)把「别把 409 running 当绝对真值」写成了四条成文的洞:
524
+ // ①park 态不在 reapStale 射程;②`updated_at` 误杀窗;③409 与 poll 口径短暂相左;
525
+ // ④LOCAL(file)车道无周期自愈腿。SQL 车道死 run 的自愈上界 ≈180s(心跳 30s +
526
+ // REAP_RUN_STALE_SEC 120 + tick 60)——**LOCAL 车道无上界,勿假设 180s**,所以本腿不做
527
+ // 「等它自愈」的时间性假设,只做一手读面复核。
528
+ // 真源两级里它是二手货,出卡(把把手递给人)前用一手读面复核一次:
529
+ // · get 读回非 running 的**已知**词 ⇒ wire 读数过期,按真态走(park 词进对应重开臂,其余落
530
+ // not-parked 如实说)—— 与「kind 缺席 ⇒ status 表回退」同一张表,不新铸分臂;
531
+ // · get 撞 404 ⇒ 幽灵 claim(`running-not-found` 判别位;为什么不算「已释放」见该臂注);
532
+ // · get 网络失败 / 本发截止 / 奇形记录 ⇒ **读不到 ≠ 非 running**,保守维持出卡 —— 误关卡会把
533
+ // steer/cancel 两条真把手一起藏掉;
534
+ // · status 本就是本层刚 runs.get 查来的(statusFromWire=false)⇒ 已新鲜,不打第二发。
535
+ // 🔴 经函数读 aborted(waitForClaimRelease.isAborted 同注):它在 await 两侧会变,直接读两次会被
536
+ // tsc 控制流分析把第二次窄成恒假比较(TS2367)。
537
+ const probeCallerAborted = () => deps?.signal?.aborted === true;
538
+ if (statusFromWire && typeof runs?.get === 'function' && !probeCallerAborted()) {
539
+ let fresh = null;
540
+ let probeAnswered = false;
541
+ let ghost = false;
542
+ const lease = claimProbeLease(deps?.signal, RUNNING_LIVENESS_RECHECK_TIMEOUT_MS);
543
+ try {
544
+ fresh = readStatus(await runs.get(taskId, { signal: lease.signal, ...sessionOpts(deps) }));
545
+ probeAnswered = true;
546
+ }
547
+ catch (e) {
548
+ if (e?.status === 404)
549
+ ghost = true;
550
+ }
551
+ finally {
552
+ lease.release();
553
+ }
554
+ if (ghost)
555
+ return { kind: 'running-not-found', taskId };
556
+ // (codex 复审 [medium] 采纳)对账期间用户中止 ⇒ 后续一切动作(派臂/出卡/登记)都不做 ——
557
+ // Esc 之后还弹卡/重开 = 用户说停还在动。零动作现状行收口(与呈卡腿抛错同一条诚实收口)。
558
+ if (probeCallerAborted())
559
+ return notParked;
560
+ if (probeAnswered && fresh !== null && !RUNNING_STATES.includes(fresh)) {
561
+ if (PLAN_REVIEW_STATES.includes(fresh))
562
+ return planReviewArm(taskId, busy, deps);
563
+ if (ASK_PARK_STATES.includes(fresh))
564
+ return askParkArm(taskId, busy, deps);
565
+ // (codex 复审 [high] 采纳)终态词 ⇒ 专属结局:not-parked 的「wait for it to finish」对一条
566
+ // 已终结的 run 是永远等不到的假话。表外的未知词仍落 not-parked 如实说(不替引擎断言终结)。
567
+ if (CLAIM_RELEASED_STATES.includes(fresh))
568
+ return { kind: 'running-settled', taskId, status: fresh };
569
+ return { kind: 'not-parked', taskId, status: fresh };
570
+ }
571
+ // 确认仍 running(或读不到)⇒ 按原判继续出卡。
572
+ }
573
+ // ── 已呈现登记(Inkglow-1085 P0b①):同 (sessionKey,taskId) 用户选过「Do nothing」⇒ 不再整卡
574
+ // 重弹,给 `alreadyOffered` 判别位让端降级渲一行(登记语义与清口见 runningChoiceDeclined 顶注)。
575
+ const declineKey = runningChoiceDeclineKey(deps?.sessionId, taskId);
576
+ if (runningChoiceDeclined.has(declineKey)) {
577
+ return { kind: 'not-parked', taskId, status, alreadyOffered: true };
578
+ }
483
579
  let choice = 'wait';
580
+ let offerDelivered = false;
484
581
  try {
485
582
  choice = (await offer({ taskId, status, canSteer, canCancel })) ?? 'wait';
583
+ offerDelivered = true;
486
584
  }
487
585
  catch {
488
586
  choice = 'wait'; // 呈卡腿出意外 = 用户没做决定 ⇒ 零动作(绝不掉进破坏性分支)
489
587
  }
588
+ // wait = 用户看过卡并选择留着它(呈现层把 Esc/空答也收口成 wait —— 同样是「这次不动它」的表态);
589
+ // steer/cancel = 用户重新表了态 ⇒ 清登记。呈卡腿抛错那次**不**登记(用户没看到卡)。
590
+ if (choice === 'wait') {
591
+ if (offerDelivered) {
592
+ runningChoiceDeclined.add(declineKey);
593
+ // (codex 复审 [medium] 轻形采纳)FIFO 有界:长命桌面/多会话宿主不许无界长住。驱逐最老条目的
594
+ // 代价 = 那条 run 的卡重弹一次(= 修前行为,方向安全);Set 按插入序迭代,首项即最老。
595
+ if (runningChoiceDeclined.size > RUNNING_CHOICE_LEDGER_CAP) {
596
+ const oldest = runningChoiceDeclined.values().next().value;
597
+ if (oldest !== undefined)
598
+ runningChoiceDeclined.delete(oldest);
599
+ }
600
+ }
601
+ }
602
+ else {
603
+ runningChoiceDeclined.delete(declineKey);
604
+ }
490
605
  if (choice === 'steer' && canSteer && runs?.steer !== undefined) {
491
606
  try {
492
607
  // 🔴 正文原样送(untrusted DATA,server 侧围栏);恰一次,绝不重试。方法形调用保接收者。
@@ -621,6 +736,19 @@ function governanceOriginClause(signal) {
621
736
  ? ' That gate is enforced by this deployment\'s governance policy, not by a permission rule you set.'
622
737
  : '';
623
738
  }
739
+ /**
740
+ * 活性证据(Inkglow-1085 P0b,[4664]:409 体内 `msSinceLastActivity` 是唯一可消费的活性证据位)。
741
+ * 🔴 只在位真在场时说话(缺席一个字都不说 —— 缺席 = 无法证明,不是「没有活动」);它是**展示**
742
+ * 判别,不改变任何动作。只挂在 running 形的 not-parked 行上:那正是「它到底还活着吗」这个问题
743
+ * 被问出来的地方。
744
+ */
745
+ function lastActivityClause(signal) {
746
+ const ms = signal?.msSinceLastActivity;
747
+ if (typeof ms !== 'number' || !Number.isFinite(ms) || ms < 0)
748
+ return '';
749
+ const human = ms < 120_000 ? `${String(Math.max(1, Math.round(ms / 1000)))}s` : `${String(Math.round(ms / 60_000))}m`;
750
+ return ` The engine last recorded activity on that run ${human} ago.`;
751
+ }
624
752
  /**
625
753
  * 每种结局的上屏整行。**每一句出路都真的接了线**;没有真出路的分支就直说「这个会话暂时无法
626
754
  * 继续」。重开成功那条也要上屏 —— 卡是替用户重新打开的,他得知道去答哪张、答完做什么。
@@ -638,7 +766,9 @@ export function activeRunSelfHealRow(outcome, signal, copy, origin) {
638
766
  const base = activeRunSelfHealBaseRow(outcome, signal, copy?.wayOut ?? DEFAULT_WAY_OUT);
639
767
  switch (outcome.kind) {
640
768
  // 这两条是「用户此刻卡住了、而且没有别的把手」的结局 —— wire 给的 decide 入口在这里才有用。
769
+ // not-parked(running 形)另挂活性证据([4664] 判别位):「它到底还活着吗」在这一行被问出来。
641
770
  case 'not-parked':
771
+ return base + lastActivityClause(signal) + decidePathClause(signal) + governanceOriginClause(signal);
642
772
  case 'state-unknown':
643
773
  return base + decidePathClause(signal) + governanceOriginClause(signal);
644
774
  // 其余结局手上都另有一张更好的把手(重开的卡 / 已开的卡 / 失败臂基句自己渲的 decidePath)
@@ -822,7 +952,30 @@ function activeRunSelfHealBaseRow(outcome, signal, wayOut) {
822
952
  return (`sema could not cancel run ${outcome.taskId} — the engine rejected that request (${outcome.detail}), so ` +
823
953
  `nothing was cancelled and sema cannot tell whether that run is still holding this session. Your message ` +
824
954
  `was NOT sent; send it again to find out, or ${wayOut}.`);
955
+ case 'running-not-found':
956
+ // Inkglow-1085 P0b②:幽灵 claim —— 引擎一边说这条 run 占着会话、一边查无此 run。「已解锁」
957
+ // 证不出(404 与 non-owner 同码刻意不可分辨),所以只说已证的事实 + 「再发一次」这条真出路
958
+ // (与 running-cancel-timeout 读不出那格同款措辞方向:真没了就会直接跑起来)。
959
+ return (`The engine reported this session as held by run ${outcome.taskId} (status running), but it no ` +
960
+ `longer recognizes that run when asked directly — that claim looks stale (the run most likely no ` +
961
+ `longer exists). sema did not offer to steer or cancel it: there is nothing left to act on. ` +
962
+ `Your message was NOT sent; send it again (if the session really is free it will just run), or ${wayOut}.`);
963
+ case 'running-settled':
964
+ // codex 复审 [high] 采纳:run 已终结而 claim 仍报 running([4664] 洞③的窗口形)——
965
+ // 「wait for it to finish」对它是永远等不到的假话;真出路 = 再发一次(claim 落地即跑)。
966
+ return (`Run ${outcome.taskId} has already finished (status ${outcome.status}), but the engine still ` +
967
+ `reported it as holding this session — that claim looks stale or mid-release. sema did not offer ` +
968
+ `to steer or cancel it: the run is over. Your message was NOT sent; send it again (once the claim ` +
969
+ `clears it will just run), or ${wayOut}.`);
825
970
  case 'not-parked':
971
+ // Inkglow-1085 P0b①:同一条 run 的三选卡已呈现过且用户选了「Do nothing」⇒ 降级成一行,
972
+ // 不再整卡打断;两条真出路(引擎 cancel 端点 / wayOut)必须还在这一行里 —— 压噪不压把手。
973
+ if (outcome.alreadyOffered === true) {
974
+ return (`Run ${outcome.taskId} is still holding this session (status ${outcome.status}). sema already ` +
975
+ `asked what to do with it and you chose to leave it alone, so it is not asking again. ` +
976
+ `Your message was NOT sent; wait for that run, cancel it on the engine ` +
977
+ `(POST /v1/runs/${outcome.taskId}/cancel), or ${wayOut}.`);
978
+ }
826
979
  // 🔴 「等它跑完」这句只在**它真的在跑**的时候是真话。分诊表外的门/状态里也可能是 park
827
980
  // (表外 gate kind 正是走这条),而 wire 恰恰会在那种情况下带 pendingGate —— 对一个
828
981
  // parked run 说「wait for it to finish」是一句永远等不到的假出路。动作仍然一个字不改
@@ -268,11 +268,15 @@ export function eventToSdkMessage(ev, ctx) {
268
268
  // phase/detail/retryInSec(SDK 面尚未跟上 core)。类型面缺席不等于 wire 上缺席 —— 按
269
269
  // unknown 读、按 number 窄化后原样透传,否则引擎真发的量在这一层就被剥掉了。
270
270
  // 键集真源 = retryStatus.ts 的 `BRAIN_STATUS_PAYLOAD_KEYS`(engine-vocab G2-c 对账)。
271
+ // #307 S44(2026-08-19):core 5.43.0 又加了 `errClass`(这次等待的**原因分桶**,
272
+ // provider 中立闭集)—— SDK 的 `status` 臂类型同样还没跟,同款 unknown 读 + 串窄化透传。
271
273
  const st = ev;
272
274
  const num = (v) => typeof v === 'number' && Number.isFinite(v) ? v : undefined;
273
275
  const attempt = num(st.attempt);
274
276
  const maxRetries = num(st.maxRetries);
275
277
  const retryInMs = num(st.retryInMs);
278
+ // 非空串才透传(空串既不是桶也不是「不知道」,只会在下游被渲成一个空的原因)。
279
+ const errClass = typeof st.errClass === 'string' && st.errClass.length > 0 ? st.errClass : undefined;
276
280
  return projected(stamp(ctx, armBody({
277
281
  type: 'retry_status',
278
282
  phase: ev.phase,
@@ -281,6 +285,7 @@ export function eventToSdkMessage(ev, ctx) {
281
285
  ...(retryInMs !== undefined ? { retryInMs } : {}),
282
286
  ...(attempt !== undefined ? { attempt } : {}),
283
287
  ...(maxRetries !== undefined ? { maxRetries } : {}),
288
+ ...(errClass !== undefined ? { errClass } : {}),
284
289
  // 🔴 §E2 lane 身份必须透传(2026-08-08 对抗复审二轮复审命中的**跨 lane 状态破坏**)。
285
290
  // `status` 臂本来就是 `& EventIdentity`(SDK events.d.ts),server 两腿共用的
286
291
  // `brainStatusEventData` 也经 `identityFields` 发 eventId/parentToolCallId —— 而本层此前
@@ -111,10 +111,23 @@ export declare function pendingGateIsProvablyDifferent(prev: ActiveRunPendingGat
111
111
  export interface ActiveRunBusySignal {
112
112
  /** 占锁 run 的 id;wire 没带 ⇒ null(诚实缺席,绝不铸造)。 */
113
113
  readonly activeTaskId: string | null;
114
- /** server 3.21+ 真发的占锁 run 状态;缺席 ⇒ null(由调用方走 runs.get 一级)。 */
114
+ /** server 3.21+ 真发的占锁 run 状态;缺席 ⇒ null(由调用方走 runs.get 一级)。
115
+ * 🔴 [4664] 成文的可信度边界(2026-08-19,四个洞逐条在案):park 态不在 reapStale 射程 /
116
+ * `updated_at` 误杀窗 / 409 与 poll 口径短暂相左 / LOCAL(file)车道无周期自愈腿 ——
117
+ * **别把这一位的 `running` 当绝对真值**;SQL 车道死 run 的自愈上界 ≈180s(心跳 30s +
118
+ * REAP_RUN_STALE_SEC 120 + tick 60),LOCAL 车道**无上界**。消费方出卡前的存活对账腿见
119
+ * `activeRunSelfHeal.runningChoiceArm`。 */
115
120
  readonly activeTaskStatus: string | null;
116
121
  /** 409 body 的 pendingGate(wire 给的真路由,不自造);缺席 ⇒ null。 */
117
122
  readonly pendingGate: ActiveRunPendingGate | null;
123
+ /**
124
+ * 409 体上的占锁 run **最近活动距今毫秒数**(Inkglow-1085 P0b,[4664]:这一格是 409 体内唯一
125
+ * 可消费的活性证据位)。在场 = 有限非负数;缺席 = 老 server / 坏形(两者同形,不猜)——
126
+ * **缺席 ≠「没有活动」**。展示判别位:端据此渲「该 run 已 N 分钟无活动」类提示;
127
+ * 不参与本包的分诊动作(存活对账腿的判据仍是 runs.get 读回的状态词)。
128
+ * ⚠️ 结构视图读(SDK 7.2.0 类型面尚无此键)—— probeCause 同款「领先锚」姿势,锚补上跟批。
129
+ */
130
+ readonly msSinceLastActivity?: number;
118
131
  }
119
132
  /**
120
133
  * 判别一个**原始 AgentEvent** 是不是 409 active-run 拒收终帧;非 busy ⇒ null。
@@ -91,10 +91,14 @@ export function activeRunBusySignal(ev) {
91
91
  ...(typeof cp === 'string' && cp.length > 0 ? { checkpointId: cp } : {}),
92
92
  };
93
93
  }
94
+ // [4664] 活性证据位:只认**有限非负数**(NaN/Infinity/负数/串都是坏形 ⇒ 键不 stamp —— 一个
95
+ // 编造的「N 分钟无活动」比没有更坏)。
96
+ const msIdle = carrier.msSinceLastActivity;
94
97
  return {
95
98
  activeTaskId: cls.handle,
96
99
  activeTaskStatus: typeof status === 'string' && status.length > 0 ? status : null,
97
100
  pendingGate,
101
+ ...(typeof msIdle === 'number' && Number.isFinite(msIdle) && msIdle >= 0 ? { msSinceLastActivity: msIdle } : {}),
98
102
  };
99
103
  }
100
104
  /**