@sema-agent/client-core 0.28.0 → 0.30.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 (50) hide show
  1. package/CHANGELOG.md +400 -0
  2. package/README.md +19 -2
  3. package/dist/adapt/arms.js +24 -1
  4. package/dist/adapt/wireShapes.d.ts +7 -0
  5. package/dist/adapt/wireShapes.js +7 -0
  6. package/dist/adapter/activeRunSelfHeal.d.ts +407 -0
  7. package/dist/adapter/activeRunSelfHeal.js +771 -0
  8. package/dist/adapter/runStream.d.ts +23 -6
  9. package/dist/adapter/runStream.js +19 -5
  10. package/dist/engineWireSdk.d.ts +10 -2
  11. package/dist/engineWireSdk.js +7 -3
  12. package/dist/hitl/approvalDecisionNoteAudit.d.ts +58 -0
  13. package/dist/hitl/approvalDecisionNoteAudit.js +91 -0
  14. package/dist/hitl/armedGateRegistry.d.ts +19 -0
  15. package/dist/hitl/armedGateRegistry.js +92 -0
  16. package/dist/hitl/askParkRowRouting.d.ts +150 -0
  17. package/dist/hitl/askParkRowRouting.js +183 -0
  18. package/dist/hitl/frameRouter.d.ts +5 -1
  19. package/dist/hitl/frameRouter.js +1 -1
  20. package/dist/hitl/gateIdentity.d.ts +50 -0
  21. package/dist/hitl/gateIdentity.js +64 -0
  22. package/dist/hitl/hitlBridge.d.ts +7 -0
  23. package/dist/hitl/hitlBridge.js +11 -2
  24. package/dist/hitl/parkOwnership.d.ts +65 -0
  25. package/dist/hitl/parkOwnership.js +49 -0
  26. package/dist/hitl/parkResolver.js +3 -1
  27. package/dist/hitl/parkRowBirthWait.d.ts +63 -0
  28. package/dist/hitl/parkRowBirthWait.js +192 -0
  29. package/dist/hitl/planReviewWire.d.ts +51 -0
  30. package/dist/hitl/planReviewWire.js +172 -17
  31. package/dist/hitl/resumeRunningCard.d.ts +134 -0
  32. package/dist/hitl/resumeRunningCard.js +177 -0
  33. package/dist/hitl/toolApprovalWire.d.ts +69 -23
  34. package/dist/hitl/toolApprovalWire.js +60 -24
  35. package/dist/index.d.ts +9 -0
  36. package/dist/index.js +34 -0
  37. package/dist/liveQuestionStore.d.ts +11 -0
  38. package/dist/liveQuestionStore.js +13 -0
  39. package/dist/seatContract.d.ts +27 -0
  40. package/dist/seatContract.js +42 -0
  41. package/dist/subagent/engineSubagentTail.d.ts +0 -2
  42. package/dist/subagent/engineSubagentTail.js +7 -15
  43. package/dist/subagentContentStore.d.ts +58 -2
  44. package/dist/subagentContentStore.js +95 -6
  45. package/dist/toolResult.d.ts +26 -0
  46. package/dist/toolResult.js +38 -6
  47. package/dist/workflowClient.d.ts +6 -1
  48. package/docs/INTEGRATION-CLIENTS.md +844 -0
  49. package/docs/REFACTOR-LEDGER.md +392 -0
  50. package/package.json +7 -4
package/CHANGELOG.md ADDED
@@ -0,0 +1,400 @@
1
+ # Changelog
2
+
3
+ > 建档于 0.29.0(2026-08-12)。更早版本的沿革见 `src/index.ts` 文件头的批次记账与
4
+ > `docs/REFACTOR-LEDGER.md`;本档从此随每次版本 bump 增补。
5
+ >
6
+ > 🔴 **已发段是冻结面**(#252 立规,2026-08-14):一个 `## x.y.z` 标题下只许记**那一版真的发出去的
7
+ > 东西**;未发的一律进 `## <next>(未发布)` 段。记进已发段 = 对三端谎报「装那一版就有」——
8
+ > 实翻:0.29.0 发布(gitHead `0ab959d`)之后,[C170]/[C175] 三批被整段追加在 `## 0.29.0` 底下,
9
+ > 而 npm 上的 0.29.0 一件都没有,连 peer floor 都还是 `>=6.16.0`。
10
+ > 常驻门 `scripts/run-integration-doc-freshness-test.mjs` ④ 段把**每一个发过 npm 的版本**都按 sha256
11
+ > 钉进一本 **append-only 冻结账**并逐版核(不是只钉最近那一版 —— 只钉一版的话,下次 bump 那一刻上一版
12
+ > 就从此不受约束)。三个反向一起堵:自称已发却不在账上、账上的段整个消失、`package.json` 的 version
13
+ > 不在账上,都红。**发包批的义务** = 往 `FROZEN` **追加**一行(版本号 + 发布 commit + 段 sha256),
14
+ > **绝不改写既有行** —— 既有行是「那一版当时发了什么」的历史,不是可变配置。
15
+ >
16
+ > 🔴 **互链**(web [C166]⑦):各版「已知局限」段只记**该版新增**;接入面已知局限的完整台账在
17
+ > `docs/INTEGRATION-CLIENTS.md` §6e/§7 —— **只读其一会漏**,两处都过。
18
+
19
+ ## 0.30.0 — 2026-08-14
20
+
21
+ > 发车门清账完成:0.30.0 发版扫描 36 条 confirmed 全处置(codex 八轮终轮零新),
22
+ > 最后一门 P-30(`findPendingForTask` 门种闸)红先绿后闭环,29/29 门绿后发布。
23
+
24
+ - **P-30:`findPendingForTask` 门种闸**(2026-08-14;**行为面** + 1 新导出;0.30.0 发车前最后一门):
25
+ - 🔴 **行为面**:`findPendingForTask` 两腿(toolName 谓词匹配 + 同 `taskId` 任意行回落)此前都不读
26
+ `gateKind` —— `/v1/approvals` 是 scope 级单队列,同 task 停着 `plan_review` / `resource_limit`
27
+ 这类**不归 ask/审批臂决**的行时,回落腿会把它原样递给工具审批 wire ⇒ 弹 `toolName` 空的工具卡,
28
+ decide 撞 409 `gate_not_tool_approval`(fail-closed 不误批,但卡是假的、checkpoint 仍 parked)。
29
+ 现在两腿都只在 **ask 门族行**里找(族外 kind 行被过滤,不是拒整队列:同 task 的真审批行照常
30
+ 命中);kind 缺席的 pre-`gate_kind` 历史行维持原判(旧行没有更强信号)。找不到族内行 ⇒ 既有
31
+ typed `no_pending` 良性缺席,调用方按既有分支收口,不再弹空卡。
32
+ - **新导出** `askParkForeignGateKind(row)`(`adapter/activeRunSelfHeal.ts`):「行上在场且不属
33
+ ask 门族的门种,`null`=可入臂」的**单源判据** —— `classifyAskParkRows` 的门种闸与
34
+ `findPendingForTask` 的行过滤自此共用一只(各写各的 includes 必漂)。
35
+ - 常驻回归:`run-park-decision-layer-test.mjs` G3⑧ 六臂(plan_review/resource_limit 拒 + 历史行/
36
+ 族内行兜底保持 + 矛盾行同拒 + 族外行不遮蔽真审批行)。
37
+ - **0.30.0 发包扫描清账批**(2026-08-14;**一处行为面** + 门/棘轮/JSDoc 面,零新增导出、零签名变更):
38
+ - 🔴 **行为面:子代查看态的工具结果栏改渲人可读正文**(`src/subagentContentStore.ts` 的
39
+ `coerceOutput`)。§E1 的 wire `tool_end.output` 是**非均匀**的(`string |
40
+ (TextContent|ImageContent)[]`);此前块数组形一律走 `JSON.stringify`,于是**同一份 wire 素材**
41
+ 在主 lane 渲人话(工具卡体 / print 帧 / 面板 settle 三处走 `adapt/wireShapes.ts` 的
42
+ `flattenWireOutput`)、在子代查看态渲 `[{"type":"text","text":"…"}]` 这种机器形;带 image 块时
43
+ 整段 base64(sdk `events.d.ts` 的 `totalChars` 注释逐字:image/document = base64 真实体量)
44
+ 被原样灌进内容账本并上屏(条目数有 200 上限,**单条体量无上限**)。现在块数组走与主 lane 逐字
45
+ 同语义的展平(text 块贡献 `.text`,image 块贡献中性占位 `[image]`)。
46
+ **可观察差异**(两条腿同时受益,它们喂的是同一个账本):C1 分流臂(`adapter/runStream.ts`)
47
+ 与 tail 腿(`subagent/engineSubagentTail.ts`)的块数组结果栏,由**转义 JSON**变为**正文**;
48
+ 裸串形逐字节不变;`undefined`/`null` 仍是诚实缺席(不铸空串);非串非数组的对象形仍走
49
+ `JSON.stringify`(那一支服务的是 server `redactDeep` 之后原型不保的形,刻意不变)。
50
+ ⚠️ 端若在自己那侧**手工 JSON.parse 过这一位**(把机器形还原成正文的补偿),现在应当撤掉 ——
51
+ 这一位从此就是正文。
52
+ - 门/棘轮(无行为面):pure 段下限五处贴回实测零松量(B1 174→179 / B2 289→293 / B3 204→209 /
53
+ B4 101→108 / B5 210→214);`run-sdk-floor-test.mjs` 的 `FLOOR` 6.9→**6.17.2(三位含 patch)**
54
+ **并新增 ①c** 把它与 `package.json` 的 peer 声明地板**逐位绑死**(此前两门合起来只把实装夹在
55
+ [6.9, 6.17.2],没有一条要求实装真站在声明线上;patch 位尤其不能丢 —— 6.17.2 恰恰是**按 patch
56
+ 抬**的一版,比较器丢 patch 就等于丢掉这次抬升的全部理由,配套加了 6.17.1 必红的正控);
57
+ `run-durable-card-display-keys-test.mjs` **新增 ⑦ 富行键集普查**(上游 `PendingCheckpoint` 的
58
+ 声明键**路径集**,含递归进 `riskDescriptor` 的逐成员 ⇄ 转发账 + 未投影账,双向;上游 additive
59
+ 加展示键当天红 —— 正是 [C170] 那次「重铸处闭集、行上新键静默丢失」缺的那道门);
60
+ `run-type-superset-ledger-test.mjs` **新增 ④a 镜像版本见证**,且 `agent-types` **声明了却没装**
61
+ 从 exit 3 改判 **exit 1**(全仓唯一的「CC 加同名位当天红」警报不许静默退场)。
62
+ - **新登记缺口 P-30**(`docs/INTEGRATION-CLIENTS.md` §7d;本批扫描发现、**非本窗引入**、
63
+ 本批**不改行为**):durable 审批腿不按 `gateKind` 路由,且 `findPendingForTask` 在工具名谓词
64
+ 无命中时有一条「同 taskId 任意行」回落 ⇒ 同一 task 上停着 `plan_review` / `resource_limit` 行时,
65
+ 会弹出 `toolName` 为空串的工具卡。**不会误批**(server 在 `/v1/approvals/:sessionId/decide` 上对
66
+ 非工具门回 409 `gate_not_tool_approval`),但后果不止一次失败的决断:decide 抛错 ⇒ 本腿折成
67
+ `failed` ⇒ `parkResolver` fail-soft 结束本次客户端 turn,而 server 侧 checkpoint 未被消费、
68
+ run/session 仍 suspended 仍持 claim,重试会再撞一次。⚠️ **终帧按入口分两形**:初始 park 入口
69
+ (`done{…park…}`,`pendingDone` 在场)先 flush 出 park 期被 HOLD 的毒化帧
70
+ (`tool_end{isError:true, output:'Operation aborted'}` = `ENGINE_ABORT_TOOL_RESULT`;
71
+ ⚠️ 它与「用户真按了拒绝」**可按 `output` 分辨** —— 真 deny 会被 `frameRouter` 改写成
72
+ `HITL_REJECT_MESSAGE`,端做归因别只看 `isError`)再**原样回吐那条 done**,**不合成** `failed` 终帧、**没有** `hitl_unanswered` 码;只有续流 / durable
73
+ re-attach 入口(无 `pendingDone`)才合成 `failed{hitl_unanswered}` —— 端做埋点别只锚那个码。
74
+ ⚠️ 那条 fail-soft 链是 durable 腿**通用**失败路径(非本缺口独有);本缺口的增量 = 一张不该弹的卡
75
+ + 一次注定 409 的 decide + 浪费用户一次表态。
76
+ 端侧处置:拿到 `toolName` 为空串的 durable 卡按异常处置,别渲成可决断卡。
77
+ 🔴 **本条是否阻断 0.30.0 发车,归主会话/clay 裁**(「台账已知问题清零才发」)—— 本批只做登记与
78
+ 取证,不改路由行为:正位解要同批裁定 pre-`gate_kind` 历史行(`gateKind` 缺席)怎么降级。
79
+ - JSDoc / 台账订正(随 `dist/*.d.ts` 出包,消费端可见):`ApprovalCardRequest.workerBadge` 的
80
+ 「CC swarm 原生位」出身声明被普查证伪(CC 语料九份 + agent-types 镜像全 0 命中),改记为
81
+ 「按现有证据是 sema 自铸,终判候台账清账」;`ApprovalCardRequest.governanceForced` 补记
82
+ **双源合流**(活卡帧腿 + durable park 行腿,上游逐字声明两者「语义同族、判据不同源」,
83
+ 在场性可以不一致);`runStream.ts` / `subagentContentStore.ts` 两处互相矛盾的 wire 事实注释
84
+ 按 sdk `events.d.ts` 直证订正(`EventIdentity` 上**没有** `taskId`)。
85
+
86
+ - **#265 决策卡死墙病族的三端公共判定层上收**(2026-08-14;新增 4 个模块 + 34 个运行期导出,
87
+ 公面 638→672;**新增行为面,端未换装前零影响** —— 既有导出的签名与默认串一律字节不变):
88
+ - **`hitl/parkRowBirthWait.ts`(新)**:`waitForParkRowBirth` —— 「待决行还没出生」这个**第三态**的
89
+ 有界可中断重查环。`/v1/approvals` 里读不到某个 park 的行,过去被单义化成「这件事已经决了」;
90
+ 真实世界里引擎刚被推进到下一只门,而门的行要等一次真实模型调用(实测 ~3s)才落库。判决三态
91
+ `row` / `unborn` / `settled`,而 **`settled` 必须由正面证据得出**,缺席只能得 `unborn`。
92
+ 边界:`budgetMs <= 0` 恰探一次(与「不等」逐字同语义)、每一拍 probe 都带截止 signal(挂死的
93
+ 读面被真掐断而不是被放弃)、窗尽如实回 `unborn`(不是 `settled`)、全程可中断。读面/时钟/sleep
94
+ 全注入;等待原语借包内共享叶 `abortableSleep`,**不另铸第二份**。
95
+ - **`hitl/approvalDecisionNoteAudit.ts`(新)**:`readDecisionNoteAudit` / `decisionNoteAuditLine` ——
96
+ 回决备注(`decisionNote` / `noteRecorded`)的三态读面。`noteRecorded === false` **压过**正文回显
97
+ (显式 per-call 真相优先),`unknown ⇒ 整行不渲`(缺席 ≠ false),UNTRUSTED 正文控制字符折平 +
98
+ 200 字上界。`not-recorded` 的人话行必须说清**决断照旧成立** —— 它说的是「理由没落档」,不是
99
+ 「审批失败」。
100
+ - **`hitl/askParkRowRouting.ts`(新)**:ask park 重开链的四问 + 一件构造保证。
101
+ `classifyAskParkRows` 把队列读数分成三态(空表 / 多行无点名行 / 唯一行归属证不出 / 行停在非 ask
102
+ 门上 —— **全部** `unborn`,一条 `settled` 都不产,因为队列读面给得出的一切都只是缺席类证据);
103
+ `askParkRowArm` 把行路由锚在**决定量**(这行到底有没有问句)上而不是门种的名字 —— 🔴 引擎真
104
+ wire 直证:plan 批准之后铸出的 Write 审批行带的是 `gateKind:"human" · toolName:"Write"`,把
105
+ `human` 单义化成「这是提问」会让这一行被送进没有问句 payload 的臂而静默失败;
106
+ `classifyAskParkChainFailure` 把「链失败时行已不在表」按**卡有没有呈现过**分成
107
+ `decided-elsewhere` 与 `row-unborn` 两义(后者不是失败,是还没轮到);`createRowArmSingleFlight`
108
+ 把「一个待决项至多一张卡」做成构造保证(按**行键**合流,不是按请求者的 taskId)。
109
+ - **`hitl/resumeRunningCard.ts`(新)**:resume 撞上在飞 run 时那张三选卡的判定/选项集/文案。
110
+ 🔴 `resumeRunningOptions` 的**顺序即默认焦点**:零副作用那一项恒在 `options[0]`(帧里没有默认
111
+ 焦点位,一个手滑回车不许 attach 或 cancel);按供给渲(动词缺席 = 那条路整个不渲);
112
+ `resumeChoiceFromLabels` 只认逐字命中,空答/多选/自由文本一律塌缩成零动作;活性证据缺席时
113
+ 那一行**整行不渲**(绝不折成 0);没有卡可呈时输出**机读三选指路** + 专码
114
+ `RESUME_RUNNING_NO_UI_EXIT_CODE = 75`(EX_TEMPFAIL),绝不静默 0 谎报成功。
115
+ - **`adapter/activeRunSelfHeal.ts` 扩面**(既有导出零改动):
116
+ - 🔴 **分诊表补齐**(行为改动,端换装前不生效):`PLAN_REVIEW_GATE_KINDS` 新增 `dry_run_review`
117
+ (server 侧与 `plan_review` 同一个决议入口 ⇒ 同臂),`ASK_PARK_GATE_KINDS` 新增 `policy_ask`
118
+ (与 `human` 同族同入口)。漏词的代价 = 那一类门恒走结局④「不动 + 如实说」,卡不重开、会话
119
+ 继续锁着。gate kind `needs_review` 仍是**表外词**(它是 status 的同名词,不是 plan 门 kind)。
120
+ - **`running` 三选卡臂**:`offerRunningChoice` 缺席 ⇒ 本臂整个不走(退回 `not-parked` 现状行,
121
+ 与 0.29.0 字节相同);在场时 steer / cancel 按**供给**渲。steer **恰一次**(非幂等,零重试、
122
+ 绝不回退 cancel),三种 `delivery` 文案分形(`queued` 是 wire 在纠正分诊输入 ⇒ 按回执里的 park
123
+ 词重开那张门卡;未知 park 词两臂都不进),失败按 `atMostOnceFailureClass` 分成「4xx 已证没送到」与
124
+ 「送达未知」——对后者说「再发一遍」就是诱导用户把一条非幂等指令注入第二次。
125
+ - **`CLAIM_RELEASED_STATES` + `waitForClaimRelease`**:cancel 之后「那条 run 真的把会话交出来了」只由**终态
126
+ 白名单**断言(`completed`/`failed`/`blocked`/`timeout`)。旧的 `!running` 反面判据会把一条正在
127
+ 收尾、这一拍恰好读成 park 的 run 判成「已释放」⇒ 重发那条消息一头撞进还锁着的会话。读失败是
128
+ 「不知道」不是释放;只有 404 算释放;每一发探测都带截止 signal;`waitedMs` 是**真等了多久**。
129
+ - 新增 `RUNNING_STATES` / `CLAIM_HELD_STATES` / `CANCEL_RELEASE_WAIT_MS` / `readSteerDelivery` /
130
+ `readSteerReceiptStatus` / `atMostOnceFailureClass` / `RunningChoiceRequest`;
131
+ `reopenPlanReview` 放宽到同步/异步两式通吃(端的生产口要等呈现回执)。
132
+ - **门**:新增常驻门 `scripts/run-park-decision-layer-test.mjs`(146 checks;对空实现先跑红留证),
133
+ `run-selfheal-reopen-test.mjs` 扩到 219 checks(G8 段守本批的分诊表/三选卡/claim 释放/steer 三分形)。
134
+ 棘轮同批逐条登记:index 闭包 119→123、unknown 出境 218→223、已注释豁免的吞 27→29、
135
+ singleton 清单 226→231;裸定时器创建点新登记两处(`hitl/parkRowBirthWait.ts` 的每拍截止定时器 +
136
+ `adapter/activeRunSelfHeal.ts` 的 `claimProbeLease` 截止定时器 —— 都是「有界」这条承诺的执行手段;
137
+ 两边的轮询等待那一半都走共享叶 `abortableSleep`,不另铸)。
138
+ - 🔴 **二次评审处置(与 cli 源形的三处刻意分叉,端换装时按本段读)**:
139
+ - **404 不再算「claim 已释放」**:`runs.get` 的 404 在 SDK 契约上是 `not_found.run` —— 「这条 run
140
+ 不存在」与「它不是你的」共用同一个码且**刻意不可分辨**(跨租户不给存在性预言机)。拿它断言
141
+ 「已释放」就是把缺席当正面证据(本批到处在修的同一个错),而误判方向不对称:说成已释放会让
142
+ 调用方立刻重发、那条消息可能撞回还锁着的会话。现在它只是「读不到」,窗尽走诚实收口。
143
+ - `ClaimReleaseVerdict` 新增 `lastStatus`,`running-cancel-timeout` 新增 `confirmedHeld` ——
144
+ 「读到过 park 词」与「一次都没读出来」的收口文案**必须分形**:后者只许说「确认不了」。
145
+ - **中止必须传导到真正交给 probe 的那只 signal**:`AbortSignal.any` 缺席的宿主上,外部中断此前
146
+ 只赢竞速而不 abort 内部 controller,`finally` 又清掉了 deadline timer ⇒ 那一拍挂死的读面**永远**
147
+ 不会被 abort(判决说「停了」,后台请求活到进程结束)。已修 + 门内以「临时摘掉 `AbortSignal.any`」
148
+ 的语料反钉。
149
+ - **`queued` 臂的文案改走 `reopenDelivered`**:`{reopened:true, presented:false}`(宿主确知没渲)
150
+ 此前会渲成「卡已呈上,去答它吧」——一句已知为假的话。
151
+ - **行级单活闸的占位先于 `start()`、清账认身份不认键**:前者堵同步再入另起第二条链,后者堵
152
+ 「`clear()` 后同键新链被旧链落地时抹掉登记」;占位形自带**自依赖 fail-fast**(`start` 把占位
153
+ 原样交回来 ⇒ 当场拒绝,否则那只 promise 永不 settle、这一行的后续重开全合流到它 = 死锁,
154
+ 比多铸一张卡坏得多。间接环判不出来,靠调用协议:`start` 不许 join 自己的键)。
155
+ - **`ClaimReleaseVerdict.lastStatus` 只记最近一次探测**:先读到 running、此后到窗尽全是 404 的
156
+ 时序里,最新证据是「读不出来」——留着陈旧读数会让收口行断言一件它已经不知道的事。
157
+ - **「行已消失」的判决值改名 `row-gone-after-card`**(原 `decided-elsewhere`):呈现只证明卡渲出来
158
+ 过,证明不了有人做过决定。判决值只说可观察事实,宿主据此**静默收口**,不许对用户宣布成功。
159
+ - **`policy_ask` 与 `human` 同属「泛化的要人来决定」**(R3 [high]):core `ResumeOutcome` 的
160
+ `{gate:'policy_ask', …, answer?: QuestionAnswer}` 那个 `answer?` 位就是「问句门也走这个信封」的
161
+ 直证 ⇒ 它同样按**决定量**(行有没有问句)判臂,按名字硬判工具臂 = #269 那一病换个 kind 复发。
162
+ `irreversible_ask`(core 形 toolName 必填)与 `tool_approval`(按定义是工具审批)不进这张表。
163
+ - **单活闸补第二道闸**:`start` 的**同步执行窗**内再入同键 ⇒ 当场拒绝(`async () => { await
164
+ join(同键) }` 这一形正落在那个窗里)。残余:更晚的 tick 才自 join 两道闸都判不出,靠调用协议。
165
+ - **cancel 的部分失败同样分两类**(R4 [high]):此前 `runs.cancel` 的**任何**异常都收口成
166
+ 「取消失败,那条 run 还占着会话」——而连接在回执路上断掉时那一枪可能**已经被受理并在异步执行**。
167
+ 现在 4xx(明确拒)立即如实说;5xx / 连接断照走有界确认腿,**会话真被交出来就是 `running-cancelled`**
168
+ (判据锚在决定量,不锚那一枪的回执长什么样),确认不到才落 `running-cancel-failed` 且带
169
+ `delivery:'unknown'`,文案只说「可能还占着」。判据函数随之改名 `steerFailureClass` →
170
+ `atMostOnceFailureClass`(steer 与 cancel 两条至多一次腿共用同一把尺,各写一份必漂)。
171
+ - **cancel 那一枪也带中止口与截止**(R5 [high]):此前 `await cancel(taskId)` 不传 signal —— 它的
172
+ promise 不落地,后面那条有界确认腿就根本起不来,用户按 Esc 既停不掉等待也停不掉出站请求,整条
173
+ 腿零结局。现在把合流后的 signal 交到请求上,中止当拍收口(且**不再**接着跑满窗确认腿)。
174
+ - **新增 `CLAIM_HELD_STATES`**(R5 [medium]):「敢不敢说它还占着」也只能由正面词表得出。两张表
175
+ **刻意不互补** —— server 新加一个终态(如 `cancelled`)时,`lastStatus !== null` 会把它读成
176
+ 「还占着」并渲上屏,而会话早就释放了。表外非空词 ⇒ 既不算释放也不算持锁,收口成「确认不了」。
177
+ - **明确拒的 cancel 文案不再顺带断言「它还占着」**(R5 [medium]):拒收码里 404 是
178
+ unknown-run ∪ non-owner 的不可分辨并集、409 是 checkpoint 并发决定/过期的竞态,两者都可能发生在
179
+ 会话**已经释放**之后;那一句现在只说被拒的事实。
180
+ - **durable run 三个动词逐次透传 `session`**(R6 [high]):SDK 头注逐字 —— 这些 run 面是会话作用域的,
181
+ 今天缺席只让 server 记一条迁移告警,而「enforcement train will 404 absence too」。到站那天不带
182
+ session 的 `get`/`cancel`/`steer` 会整体 404:三选卡照渲(动词都在场)、按下去必然失败、cancel
183
+ 之后的释放确认也永远读不到 —— 一张按了没用的卡。新增 `ActiveRunSelfHealDeps.sessionId` 与
184
+ `DurableRunCallOpts`;宿主没给就**不置键**(`{session: undefined}` 与「没有这个键」不是一回事)。
185
+ - **等待预算入口规范化**(R6 [medium]):`budgetMs` 是从配置解析来的量,`NaN` 会让
186
+ `remaining <= 0` 恒假 ⇒ 环永不收口(每拍的截止 signal 抛错又被 catch 吞掉,退化成零延时空转),
187
+ `Infinity` 同样到不了截止条件。坏值一律读成「不等」。
188
+ - 🔴 **截止定时器改用 ref'd 自持形**(R6 门实测逼出):原先走 `AbortSignal.timeout` —— 它的内部
189
+ 定时器在 Node 上是 **unref** 的,等待窗里它可能是事件循环里唯一的活,进程会在截止到点之前直接
190
+ 退出、连那句诚实的收口行都一起蒸发(与 `abortableSleep`/重查环同一条判据)。现在自持一只普通
191
+ 定时器并把清理做成 `release()` 的义务;合并调用方 signal 也自己做,不再依赖宿主运行期有没有
192
+ `AbortSignal.any`(行为在所有宿主上逐字相同)。
193
+ - 🔴 **durable 动词一律方法形调用,保接收者**(R7 [high]):真 SDK 的 `RunsResource` 方法体走
194
+ `this.t.request(...)`,把方法从对象上摘下来单独调会当场 `TypeError` —— 后果是 steer 恒报「送达
195
+ 未知」、cancel 先失败再拿一个同样失了接收者的 `get` 轮询到预算耗尽,三选卡的两条路在生产上
196
+ **全废**(而箭头函数 mock 恰好把这一形完全遮住)。现在只用 `typeof runs?.x === 'function'` 判
197
+ 在场,真调用一律 `runs.x(...)`;交给等待器的那一发用保接收者的包装。门同批改用**依赖 `this`
198
+ 的类实例**当被测客户端,并带一条「同一批方法摘下来裸调确实会炸」的正控。
199
+ - **`parked_for_wake` 补重复执行披露**(R8,采纳一半):那条 parked 副本**没有被丢弃** —— 将来
200
+ 任何人 wake 这个会话,同一条指令会再跑一次;带写/删/外部副作用的指令上这是真代价,现在说在
201
+ 同一句里。🔵 驳回的另一半(「干脆别建议重发」):本客户端既没有 wake 也没有撤销 parked 消息的
202
+ 接线,不建议重发 = 用户手上一条路都没有,那正是这条腿要消灭的死墙;取舍 = 给唯一真出路 +
203
+ 把代价说全。
204
+ - 🔵 **驳回两条**:①「queued 臂用 `receipt.status` 猜臂违反 kind 优先」——`SteerReceipt` 上**没有
205
+ gate kind 位**(只有 taskId/status/messageId/priority/note),这就是那条成文的「kind 缺席 ⇒
206
+ status 表回退」路径本身;且 SDK 头注把 `queued` 的 status 写死为 `"suspended"` OR
207
+ `"needs_review"` 并分别绑在两个 park 族上,回退表与它逐词同形(第三轮复核确认驳回成立)。
208
+ ②「`row-gone-after-card` 应退回 `row-unborn` 继续重查」—— 代价对称且方向更差:那会把**最常见的
209
+ 良性时序**(决断真落地 → 座位释放 → 队列从此空表)变成「白等满一个窗再说一句『没能重开那张卡』」,
210
+ 一句同样为假、而且**每次成功决断都发生**的话;本格的代价只是这一轮不重开(下一条消息自愈腿重跑,
211
+ 可收敛)。两条理由都已钉进码注防重复误判。
212
+ - **端接线指引**(壳/web/desktop 换装时读这一段):判定层全部经 deps 注入,**没有任何模块级状态** ——
213
+ `waitForParkRowBirth({probe, budgetMs, intervalMs, signal})` 的 `probe` 必须把 signal 透传进真实
214
+ 读面(否则「有界」退化成口号);`classifyAskParkRows(rows, taskId, {isOwned})` 的归属腿多会话宿主
215
+ 必须注入会话粒度的 `isOwned`(进程级缺省腿只在默认会话键下启用);`createRowArmSingleFlight()`
216
+ 的实例由端持有(一会话一只,单例化反而会让两个会话互相合流);`offerRunningChoice` 与
217
+ `deniedMessage` 由端在装配点接,缺席即整臂不走。
218
+
219
+ - **[C170]/[C175] 三批**(2026-08-13,commit `835d33f`;sdk devDep ^6.16.0→^6.17.2;peer floor
220
+ `>=6.16.0`→`>=6.17.2`;README/INTEGRATION-CLIENTS/lockfile 三面同批跟):
221
+ - 🔴 **peer floor 抬升的真实理由是类型面**(#252 直证订正,2026-08-14):本批的 durable 卡两展示键
222
+ 读的是 `PendingCheckpoint.ruleSuggestions` 与 `PendingCheckpoint.governanceForced`,这两位在
223
+ sdk **6.16.0 的 `.d.ts` 上根本不存在**(实拉 `npm pack @sema-agent/sdk@6.16.0` 直证:
224
+ `dist/types.d.ts` 全文 `ruleSuggestions` 零命中,`governanceForced` 只长在 `PendingGateMaterial`
225
+ 上、不在 `PendingCheckpoint`)⇒ 低于 6.17.2 的宿主**编译期**红。
226
+ ⚠️ 同批订正的另一半:本批**没有**新增任何值级 sdk import(值级面仍是 `AgentClient` /
227
+ `SseIdleError` / `probeHealth` / `APIError` / `TaskStopConflictError` 五个符号,零增减),
228
+ 6.16.0 也早已带 `same-origin-relay` 守卫、也已导出 `PendingCheckpoint` / `RuleSuggestion` ——
229
+ 原先写进 `docs/INTEGRATION-CLIENTS.md` §8-A 的「不抬会在值级 import 上炸」是**假的**,已改正。
230
+ 见证卫生那一半仍成立(见证门 C-CC-3 实证仓内已无 6.16.0 实装物料,没人跑过的地板是空头承诺)。
231
+ - **durable 卡两展示键**([C170] 答问②认领半场):`surfaceFsApprovalAndDecide` 的「行 → 卡」
232
+ 重铸补 stamp `governanceForced`(严格 `true` 收窄,缺席纪律与活卡腿同款)与
233
+ `ApprovalCardRequest.ruleSuggestionsReadOnly`(**新键**,行上候选过 `readRuleSuggestions`
234
+ 同一把合形窄化)。🔴 只读键刻意不落既有 `ruleSuggestions` 位:durable `/decide` 体无规则位,
235
+ 唯一兑付口=同副本活卡腿 `respond.persistRule`,落可兑付位=按下去规则不落地的假 affordance
236
+ (SDK 6.17.2 行 JSDoc 红线)。既有卡口不读新键 ⇒ 卡形字节不变。
237
+ - **same-origin-relay 形**([C175] 浏览器宿主结构性不可构造案):`EngineWireClientConfig.token`
238
+ 与 `LiveWorkflowConfig.authToken` 放宽 `| { mode: 'same-origin-relay' }`(SDK 浏览器 `window`
239
+ 守卫的豁免形,该形下 `baseUrl` 允许相对路径;⚠️ #252 订正:SDK 另有 `allowBrowser: true` 逃生舱,
240
+ 只是 `EngineWireClientConfig` 不暴露它、本包也从不设 —— 「唯一豁免形」的说法只在**本包这条构造
241
+ 路径上**成立)。relay 形只能显式传入、直传 SDK,
242
+ `resolveWireAuth` 三态解析一字不动;串形语义与 0.28.x 字节不变。唯一构造点纪律不破
243
+ (relay 形也走 `makeEngineWireClient`)。
244
+ 🔴 **已知边界(#252 补记)**:放宽只到这两个入参面 —— `EngineWireTarget.token` 仍是 `string`,
245
+ 而 `installEngineWireTarget()` 是非 Node 宿主**唯一**的装配入口,于是经 `engineWireTarget()`
246
+ 取址的 **10 处**构造点在同源反代部署下没有合法凭证形可传。失效形**逐点不同**(A 响亮但用户
247
+ 可见性取决于通知队列口装没装 = `decidePlanReview` / B 结构化 reason = steer 与 taskStop /
248
+ C 无条件静默 = 其余七处),但构造失败一律被 `makeEngineWireClient` 的 `catch { return null }`
249
+ 吞成 null、**从不抛异常**。台账 = `docs/INTEGRATION-CLIENTS.md` §5d(两张矩阵)+ 缺口 **P-28**。
250
+ - **SDK 行形 type 再导出**([C173] 尾注兑现):`index.ts` 新增
251
+ `export type { PendingCheckpoint, RuleSuggestion } from '@sema-agent/sdk'`——三端从包名下
252
+ 取形,不再各自 import sdk(type-only,零运行时导出,不进 public-export 运行期基线)。
253
+ - 门:新增常驻套 `scripts/run-durable-card-display-keys-test.mjs`(7 checks,含反假
254
+ affordance 常驻钉);pure 套 ⑯ 段 +4 checks(relay 形构造合法/出站零 Authorization 头/
255
+ 三态不变反钉)。
256
+
257
+ - **#158 移交①② + P-26 正向铸口**(2026-08-12,commits `c8ffa1b` / `992533e` / `3395f08`;
258
+ 🔴 此前 CHANGELOG **整批零记账**,#252 补记 —— 三端的提货对表源里少了三个新公面导出与两处行为变更):
259
+ - **新公面导出 3 个**(导出基线 635 → 638):
260
+ `toolPermissionRequestId(domain, id)`(`src/seatContract.ts`)—— P-26 的**正向铸口**:往返性成
261
+ 契约(铸出的键必被 `toolPermissionRequestIdDomain` 认回同域),三类坏入参 fail-loud(未登记域 /
262
+ 空串与非串 id / id 自带域前缀的双前缀键),**绝不静默吐坏键**;
263
+ `readAsyncLaunchedAgentReceipt(structured)`(`src/toolResult.ts`)—— `type:"agent"` 的
264
+ `async_launched` **启动回执**的 typed 读口(只带身份,不带可渲染性;分臂判据是 `status` 不是
265
+ 「哪些键在场」);
266
+ `registerSubagentContentAlias(parentToolCallId, taskId)`(`src/subagentContentStore.ts`)——
267
+ bg 子代内容归账 alias。
268
+ - 🔴 **行为变更①(C1 子代 `tool_end.output`,`src/adapter/runStream.ts`)**:此前是
269
+ `typeof sub.output === 'string' ? sub.output : undefined`,而 §E1 的 wire `output` 是**非均匀**的
270
+ (`string | (TextContent|ImageContent)[]`)—— 块数组形经这条臂进内容账本**恒空**(子代查看态
271
+ 卡上有工具、结果栏永远空白)。改走 store 自己那个唯一字符串化口 `coerceOutput`(与 tail 腿同
272
+ 一份:非串 ⇒ `JSON.stringify`,序列化失败退 `String(v)`)。
273
+ **可观察差异** = 块数组形的结果栏从「恒空」变成有值。
274
+ ⚠️ **本条已被同段的「0.30.0 发包扫描清账批」再修一次**(同为未发内容,端只会看到最终形):
275
+ 那一批把块数组这一支从 `JSON.stringify` 改成与主 lane 逐字同语义的展平 —— 所以 0.30.0 装机后
276
+ 看到的是**正文**,不是这里写的 JSON 串。两条记在一起是为了让「这一位在 0.29.0→0.30.0 之间
277
+ 动过两次」有据可查。
278
+ - 🔴 **行为变更②(`structuredToToolUseResult` 的 `case 'agent'`,`src/toolResult.ts`)**:身份两位
279
+ 改走 `readAsyncLaunchedAgentReceipt`。`task_id: ''`(空串)**修前过关**并铸出 `agentId: ''` 的
280
+ 富卡 + `registerOutstandingBgTask('')`;**修后**整体判 null、回落模型面 text 路径。
281
+ **可观察差异** = 空 `task_id` 的启动回执不再产生一张身份为空串的富卡。
282
+ - 门/基线:pure 套 B2/B5 两段 +5 段断言并抬地板;typeshape `unknownExport` 基线同批跟。
283
+ - 文档:收 web [C166]④⑤⑦ 三条建议进 `docs/INTEGRATION-CLIENTS.md`(§8 A+ 禁直连门枚举器盲区
284
+ 两形 / CHANGELOG ⇄ 接入档互链);修 `992533e` 漏刷的公面计数漂移(635→638,档内五处)。
285
+
286
+ - **类型/wire 超集台账立册**(2026-08-13,commit `e88bd11`;**零行为面、零公面变更**):
287
+ `docs/type-superset.json` 16 条(sema-prefix 2 / cc-counterpart 11 / parallel-surface 3)+ 新常驻套
288
+ `scripts/run-type-superset-ledger-test.mjs`(55 checks;四向对账锚 TS AST,动态铸键 fail-closed,
289
+ 证据形与 key 机械绑定,物料缺席 exit 3 绝不假 PASS)。同批 `src/hitl/toolApprovalWire.ts` 的 JSDoc
290
+ 校旧物修正:live 腿 note 通道 #229 已兑现,原「候 server 补位」句作废。
291
+
292
+ - **#252 接入文档宪法落地**(2026-08-13 建档 commit `909ec3f`,2026-08-14 清账批):
293
+ - 新增 `docs/INTEGRATION-CLIENTS.md` —— 本包对三端(cli / web / desktop)的**正式接入契约档**
294
+ (接入文档宪法 [3680]:消费上游先要详细全接入文档,文档报错可直接打回)。
295
+ - 清账批逐条修其中的失真(全部对实现直证后改):§0a 门数格手抄数字 · §3a `hitl_out_of_slice`
296
+ 成员写 1 条实为 5 条 · §3c `not_in_slice` 臂表漏 `error`(该臂静默 `none`,按原文规则会被端推断
297
+ 成留痕 `dropped`)· §4d 的 durable ack 缺口号误指 P-6(实为 P-7)· §8-B 的卡口默认键局限误指
298
+ P-4(实为 P-10)· §7f 指向一份**从未入库**的扫描文件 · §8-G 单例清单坐标写成不存在的
299
+ `scripts/singleton-manifest.json` · §8-A 把 peer 抬升理由写成「值级 import 上炸」· §1a 把 cli 树
300
+ 坐标 `src/seam/adapter/` 写成本仓坐标并手抄行数 · §5a 与 §5d 把 `notificationQueuePortMisses()`
301
+ 一类 miss 计数当成「装没装」的启动自检(它们初值就是 0,没装 + 没调用时照样是 0 ⇒ 启动断言必然
302
+ 假绿)—— 已按端口拆成 **(a) 存在性读口**(`hasApprovalCardPort(For)` / `hitlHostSurfaceFor` /
303
+ `hostSettings()` 族 / `engineWireTarget()`,这才是启动校验)与 **(b) 回归探针**(miss 计数,
304
+ 跑过真流量后仍为 0 才有意义),并把「**通知队列口没有存在性读口**」这一条单独立缺口 **P-29**;
305
+ §5a (a) 表与 §8-B checklist **逐口**钉哨兵形并由新门 ⑥ 段看守(修前 `hitlHostSurfaceFor` 被写成
306
+ `!== undefined` 而实现返回 `| null` ⇒ 那句自检**恒真**)。⑥ 覆盖零参与 `*For` 两支,
307
+ §5a 与 §8-B **分别**对源码返回型注解 —— 端照 checklist 打勾,只守 §5a 等于漏了端真正在读的那一行。
308
+ - 新增 **§5d**(`same-origin-relay` 凭证形:两个已放宽入参面 + 未放宽的装配入口 + **构造失败三档
309
+ 可观察性矩阵**——A 响亮但用户可见性取决于通知队列口装没装 / B 结构化 reason / C 无条件静默)与
310
+ 缺口 **P-28**;§3b/§5a/§8-D 把丢帧 sink 的**唯一注入位**点名到 `EmitContext.onDroppedFrame`
311
+ (`reportDroppedFrame` 是包内实现、不出公面,端 import 会拿到 `undefined`)。
312
+ - **新常驻套 `scripts/run-integration-doc-freshness-test.mjs`** —— 档的对账门,五向:
313
+ ①**计数纪律**(门数格不许手抄;仍手抄的导出计数逐个对 `public-export-baseline.json` **算出来**的值);
314
+ ②**坐标在盘且入库**(在盘 ≠ 在库 —— §7f 那条断锚只有入库方向抓得住;同批新增件走具名豁免,
315
+ 文件真入库后该豁免**自动失效**并打提示,刻意不判红:落库那一刻判红只会造一个必须靠补丁 commit
316
+ 才能清的红窗);
317
+ ③**臂表 AST 双向对账**(§3a 行 + §3c 机读围栏 ⇄ 生产 switch;提取器锚到**唯一**
318
+ `eventToSdkMessage` + 体内**唯一** `switch (ev.type)` + 无重复 case,任一不成立判工具红 ——
319
+ 否则文件里第二个同形 switch 会覆盖生产结论,档与一个没人跑的 switch 对上 = 假绿);
320
+ ④**已发段冻结账**(append-only,**每个发过 npm 的版本各一行并逐版核**,不是只钉当前那一版;
321
+ 账**不能自证**,三层外证:每条哈希由 `git show <releasedAt>:CHANGELOG.md` **自铸复核**(堵「改旧段 +
322
+ 顺手改账上的串」)· `releasedAt` 由 **npm registry 的 `gitHead`** 认证(堵「两个提交把旧版本指到
323
+ 新写的 commit」)· **应冻结的版本集合**由 registry 的 versions 列表按建档下限过滤后与账**等值**
324
+ 比对(堵「同时删掉旧账行与旧段 —— 删完之后所有仓内集合都不再问它」)。npm 失败**分三态**:
325
+ 只有连接层不可达才降级,**E404 / E401·E403 / 空或畸形 `gitHead` 一律红**(registry 明确说了话却被
326
+ 当成「离线」跳过 = 给「登记一个不存在的版本」开免检路);被查询的包名在门里钉死,不跟
327
+ `package.json` 的 `name` 走。三反向:自称已发却不在账上 / 账上的段消失 / `package.json` version
328
+ 未冻结);
329
+ ⑤**打包面与 README 指路一致**;
330
+ ⑥**存在性读口哨兵对账**——§5a (a) 表让宿主判「装没装」的读口,档里写的判据形必须与源码**返回型
331
+ 注解**一致(三形不同源:`hasXxx()` 是 `boolean` / 卡口·HITL 面·wire 目标是 `T | null` /
332
+ `installHost` 族是 `T | undefined`;拿 `!== undefined` 判一个返回 `null` 的读口是**恒真**,
333
+ 「装了为真、没装也为真」的自检比没有自检更坏 —— 它让端撤掉自己的兜底),两向都判。
334
+ 冻结账走**两阶段协议**(发布前允许至多一行 `pending: true` 且必须恰是当前版本、免三项外证;
335
+ 发布后必须转正,否则红;🔴 registry **不可达时不许存在 pending 行** —— 它的豁免押在「尚未发布」上,
336
+ 那件事此刻证实不了,fail-closed)—— 没有它,「账 ⊆ registry」与「当前版本 ∈ 账」会在**发版转换期
337
+ 互锁**,发布提交永远跑不绿常驻门(已实测:模拟发布提交 exit 0、发布后仍挂 pending 判红)。
338
+ 版本集合与**标题解析/比较共用同一份完整 SemVer**(含 prerelease):只认 `x.y.z` 会把
339
+ `0.30.0-beta.1` 静默滤掉,而把 `## 0.29.0-rc.1` 截成 `0.29.0` 会让**稳定版账行替 prerelease 段
340
+ 背书**;npm 错误分类**任意 `E<三位数>`/HTTP 状态短语优先判红,再谈连接层**(E400/E422 这类未枚举码
341
+ 带 `network`/`timed out` 字样时一律不许搭便车降级)。物料缺席不吃整门判词:git / npm / registry
342
+ 不可用时 ②b、④a2/④a3/④a4、⑤ 各打 `SKIPPED-SECTION`,而账上写了一个**取不出来的发布 commit**
343
+ 或 registry **明确报错**是**红**不是跳过。
344
+ - **`files` 白名单收入 `CHANGELOG.md` / `docs/INTEGRATION-CLIENTS.md` / `docs/REFACTOR-LEDGER.md`**:
345
+ 随包发的 README 开篇就把宿主指向接入档、§8 checklist 又把另两件列为提货必读,而这三件此前
346
+ **一件都不进 tarball** —— 对按 npm 装包的 web/desktop 是一条死相对链接(npmjs 会把相对链接改写
347
+ 到 `repository` 字段 = 私仓,组织外 404)。
348
+
349
+ - 门与基线汇总(相对 0.29.0):常驻门 25 → 28 套(+`run-durable-card-display-keys-test.mjs`
350
+ +`run-type-superset-ledger-test.mjs` +`run-integration-doc-freshness-test.mjs`);
351
+ 公开导出基线 635 → 638(测试钩 32 不变)。
352
+
353
+ ## 0.29.0 — 2026-08-12
354
+
355
+ #244 族A 包半场:决断卡链四件上收(A-028.1~.4;证据正本 = cli 仓 `docs/boundary-census/`
356
+ top-01/top-08;登记详表 = `docs/REFACTOR-LEDGER.md` REF-CC-168~171)。全部 additive,零 BREAKING;
357
+ 壳侧换抄件(删本地 + 换 import)在后续提货批。
358
+
359
+ - **A-028.1** `adapter/activeRunSelfHeal.ts`(新):409 active-run 自愈的四路分诊树
360
+ (`attemptActiveRunSelfHeal`,`pendingGate.kind` 优先 / status 表回退,表外词保守)+ 结局文案层
361
+ (`activeRunSelfHealRow` / `activeRunBusyHeadlessRow`,端可覆盖出路句)。`runStream.ts` 的
362
+ `ActiveRunBusySignal.pendingGate` 具名化为 `ActiveRunPendingGate` 并补 `governanceForced?: true`
363
+ (严格 true 收窄)。`ReopenCardVerdict` 成文「真呈现回执」契约(可选 `presented` 位,
364
+ `false` ⇒ 重开断言降级)。
365
+ - **A-028.2** `hitl/parkOwnership.ts`(新):ask park 重开的纯判据三件 ——
366
+ `pendingRowIsOwnedByThisSession`(fail-closed 正向归属证明,own-run/会话两腿,缺省走包
367
+ `isOwnEngineRun` + `SessionPort`)、`askQuestionsFromPending`、`isHitlSafetyErrorLike`。
368
+ - **A-028.3** `hitl/gateIdentity.ts` + `hitl/armedGateRegistry.ts`(新):决断卡身份键字面
369
+ (`hitl-ask:` / callKey / `plan-review:`,外加 `hitl-frame:` 同族)单源化,`planReviewQuestionId`
370
+ 转公面;呈现台账 per-session(sessionSlot 键,多会话宿主互不串账),新增 `clearArmedGate`
371
+ 消费动词;包内三处铸口(parkResolver / toolApprovalWire ×2)改吃唯一铸口。
372
+ - **A-028.4** `hitl/planReviewWire.ts`:`reopenPlanReviewCard`(新)与 `armPlanReviewApproval`
373
+ 合成一条(题面/标签/三态判决/投递管道单源);「铸新 id vs canonical 重呈」按宿主去重语义参数化;
374
+ 决断投递口可注入;A-024.4 键粒度收口 —— 决断递交即消费呈现史,同 run 下一个 plan gate 读回首见。
375
+ - 复审轮收紧(三条 [high] 全修):①`pendingRowIsOwnedByThisSession` 补 `sessionKey` 位 ——
376
+ 非默认键且未注入 `isOwnRun` 时进程级 own-run 缺省腿整条跳过(fail-closed,跨会话不认领);
377
+ ②`reopenPlanReviewCard` 单活纪律 —— 铸新重开卡前退役上一张(注销 responder +
378
+ `question_complete` 撤帧,同一 gate 至多一张能投递决断的卡);③`ReopenPlanReviewOpts.sessionKey`
379
+ 全链 `*For` 路由(overlay/帧/台账;缺省 DEFAULT_SESSION_KEY,cli 零参装配不动;
380
+ ⚠️ 限定=reopen 侧——`armPlanReviewApproval` 的 arm 臂仍默认会话装配,非默认键会话对同一
381
+ taskId 混用 arm 臂+`mintFreshQuestionId:false` 重呈臂的呈现史消费局限见该 JSDoc,候跟进票)。
382
+
383
+ 已知局限(0.29.0 发包扫描门,三条 low,均零行为面/零消费方现状,成文候跟进):
384
+ canonical 重呈短路臂沿用 arm responder ⇒ `deliverDecision` 注入口不生效(JSDoc 成文例外+
385
+ debug 留痕);`armPlanReviewApproval` 无 sessionKey 位(跟进票=补位或重呈臂按注册键清账)。
386
+ - 复审二轮:`unregisterLocalQuestionResponder`(新导出)—— fresh 重开把 canonical(arm)
387
+ responder 一并退役(撤帧≠出表,在途迟到作答的双决断残口闭合);`armPlanReviewApproval`
388
+ 对偶退役名下在场的 fresh 重开卡(durable 重放 re-arm 不复发双活)。默认键下 own-run 进程级
389
+ 证据的会话代际语义判定为出货形并成文进 `ParkOwnershipDeps` JSDoc(不改行为)。
390
+ - **#229 respond-note 半场**(2026-08-12 二批,sdk devDep/peer 抬 ^6.16.0):live 帧腿 deny 归因经
391
+ respond 的 `note` 位上 wire(与 durable `AskDecisionBody.note` 同词同源同一列 `decision_note`;
392
+ 此前 0.28.0 记账「候 server」,server 7.15.0 已到货)。两道前置门:能力位
393
+ `ToolApprovalFrameLaneOpts.approvalDecisionNoteCapable`(SDK 6.16 成文「位缺席就别发」)+ 2048
394
+ 上限(server 超限响亮 400 连决断一起拒 ⇒ 超限整条不发,诚实缺席优先于静默截断);ack 的
395
+ `noteRecorded` 窄化进 `readToolApprovalRespondAck`(缺席 ≠ false),note 发而未落行 debug 留痕。
396
+ pure 门 B7 +10 checks(floor 248→258)。
397
+ - 门:新增常驻套 `scripts/run-selfheal-reopen-test.mjs`(G1~G7,168 checks);25/25 全绿。配套基线:
398
+ 导出基线 606→635(+测试钩 32)、portability index 闭包 115→119、typeshape unknownExport
399
+ 211→217、singleton-manifest +5(high 87→88,activeReopenResponders 带理由)、failloud 豁免
400
+ 棘轮 26→27。
package/README.md CHANGED
@@ -7,6 +7,18 @@ that each UI is left with rendering and input handling only.
7
7
 
8
8
  Renamed from **`@sema-agent/wire-cc-adapter`** (0.1.x, deprecated — see *Migration* below).
9
9
 
10
+ > **Integrating a host?** Read **[`docs/INTEGRATION-CLIENTS.md`](docs/INTEGRATION-CLIENTS.md)** first —
11
+ > the formal integration contract for the three consumer hosts (TUI / web / desktop): export-surface map,
12
+ > the event-projection contract, **the ack-consumption obligations** (which receipt keys may be absent and
13
+ > what absence means), capability-gate duties, the multi-session `sessionKey` contract, the open-gap ledger,
14
+ > and a new-host checklist. Every section carries an implementation anchor so you can audit it against the
15
+ > source — if the doc is wrong, that is a bug in the doc, so report it.
16
+ >
17
+ > That file ships **inside the tarball**, together with `CHANGELOG.md` and `docs/REFACTOR-LEDGER.md` (the two
18
+ > the checklist names as required reading before an upgrade), so the link above resolves for someone who
19
+ > installed the package rather than being rewritten onto a repository they cannot read.
20
+ > `scripts/run-integration-doc-freshness-test.mjs` checks that against the real `npm pack` manifest.
21
+
10
22
  ## Design axioms
11
23
 
12
24
  1. **Every client copies the CC UI/UX — but no CC shape reaches the wire.** The CC vocabulary
@@ -23,7 +35,7 @@ Renamed from **`@sema-agent/wire-cc-adapter`** (0.1.x, deprecated — see *Migra
23
35
 
24
36
  ## Scope
25
37
 
26
- **Version:** 0.28.0
38
+ **Version:** 0.30.0
27
39
 
28
40
  - **Today** — the adapter seam, the whole `adapt()` pipeline (all 14 A-layer arms plus the
29
41
  B/D/E tool-card layers), the notification/caps/model families, the adapter kernel (stream driver
@@ -53,7 +65,7 @@ Renamed from **`@sema-agent/wire-cc-adapter`** (0.1.x, deprecated — see *Migra
53
65
  `SseIdleError`, `probeHealth`, `APIError` and `TaskStopConflictError` are imported as values in
54
66
  five modules, and the browser bundle really bundles the SDK through (the portability guard would
55
67
  exit 3 rather than quietly mark it external).
56
- - The declared floor is `>=6.14.0`, and it is *witnessed*: the guard checks that an actually
68
+ - The declared floor is `>=6.17.2`, and it is *witnessed*: the guard checks that an actually
57
69
  installed SDK at that line still exports every value-level symbol this package imports and still
58
70
  declares `TaskStats.costMicroUsd` (the key `costOrNull` reads). A floor nobody ever ran is a
59
71
  promise, not a contract.
@@ -236,6 +248,11 @@ public-surface guard checks that last one).
236
248
  | `scripts/run-client-core-singleton-test.mjs` | Module-level singletons ⇄ `docs/refactor/p1-scan/singleton-manifest.json`, **both directions**: an unregistered singleton is red (registering it forces someone to answer "what if this got duplicated"), a stale entry is red, and the `dupRisk: high` count only goes down |
237
249
  | `scripts/run-catalog-loader-gates-test.mjs` | The model-catalog candidate chain (`loadCatalogWithSources`) and the provider device-code seam: offline ⇒ `bundled` with an honest `online.reason`, a good source ⇒ `online` plus a cache write, a second offline run ⇒ `cacheHit`; the three hostile source shapes (malformed JSON, `schemaVersion: 99`, off-domain `http`) each fall through to the bundled table, and an off-allowlist target is **never dialled** — including a `302` to another host, proven by a real loopback server's hit counter staying at zero; a one-byte edit to `catalog.sha256` drops that source while an unavailable sidecar only warns; and the device-code poller's `pending → ok` / `expired` arms run against a real loopback HTTP server with an injected clock |
238
250
  | `scripts/run-abortable-sleep-test.mjs` | The shared `abortableSleep(ms, signal)` leaf (consumed by `workflowClient.ts` and `agentSession/backgroundView.ts`'s poll backoff): normal timeout resolution, immediate wake-up on `abort` mid-wait, `clearTimeout` really firing on that path, and a post-resolve late abort staying a no-op |
251
+ | `scripts/run-durable-card-display-keys-test.mjs` | The durable approval row's two display keys survive the row→card recast in `surfaceFsApprovalAndDecide`: `governanceForced` stamps on strict `true` only (absence is "no evidence", never `false`), `ruleSuggestions` passes through the same shape-narrowing reader as the live-frame leg and lands on the **read-only** card key — plus a standing pin that the durable leg never stamps the redeemable `ruleSuggestions` card position (the `/decide` body has no rule slot; offering a "don't ask again" option there would be an affordance nothing can honour) |
252
+ | `scripts/run-integration-doc-freshness-test.mjs` | The **integration contract** (`docs/INTEGRATION-CLIENTS.md`) and the **changelog** (`CHANGELOG.md`) checked against the code, because a document with no guard rots — this one had a whole nest of drift found on it within a day of being written. Five directions, each a claim a machine can actually evaluate. (1) *Counting discipline*: the version-anchor row for the guard count may no longer carry a hand-copied number at all — it changes every time a guard is added, and writing it down is planting a timer; the export counts that are still hand-copied (the surface total, the test-hook count, the sentence describing the surface's internal composition, the sum of the sixteen domain rows, and the three sub-counts) are each compared against a value **derived** from `public-export-baseline.json`, which is the drift a human reviewer caught last time. (2) *Coordinates alive*: every `src/` `scripts/` `docs/` path the doc quotes must be on disk **and tracked by git** — on disk is not in the repo, and a doc that points readers at a file living only in its author's working tree sends every clone to nothing. A file landing in the same commit takes a named carve-out that **stops applying** the moment the file is really tracked (it can no longer let anything through, and the guard prints a line asking for it to be deleted) — deliberately not a red, since turning red on the very commit that lands the file would just manufacture a break that only a follow-up commit could clear. (3) *Arm tables*: the `hitl_out_of_slice` row and the `not_in_slice` fenced list must equal, name for name and in **both** directions, the case labels that really fall into those two buckets — read through the **TypeScript AST**, since which bucket an arm lands in is decided by the argument to `nothing(...)` and by nothing a comment says. The extractor is anchored to the one production projector: exactly one function named `eventToSdkMessage`, exactly one `switch (ev.type)` inside it, and no repeated case label — anything else is a broken anchor rather than a verdict, because a second same-shaped switch elsewhere in the file would otherwise overwrite the real one's conclusions and leave the doc agreeing with a switch nobody runs. The list is delimited by a machine-readable fence rather than by section headings, because the same section also names the terminal arms as a counter-example and prose boundaries cannot tell a member from a foil. (4) *Released sections are frozen*: an **append-only ledger** carries every version ever published — its number, the commit it was published from, and the sha256 of its section — and each one is checked, not just the current release, since pinning only the latest would set every earlier version free the moment the next one ships. The ledger cannot vouch for itself either: each recorded hash is **re-derived from that release commit** through git, so editing an old section and its constant together no longer passes — and the commit the row names is in turn checked against the `gitHead` npm recorded at publish time, which is the one value this repository cannot rewrite, so pointing an old version at a freshly written commit does not pass either. The *set* of versions that must be frozen comes from the registry too, so deleting an old row together with its section — which would otherwise remove that version from every set the guard looks at — is red rather than invisible. A failed registry call is classified rather than swallowed, and the classification consults the registry's own status code *before* it considers connection-level symptoms, so an auth refusal whose body happens to mention the network is still red rather than a skip. The version set is compared as full SemVer including prereleases — matching only `x.y.z` would silently drop a published `0.30.0-beta.1` and reopen the very hole this direction closes — and section headings are matched on a whole-version boundary so a stable release cannot bind itself to the release-candidate section sitting above it. Publishing itself is a two-phase protocol rather than a paradox: before a release, exactly one row may be marked pending and must name the current `package.json` version, exempt from the checks whose inputs do not exist yet; once the registry has that version the row must be promoted, so the temporary state cannot survive its own release. And because the pending exemption rests entirely on "this version is not out yet," it is refused outright when the registry cannot be reached to confirm that — an unverifiable premise is not a licence. Three reverse directions close the rest: a section claiming to be released but absent from the ledger, a ledger entry whose section has vanished, and a `package.json` version that was never frozen. Publishing appends a row; it never rewrites one. (6) *Sentinels*: the readers §5a hands hosts for "is this port installed" are checked against what the source actually declares it returns — `hasXxx()` is a `boolean`, the card port / HITL surface / wire target return `T | null`, the `installHost` family returns `T | undefined`. Testing a `null`-returning reader for `!== undefined` is *always true*, and a self-check that passes whether or not the port is installed is worse than none, because hosts retire their own fallback on the strength of it. Both directions are red: an implementation that changes its sentinel without the doc following, and a doc that names the wrong one. The roster covers the zero-argument readers and their `*For` variants alike — a multi-session host reads the variants, so leaving them off would let exactly the surface desktop depends on drift unwatched — and the §5a table and the §8-B checklist line are each checked against the source, because hosts tick the checklist, and a guard that only watches the prose table misses the line people actually follow. (5) *Packaging*: the README ships with the package and opens by pointing hosts at the integration doc, and the checklist names two more files as required reading before an upgrade — all three must really appear in the `npm pack` manifest, or an npm consumer follows a relative link that npmjs rewrites onto a private repository. Missing tooling never takes the whole verdict down with it: when git, npm or the registry is unreachable those legs print the `SKIPPED-SECTION` marker and the rest still judges, while a release commit the ledger names but git cannot resolve is red rather than skipped. The guard says in its own header what it does **not** do: it judges counts, coordinates, arm sets, released bytes and the packing list — whether a sentence is *right* is still for review and for the hosts to report |
253
+ | `scripts/run-type-superset-ledger-test.mjs` | The type/wire **superset ledger** (`docs/type-superset.json`): positions this package adds on top of a CC-shaped contract, each carrying the evidence for what CC's own type surface does or does not have there. Completeness is deliberately uneven and the ledger says so. The `_sema_*` private-key class is checked in **both** directions (a key in the source that never entered the ledger is red, naming key and file; a ledger row whose key left the source is red) — but only for keys written as literals, which is the convention the ledger mandates. A key assembled by string arithmetic is beyond what any static rule can enumerate, so the guard fails closed on every shape it *can* decide (a bare `_sema_` prefix is red wherever it appears, save one pinned guard site) and leaves the rest as a convention violation for review to catch, rather than claiming a completeness it does not have. The two hand-surveyed classes are only checked for coordinate and evidence integrity, never discovered. Both directions read the source through the **TypeScript AST**, not a text scan, and they read two different sets out of it. A *key site* is an identifier, or a string whose whole value is the key — so `'_sema_decision-v2'` is carried whole rather than truncated at the first non-identifier character into some *other* key that happens to be registered. A *mention* is the key appearing inside a longer string, which is prose, not usage. The staleness direction counts key sites only: a comment or a doc sentence left behind after the last real mint site is deleted must not keep the row alive (mutation-proven — with both the comment and the prose string untouched, removing the one real site turns the guard red). And because a prefix can be concatenated or interpolated into a key no static set will ever see, the bare `_sema_` literal is refused outright rather than traced: every occurrence is red except the single inline `startsWith` guard the sanitizer needs, because the set of expressions a bare prefix can travel through on its way to a concatenation is open-ended and enumerating it is always one form behind. Every row's `host` must still resolve, with the key being a real **member of that declaration** rather than a string occurring somewhere in the same file — `governanceForced`/`delegation` each live on two different shapes in one file, and a member commented out is a member deleted, which a text-shaped check happily reads as still present. And the direction worth the most: each machine-form `ccAbsenceEvidence` is re-derived from the row's own `key` — the ledger's recorded string must match that derivation verbatim, since a row quietly witnessing `\bnever_present\b` is green forever while watching nothing (mutation-proven: the same edit passes the unbound form and is caught by the bound one) — and the check runs against the names the installed `@sema-agent/agent-types` `.d.ts` set actually declares, parsed with the TypeScript AST rather than grepped, so a name CC merely mentions in a comment cannot force the row into the manual escape hatch and thereby retire the very witness that was supposed to fire the day CC declares that name for real. That escape hatch is gated by an allowlist living **in the guard**, not the ledger, so claiming it costs a reviewed diff. Missing material never reads as a pass, and the verdict splits by *why* it is missing: no TypeScript parser skips the suite before it starts; a missing `agent-types` still runs and prints the first three directions, then exits **1** when `package.json` declares the mirror but it is not installed — a broken install must not retire the repository's only "the day CC declares this name" alarm, and reporting it as a skip would leave "never evaluated" and "evaluated, no drift" indistinguishable to the runner — and exits 3 only when nothing declares the mirror at all, which is the one case where the direction genuinely does not apply. Either way a run that evaluated no witness is never counted as one that did. When the mirror *is* present its **installed version** is witnessed too (the two declared floors must agree with each other and the installed copy must meet them), since four preflight probes are satisfied by an arbitrarily stale mirror — they prove the extractor speaks, not that it is current. Every direction carries a positive control — known-present CC symbols, a comment-only sample proving the extractor distinguishes declaration from mention, and synthetic corpora fed through the **same** discriminator function the real verdict uses, so a verdict quietly rewritten to return nothing takes its own control down with it |
254
+ | `scripts/run-park-decision-layer-test.mjs` | The decision layer behind the "stuck behind a card" family, shared by every client. A pending row that is **not in the queue** is three states, not one: a bounded, interruptible re-probe loop distinguishes *a decidable row*, *not born yet* (no positive evidence that anything settled — an empty queue proves nothing) and *settled elsewhere*, always probes at least once so a zero budget keeps the pre-fix semantics verbatim, cuts a hung read face off at the window rather than only noticing afterwards, and reports the honest failure when the window is spent instead of inventing a decision. The decision-note reader is likewise three-state: an explicit `noteRecorded: false` outranks an echoed note body, absence renders **no line at all**, and untrusted note text is flattened and bounded before it ever reaches a renderer. Row routing anchors on the deciding quantity — a row carrying `gateKind: "human"` with `toolName: "Write"` is a tool gate, because `human` is the engine's *generic* "someone must decide", not a synonym for a question — and the queue scan refuses to surface a row it cannot positively prove belongs to this session. A chain that fails after the row vanished is split by whether a card was ever presented: decided-elsewhere, or not-its-turn-yet. A row-level single-flight makes "at most one card per pending item" structural rather than incidental. The resume three-way card pins the option **order** (the zero-effect choice sits at index 0, because the frame carries no default-focus field and a stray Enter must not attach or cancel), renders only options the wired verbs can honour, collapses every ambiguous answer to zero action, omits the liveness line entirely when the engine gave no evidence, and — when there is no card lane at all — prints three real routes and exits on a dedicated code rather than reporting success |
255
+ | `scripts/run-selfheal-reopen-test.mjs` | The 409 active-run self-heal decision chain: `governanceForced` narrows on strict `true` only; triage prefers the wire's `pendingGate.kind` and falls back to the status table (an off-table kind is never guessed into a card arm — hands-off plus the honest wording); a first-sight card makes zero closed/reopened claims and a host presentation receipt of `presented: false` demotes the outcome to reopen-failed; park-row ownership is a fail-closed positive proof (own-run ledger or session id — unprovable is not owned); the three gate-identity key literals live in exactly one mint (`hitl/gateIdentity.ts`, AST string-token scan); the armed-gate presentation ledger is per-session; and the `plan_review` reopen arm shares the arm arm's card body, three-state verdict and delivery pipe, consuming the presentation history once a decision is delivered. The same chain also carries the `running` three-way card: both plan-family gate kinds route to the plan arm and all four ask-family kinds to the ask arm (an off-table kind still never gets guessed into either); the card is offered only for verbs that can actually be honoured and a missing presenter means zero action rather than a silent cancel; a steer is sent **exactly once** with its three delivery outcomes worded apart (a `queued` receipt is the wire correcting the triage input, so the named park word decides which card gets reopened, and an unrecognised park word drives neither arm), and a steer failure is split into *provably not delivered* (4xx) and *delivery unknown*, because telling a user to resend a non-idempotent instruction that may already have landed is how duplicates get made. After a user-chosen cancel, "the session is free" is asserted only from a whitelist of terminal states — park states hold the claim, an unrecognised state word is not a release, a failed read is *unknown* rather than a release, and only a 404 counts as one — and the honest timeout line quotes how long it really waited |
239
256
 
240
257
  Each suite carries a floor that only moves up — a refactor that stops executing a group of
241
258
  assertions is a failure, not a quieter pass. Guards anchor on the **installed artefact's content**
@@ -3,7 +3,8 @@ import { mapBrainStatusToRetry } from '../retryStatus.js';
3
3
  import { steeringInjectedToAttachments } from '../steering.js';
4
4
  import { projectDiagnosticsFrame } from '../diagnostics.js';
5
5
  import { recordEngineToolLabel } from '../engineToolLabelStore.js';
6
- import { registerSubagentAlias } from '../subagentContentStore.js';
6
+ import { registerSubagentAlias, registerSubagentContentAlias } from '../subagentContentStore.js';
7
+ import { readAsyncLaunchedAgentReceipt } from '../toolResult.js';
7
8
  import { isWorkflowAgentTaskId, recordWorkflowAgentTaskId } from '../workflow.js';
8
9
  import { chrome, MAIN, transcript } from './ids.js';
9
10
  import { CANCEL_MESSAGE, decisionOf, flattenWireOutput, REJECT_MESSAGE, sanitizeToolUseBlock, shortTaskLabel, TASK_TOOL_NAMES, WORKFLOW_TOOL_NAMES, } from './wireShapes.js';
@@ -276,6 +277,28 @@ const toolEndResultArm = function* (m, { ctx, idOf, cards, panel }) {
276
277
  const callId = typeof m.toolCallId === 'string' ? m.toolCallId : undefined;
277
278
  // 🔴 时序留钉(矩阵 §2.1①):同 assistant 臂 —— label 登记恒在任何 yield 之前。
278
279
  recordEngineToolLabel(callId, m.label);
280
+ // ── #158② bg 子代 alias(2026-08-12)────────────────────────────────────────────────────────
281
+ // `async_launched` 回执是**唯一**同时握着 {本卡 callId, 引擎 taskId} 这一对的帧,而内容帧
282
+ // (EventIdentity)只带 parentToolCallId = 本卡 callId。此前 `registerSubagentAlias` 的唯一喂点
283
+ // 是 task_progress tick,而 bg(async)子代**恒零 tick** ⇒ C1 分流进来的内容永远停在
284
+ // parentToolCallId 键下,查看态按引擎 taskId 一律取不到(详情页拿不到 per-tool 历史)。
285
+ // 🔴 位置两条硬理由:① 恒在任何 yield 之前(同 label 登记,矩阵 §2.1①);② 在下面「无对应
286
+ // 开卡即丢」的早退**之前** —— alias 是 store 的事实,不该由「这张卡还在不在台账里」决定
287
+ // (durable 重放/乱序时开卡可能已出栈,而内容账本照样需要这条键)。
288
+ // 🔴 **只登记内容归账那一半**(`registerSubagentContentAlias`,不是 `registerSubagentAlias`;
289
+ // 复审收货定形,2026-08-12):那张表承载两件不同的事,方向与基数都不同(分家说明见 store 侧
290
+ // 该函数头注)。回执**足以**宣告「这个 callId 下的内容属于这个 taskId」(多对一,加一条只会
291
+ // 更全),但**不足以**改写单值的 `taskToParent` —— 那一位的读口 `parentToolCallIdOf` 是对外的
292
+ // C2 steer 投递目标,权威来源是 **wire 的内容帧**;而 SendMessage 复活会为同一 taskId 再发一条
293
+ // 回执、骑的是**复活那次调用**的 toolCallId,拿它盖掉真正的委派调用 = 用户插话投到错的目标
294
+ // (「复活后该按哪个 target 投」要 server 语义作证,本包无从判定 ⇒ 不猜、不写那一位)。
295
+ // 于是本喂点在 steer 面**零影响**:冲突向不存在,也就不需要「先到者持号」那类到达序启发式。
296
+ // 幂等:同对重复登记 = 同一个 set 覆盖同值 + 迁移块第二次自然空转。
297
+ if (callId !== undefined) {
298
+ const launched = readAsyncLaunchedAgentReceipt(m.structured);
299
+ if (launched !== null)
300
+ registerSubagentContentAlias(callId, launched.taskId);
301
+ }
279
302
  if (callId !== undefined) {
280
303
  // ⟨帧序耦合 4/7⟩ 关卡 settle(sweep 点 1/4)只结**已发布**的面板行:tick 没到过就没有行可结。
281
304
  yield* panel.settle(callId, m.isError === true, m.output !== undefined ? flattenWireOutput(m.output) : undefined);
@@ -68,6 +68,13 @@ export declare function estimateCjkTokens(s: string): number;
68
68
  * B4 把它从 B 层(`wireOutputToBody`)提到独立导出:A 层的 `settlePanelTasks(report)` 也要用它
69
69
  * (关卡时把子代最终报告串到面板行 end 事件上),两处必须同一份实现。
70
70
  *
71
+ * 🔴 **包内另有一份块数组展平**(0.30.0 发包扫描登记,2026-08-14):`subagentContentStore.ts` 的
72
+ * `coerceOutput`(子代查看态结果栏那一位)。它**不能** import 本文件 —— 那个 store 在 `runStream.ts`
73
+ * 内核可移植闭包(`MAX_CLOSURE_FILES`,只许降)之内且以 dep-free 立身。两份由 **pure 门 ⑨b 段的
74
+ * 等值断言**逐形对拍钉住(六形 + 逐字面值);改本函数的块数组语义 ⇒ 同批改那一份并看那条断言。
75
+ * ⚠️ 两者只在**块数组与裸串**这两形上等值:非串非数组时本函数按「转录卡是文本粒度」返回 `''`,
76
+ * 而 `coerceOutput` 走 `JSON.stringify`(它服务的是诚实缺席/原型不保的对象形),这一分叉是刻意的。
77
+ *
71
78
  * 公面。
72
79
  */
73
80
  export declare function flattenWireOutput(output: unknown): string;
@@ -68,6 +68,13 @@ export function estimateCjkTokens(s) {
68
68
  * B4 把它从 B 层(`wireOutputToBody`)提到独立导出:A 层的 `settlePanelTasks(report)` 也要用它
69
69
  * (关卡时把子代最终报告串到面板行 end 事件上),两处必须同一份实现。
70
70
  *
71
+ * 🔴 **包内另有一份块数组展平**(0.30.0 发包扫描登记,2026-08-14):`subagentContentStore.ts` 的
72
+ * `coerceOutput`(子代查看态结果栏那一位)。它**不能** import 本文件 —— 那个 store 在 `runStream.ts`
73
+ * 内核可移植闭包(`MAX_CLOSURE_FILES`,只许降)之内且以 dep-free 立身。两份由 **pure 门 ⑨b 段的
74
+ * 等值断言**逐形对拍钉住(六形 + 逐字面值);改本函数的块数组语义 ⇒ 同批改那一份并看那条断言。
75
+ * ⚠️ 两者只在**块数组与裸串**这两形上等值:非串非数组时本函数按「转录卡是文本粒度」返回 `''`,
76
+ * 而 `coerceOutput` 走 `JSON.stringify`(它服务的是诚实缺席/原型不保的对象形),这一分叉是刻意的。
77
+ *
71
78
  * 公面。
72
79
  */
73
80
  export function flattenWireOutput(output) {