@sema-agent/client-core 0.104.1 → 0.105.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 (86) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/README.md +11 -4
  3. package/dist/adapt/arms.js +14 -6
  4. package/dist/adapter/activeRunSelfHeal.d.ts +1 -0
  5. package/dist/adapter/activeRunSelfHeal.js +10 -5
  6. package/dist/adapter/downstream/eventToSdkMessage.js +4 -4
  7. package/dist/adapter/downstream/wiringManifestView.d.ts +6 -0
  8. package/dist/adapter/downstream/wiringManifestView.js +25 -0
  9. package/dist/adapter/runStream.js +5 -1
  10. package/dist/cloudConfigWireCaps.js +3 -0
  11. package/dist/compensations.js +6 -6
  12. package/dist/coreValuePorts.d.ts +3 -1
  13. package/dist/coreValuePorts.js +4 -0
  14. package/dist/displayUntrusted.d.ts +4 -2
  15. package/dist/displayUntrusted.js +45 -22
  16. package/dist/engineGenerationPort.d.ts +11 -2
  17. package/dist/engineGenerationPort.js +63 -9
  18. package/dist/engineHealthFacts.d.ts +5 -0
  19. package/dist/engineHealthFacts.js +11 -0
  20. package/dist/eventLeg.d.ts +1 -1
  21. package/dist/eventLeg.js +23 -3
  22. package/dist/generated/engineFactTables.d.ts +1 -0
  23. package/dist/generated/engineFactTables.js +1 -0
  24. package/dist/generated/sdkFactTables.d.ts +2 -0
  25. package/dist/generated/sdkFactTables.js +9 -0
  26. package/dist/headlessReconnectWire.js +31 -4
  27. package/dist/hitl/askGateWire.js +26 -2
  28. package/dist/hitl/frameRouter.d.ts +4 -0
  29. package/dist/hitl/frameRouter.js +14 -0
  30. package/dist/hitl/gateIdentity.d.ts +1 -0
  31. package/dist/hitl/gateIdentity.js +3 -0
  32. package/dist/hitl/planReviewCard.d.ts +21 -0
  33. package/dist/hitl/planReviewCard.js +229 -0
  34. package/dist/hitl/planReviewWire.d.ts +38 -13
  35. package/dist/hitl/planReviewWire.js +355 -133
  36. package/dist/hostSettingsSource.d.ts +1 -1
  37. package/dist/hostSettingsSource.js +8 -4
  38. package/dist/hostTurnSettings.d.ts +1 -1
  39. package/dist/hostTurnSettings.js +29 -10
  40. package/dist/i18n/messages.en.d.ts +50 -0
  41. package/dist/i18n/messages.en.js +50 -0
  42. package/dist/i18n/messages.zh.js +51 -0
  43. package/dist/index.d.ts +15 -3
  44. package/dist/index.js +9 -1
  45. package/dist/mcpTransportKindsCapability.d.ts +18 -0
  46. package/dist/mcpTransportKindsCapability.js +75 -0
  47. package/dist/mcpWireCaps.d.ts +4 -1
  48. package/dist/mcpWireCaps.js +24 -13
  49. package/dist/notifications.d.ts +9 -0
  50. package/dist/notifications.js +11 -2
  51. package/dist/ownKey.d.ts +4 -0
  52. package/dist/ownKey.js +8 -0
  53. package/dist/permissionModeWriteCapability.d.ts +13 -0
  54. package/dist/permissionModeWriteCapability.js +35 -0
  55. package/dist/permissionsAllowGrantCapability.d.ts +3 -0
  56. package/dist/permissionsAllowGrantCapability.js +26 -3
  57. package/dist/permissionsDeclaration.d.ts +23 -1
  58. package/dist/permissionsDeclaration.js +60 -30
  59. package/dist/planReviewCardFacts.d.ts +116 -0
  60. package/dist/planReviewCardFacts.js +469 -0
  61. package/dist/reattachWindow.d.ts +8 -1
  62. package/dist/reattachWindow.js +44 -12
  63. package/dist/runFollow.d.ts +30 -0
  64. package/dist/runFollow.js +117 -0
  65. package/dist/runLedgerSnapshot.d.ts +20 -0
  66. package/dist/runLedgerSnapshot.js +272 -0
  67. package/dist/runPermissionModeSwitch.d.ts +57 -0
  68. package/dist/runPermissionModeSwitch.js +135 -0
  69. package/dist/runReattachPolicy.d.ts +13 -5
  70. package/dist/runReattachPolicy.js +58 -23
  71. package/dist/seam.d.ts +3 -0
  72. package/dist/seam.js +1 -1
  73. package/dist/seatContract.js +1 -0
  74. package/dist/sessionMemoryErase.d.ts +1 -1
  75. package/dist/sessionMemoryErase.js +5 -0
  76. package/dist/sessionMemoryStatus.d.ts +27 -1
  77. package/dist/sessionMemoryStatus.js +117 -0
  78. package/dist/sseIdleTriage.d.ts +3 -1
  79. package/dist/sseIdleTriage.js +19 -4
  80. package/dist/subagent/engineSubagentTail.js +2 -0
  81. package/dist/subagentContentStore.d.ts +3 -0
  82. package/dist/subagentContentStore.js +10 -1
  83. package/dist/turnFailurePresentation.d.ts +5 -2
  84. package/dist/turnFailurePresentation.js +12 -4
  85. package/docs/INTEGRATION-CLIENTS.md +493 -10
  86. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -49,6 +49,96 @@
49
49
  > 挡住 ⇒ 本批把它机械化——④a0 对 `pending` 行**要求段头已是日期形**(`(未发布)` 直接红),阶段一
50
50
  > commit 漏转在发布前就红,不再靠人记。
51
51
 
52
+ ## 0.105.0(2026-10-31)
53
+
54
+ > 主题:决断正文载体档(CC-875;接入文档 **§134 DB-1 – DB-3**)—— 计划审阅卡正文的一档读法,三端同读:空格类(全角空格、不换行空格与同类)与 `\r\n` 行断不标,谚文填充 / 盲文空点 / 零宽与双向控制等格式类 / 孤 `\r` 仍标;展示出口选项 `blanks` 加值 `'fillers'`、`keepLayout` 加值 `'crlf'`(型面加宽,旧值逐字节不变)。 ‖ 「这块屏不再跟着 run X」只有一个形(CC-877;接入文档 **§134 RF-1 – RF-9**)—— 三只整轮重挂判官放弃的那一拍铸同一只事实 `RunNotFollowed`、包出同一句话;腿内排空梯尽只是交给上一层预算的一次断因(问答桥依赖口 `onBreak`);不重挂的车道用零预算;放弃不是失败判决,重试位不给「再试一次」。🔴 按次策略口型面 BREAKING(`hasRunId` → `runId?`、`give_up.reason` → `give_up.notFollowed`、`ReattachGiveUp` 退出)。 ‖ 计划审阅卡只有一份卡事实(CC-890;接入文档 **§134 PC-1 – PC-12**)—— 一张计划审阅卡问的是哪道门、要批的是哪份正文(或为什么说不出),由本包在停住那一拍算出、冻结,chrome 臂、包卡帧、卡上的选项与作答判官的读域全部从这一份派生;只有本包看见过正文的门给批准档,说不出正文的卡只给「拒绝」与「要模型重新呈上计划」。每道门一个卡实例(换门 ⇒ 撤旧卡、新卡新身份)。续开 / 重开 / 进程重启读引擎记录取卡事实(与首呈同一条取事实规则),重开卡按卡事实出选项。🔴 型面 BREAKING:决断口第二参、自带投递口第二参、结局机读位 `decision`、四只作答读口与已呈档位读口的值域都加宽。 ‖ 换钉到服务端 7.140.0 / 7.141.0 与类型包 31.0.0(开发依赖 `@sema-agent/sdk` `^31.0.0`;peer 地板 `>=23.1.0` 不动;契约包开发依赖仍 `^15.1.0`)接入文档 **§134 PK-1 – PK-12** —— 设置层 deny / ask 条目在能力位恰真时带出处层 `{ rule, layer }` · MCP 传输种类能力位与投影期过滤(🔴 型面 BREAKING:丢弃原因闭集变宽)· 记忆抹除拒档 `personal_store_unjudged`(🔴 型面 BREAKING:拒因闭集变宽)· 记忆同步停驻读数 · workflow 完成的诊断文本 · 子代车道「未执行」卡 · 权限模式写能力位读数 · 引擎租约窗 · MCP 条目 `sessionMetaKey` 两条腿承运 · `heldBy` 终态 `null` 读点核(零代码改动)。 ‖ 换代收尾只有一张表(CC-1001;接入文档 **§134 EG-1 – EG-3**)—— 那一代引擎结束之后「问引擎一句 run 行」的收尾决策表 `endedGenerationSettleStep` 换成终形:换代窗里的 401 与「问不到」同入有界等待(一次收尾一份 245 s 预算 + 8 轮顶),等不出稳定的后继而收口时手上是 401 ⇒ 新出口 `window-exhausted`(结局未知,不说凭证被拒、不补「被信号 9 杀掉」);入参坏形 ⇒ `window-exhausted`。🔴 型面 BREAKING:事实位 `awaitedNextEngine` 退,改为逐轮等待读数 `waits?`;出口词闭集 +1;端口 +可选证据位 `endedBy?`。 ‖ 同一次完成多通道到达只上屏一次 —— 任务通知的渲染去重改读服务端给的完成 id,老服务端与不带完成 id 的记录照旧按自算键(CC-909;接入文档 **§134 RL-1 – RL-3**)。 ‖ 值级端口第二十二件 `IMPORTED_SETTINGS_LAYERS`(导入器三层设置层名的冻结元组,引擎包根常量;🔴 型面 BREAKING:`CoreValuePorts` 多一只必填成员)接入文档 **§134 ISL-1**。 ‖ 在飞 run 切权限档(CC-1080;接入文档 **§134 PM-1 – PM-4**)—— 一只发射口 + 一份回执投影:能力位判官为真才发,此刻生效的档只读引擎回执的 `permissionMode.effective`,结局恒是判别值、只有切成了才带模式。
55
+
56
+ ### Changed
57
+ - **🔴 型面 BREAKING:按次策略口 `runReattachPolicy` 的入参与放弃形**(CC-877;**§134 RF-2**):*变了什么*:事件的 `hasRunId: boolean` 改为 `runId?: string`(这一轮跟的那条 run 的句柄;缺席 ⇒ 放弃成因 `no_run_id`,在场但不是非空串 ⇒ `bad_input`;事件形先判 —— 判别词不在三词里、排空臂两形不合 ⇒ `bad_input`,不因缺句柄说成 `no_run_id`;坏形出口带读得出的 run);`give_up` 不再带 `reason`,改带事实 `notFollowed: RunNotFollowed`(成因 `reattach_exhausted` / `no_run_id` / `bad_input`;`reattach_exhausted` 带 `taskId` 与这一次事件申报的断因 `lastBreak`)—— 修前的放弃词 `budget_exhausted` 改名 `reattach_exhausted`(避开 CC 终局成因里「花费上限用尽」的同名词);型 `ReattachGiveUp` 退出根入口(一张词表,不并存)。排空臂多一形 `{ cause: 'stream_draining', ladder: 'exhausted', runId? }` = 梯已在包腿里跑完:只花一次普通预算、立即重挂、排空两计数原样。*谁受影响*:直调策略口的宿主编译红一次(`hasRunId` / `.reason` 两处),改法见 §134 三端升级必读 ①。
58
+ - **按墙钟窗口机带跟丢事实与换挂进窗**(CC-877;**§134 RF-3**;窗口机此前未被任何已发布消费者装上):`reattachWindowOpen(now, tuning, runId?)` 记下窗跟的 run(`state.runId`,宿主重挂目标与事实 `taskId` 的唯一真源);`backoff` 可带 `break`(这一轮为什么没结论)与 `runId`(断掉的那条腿当时跟的 run;与窗的 run 不同 ⇒ 窗换到它、只清旧 run 上的判活确认,窗时与 `attempt` 不动);`window-exhausted` 带 `notFollowed`(真窗尽 ⇒ `reattach_exhausted` + 最后一次 `backoff` 的断因;坏状态 / 坏钟〔起点与当前读数都验〕/ 判官自抛 ⇒ `bad_input`,不再说成预算用尽;坏状态上读得出的窗 run 照带);`run-unknown` 带 `notFollowed{ cause: 'run_unknown' }`;`new-segment` 沿用 run、清最后断因。窗状态多两只可选位 `runId` / `lastBreak`。
59
+ - **🔴 行为变:无头续收放弃时登记跟丢事实,失败呈现不再对它给「再试一次」**(CC-877;**§134 RF-4 / RF-5**):`withHeadlessR1Reconnect` 放弃时照抛原错误对象(字节不变),另按对象登记事实(`runNotFollowedOf(err)` 读回):撕裂预算尽 ⇒ `reattach_exhausted`(带 run 与最后一腿的断因:排空梯尽 ⇒ `stream_draining`、续收抛传输形 ⇒ `connection_lost`、其余说不出)· run 句柄缺席 ⇒ `no_run_id` · 坏形 / 读抛 ⇒ `bad_input`(修前句柄读口抛会顶掉原错误,一并修)。`turnFailurePresentation` 遇到带登记的抛出物 ⇒ `retry: { offer: false, basis: 'not_followed' }`(压过服务端 `retriable: true`;类别 / 头句 / 补充句不变)—— 修前对一条可能还在跑的 run 给「再试一次」。
60
+ - **`TurnFailureRetry` 改判别联合;座位键 `_sema_turnFailure` 读口收第三词并拒矛盾对**(CC-877;**§134 RF-5**):`{ offer: boolean; basis: 'wire' | 'category' } | { offer: false; basis: 'not_followed' }`;`readSeatTurnFailure` 认 `basis: 'not_followed'`(只与 `offer: false` 成对),`{ offer: true, basis: 'not_followed' }` ⇒ 整只读数 `undefined`(只渲 `error` 文本)。老版本读口读到这个表外词同样整只 `undefined`,不是「可重试」。
61
+ - **`engineAliveIdleContent` 第三参(可选)**(CC-877;**§134 RF-6**):传 `runNotFollowedOf(err)` 且在场 ⇒ 合成一句(数据面英文):起因段 + 「这块屏不再跟着 run X」+ 空闲宽限建议(条件句、置末),分离态与否同一句,不再断言「引擎会把它跑完」/「关流即中止、请重发」;事实缺席 ⇒ 与修前逐字节同。
62
+ - **`markEngineAliveIdle` 写不进不抛**(CC-877;**§134 RF-6**):冻结对象 / 拒写代理上打标修前抛 `TypeError` 并顶掉原错误;自本版起写不进就不打标(空闲句不出),原错误照原样冒出。
63
+ - **🔴 行为变:计划审阅卡的批准档只给本包看见过正文的那道门**(CC-890;**§134 PC-2 / PC-4**):*变了什么*:`armPlanReviewApproval(result, …)` 按停驻结果读出的任务号与门身份(paused 因由的 `checkpointId`)取卡事实 —— 本包流入口在这一跑停在这道门那一拍认领到正文、判为可照原样显示(`shown`)⇒ 两选 / 三选卡照旧;否则(本包没看到这一跑的计划 / 结果不带门身份 / 正文有被标出的字符 / 超长 / 同一回合连呈两份 / 引擎没带正文……)⇒ **撤档卡**:只有「拒绝」与「要模型重新呈上计划」两项,卡上一句原因。*为什么*:修前卡上的批准不绑任何正文,用户可以在没看到(或看到的不是)要批的计划时按下批准。*端要做什么*:用本包 `runStream` 跟流的端零改(停驻那一拍本包已认领);自己跟流而不经 `runStream` 的端,arm 改读引擎记录取卡事实(**§134 PC-10**;客户端没有事件记录读口 ⇒ 批准档消失,直到改经本包的流入口或带上读口)。
64
+ - **🔴 行为变:每道门一个卡实例**(CC-890;**§134 PC-4**):同一任务换了门(或同门的正文换了)再 arm ⇒ 撤下旧卡(`question_complete` + 注销作答口)、新卡用新身份(`plan-review:<taskId>#reopen-<后缀>`;一跑在本进程里只有第一张卡用 `plan-review:<taskId>`);旧卡身份上迟到的作答取不到新卡的作答口。同门、同正文、作答口仍在 ⇒ 重呈同一实例(与修前同)。没有门身份的两次停驻不判同门(每次一个新实例)。
65
+ - **🔴 型面 BREAKING:作答没读成决断的两分**(CC-890;**§134 PC-6**):`planReviewDecisionFromAnswer` / `planReviewChoiceFromAnswer` / `planReviewAnswerChoiceOf` / `planReviewAnswerDecisionOf` 的非决断值从 `'dismissed'` 一个变成 `'dismissed'`(没选、没写:Esc / 空答)与 `'inadmissible'`(选了这张卡没展示的项 / 矛盾多选 / 多题)两个;决断多一臂 `ask_again`。本包的两只作答口收到 `inadmissible` ⇒ 不投递、不记「用户关卡」、同一作答口重新登记(卡仍可答),回 `{ ok: false, inadmissible: true }`;修前同一形按「用户关卡」处置。对这几只读口做穷尽 `switch` 的端编译红。
66
+ - **🔴 型面 BREAKING:决断口与自带投递口收「要模型重呈」**(CC-890;**§134 PC-7**):`decidePlanReview` 第二参 `'approve' | 'reject' | 'ask_again'`;重开口自带投递口 `deliverDecision` 第二参改为型引用 `Parameters<typeof decidePlanReview>[1]`(收到的是语义值原样,含 `ask_again`;转交 `decidePlanReview` 即可);结局机读位 `PlanReviewOutcomeMeta.decision` / `_sema_planReviewOutcome.decision` 多一词 `ask_again`(宿主交给决断口的语义值;结局正文 / 尾句按上 wire 的拒绝说)。`ask_again` 上 wire = `reject`,理由 = 按卡事实取的一句给模型的话(数据面英文)+ 用户另写的话接在后面;映射只在决断口一处。投递口的函数参数型写成 `'approve' | 'reject'` 的端编译红。
67
+ - **🔴 型面 BREAKING:已呈档位多一态 `withheld`,不再按身份尾缀推断**(CC-890;**§134 PC-5**):`PlanReviewOfferedMode` = `three_choice` / `two_choice` / `withheld` / `unknown`;每张卡(含重开卡、撤档卡)按卡身份精确记,修前「`#reopen-` 尾缀 ⇒ 两选」的推断删除(同一跑第二张起的卡也用这一形身份,可以是三选 / 撤档)—— 从没发布过的这一形身份答 `unknown`。`planReviewShownLabelsOf` 同律。`planReviewTaskIdFromQuestionId(...).reopened` 改口:说「是不是新身份」,不说卡形。
68
+ - **计划门的判据改读决断口受理门种表**(CC-890;**§134 PC-9**;零可见变化):`isPlanReviewPark` 与认领折叠的停驻标记比的是计划审阅决断口受理的门种(类型包 `PLAN_REVIEW_GATE_KINDS` 的构建期生成物,今天恰 `plan_review` 一词),不再另写一份同值常量。
69
+ - **🔴 型面 BREAKING:`McpConfigDropReason` 多一员 `transport_not_advertised`**(CC-999;**§134 PK-8**):*变了什么*:`projectMcpConfigs(servers, { transportKinds })` 收到能力位读数 `stated` 时,传输种类不在其中的那一台服务器不上请求体,记一行披露(`path: 'transport.kind'`,`problem` = 服务说支持的种类,`notice` 点名传输);其余台照投。读数 `not_stated` / `unreadable` / `unobserved` 或不给选项 ⇒ 与 0.104.0 同;能力位数组有坏成员(非串 / 空洞 / 访问器)⇒ `unreadable`,宿主自拼的读数有坏项 ⇒ 不筛(读不懂的表不当成确认的窄表)。对这一联合穷举的端补一臂。
70
+ - **🔴 型面 BREAKING + 行为变:记忆抹除 409 `memory.erasure_store_unjudged` 读成拒档 `personal_store_unjudged`**(CC-1120;**§134 PK-3**):*修前*:这一码落「认不出的拒」⇒ 结局「没法确认擦没擦」;*修后*:`refused` / `personal_store_unjudged` / `retry: 'pointless'`,句子说一条都没擦(会话记忆有一部分住在这台部署还擦不了的个人店上,服务端在任何删除之前整次拒)。`SessionMemoryEraseRefusalReason` 与运行期镜像 `SESSION_MEMORY_ERASE_REFUSAL_REASONS` 各多一词。
71
+ - **🔴 行为变(只在能力位恰真时):设置层 deny / ask 条目带出处层**(CC-1113 deny / ask 半场;**§134 PK-9**):服务端报 `settings.permissions.layers: true` 时,宿主设置入口把设置文件来源(`policySettings` / `flagSettings` / `userSettings` / `localSettings` / `projectSettings`)的 deny / ask 铸成冻结的 `{ rule, layer }`;命令行与会话来源照旧纯串;同层同串去重、跨层同串各一条;allow 恒纯串。位不真(缺席 / `false` / 坏形 / 没观测)⇒ 请求体与 0.104.0 逐字节同。沿用期停发确认的身份同改为 `(层, 串)`(另一层仍写着同一串不再掩盖这一层的撤销)。能力位读数 `PermissionsAllowGrantReading` 的在场臂多一只可选位 `layers?: boolean`(第三叶坏形只丢它自己)。
72
+ - **🔴 行为变(fail-closed):直调口 `permissionsDeclaration` / `permissionsDeclarationReport` 收紧两桶里的对象元素拒绝装配**(**§134 PK-9**):deny / ask / `cliDeny` 里出现对象元素(类型包 31.0.0 的带层条目那一形)时抛 `TypeError`(只报形状);修前与其余非串一样静默跳过,一条收紧规则不声不响地丢掉。原始值非串 / 空串照旧跳过;allow 桶不变。
73
+ - **🔴 行为变:子代车道的「未执行」工具卡**(**§134 PK-6**):引擎把一只工具调用收口为未执行(`allowed` ∧ `isError` ∧ `notExecuted`)时,子代内容事件、账本工具项与查看态槽位都带 `notExecuted: true`,查看态先于 `isError` 读它(修前读成执行失败);同一调用后一发收口不带它 ⇒ 摘掉;座位出站门只收字面 `true`。主车道读口照旧。
74
+ - **行为变:MCP 条目 `sessionMetaKey` 两条腿原样承运**(**§134 PK-12**):本地 `.mcp.json` 腿与中心配置腿都在入口读一次,声明了就原样上请求体(值形不判,键判官在引擎);缺席不铸(请求体与 0.104.0 逐字节同)。*修前*:两条腿都不承运,设置里写了它的用户看不到效果。服务端 <7.138.0 不认这把键 —— 写了它的那次提交整单 400、拒句点名键。
75
+ - **三处过渡结构读随类型包到货退役**(零可见变化;退役登记 RL-303 / RL-308 / RL-309):类型包 31.0.0 的 `status` 臂声明 `sleptMs` / `waitSegment`、`tool_end` 臂声明 `rewrite`、接线回执行与重拨状态声明 `authorization` —— `status` 两位直读类型面的帧(结构视图删),改写记录与授权记录的读视图改由类型包钉住;逐键受保护读与窄化照留。
76
+ - **开发依赖类型包换钉 31.0.0**(**§134 PK-1**):构建期生成表按 31.0.0 重生成(内容零变化)。类型包三处 BREAKING 对本包:deny / ask 条目型放宽 —— 本包只写这两桶;记忆后端词 `tidb` → `mysql`、side-query `errorKind` 加 `image_unprocessable` —— 本包零读点。
77
+ - **🔴 型面 BREAKING:收尾决策表的入参 `EndedGenerationSettleFacts` 退 `awaitedNextEngine`、进 `waits?`**(CC-1001;**§134 EG-2**):*变了什么*:「已经等过一次」的布尔位改成本收尾里各轮等待的读数数组 `waits?: readonly EndedGenerationWaitReading[]`(按发生序;缺席 ≡ 零轮;每轮 `{ replacing, installed, generationBefore?, generationAfter?, replacingAfter? }`,后两位只在 `installed` 时在场)。退役键不留别名、表不读它(传了也不改出口)。*谁要改*:构造这份入参的宿主(今天的已发布消费者里没有调用者)。*怎么改*:执行 `'await-next-engine'` 时先读代号、再读是否在换,再等;装上 ⇒ 再问一次判活、再读代号与是否又在换;把这一轮读数追加到 `waits` 末尾带回来。
78
+ - **收尾决策表判序终形**(CC-1001;**§134 EG-1**;🔴 行为变):① 入参读不出(判活读数不是对象 / 状态词表外 / `refreshedCredentials` 不是 `null` 也不是布尔 / `waits` 不是数组或有一项坏形 / 取值器抛)⇒ `'window-exhausted'`(修前 ⇒ `'engine-stopped'`,即在拿不准时许可宿主补「被信号 9 杀掉」;`refreshedCredentials` 缺席在 401 上修前被读成「刷过」⇒ `'credentials-rejected'`);② 可等的触发面加 401(问不到 ∨ 401)且可多轮:零轮,或末轮装上了却不稳(再问前后代号变了 ∨ 宿主又在换)且不足 8 轮 ⇒ `'await-next-engine'`;③ 结局未知 = 至少等过一轮 ∧ 答过 401(刷过,或此刻是 401)∧(末轮在换而没装上 ∨ 末轮不稳且已满 8 轮);④ 401:没刷过 ⇒ `'refresh-credentials'`,刷过 ⇒ 结局未知 ? `'window-exhausted'` : `'credentials-rejected'`;⑤ 结局未知 ∧ 问不到 ⇒ `'window-exhausted'`;其余(`done` / `parked` / 其余 ⇒ `'engine-stopped'`)不变。判序与参照终端的收尾状态机逐臂同答(同一份已完成的事实记录下出口、判活次数、刷新次数、每轮等待预算四项全等)。
79
+ - **`REATTACH_AWAIT_NEXT_ENGINE_MS` 改义、值不变(245000)**(CC-1001;**§134 EG-1**):从「每次等的上限」改为「一次收尾里所有等待共用的一份上限,从首问返回那一刻起算」。宿主按它求每轮 `maxMs`:发起时没在换 ⇒ `0`(只核一次后继是否已就绪);在换且是首轮 ⇒ 整份;之后各轮 ⇒ 截止刻余量。轮数顶 8 是包内常量,不导出。
80
+ - **同一次完成的通知按服务端给的完成 id 去重,老服务端照旧**(CC-909;**§134 RL-1 / RL-2**;公面零增;行为变只在「同一次完成的两份自算键对不上」时):*变了什么*:适配器渲染 `task_notification` 时,终态、非 external、带 `completionId` 的通知多一把身份键 `completion:<id>`,与原有自算键 `[external:]taskId:status[:seq]` 并认,见到就两把都记进渲染去重账(三份混到时也认得出)。同一次完成在 live 帧、收件条目回放与断线重放上的完成 id 同值;修前这几份的自算键一旦对不上(例如一份带 `seq`、一份不带),同一条完成在屏上出现两次,现在一次 —— 第二份只是不再渲行,撤掉队列里同一 run 的排队条目、面板收尾与 `notification_terminal` 提示照旧(与修前这一份做的一样)。自算键相同的一份照旧整条跳过(修前同律);完成 id 不同、自算键也不同的两次完成照旧各渲一次,自算键相同的两次完成(例如都不带 `seq`)仍按自算键只渲一次。*没变的*:不带完成 id 的通知与记录 —— external 通知(恒不带)、收件条目 / 重放帧尚未带键的服务端、从转录回植或经 `importLedger` 回植的键 —— 照旧按自算键,带键的通知也认它们;导出账里自算键照记,端按 `taskNotificationDedupKey` / `taskNotificationDedupKeyFromWire` 查这本账的读法不变(账里会多出 `completion:` 前缀的串,不要解析它);完成卡已入队、补发门、轮询卡预记与队列撤单这几道跨通道判定不变(**§134 RL-3**)。`MIGRATED_COMPENSATIONS` 的 T2 / T25 / T53 三行 `retireOn` 归 `null`(长期职责),T2 的 `what` / `libraryHalf` 与 T53 的 `what` 文案改写。
81
+ - **🔴 行为变:续开 / 重开 / 进程重启后的计划卡读引擎记录取卡事实**(CC-890;**§134 PC-10**):*变了什么*:取卡事实只有一条规则,arm、重开口与新读口 `readPlanReviewCardFacts` 同走 —— 本进程登记过这道门的当代卡事实 ⇒ 直接用(零读;调用方没给门身份时先读一次 run 行认门,同门即用,这一读读不出 ⇒ 撤档且不读引擎记录);否则读引擎记录:读 run 行的门身份,把这一跑的事件记录并发读两遍、按流入口同一只认领折叠取这道门的正文,再读一次 run 行;两遍逐值同、读前读后门身份相同且等于调用方给的门身份(给了时)⇒ 采信,按读到的卡事实出卡(干净正文给批准档,其余撤档)。引擎记录里两枚计划停住标记之间没有计划调用、而标记自带的门身份不同 ⇒ 后一道门不继承前一道门的正文(`not_presented`,撤档);门身份相同或缺席 ⇒ 按同一道门的重写沿用。读不下来(3 秒内读不完 / 两遍不一致 / 帧号回头 / 计划帧或停住标记不带帧号)⇒ `unknown{unreadable}`;读前读后换了门 / 与调用方给的门身份不符 ⇒ `gate_moved`;run 行没有门身份 ⇒ `gate_unidentified` —— 三者都撤档且不入表,下一次重开重读。没有引擎记录读口(没装目标、注入的客户端缺 `runs.events`)而本进程又没登记这道门(或重开不带门身份)⇒ `not_observed` 撤档卡。arm 遇到本进程没看到这道门(不经本包跟流)时同走这条规则:`armPlanReviewApproval` 照旧同步回 `true`,卡在读完之后(至多约 3 秒)出。*为什么*:修前这几条路的卡一律没有正文 ⇒ 撤档。*端要做什么*:带引擎记录读口的端零改动即生效;知道门身份的端(409 `pendingGate.checkpointId` / 待批行)重开时传 `checkpointId` —— 没有引擎记录读口又不传门身份的重开,卡从修前的两选变为撤档卡。
82
+ - **🔴 行为变:重开卡按卡事实出选项**(CC-890;**§134 PC-11**):重开口铸的新身份卡与首呈卡同一张选项表、带包卡帧 `_sema_planReview` —— `shown` ⇒ 两选,其余 ⇒ 撤档两项(「拒绝」与「要模型重新呈上计划」);已呈档位记 `two_choice` / `withheld`,作答读域按这张卡呈出的选项。修前新身份的重开卡恒两选、不带卡事实:撤档卡被关掉后再重开,重开卡上出现批准档且批准会被投递。复用原卡的那一臂只在「同门、同卡事实、原卡仍在」时复用。
83
+ - **重开按门去重、按代作废**(CC-890;**§134 PC-11**):同一任务同一道门的并发重开只读一次引擎记录、只出一张卡;不同门各走各的。一次引擎记录读在路上时这一跑已有更新的取事实(例如流上已停到下一道门)⇒ 这次读数作废:不出卡、不撤新卡,重开回 `{ reopened: false, _sema_reopenRefusal: 'not_presented' }`;arm 的读数作废时,同一道门在本进程有当代那一份(例如读的同时流上停到了这道门)⇒ 用它出卡(arm 答过 `true` 不丢卡)。
84
+ - **自愈重开递门身份**(CC-890;**§134 PC-12**):活跑自愈的依赖口 `reopenPlanReview(taskId, opts?)` 的 `opts` 多可选位 `checkpointId`;只在这一次 409 本身判出计划门、且这个门身份就是本进程为这一跑登记的当代门时递它的 `pendingGate.checkpointId`(409 的待批门材料与占锁任务号不保证出自同一行 —— 同会话可并存多条待批行;证不出同一任务就不递,重开按 run 行认门),其余入口不递。本进程登记过这道门的当代卡事实时,这次重开零引擎记录读。
85
+
86
+ ### Added
87
+ - **决断正文载体档 `DISPLAY_DECISION_BODY`,以及支撑它的两个选项值**(CC-875;**§134 DB-1 – DB-3**;根入口运行期 +1〔`DISPLAY_DECISION_BODY`〕;`DisplayUntrustedOptions` 的 `blanks` 型 `boolean` → `boolean | 'fillers'`、`keepLayout` 型 `boolean` → `boolean | 'crlf'`,旧值逐字节不变):*为什么*:中文计划正文写全角空格、Windows 编辑器存 CRLF,这两样是正当排版;修前展示出口要么把整张名字位空白网开着(全角空格与不换行空格被标)、要么不开(谚文填充 / 盲文空点这类画成一格空白却不是空格的字符原样上屏),CRLF 里的 `\r` 在保排版读法下恒被标。*现在*:`DISPLAY_DECISION_BODY`(冻结选项对象,键集恰 `foldLines: false` · `keepLayout: 'crlf'` · `blanks: 'fillers'` · `mark: 'escape'`)—— 不标:空格类 Zs 全族(U+3000 / U+00A0 / U+1680 / U+2000–U+200A / U+202F / U+205F)与紧跟 `\n` 的 `\r`(`\r\n` 行断);照标:U+115F / U+1160 / U+3164 / U+FFA0 / U+2800、零宽与双向控制等格式类、其余控制符、孤 `\r`(后面不是 `\n`)、U+2028 / U+2029 / U+FEFF、孤代理项;`escapeBackslashes: true` 展开的单射形与本档同一份标记集。凭据两网保持默认(开);只当判官读标记、想让凭据记号不吞掉它里面的标记位时,展开后把 `credentialUrls` / `credentialWords` 关掉。`blanks: 'fillers'` 只标名字位空白里不是空白的那一半、空格类放行(`true` 仍两半全标;两半由同一张区间表派生);`keepLayout: 'crlf'` 在 `true` 的基础上另放行 `\r\n` 里的 `\r`(只在 `foldLines: false` 且控制符网开着时有意义)。表外取值按缺席读。*谁受影响*:不接线的端零变化;把这两位的型当 `boolean` 读的端随型加宽;终端壳 / 网页端 / 桌面端的计划审阅卡改读本档后,计划正文里的全角空格 / 不换行空格 / CRLF 行断不再被标出。
88
+ - **「这块屏不再跟着 run X」的事实与句**(CC-877;**§134 RF-1**;根入口运行期 +2〔`runNotFollowedNotice` / `runNotFollowedOf`〕,型 +3〔`RunNotFollowed` / `RunNotFollowedCause` / `ReattachBreak`〕):`RunNotFollowed{ kind: 'not_followable', cause, taskId?, lastBreak? }`(冻结;`lastBreak` 只住 `reattach_exhausted` 那一臂、只在最后一次断因已证出时带);`runNotFollowedNotice(fact, lang?)` 出一句(消息目录 `follow.notFollowed.*`,en / zh):哪条 run、最后一次为什么断、引擎没说它怎么结束(可能还在跑)、要重新附着之后才会在这里看到它接下来的动作;不说失败 / 完成 / 重发,不说审批卡;手搓坏对象 ⇒ 中性句(仍说「没在跟」)。成因闭集另含计划审阅决断座位的 `target_unconfirmed` / `cursor_unknown`(本版这两词只出中性段)与窗口机的 `run_unknown`。
89
+ - **问答桥依赖口 `onBreak`**(CC-877;**§134 RF-7**;`AskGateWireDeps` +1 可选位;组合器端口名 +1〔`askBridge.onBreak`〕):桥没到终帧就结束、且包知道是排空所致时恰调一次 —— 梯尽 ⇒ `{ cause: 'stream_draining', ladder: 'exhausted', runId? }`,没能重开(说不出 run 句柄)⇒ `{ cause: 'stream_draining', frame }`;`runId` = 桥此刻跟的 run(决断回执换挂之后是新 run);已中止不调。根入口 `bridgeAskUserQuestionGates` 与组合器 `askBridge` 同一个口;两种形都走包内宿主回调隔离口(同步抛 / 返回被拒的 promise / 抛出物字符串化再抛都不外溢、不产生未处理拒绝),读到口之后、调之前再看一次中止;组合器闸 `isCurrent`(旧代不调;读位之后再闸一次,并再看一次中止)、抛计 `report().portFaults['askBridge.onBreak']`、缺席不进 `missingPorts`;组合器上桥的 run 句柄缺席时回落这条腿的绑定(活流腿的头 / `meta`)。🔴 **行为变(组合器路径)**:这条回落不论宿主注没注 `onBreak` 都登记 —— 桥说不出 run 句柄、而腿以带 `reconnectAfterMs` 的排空终帧收场时,修前不重开、按断线形收场,现在等一格后按回落句柄从上一游标重开 `runs.events`;宿主没注 `onBreak` ⇒ 帧逐字节同、收场不同(多这一次重开;`onBreak` 当然不调)。根入口 `bridgeAskUserQuestionGates` 直装不登记回落,句柄缺席时与修前同。
90
+ - **零预算 `REATTACH_BUDGET_NONE` 与排空臂型 `ReattachDrainingEvent`**(CC-877;**§134 RF-2 / RF-7**;根入口运行期 +1、型 +1):不重挂某条腿的车道拿零预算当上一层,任一良形事件 ⇒ `give_up`(没有「梯尽之后没人接」的腿);`ReattachDrainingEvent` = 策略口入参的排空臂(`onBreak` 的入参就是它)。
91
+ - **谓词 `isStreamEndedWithoutTerminalError`**(CC-877;**§134 RF-8**;根入口运行期 +1):与 `isSseIdleError` 同律 —— instanceof 优先、`name` 兜底只读自有数据位(取值器不执行),永不抛;只答「是不是这一类」,不看 `status`。类型包给这只错误加上两种耗尽的机读因(`reason` + `cause`)之后的下一个本包版本换成按因读(退役登记 RL-877)。
92
+ - **卡事实与正文判官**(CC-890;**§134 PC-1**;根入口运行期 +1〔`planReviewBodyOf`〕、型 +3〔`PlanReviewCardFacts` / `PlanReviewBody` / `PlanBodyUnknownCause`〕):`PlanReviewCardFacts{ taskId, checkpointId?, body }`(冻结;门身份缺席 ⇒ `body` 恒 `unknown{gate_unidentified}`);`body` 四臂 `shown{markdown}` / `marked{literal}` / `too_large{length}` / `unknown{cause}`(成因闭集九词);`planReviewBodyOf(text)`:原文超 200 000 码元 ⇒ `too_large`,`\r\n` 归一后在决断正文载体档下有被标出的字符 ⇒ `marked`(可见转义、单射),否则 `shown`(空计划也是 `shown`,卡上另有一句)。
93
+ - **chrome `plan_review_park.planReview`**(CC-890;**§134 PC-2**):终帧停在计划门时带这张卡的卡事实(与包卡帧、arm 用的是同一个冻结对象);停在别的复核门 / 扁平老形 ⇒ 键缺席,臂照发。正文只认主道计划工具成功收尾那一帧的结构化正文(内联形与计划文件形同读),子流的计划调用不作证。
94
+ - **包卡帧 `_sema_planReview`、视图与选项投影**(CC-890;**§134 PC-3**;根入口运行期 +3〔`planReviewCardViewOf` / `planReviewOptionsOf` / `planReviewCardNote`〕、型 +3〔`PlanReviewCardView` / `PlanReviewOption` / `PlanReviewOptionChoice`〕):包铸的卡帧带卡事实;`planReviewCardViewOf(frame, lang?)` ⇒ `{ facts?, options, note? }`(事实读不出 / 缺 / 不是这张卡的 ⇒ 只给撤档两项 + 原因句);`planReviewOptionsOf(facts, { submittedInPlanMode? })` ⇒ 带语义值 `choice` 的选项(作答交决断口的就是它);`planReviewCardNote(facts, lang?)` ⇒ 卡上原因格那一句(消息目录 en / zh)。
95
+ - **记忆同步停驻读数与提示句**(**§134 PK-2**;根入口运行期 +3〔`readMemorySyncParking` / `memorySyncParkingNotice` / `MEMORY_SYNC_WAY_OUT_DELETE_CURSOR`〕、型 +4〔`MemorySyncFacts` / `MemorySyncParkedFacts` / `MemorySyncParkingReading` / `MemorySyncParkedLayerReading`〕):`readSessionMemoryStatus` 的事实上多可选位 `memorySync`(合形才在场);`readMemorySyncParking(facts)` 答 `syncing` / `parked{ layers }` / `not_reported`(缺席不是「在同步」);`memorySyncParkingNotice(reading, lang?)` 只对 `parked` 出一句(层名可打印且不超长才点名;每层出路都是删游标时说「运维在服务器上删掉暂停层的同步游标后恢复,只在一侧删掉的条目会回来」,否则不编出路)。
96
+ - **workflow 完成的诊断文本与完成 id**(CC-1084;**§134 PK-5**):一只读叶三处同读(非空自有数据串才带)—— wire 投影的 `workflow_complete` 臂体带 `completionId?` / `diagnostics?`;chrome 臂 `workflow_notification_enqueue` 的通知事实带 `completionId?`,`diagnostics` 只上同一臂的展示位 `diagnostics?`(不可信展示文本;不进通知事实、模型面正文与队列项 —— 渲染即可、永不回喂模型);直调口 `enqueueEngineWorkflowNotification` 的 `EngineWorkflowCompletion` 多可选位 `completionId?`。
97
+ - **权限模式写能力位读面**(**§134 PK-7**;根入口运行期 +3〔`projectPermissionModeWriteCapability` / `observedPermissionModeWrite` / `permissionModeWriteAvailable`〕、型 +1〔`PermissionModeWriteReading`〕):`false` 是正面陈述;判官只认 `present ∧ true`。写口动词见下方「在飞 run 切权限档」一条(CC-1080;**§134 PM-1**)。
98
+ - **MCP 传输种类能力位读面**(CC-999;**§134 PK-8**;根入口运行期 +2〔`projectMcpTransportKindsCapability` / `observedMcpTransportKinds`〕、型 +2〔`McpTransportKindsReading` / `McpRequestTransportKind`〕):`stated{ kinds, unrecognized }` / `not_stated{ kinds }`(契约两词)/ `unreadable` / `unobserved`。
99
+ - **引擎租约窗**(**§134 PK-4**;型 +1〔`EngineHealthLeaseFacts`〕):`readEngineHealthFacts` 多可选位 `engineLease{ lingerMs, tickMs }`(两数齐且各为非负安全整数才在场;缺席不是「永不自退」)。
100
+ - **出口词 `'window-exhausted'`**(CC-1001;**§134 EG-1**;`EndedGenerationSettleStep` 六词 → 七词):结局未知、停自动重试,与窗口式重连机判词 `ReattachWindowDecision` 的同名动作同一字面、同义(宿主渲同一只收尾:这块屏不再跟着那一跑,它可能仍在引擎上)。七个出口里只有 `'engine-stopped'` 说「这一跑随那一代引擎没了」,是唯一许可宿主给在飞调用补「被信号 9 杀掉」的出口。对 `EndedGenerationSettleStep` 写了穷尽分支的端编译红一次。
101
+ - **端口证据位 `EngineGenerationPort.endedBy?(generation)` 与型 `EngineGenerationEndFact`**(CC-1001;**§134 EG-3**;型 +2〔`EngineGenerationEndFact` / `EndedGenerationWaitReading`〕,运行期 ±0):第 `generation` 代是怎么结束的 —— `'own-supervision'`(在本宿主监护下结束,含外部 `kill -9` 本宿主的子进程)/ `'external-replacement'`(答话的已是另一只引擎);口缺席 / 回 `undefined` / 抛 / 回两词以外 ⇒ 证不出(不归因)。决策表不读它(入参型上没有这一键);唯一读者是宿主渲「凭证被拒」那一句的致因从句。
102
+ - **值级端口第二十二件 `IMPORTED_SETTINGS_LAYERS`**(CC-1133;**§134 ISL-1**;根入口运行期 +1〔读口 `importedSettingsLayers`〕;🔴 型面 BREAKING:`CoreValuePorts` 多一只必填成员):引擎包根的导入器三层设置层名(冻结元组 `["userSettings", "projectSettings", "localSettings"]`;引擎的 `ImportedSettingsLayer` 型由它派生),值级常量件,与 `MCP_NAMESPACE` / `ANTHROPIC_DEFAULT_BASE_URL` 同类。类型包只转出同值的型(`CcImportSettingsLayer`),没有同值的值,所以走端口而不是转出:宿主从引擎包根原样装进来(零型断言;按完整型建端口袋的宿主编译红一次、点名它),读口 `importedSettingsLayers()` 交回装进来的那一只元组(同一引用:不拷贝、不重冻 —— 引擎那份的冻结原样带到读点)。运行期没装 ⇒ 读口 `undefined`(不回落三层字面量)。读这三个层名的端装上之后改读读口,删掉自己手抄的那一份。
103
+ - **读口 `readPlanReviewCardFacts(taskId, opts?)`**(CC-890;**§134 PC-10**;根入口运行期 +1、型 +1〔`ReadPlanReviewCardFactsOptions`〕):宿主自有补卡路(待批列表补卡、停驻面)读某一跑停住那道门的卡事实,与 arm / 重开同一条取事实规则;`opts.checkpointId`(要哪道门)/ `opts.wire`(同决断口)/ `opts.signal`;永不拒绝 —— 中止 / 读不下来 ⇒ `unknown{unreadable}`(没给门身份又认不出门 ⇒ `unknown{gate_unidentified}`),任务号不是非空串 ⇒ `unknown{gate_unidentified}`。读到的稳定读数入表(之后同门的 arm / 重开零读)。
104
+ - **账本快照读口 `readRunLedgerSnapshot(taskId, callId, deps, signal?)`**(CC-890;**§134 PC-10**;根入口运行期 +1、型 +3〔`RunLedgerSnapshot` / `RunLedgerFold` / `RunLedgerSnapshotDeps`〕):一跑停在门上那一刻的引擎记录快照 —— 读前读后各问一次「是不是停着」、并发读两遍、三道读完证据(收尾证据 / 尾巴落在停住标记附近 / 两遍同一最大帧号)缺一不认;排空终帧收场按包的排空决策整遍重读;交最大帧号 `highWater`、停住那一通调用的开始帧与(可选)按序折叠的结论(`strictOrder` / `agree` / `substantive` 三位收紧)。读口全部注入,永不抛,读不出 ⇒ `null`;预算(缺省 8 秒)到点即答 `null`,不等不看中止信号的读口(读口每出一帧都让出事件循环时是墙钟硬界 —— 真网络读口如此;KL-10465)。
105
+ - **重开口选项 `ReopenPlanReviewSharedOpts.checkpointId?`**(CC-890;**§134 PC-10**):要重开的是哪道门(非空串);给了 ⇒ run 行的门身份必须是它,否则卡事实 `gate_moved`(撤档);缺席 ⇒ 按 run 行的门身份。
106
+
107
+ - **在飞 run 切权限档 `setRunPermissionMode` 与结局句 `runPermissionModeSwitchLine`**(CC-1080;**§134 PM-1 – PM-4**;根入口运行期 +2〔`setRunPermissionMode` / `runPermissionModeSwitchLine`〕、型 +5〔`RunPermissionModeSwitchOutcome` / `RunPermissionModeSwitchRuns` / `RunPermissionModeSwitchOptions` / `PermissionModeAdmissionView` / `PermissionModeReceiptView`〕;服务端 ≥ 7.141.0 `POST /v1/runs/:id/permission-mode`,类型包 31.0.0 `runs.setPermissionMode`):只在能力位判官 `permissionModeWriteAvailable` 为真时发(读数取 `observedPermissionModeWrite(baseUrl)`;不真 ⇒ `unavailable`、一发都不打)。结局六形恒是值、绝不抛 —— `switched`(此刻生效的档 = 引擎回执 `permissionMode.effective`,不是发出去的词;本部署的自主度地板或任务设置把词折了 ⇒ `admission{ requested, admitted, foldedBy }`,与引擎自己的折因 `permissionMode.foldedBy` 分开承载;`persisted: false` 带成因 `persistMiss`,句子加「没随 run 存下,续跑会回到切档前的模式」)/ `not-running`(409 `steering.not_running`)/ `not-found`(404)/ `refused`(其余 4xx,以及 500 `corrupt` —— 存着的续跑上下文读不出,服务端在把词交给引擎之前就答了;带状态码与机读码)/ `unconfirmed`(其余 5xx、传输断、200 回体不合封闭七键形或自相矛盾 —— 例如引擎回执上交给它的词不等于 `admission.admitted`)/ `unavailable`(位不真、客户端没有这只动词、或 501)。只有 `switched` 带模式;回执里两个 `requested` 各在原位(`admission.requested` = 调用方原词,`permissionMode.requested` = 交给引擎的词)。不带幂等键,不重发。
108
+
109
+ ### Deprecated
110
+ - **`planReviewCardOptions(offerModeAfter)`**(CC-890;**§134 PC-8**;退役登记 RL-320):由选项表派生(与 `planReviewOptionsOf` 同一张表,逐字同旧两形),说不出撤档卡;消费端删掉调用之后的下一个本包版本删除。
111
+
112
+ ### Removed
113
+ - **🔴 型面 BREAKING:型 `ReattachGiveUp` 退出根入口**(CC-877;**§134 RF-2**):放弃理由由事实 `notFollowed.cause`(`RunNotFollowedCause`)承载。
114
+
115
+ ### Gates
116
+ - **`scripts/run-display-decision-body-profile-test.mjs`**(CC-875;**§134 DB-1 – DB-3**;包侧投影门):档本身(根入口导出 / 冻结 / 键集恰四键 / 两位选项的型面)· 不标矩阵(U+3000、U+00A0、CRLF 与 `\p{Zs}` 全族 × 四种语境)· 照标矩阵(填充字、盲文空点、零宽、双向控制、孤 `\r`、行 / 段分隔符等,恰一条标记且码位对)· exact 形同一份标记集 · 全 BMP 暴力分区(`blanks: true` 比关多标的 = `\p{Zs}`〔除 U+0020〕∪ 填充五枚,`'fillers'` 多标的恰填充五枚;源码里五枚码位只在区间表一处)· 旧选项空间(648 组合 × 104 条文本,含带凭据的串)输出与标记集摘要对 0.104.0 产物逐位同 · 凭据两网开着时三态一致(36000 格随机语料:`'fillers'` 在无填充字文本上 ≡ `false`、无 Zs 文本上 ≡ `true`,`'crlf'` ≡ `true` 去掉 CRLF 的转义)· 门内负控三枚(Zs 改回标出 / CRLF 改回标出 / 填充字改回放行 ⇒ 判据必红并打印红读数)。
117
+ - **`scripts/run-run-follow-projection-test.mjs`**(CC-877;**§134 RF-1 – RF-8**;包侧投影门):F0 公面 / 包内件不上根 · G11 句表逐因 × 断因 × run id 在 / 缺 × en / zh 对目录独立拼句、禁词、坏形中性句 · G5 – G7b 策略口(梯尽形只花一次普通预算、三因坏形、断因逐因、零预算四形)· G8 窗口机(真窗尽 / 断因三态 / 坏状态坏钟 / 新一段 / 换挂进窗 / run-unknown)· G9 / G9b / G10 无头(原错误照抛、冻结与拒写代理经真链不被顶掉、其余出口逐个)· G12 / G12b / G16 重试位与座位往返 · G15 合成句(事实缺席两形对 0.104.0 金样逐字节同)· G1 – G4b 断因口(组合器 / 根入口同口恰一次、换挂后的 run、原帧形、句柄回落、中止、端口缺席 / 抛 / 旧代、`async` 口被拒与不可打印抛出物零外溢、读位时中止 / 切代不调)· G13 ③ `kind: 'not_followable'` 型声明全包一处 · G17b 跟丢零卡调用结构钉 · P 谓词。负控一枚(放弃事实的最后断因改取常量 ⇒ G7 红)。
118
+ - **改钉 `scripts/run-turn-failure-presentation-test.mjs` RP 段**(CC-877;**§134 RF-2**;成文改口):入参 `hasRunId` → `runId`,放弃读 `notFollowed.cause`(`budget_exhausted` → `reattach_exhausted`);与网页端 / 桌面端参照循环的对拍不变。
119
+ - **消息目录门域闭集加 `follow`**、句口语料 `cc877`(目录路由门 CR3 – CR6)、退役登记 RL-877、单例清单补登(事实载体与桥句柄回落两只 WeakMap、三只冻结词表 / 常量)、公面基线 +4。
120
+ - **`scripts/run-engine-generation-test.mjs` EG5 重写**(CC-1001;**§134 EG-1 – EG-3**):EG5a 全穷举 54,810 格 ≡ 门内独立表 · EG5b 保守边与读法(每个位只读一次、证据位与退役键不读、代号读不出 ⇒ 没动)· EG5c / EG5c2 出口词七词与证据位不进表(型级钉,消费方视角 tsc 探针)· EG5d 宿主驱动 + 表 vs 参照终端收尾状态机转写(穷举 1,492,992 + 定种随机 300,000,四项全等)与八枚负控(各自差 > 0)· EG5e 六形出口序列 · EG5f 坏形七行。负控常驻套加一枚(把规则 2 的 401 去掉 ⇒ EG5e 先红),该门负控归类由行为套改为自动化负控。
121
+ - **`scripts/run-notification-completion-dedup-test.mjs`**(CC-909;**§134 RL-1 – RL-3**;新门):身份全集的形(键缺席 / external / 非终态 / 空串只有自算键;带键终态非 external 两把、自算键在前;与公面两只去重口同字节)· 同一次完成两份自算键对不上恰一行、第二份的撤单与终态提示照做、三份混到身份桥接可传递(含导出 → 导入)、两次不同完成两行 · 缺席语义四形(带键之后的不带键重放、转录回植之后的带键重投、宿主回植之后的带键帧不重渲;两份都不带键照旧)· external 形不变 · 跨通道几道与队列撤单的反钉(探针卡 / fleet 卡已上屏后带键回声零行;转录回植 / 回声已记账 / 轮询卡之后的带键补发帧零入队臂;带键回声撤掉排着的同 run 卡、行恰一条)· 反证三维(四通道 24 种到达序 × 两种出队节拍上屏恰一次;同一实例两条流交错恰一行;完成 id 读不出回落自算键、不抛不丢)。
122
+ - **`scripts/run-retirement-ledger-test.mjs`**(CC-909):补偿层在册行 5 → 2;挂链首行 17 → 14;G11b 改钉三行执行后的形(在已退役栏、退于 0.105.0、锚在补偿表零处、三条 `retireOn` 归 `null`)。
123
+ - **`scripts/run-client-core-pure-test.mjs`**(CC-909):B6 / B7 补偿登记格改读执行后的形(非空 `retireOn` 5 → 2;T2 / T25 / T53 是长期职责;T53 点名第四道是队列撤单)。
124
+ - **`scripts/run-run-ledger-snapshot-test.mjs`**(CC-890;**§134 PC-10**;新门):公面在场 · 三证矩阵(收尾证据三形 × 尾巴落点 × 两遍同号,缺一 ⇒ `null`)· 停住夹住(读前 / 读后不是停着 ⇒ `null`)· 排空收场整遍重读与梯尽 · 无号帧上限 · 开始帧取最后一枚相符的 · 折叠(严格帧序 / 两遍一致 / 无号实质帧作废 / `step` 抛)· 读口挂起与读口抛在预算内答 `null` · 中止与坏任务号 · 日志口抛不外溢。
125
+ - **扩 `scripts/run-plan-review-card-facts-projection-test.mjs` L1 – L13**(CC-890;**§134 PC-10 – PC-12**):读口与续开重开读回正文 · 读前读后换门 / 与给定门身份不符 · run 行无门身份 · 两遍不一致 / 帧号回头 / 无号计划帧 · 读口挂起 3 秒预算 · 自愈递门身份与注册表命中零读 · 同门并发重开一次读一张卡 · 代次作废不撤新卡 · 临时读数不入表 · 同门两份认领 ⇒ 换实例 · 被采信那一遍的高水位 · 不给门身份时按 run 行认门(同门零引擎记录读、换门走引擎记录、认门读挂起共用 3 秒)· 认门读失败的读数不领新门的代号(新门的卡不被撤)· 409 的门身份不是这一跑的当代门 ⇒ 自愈不递 · 引擎记录里换门的停住标记不继承前一道门的正文;F20 改钉(重开带门身份,成文改口)。
126
+ - **改钉受影响的计划审阅门**(CC-890;**§134 PC-10 / PC-11**;成文改口):要在重开卡上批准的格先经流入口呈上计划、重开带门身份(`scripts/run-plan-review-offered-mode-test.mjs`、`scripts/run-plan-review-dismissal-test.mjs`、`scripts/run-plan-review-decide-verify-test.mjs`、`scripts/run-plan-review-injected-wire-test.mjs`、`scripts/run-plan-review-decision-ledger-test.mjs`、`scripts/run-selfheal-reopen-test.mjs`、`scripts/run-hitl-gate-honesty-test.mjs` F3、`scripts/run-hitl-single-reading-projection-test.mjs` HM-3b);重开卡不再恒两选,随机序列的卡面模型加撤档形;复用原卡(同一身份重呈)要原卡是本进程 arm 过的那一张、同门同卡事实(只登记作答口不算);新格 OM4d(不带门身份又没有引擎记录读口的重开 ⇒ 撤档、档位 `withheld`)。
127
+
128
+ - **`scripts/run-permission-mode-verb-projection-test.mjs`**(CC-1080;**§134 PM-1 – PM-4**;包侧投影门):PM0 判别力自证(五只故意写错的实现 —— 位不真仍发 / 渲 `requested` 不渲 `effective` / `persisted: false` 不出告示 / 八键体放行 / 409 读成 404 —— 各恰让点名格红)· PM1 根入口两名五型、结局恰六形(读 d.ts)、两张闭集与类型包包根常量逐词逐序同 · PM2 能力位四态与坏读数 ⇒ 零发 · PM3 调用形(保接收者 / `session` 只在非空串时带)· PM4 回执两臂与三种折 · PM5 坏形 37 形(含回执与受理段自相矛盾)⇒ `unconfirmed` 不带模式、取值器零执行 · PM6 拒臂按状态码 ∧ 机读码、恰一发(500 `corrupt` 读成拒、其余 5xx 不知道) · PM7 真类型包客户端端到端(路径 / 查询 / 体恰 `{ mode }` / 无幂等键 / 5xx 与网络错不重发)· PM8 结局句 en / zh(`unavailable` 那一句对「没发」与「服务端答 501」两种来源都真)· PM9 服务端发布包铸点对表(无夹具 ⇒ SKIPPED-SECTION)。目录路由门加句口语料 `cc1080`(CR3 – CR6)。构建期生成表 +2(`PERMISSION_MODE_ADMISSION_FOLDS` / `PERMISSION_MODE_PERSIST_MISSES`)、消息目录 +12 句、公面基线 +2、单例清单补登三只冻结元组、31.0.0 型钉 +11。
129
+
130
+ ### Known limitations
131
+ - **跟丢句只在假引擎上证过**:要首腿加三次重开、连着四条腿一连上就只收到排空帧才算梯尽;真部署上多半只在硬停期间出现,「the engine instance … was going down when its last event stream closed」那一句可能很少被看到。
132
+ - **各端不接线就看不到变化**:终端两条车道要在问答桥的依赖对象上注 `onBreak`、`-p` 空闲那一行要把 `runNotFollowedOf(err)` 传进句口、网页 / 桌面要换读策略口,才换成本包的句;`-p` 决断后续流跟丢接上之后,那一回合从「无结果帧、退出码 0」变成「一句 + 错误结果帧、退出码 1」(宿主侧改动,见 §134 三端升级必读)。
133
+ - **runs.events 读流器耗尽仍按「可能停驻」读**:类型包对两种耗尽抛同一只类、不带因,本包分不出「可能停驻」与「读不下去」,这一形不出本句(到货后换读,RL-877)。
134
+ - **审批卡的去向不在本版**:组合器形上排空重开时挂在旧流上的卡今天会静默撤、靠新流前导帧再呈;本版的断因 / 结局 / 句子都不碰卡,也不替撤卡说话。
135
+ - 续开 / 重开 / 进程重启读回正文要客户端带事件记录读口,且这一跑在约 3 秒内读得完:读不下来 / 读前读后换了门 ⇒ 撤档(下一次重开重读;KL-10462);计划正文超过引擎卡位时卡上没有正文,引擎记录里同样没有(KL-10460);同一回合连呈两份计划 ⇒ 撤档(KL-10461);arm 读引擎记录时被同一任务另一次不带门身份的宿主读作废 ⇒ 这一次可能不出卡(KL-10463);同一进程 3 秒内碰过 64 条以上别的计划审阅任务时旧读数可能被当成当代(KL-10464);只在微任务里出帧的注入读口会推后读引擎记录的截止(KL-10465)。
136
+ - 公面 `permissionsDeclaration` 直调口在位真时仍投纯串(带层条目只由宿主设置入口铸);沿用期 `wouldDrop` 只列规则串、不点名层;自助抹除不带层座,落到个人店的抹除今天没有出路;`memorySync` 任一员坏形整键不铸;受理口逐条问题的 `home_unresolvable` 不认领(通用拒绝句);`sessionMetaKey` 不按服务端代际门控(没有能力位,<7.138.0 上写了它即整单 400);通知读数型 `ContextImageRemovedFactsView` 与类型包 31.0.0 的 `ContextImageRemovedDetail` 逐键同形:自有型在册过渡,地板抬到 sdk ≥ 31.0.0 即退(改成类型包那一只的派生别名)。
137
+ - **结局未知那一句住在宿主**:本包只出出口词;宿主若沿用重连窗尽那一行,屏上会说「几分钟内、N 次尝试都没能接回」,而换代收尾实际至多等约 4 分钟、等的是引擎重启。
138
+ - **两形没问到结局也下了结论(与参照终端同)**:等新引擎时接替的引擎先拒了令牌、再换一次又没装上 ⇒ `'engine-stopped'`(在飞命令会被补「被信号 9 杀掉」);刷新令牌的那一瞬间若又换了一次引擎 ⇒ `'credentials-rejected'`。表只读收口那一刻的答复与再问那一刻的代号 / 是否在换;宿主说不出自己在不在换引擎时同理按「不在换」读。
139
+ - **致因从句两形没在真机上造出来过**:「另一只引擎在答」与「本宿主监护下结束」两种致因只由离线格兜着;不实现 `endedBy` 的宿主只给不归因的那一句。
140
+ - **切档口的两处已知限制**(CC-1080;**§134 PM-4**):`unavailable` 不分「位为 `false`」与「没观测 / 读不出 / 没报」(要分请读 `observedPermissionModeWrite` 的读数);接线回执的 `permissionMode` 段本版不投影(切档回执与它共用同一只段读口,接线回执那一段在后续版本)。
141
+
52
142
  ## 0.104.1(2026-10-30)
53
143
 
54
144
  > 主题:悬挂审批在说呈卡协议的服务端上恢复全卡(接入文档 **§133 AR-1 – AR-4**)· 压缩后后台任务表里带读句尾巴的行照认(**§133 BT-1 – BT-4**)。
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.104.1
38
+ **Version:** 0.105.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
@@ -342,7 +342,7 @@ guard still cross-checks the table by name).
342
342
  | `scripts/run-live-pending-ask-test.mjs` | Suspended in-stream asks (the `livePending` section of `GET /v1/approvals`): an absent key reads as not reported (never an empty list or a zero count), an unreadable section leaves the previous snapshot in place, rows are narrow-read (malformed rows dropped and counted, optional flags only-if-true, extra members never cross), the feed digest covers the section so an ask appearing or settling produces a snapshot, the host-gated reconcile cadence re-lists while the push leg is connected and backs off when nothing changes, the tracker surfaces each `approvalId` once and reports it gone once, and the decision runs through the same respond chain as the in-stream frame leg — with the card stating that the tool arguments are not visible and edits on that card never forwarded. Since 0.73.4 a push-side fetch that did not get a readable body (thrown, or an unreadable `livePending` section) is owed a look: it is retried with doubling back-off until one fetch succeeds (never abandoned by count), cancelled by any successful fetch, cleared on stop, and switchable off. The session scope of the suspended-ask view, count and tracker must be chosen explicitly (one session, all sessions, or identity pending): an unresolved or malformed scope answers null counts, an empty view and a tracker that never emits, and the three entry points refuse to compile without a scope. On servers that present approvals as `approval_request` frames (engine ≥7.133.0), a suspended row carries that frame instead of a `tool_approval` one; either opening-frame form is accepted when its `approvalId` matches the row, both go through the same card, holder table and respond call keyed by the row's `approvalId`, the card face is read from `card` and the out-of-card facts (mandated, origin, …) from the frame's top level, and anything outside the two forms still yields the blind card; the `tool_approval` card is byte-identical to the previous release. A malformed top-level `requiresRealApproval` on such a card is recorded in `unreadableEvidence` (the automatic-answer verdict stays `unknown`), and a parked row without an approval id still joins the same live card by run, call id and ask signature — call id, sub-agent run and ask classes read from the card face, origin and mandate from the frame's top level, and none of the out-of-card facts for an unrecognised version — so no second card or second decision appears. |
343
343
  | `scripts/run-leader-conflict-test.mjs` | The leader-run terminal `needs_human` + `result.conflict` (engine ≥7.83.0), read once for all three shells. A `result` without a `conflict` key is reported as `none` and worded as "no conflict details" rather than "no conflict": on the wire it is indistinguishable from an older engine that never reports one, and the reader does not pick a side. A `conflict` that is present but unreadable is a third word, never folded into `none`, because a tree that carries conflict markers must not be rendered as clean. `filesTruncated` is honoured only as `true` (absent means the list is complete; any other value makes the section unreadable), `workers[].applied` is passed through as the boolean fact it is, `salvaged[].patch` is handed to the save path byte-for-byte (an artifact, not screen text; a malformed row is dropped alone, and a salvage list that is present but cannot be fully read is flagged rather than rendered as "no patches"), and `rejHead` — the one on-screen diagnostic — is escaped and bounded before display. The record never throws on hostile input, extra members do not cross, arrays are fresh copies, and the single sentence minted here names the base, the files, which branches landed and which did not, and how many patches can be saved, without ever suggesting a retry: `needs_human` is a run waiting for a person, and the no-details sentence says plainly that it is not proof of a merge |
344
344
  | `scripts/run-mcp-reconnect-test.mjs` | The in-session MCP re-dial verb (`POST /v1/sessions/:id/mcp/reconnect`, engine ≥7.85.0), consumed. The single discriminant is `outcome` and all three answers are HTTP 200, so the reader branches on the word and never on the status; the `unsupported` answer carries exactly five keys and the reader refuses to invent a zero or an empty list for the four fields the engine did not produce, while `accepted` / `refused` treat those four as required and go malformed when one is missing. The tool roster follows the **presence** of `toolNames` (absent = untouched, empty = withdrawn), the connection record passes `errorCode` through as an open set, and every remote-authored string is sanitised and bounded before display. Failures are classified by `errorCode` alone, a missing code is reported as unknown rather than guessed, the verb never throws, and the request-side guard (non-empty name, ≤190 chars) stops a call that the contract would reject anyway. The capability bit reads absent as "cannot tell" rather than "unavailable", and the one sentence the contract insists every UI carries — that re-dialing is a transaction, not a refresh — is minted here once. The engine's answer-deadline reply — a 503 whose machine code says the request stopped waiting before the re-dial settled — is its own verdict arm: it is read by that code alone and never by the status, carries no outcome, and is never rendered as a failure, because the re-dial was not cancelled and may still be running; the one sentence for it is minted here once, and the gate pins that no server release up to 7.117.0 contains the code at all |
345
- | `scripts/run-core-value-ports-test.mjs` | The port-injection seam for ten **engine value-level** facilities (autonomous-loop prompt assembly, permission-rule loosening, tool-policy composition, protocol/retired-name/grammar lookups, rule compilation; the discussion-workflow name port left in 0.96.0 together with that engine feature). This package cannot re-export them (the engine barrel drags Node built-ins into the browser bundle), so it declares the ports and honest-absence readers; a Node host installs the engine's own functions verbatim. The guard pins: every reader returns `undefined` when nothing is installed (never a fabricated empty array or default policy), arguments and results pass through by reference, engine errors propagate unchanged, partial installs read partially, restore functions unwind to the previous bag, and the module source has zero engine imports From 0.90.0 the mirror of the allow/deny policy options is also compared with the engine's own declaration, both ways: the same set of option keys (the engine added an optional `scope` — whether an allow-list hit is a stated grant or only a bound on which tools a lane may see), with `scope` optional and its two-word union and the invalid-name union spelled identically; the value handed to the installed function is passed through untouched. From 0.101.0 the engine retires that option (passing it is refused by name when the policy is built), so the mirror has no `scope` member and the check asserts it is absent on both sides, that the engine refuses it, and that a stray one is still handed to the installed function untouched rather than dropped. Since 0.94.0 there are twelve ports: the engine's allow-list reader joins them, with the same absence and pass-through rules. From 0.94.0 the seam also carries the engine's five route-adjudication functions (`routeKeyOf`, `joinRequestUrl`, `resolveRouteCredential`, `adjudicateModelRoute`, `routePairingStatus`): a host that builds the full `CoreValuePorts` bag fails to compile until it adds them (the gate compiles that bag in memory and checks each missing member is named), the engine's own functions type-check against the mirrors, and each reader answers `undefined` while its port is not installed. Since 0.96.0 the whole engine bag also assigns into the full port type with no cast (the rule-grammar mirror follows the engine). Also in 0.96.0 the engine's rule tool-name judge (`judgeRuleToolName`, reader `ruleToolNameJudgement`) joins the seam and the retired-name table port leaves it (its one reader now asks the judge): the gate pins the swapped name list, pass-through and error propagation for the judge, and compiles the full bag both ways — missing the judge, or still carrying the retired-name table, fails and names the member. In 0.98.0 the full-bag assembly check compiles under three option sets — plain strict, strict plus exact optional property types and unchecked indexed access, and non-strict — and all sixteen engine functions must install into the full type with no error in each (before, the two strict sets rejected the combine-policies and validate-rules members, which were function-typed properties checked contravariantly), with a sabotage control that goes red under all three. In 0.101.0 the nineteenth port is the engine's default root address for the first-party hosted API: a value rather than a function, installed from the engine's package-root constant. Its reader returns `undefined` while nothing is installed (never the literal root), returns exactly what was installed otherwise (no trimming, no shape check, an empty string stays an empty string), the engine's own constant type-checks into the port type under all three option sets, a full-type bag built for the previous eighteen ports stops compiling and names the new member, and the reader body is a single read of the port with the host name absent from the module source. The twentieth port is the engine's reader for an MCP server's `toolFaces` declaration: absent when not installed (never an invented ok), passed through with the same references when installed, the engine's own function type-checks into the port under all three option sets, a full-type bag built for the previous nineteen ports stops compiling and names the new member, and the reader's signature takes a narrow input type rather than an unknown one. The twenty-first port is the engine's covering-rule matcher (whether a rule name the engine reads as covering every tool of a server covers a given tool name): absent when not installed (never an invented false), both arguments passed through and the answer returned as given when installed, a throw from the engine propagates, the engine's own function type-checks into the port under all three option sets, a full-type bag built for the previous twenty ports stops compiling and names the new member, the real engine answers twelve fixed pairs identically through the port, and the reader's signature has no unknown in it. |
345
+ | `scripts/run-core-value-ports-test.mjs` | The port-injection seam for ten **engine value-level** facilities (autonomous-loop prompt assembly, permission-rule loosening, tool-policy composition, protocol/retired-name/grammar lookups, rule compilation; the discussion-workflow name port left in 0.96.0 together with that engine feature). This package cannot re-export them (the engine barrel drags Node built-ins into the browser bundle), so it declares the ports and honest-absence readers; a Node host installs the engine's own functions verbatim. The guard pins: every reader returns `undefined` when nothing is installed (never a fabricated empty array or default policy), arguments and results pass through by reference, engine errors propagate unchanged, partial installs read partially, restore functions unwind to the previous bag, and the module source has zero engine imports From 0.90.0 the mirror of the allow/deny policy options is also compared with the engine's own declaration, both ways: the same set of option keys (the engine added an optional `scope` — whether an allow-list hit is a stated grant or only a bound on which tools a lane may see), with `scope` optional and its two-word union and the invalid-name union spelled identically; the value handed to the installed function is passed through untouched. From 0.101.0 the engine retires that option (passing it is refused by name when the policy is built), so the mirror has no `scope` member and the check asserts it is absent on both sides, that the engine refuses it, and that a stray one is still handed to the installed function untouched rather than dropped. Since 0.94.0 there are twelve ports: the engine's allow-list reader joins them, with the same absence and pass-through rules. From 0.94.0 the seam also carries the engine's five route-adjudication functions (`routeKeyOf`, `joinRequestUrl`, `resolveRouteCredential`, `adjudicateModelRoute`, `routePairingStatus`): a host that builds the full `CoreValuePorts` bag fails to compile until it adds them (the gate compiles that bag in memory and checks each missing member is named), the engine's own functions type-check against the mirrors, and each reader answers `undefined` while its port is not installed. Since 0.96.0 the whole engine bag also assigns into the full port type with no cast (the rule-grammar mirror follows the engine). Also in 0.96.0 the engine's rule tool-name judge (`judgeRuleToolName`, reader `ruleToolNameJudgement`) joins the seam and the retired-name table port leaves it (its one reader now asks the judge): the gate pins the swapped name list, pass-through and error propagation for the judge, and compiles the full bag both ways — missing the judge, or still carrying the retired-name table, fails and names the member. In 0.98.0 the full-bag assembly check compiles under three option sets — plain strict, strict plus exact optional property types and unchecked indexed access, and non-strict — and all sixteen engine functions must install into the full type with no error in each (before, the two strict sets rejected the combine-policies and validate-rules members, which were function-typed properties checked contravariantly), with a sabotage control that goes red under all three. In 0.101.0 the nineteenth port is the engine's default root address for the first-party hosted API: a value rather than a function, installed from the engine's package-root constant. Its reader returns `undefined` while nothing is installed (never the literal root), returns exactly what was installed otherwise (no trimming, no shape check, an empty string stays an empty string), the engine's own constant type-checks into the port type under all three option sets, a full-type bag built for the previous eighteen ports stops compiling and names the new member, and the reader body is a single read of the port with the host name absent from the module source. The twentieth port is the engine's reader for an MCP server's `toolFaces` declaration: absent when not installed (never an invented ok), passed through with the same references when installed, the engine's own function type-checks into the port under all three option sets, a full-type bag built for the previous nineteen ports stops compiling and names the new member, and the reader's signature takes a narrow input type rather than an unknown one. The twenty-first port is the engine's covering-rule matcher (whether a rule name the engine reads as covering every tool of a server covers a given tool name): absent when not installed (never an invented false), both arguments passed through and the answer returned as given when installed, a throw from the engine propagates, the engine's own function type-checks into the port under all three option sets, a full-type bag built for the previous twenty ports stops compiling and names the new member, the real engine answers twelve fixed pairs identically through the port, and the reader's signature has no unknown in it. The twenty-second port is the engine's frozen tuple of the three settings-layer names the importer reads (a value, installed from the engine's package-root constant; the types package carries the matching type but no matching value, so the seam carries the value): absent when not installed (never the three literals), and when installed the very same tuple is returned (no copy, no re-freeze, no shape check, so the engine's frozen tuple stays frozen at the read site). The values are checked element by element with a syntax-tree reader against the engine's own declaration, its compiled initializer, its runtime value and the tuple declared on the port member in this package's shipped declaration file, and the tuple and its element type are checked both ways against the engine's own type and the types package's type with the real type checker; every reader and comparison is first proven against planted samples (a wrong word, a missing member, a swapped order, a missing declaration, an unfrozen initializer, a renamed re-export, a copying reader, a reader with a literal fallback). The engine's own constant type-checks into the port under all three option sets, a full-type bag built for the previous twenty-one ports stops compiling and names the new member, and the reader body is a single read of the port. |
346
346
  | `scripts/run-read-face-posture-projection-test.mjs` | The operator-face `readFace: ReadFacePosture` reader (server >=7.65.0). Three ways of "can't say" are pinned to three different, literal sentences, and none of them may read as "nothing is pinned" — that statement belongs to exactly one case, `face: null`, which is a positive fact reported by the engine, not an absence: not having read an operator response yet, having read one from an engine too old to report the key, and the engine actually saying nothing is pinned are three different next steps for an operator and must not collapse into each other. `source` is read as an open set (the server's closed four words plus an escape hatch) rather than narrowed to an enum, so a new word added upstream is not silently turned into a bad reading. The free-text `note` is sanitized and length-capped before it is ever rendered. A companion pure function flags disagreement between this face and the tenant-facing `capabilities.readFace` — silent only when the two actually agree, honest-absent when either side cannot be read at all, never asserting agreement as a fact. The gate's last leg reads the installed SDK's own `openapi.yaml` directly rather than restating the schema in prose, so the package's leniency cannot quietly drift from the real contract |
347
347
  | `scripts/run-display-body-test.mjs` | The engine wraps text it hands a model in a fence — an opening marker naming the payload, the payload itself, and a closing marker — so the model reads it as data and not as instructions. That fence is minted and read in one place here, which makes stripping it for a human reader this package's job rather than each shell's: a shell that renders the envelope verbatim is showing a person a defence that was written for a model. The reader answers with a discriminated union — fenced, with the label and the payload, or not fenced, with the text as it came in — and it reaches that answer through the **same** matcher the mint side registers, never a second copy of it; the guard proves that by walking the syntax tree of every source file and requiring exactly one literal carrying the marker text, and by requiring the reader's own body to contain no matcher of its own. Eighteen shapes are run through both entry points and required to agree line for line. Anything the package does not recognise — a near-miss in the wording, a hyphen where the marker has a dash, a different case, an opening marker with no close, a close before an open, a truncated close, or any non-whitespace byte outside the pair — comes back unfenced with the input returned **verbatim**: no guessing, no trimming, no repair, because a half-stripped envelope puts a sentence on screen that nobody wrote. Only the outermost layer is removed, so a nested fence, or one forged inside the payload, survives byte-for-byte in the body — those bytes are part of what the engine said, not part of this protocol. Nothing else is washed: control characters, leading and trailing whitespace and a twenty-thousand-character payload all pass through untouched, and so does the label, because sanitising and length-capping belong to the mint point that puts a string on a screen and a passage of text must not have two launderers. A value that is not text is answered with **nothing at all** rather than with an empty payload: the reader never stringifies it, never calls its `toString`, and never emits `[object Object]`, and it does not hand back a body of zero length either — an empty payload is a real reading (a fence can legitimately wrap nothing, and an empty string is an empty string), so folding "there was no readable text" into it would leave a caller unable to show a degraded line at all. Those three stay apart: no text yields nothing, an empty string yields an unfenced empty payload, and an empty fenced payload yields a fenced one with its label. The `fenced` discriminator is always present on a reading, and the label key exists only on the fenced arm, so a missing label is never rendered as an empty one. One shape needed more than the whole-string match this started with. When the engine reports back from a delegated run, the fence is only **one section** of the report: ahead of it sit a frame header, a handful of optional field lines and a section label, behind it a closing instruction addressed to the model, an optional internal identifier and a usage block. A matcher anchored to both ends of the input answers *not fenced* on that, and the whole scaffold - written for a model - goes on screen. The reader therefore also locates the fence **inside** a recognised report frame, using four anchors that are always present and always byte-for-byte fixed, and hands the located slice back to the same single matcher rather than a second one; the guard assembles its corpus from the engine package actually installed (the fence from that package's own constructor, the frame lines read structurally out of the minting file and then compared byte-for-byte with what this package registers), so a rewording or a reordering upstream turns the guard red the day it lands. Three readings are pinned one cell each: a fenced result section yields the payload the delegate actually wrote; a partial-findings section yields that text and says which of the two it is, with the prefix line kept out of the payload; and a section the engine filled with its own no-text sentinel yields an empty payload with a reason, which stays distinguishable from an input that was simply an empty string. Whatever surrounded the fence is returned alongside rather than dropped - the bytes before it, the slice itself and the bytes after it reassemble into the input exactly - and the payload never contains a line of the frame. The criterion deliberately does **not** enumerate the lines outside the fence: several of those field lines carry interpolated untrusted text and cannot be told apart from prose, so requiring every one of them to be recognised would mean that a single new field line upstream sends every failed report back to being unreadable, and a failed report is exactly when a person most needs to read what the delegate said. Fourteen negative shapes hold the line against the easy widening, strip anything that looks like a fence: a fence sitting in ordinary text, a missing frame header, a different sentence where the closing instruction belongs, a report with no closing instruction at all while the fence sits at the very end, a header and label in the wrong order, mismatched open and close tags, an open with no close, a bare unfenced section, a stray line between the label and the fence or between the fence and the closing instruction, an entirely absent section, a no-text sentinel with another line after it, a partial-findings prefix followed by something that is not a fence, and the whole report in carriage-return line endings all come back unfenced with the input verbatim and no frame at all. Above all, **provenance is not in the text**: locating a fence inside a frame happens only when the caller states where the bytes came from, because four anchors can only recognise a shape and never prove an origin. The default reading is byte-for-byte what it was before, so a passage of ordinary prose that happens to quote a report - with real warnings on either side of the quoted part - is returned untouched and those warnings stay on screen; a caller that does state the origin gets the located reading, and even then every surrounding byte comes back alongside. Twenty-three malformed origin values fall back to the narrower default without throwing: a near-miss in case, a camel-cased spelling, the right word padded with spaces or tabs or a newline or a zero-width character, a string wrapper object, an object whose `toString` or `valueOf` reports the right word, and an object whose converters both throw. The guard compares the caller’s argument for **exact equality** and nothing else — no trimming, no stringifying, no calling the value’s own converters, because that would let a value of unknown provenance choose its own lane — and a source-level cell requires that the argument reach the comparison unreassigned and unnormalised, with its own two-way check that those patterns speak. The two accepted words are read off the published type rather than copied into the guard; adding a third word later is a type-compatibility change for any caller that switches exhaustively on them. Each of the three anchors must also be **unique** in the text, and ambiguity means the reader declines. The reason is not hypothetical: the frame header interpolates the task's own description verbatim, and the engine only requires that description to be a string, so it can carry newlines and a complete set of protocol lines. Any rule that picks one candidate out of several can therefore be made to pick the planted one, hiding the real result among the surrounding bytes - a guard cell reproduces exactly that, with a planted section and a real one, and requires the reader to decline and the real text to stay on screen. Two reports back to back are the same ambiguity and are declined the same way, with both payloads left visible; a delegate that quotes any one of the three anchor lines inside its own answer also falls back to the input verbatim, which is the registered cost of the rule, and a discrimination cell shows the same corpus reads cleanly once the quoted line is gone. The two legs are not the same shape either: the forked one ends at its closing instruction with no trailing bytes at all, and that real shape has its own cell. The witness arm reads each leg only inside its own array of lines, decodes every extracted literal to its **runtime** value rather than trusting the spelling in the source, and fails loudly if it cannot - an escape rewrite upstream leaves the runtime label unchanged while the spelling diverges, and since that same extracted value builds the corpus and serves as the expectation, trusting the spelling would close a self-proving loop. The two payload labels are therefore also pinned in the guard and compared against what was extracted, so an equivalent rewrite stays green while a real rename turns red the day it lands. A delegate that quotes the frame lines inside its own answer does not move the location, and those quoted lines survive in the payload byte-for-byte. For a recognised report frame the reader also hands back the header slots a shell used to assemble by counting lines: the state line (read only when it sits immediately after the frame header and is the only state line in the frame, so a line planted in the task description cannot masquerade as the engine's own), an error line and a usage line (each only when it is the only one of its kind), and every line it did not claim, verbatim and in order; when the state line cannot be read from that position all four slots stay absent and every byte stays in the remainder |
348
348
  | `scripts/run-display-cap-order-test.mjs` | The order in which untrusted text is sanitised and length-capped, across every mint point that puts an engine- or database-supplied string on a screen. The sanitiser rewrites each invisible character as a six-character escape, so capping the **raw** string first and escaping afterwards hands the screen six times the width that was budgeted — a forty-character allowance becomes two hundred and forty. The guard does not hardcode that allowance, because each mint point wraps its field in different fixed prose and the prose moves: it anchors on the deciding quantity instead, feeding one benign and one control-character input of the same length through the same mint and requiring the second not to come out longer. That criterion is immune to wording changes and stays sensitive to the expansion, and it is `<=` rather than `==` on purpose — a correct escape-then-cap backs the cut off a partially-consumed escape token, so the control-character line is legitimately the shorter of the two, and demanding equality would score that avoidance as a regression. Each mint is bracketed by two positive controls (the input really reaches the screen; the cap really engages) and the expansion predicate is shown to turn red against a deliberately cap-then-escape reference, so an all-green run cannot mean the guard simply measured nothing. The shared mint point is checked directly for the two avoidances it owes — never splitting an escape token in half, which would leave something on screen that looks like the beginning of a complete answer, and never splitting a legal surrogate pair, which would manufacture the very lone surrogate the sanitiser exists to catch |
@@ -584,7 +584,7 @@ guard still cross-checks the table by name).
584
584
  | `scripts/run-bare-sentence-ratchet-test.mjs` | A ratchet on bare English sentences in the package source. New user-facing sentences belong in the message catalog, where they can be rendered per language; the existing ones are being moved in batches. The gate evaluates string expressions statically before counting words (string literals, templates, `+` chains, `.join` on literal or constant arrays, array spreads and `const` bindings followed across modules), so the same sentence written nine different ways is counted as one sentence, and it records, per file, how many times each normalized sentence occurs. A new sentence, or one more occurrence of an existing sentence, fails; fewer occurrences are reported so the baseline can be lowered. Developer diagnostics (thrown errors, console output), property names and type positions are not counted. A second check keeps the set of exported English sentence constants from growing: new sentences must be functions, so a language can be passed. |
585
585
  | `scripts/run-subagent-model-word-test.mjs` | The subagent model word resolver. `resolveSubagentModelWord({ word, liveCatalog?, local? })` returns the word to send as `agents[].model` and a closed reading, never a sentence: catalog names are sent as written, tier words, provider aliases and `best` are sent in canonical lower case so the target engine expands them with its own tier table, a provider model id is replaced by its catalog name, a case-insensitive match is accepted only when it is unique, and anything else is passed through for the engine to judge. When the host asserts that it started the engine itself and passes that engine's active tier group (`local`), and that engine's catalog is also known, the reading also previews where a tier word lands, or why it cannot land (inactive tiers, nothing bound down the chain, a binding missing from the catalog); an unreadable tier table or a missing catalog means no verdict, never an inactive one, and the preview never changes the request. `subagentModelWordProblemLine` renders the one sentence for those cases through the message catalog. The gate compares the preview with the installed engine's own tier expansion on two thousand generated cases, proves the comparison catches five known wrong orders, and checks canonical spelling, absence, hostile inputs, frozen readings and the sentences in both languages. |
586
586
  | `scripts/run-event-leg-compose-test.mjs` | Event leg composer: one table decides, for each of the three leg kinds (live stream, durable attach, continuation opened by the question bridge), which stages a frame passes between the SDK event stream and the projection, and the composer only takes host ports. The frame-level stages (run and session adoption, sub-agent owner stamping, in-stream question / elicitation / tool approval frames) have pairwise disjoint frame-type domains (recomputed from the source) and run once on every stream, including each continuation the bridge opens. The run a frame belongs to is recorded per frame object and taken as a pair from one source: on a live stream the terminal frame's run wins over the header and the first frame (an idempotent replay found at row creation), a refused submission is reported with the candidate run id it never created and leaves its frame unbound, frames before a run id is known and frames a host copies without the inheritance helper are unbound, and nothing ever falls back to the most recent binding. A type-checker scan traces every place in the bridge modules where a frame flows (yield, return, frame arrays, frame tables, frame parameters) back to its source and accepts only an untouched reference or the inheritance helper; nine planted shapes (literal copies, Object.assign, structuredClone, helpers with inferred return types, concrete frame subtypes, a frame escaping through a local object, a file in a subdirectory) prove it bites. Driven through the real question bridge: a continuation's frames are stamped and swallowed stage by stage, the approval countdown is shown on the live stream and fully dropped on replayed streams, a hand-off moves frame bindings, owner stamps and the bound callback to the new run, rewritten, collateral and synthesized frames answer the run they came from, the terminal port fires exactly once per released terminal frame and never for the parked one, the bridge dependencies are read from the host object as-is (lane accessors not executed, a throwing accessor does not break the stream, private-field getters keep their receiver), and nothing is persisted once the host says the leg is stale or its staleness check cannot be read. Missing question / elicitation / tool-approval ports fail at assembly with a named host_port_missing error; non-HITL frames pass through unchanged and in order. |
587
- | `scripts/run-upstream-type-shadow-test.mjs` | Same-name types across this package and its runtime upstreams. Every type name exported from the package root that is also exported as a type by a runtime upstream (the packages listed under `dependencies` and `peerDependencies`, including the typed subpath entries of their `exports` maps, with wildcard entries expanded from the files on disk) must resolve to that upstream's own symbol, re-exported. A local declaration under the same name fails the guard even when its shape matches today, because two declarations under one name drift apart on the next upstream change, and so does a re-export of a different upstream symbol under that name; readings this package mints carry a different name (suffix `Reading`). Module augmentations of a runtime upstream from the package sources fail as well. The only registered exception is an upstream that lags behind the package that owns the vocabulary: the row names the owner, which must be a declared dependency other than this package and whose same-name type must be declared inside that package, and the guard re-proves on every run that this package's type equals the owner's while the upstream's type is a strict subset of it. Rows cover one name and one upstream entry each, fail once they are past their due-by version (at most two minor lines ahead), once the premise no longer holds, or once the name no longer collides. Two types are the same only if each is assignable to the other and their structural fingerprints match, so one extra optional member, a readonly modifier or a wider return shape counts as a difference. A row may also be held for the declared support floor: while the installed upstream already matches the package's type but the support floor still lies below the upstream version where that happened, the row stands; it fails as soon as the floor reaches that version, or when the floor cannot be read. The ruler proves itself on an in-memory corpus of package and upstream modules and on two planted declarations checked against the real upstream name set. Retired copies stay retired: five formerly hand-written types are checked to remain aliases of the upstream type (same type object, mutually assignable, same structural fingerprint, and never back in the debt register), and three runtime constants are checked to be the upstream's own frozen array by reference, with the word list and order pinned, the build-time snapshot read inside the package equal to it, and no second same-name export left inside the package. |
587
+ | `scripts/run-upstream-type-shadow-test.mjs` | Same-name types across this package and its runtime upstreams. Every type name exported from the package root that is also exported as a type by a runtime upstream (the packages listed under `dependencies` and `peerDependencies`, including the typed subpath entries of their `exports` maps, with wildcard entries expanded from the files on disk) must resolve to that upstream's own symbol, re-exported. A local declaration under the same name fails the guard even when its shape matches today, because two declarations under one name drift apart on the next upstream change, and so does a re-export of a different upstream symbol under that name; readings this package mints carry a different name (suffix `Reading`). Module augmentations of a runtime upstream from the package sources fail as well. The only registered exception is an upstream that lags behind the package that owns the vocabulary: the row names the owner, which must be a declared dependency other than this package and whose same-name type must be declared inside that package, and the guard re-proves on every run that this package's type equals the owner's while the upstream's type is a strict subset of it. Rows cover one name and one upstream entry each, fail once they are past their due-by version (at most two minor lines ahead), once the premise no longer holds, or once the name no longer collides. Two types are the same only if each is assignable to the other and their structural fingerprints match, so one extra optional member, a readonly modifier or a wider return shape counts as a difference. A row may also be held for the declared support floor: while the installed upstream already matches the package's type but the support floor still lies below the upstream version where that happened, the row stands; it fails as soon as the floor reaches that version, or when the floor cannot be read. The ruler proves itself on an in-memory corpus of package and upstream modules and on two planted declarations checked against the real upstream name set. Retired copies stay retired: five formerly hand-written types are checked to remain aliases of the upstream type (same type object, mutually assignable, same structural fingerprint, and never back in the debt register), and three runtime constants are checked to be the upstream's own frozen array by reference, with the word list and order pinned, the build-time snapshot read inside the package equal to it, and no second same-name export left inside the package. A type declared here whose shape matches an upstream type under a different name must become an alias or a re-export of it; while the declared support floor still predates the upstream version that introduced that name, the local type stands only as a registered transitional item in the retirement ledger, and that item fails as soon as the floor reaches the version. |
588
588
  | `scripts/run-session-id-canonical-test.mjs` | The session-id canonical reader (`canonicalSessionIdOf` returns the canonical lower-case id or null; `sessionIdRefusalOf` says why not: not a string, wrong shape, bad version nibble, bad variant nibble). The rule is the server's own session-id rule, copied once because the server package is neither a dependency nor exporting its judge; the suite pins the copy against the installed server fixture's source text and checks a superset relation on a generated pool (everything the server accepts is returned unchanged, everything returned is accepted by the server, the only deviation being that upper-case hex is folded to lower case). It also checks a table of shapes, hostile inputs that must neither throw nor be touched, and that exactly one regular expression carries the rule. |
589
589
  | `scripts/run-print-model-status-test.mjs` | The print-lane model-service status frames. `retryStatusFromArm` reads the internal retry arm (the same reader the interactive lane uses; sub-agent frames and recovered are told apart), and `createPrintModelStatusTracker` keeps the per-turn status ledger for the `-p` stream-json lane: a retryable failure with both counters becomes a `system/api_retry` frame in the reference client's shape (its keys keep their meaning, every extra key is `_sema_`-prefixed, the category word follows the reference client's table), waiting for the first token, counter-less reconnects, give-ups and replays of the same back-off become `system/_sema_model_status` frames, and an error result carries `_sema_last_model_status`. The sentence comes from the retry-line sentence source. The suite drives the real stream projector end to end, checks the reader against the interactive lane, the replay de-duplication, the category table, the key set, the turn lifecycle (including that a synthetic failure row does not clear the ledger), hostile input, and that the new module holds no module-level state. |
590
590
  | `scripts/run-subagent-started-projection-test.mjs` | The delegated sub-agent start frame. The stream projector turns the engine's per-cycle `task_started` frame into a neutral internal arm and the adapter turns that into a `subagent_started` chrome event (no transcript row, no adapter state; the event hangs on the delegating card's lane when the frame names the card, otherwise on the main lane), and `publishEngineAgentPanelStart` feeds the start time into the panel reducer as evidence: it only fills a missing `startedAt` and never creates a row or changes how identities merge. The suite checks the projection key by key (absent keys stay absent, bad shapes drop only the key), the end of the dropped-frame trace line for this frame type (a real unknown frame still gets one), the chrome event end to end through the real stream projector and through the real SDK's SSE parser, the reducer scenarios, and, over 600 random frame sequences, that adding start frames changes no row delta except the `startedAt` field. |
@@ -643,7 +643,7 @@ guard still cross-checks the table by name).
643
643
  | `scripts/run-transport-cause-chain-test.mjs` | The transport-failure cause-chain self-check. One reader walks the chain of causes behind a failed request (bounded depth, objects only, protected slots read as data) and one judgment, `transportChainReading`, answers whether the failure is transient, answered by the service, or unknown, with a retry facet and a code facet. The suite replays tens of thousands of generated error shapes through the previous decision-retry and timeout readers (embedded as the oracle) and requires identical answers, requires the triage reader to differ from the previous one only in the documented classes, requires all three paths to agree on one corpus, and requires hostile shapes (cycles, throwing getters, revoked proxies, very deep chains) to stay bounded and never throw. A source pass pins that no other file carries its own cause-recursion and that the retired helpers are gone, and an end-to-end cell counts real resend attempts through the approval bridge. |
644
644
  | `scripts/run-usage-limit-auto-resume-test.mjs` | The usage-limit auto-resume state machine, as a pure function of (state, event, clock reading). The host supplies the clock and runs the timers; the package only decides. The suite replays a frozen vector table recorded from the reference shell loop (every phase edge: offer, arm, fire, escape, supersede, context change, repeated-hit breaker, horizon), checks the wording in both catalog languages, walks tens of thousands of random event sequences (including bad clocks, unknown options and malformed events) and requires the machine to stay inside its closed phase set with a cleared prompt on every terminal phase, and a source pass pins that the module touches no timer, no clock and no IO. The quota-exhausted reading and the sentence are read from one place. |
645
645
  | `scripts/run-termination-intent-test.mjs` | The termination-intent ledger and the bounded settle wait for an interrupted run. The ledger remembers, per task, that the caller itself ended the run (with a source word and a time-to-live read from an injected clock) so that the next ending can be told apart from a crash; a weak abort edge never overwrites a strong source, expiry is strict at the boundary, rows are capped, and compare-and-restore and compare-and-rollback refuse when another writer got in between (checked over an exhaustive interleaving model). The settle wait polls the run row for a bounded time on its own budget timer and linked abort signal and returns one of four verdicts. The suite also drives the real active-run self-heal path and requires that only a strictly true answer shortcuts it, and a source pass pins that the ledger touches no ambient clock or timer. |
646
- | `scripts/run-engine-generation-test.mjs` | The engine-generation convergence checks. When the engine behind a session has been replaced, an approval card whose window is still certainly open can be withdrawn, and everything else is kept: the margin must be strictly greater than five seconds, the engine must not have said the window closed, and parked rows are never withdrawn, so the failure direction is always one extra card kept, never one wrongly removed. The suite pins golden cases, the window boundary, a seeded oracle comparison over thousands of generated inputs (inputs stay deeply frozen and results are frozen), the generation-reading truth table, an exhaustive decision table for what to do when a reattach ends on a replaced engine, and the bounded wait for the next engine to be installed, including a throwing clock, cancellation and the budget. |
646
+ | `scripts/run-engine-generation-test.mjs` | The engine-generation convergence checks. When the engine behind a session has been replaced, an approval card whose window is still certainly open can be withdrawn, and everything else is kept: the margin must be strictly greater than five seconds, the engine must not have said the window closed, and parked rows are never withdrawn, so the failure direction is always one extra card kept, never one wrongly removed. The suite pins golden cases, the window boundary, a seeded oracle comparison over thousands of generated inputs (inputs stay deeply frozen and results are frozen), the generation-reading truth table, and the bounded wait for the next engine to be installed, including a throwing clock, cancellation and the budget. The settle table for a run whose engine generation has ended is checked exhaustively against an independent table, and a thin host driver over it is compared with a transcription of the reference terminal settle loop over about 1.8 million scripted runs (exit, probe count, refresh count and every wait budget must match), with eight deliberate rule breaks that must each be caught. A 401 answered while the next engine is coming up waits like an unreachable engine (one shared budget, at most eight rounds); when no stable successor appears and the last answer is still a 401, the outcome is reported as unknown rather than as rejected credentials or a killed command, and malformed input ends the same way. |
647
647
  | `scripts/run-reattach-window-test.mjs` | The pure decisions behind a windowed reconnect loop. A wall-clock window (300 s by default) bounds the whole attempt; backoff starts at one second, doubles up to a 30 s cap and honours a server-sent wait; a run the engine does not recognise is only declared unknown after several consecutive not-found answers spread over a minimum elapsed time, and any frame from the stream resets that count. A poisoned stream frame is tolerated once (resume from the last good frame) and raised the second time at the same position. The suite drives the pure judgments in a loop and requires the same outcome, beat count, per-beat wait and confirmation count as a transcription of the reference terminal loop over seeded random scripts and tunings, and pins that malformed inputs stay bounded. |
648
648
  | `scripts/run-tool-history-accepted-test.mjs` | The accepted-history bit on a result frame. After a tool-history mismatch parks a session, the next ending that the engine actually accepts is flagged once with `_sema_tool_history_accepted`, so a way-out picker does not keep pre-selecting the broken turn. The suite checks the key and its reader, the ledger semantics (reported once; a rate-limit, failure or blocked ending in between does not lose the bit; a replay is not an acceptance), an independent oracle over seeded random sequences, the real stream pipeline end to end, and that the key is minted from exactly one place and registered in the type-superset table. |
649
649
  | `scripts/run-attachments-credential-word-test.mjs` | What happens when the attachments setting holds something that looks like a credential. A single unrecognised word used to fall back silently to the defaults, while naming a retired key was refused loudly. Now a single word, or any list entry, that has the shape of a known credential is refused: the thrown error and the on-screen sentence both leave the value out (the token field carries the redaction marker, never the text) and say that a credential does not belong in this setting. Ordinary unrecognised single words such as a typo keep their historical fallback to the defaults, and every recognised spelling answers byte for byte as before. |
@@ -677,7 +677,14 @@ guard still cross-checks the table by name).
677
677
  | `scripts/run-core-7600-pick-projection-test.mjs` | Readings that follow the engine's 7.60 changes. The MCP failure kinds include `host_refused` (a host-supplied pipe did not send the request; safe to retry, no liveness reading), and the wiring-receipt row and the per-server connection reading carry it like any other failure word. A `status` frame's `seat` (which seat's model call the frame is about; absent means the main loop's own call) reaches the retry status through both projection layers, open set, malformed values dropped on their own, never minted when absent; a frame about another seat is not labelled with the session's model, offers no switch-model recourse, and in `-p` stream-json takes the `_sema_model_status` frame with `seat` instead of the `api_retry` frame. The single-image size limit used by the human-turn encoder is generated from the engine's image admission table, with the same value as before. The cost breakdown type no longer names the line the engine removed, while older engines' line still passes through. The extension kernel identity reading reports contract 6 for the 7.60 kernel; the kernel the `./mods` subpath passes through and the engine's own kernel carry the same contract number and event table, with the four new stations defined and `tool.call` failing as `refuse`. Engine names this package does not read are pinned to zero code reads. |
678
678
  | `scripts/run-session-row-tag-projection-test.mjs` | Conditional writes on a session row. A row's `rowTag` is read only when it is a non-empty own string (absent means the row cannot be written conditionally; never an empty string); the seat record carries it and the seat patch request carries `expectedRowTag` copied from the same row, with both seat gates checking the shapes. After a refusal the next step reads the same way through the real client error: already applied (the row already holds the values written), decide again against the current row and its new tag (never resend as is), a read-only mirror (fork first), or an older replica that does not know the tag (keep the pending change, never drop the tag and resend). |
679
679
  | `scripts/run-server-7139-pick-projection-test.mjs` | Readings that follow the server's 7.139 changes. The repin is checked against the installed type package itself (the execution-placement and children-retire calls exist, the old device-binding read does not, the row tag, request tag and background-row session id are optional keys). Each new refusal code from this release reaches the generic failure classifier as an HTTP verdict carrying the server's own code and words without being taken for a dedicated arm, and the resume fold names it. A background-agent row carries its child session id only when the server sent a non-empty own string. Names the server or type package removed have no code-position reads left in the package sources. |
680
+ | `scripts/run-display-decision-body-profile-test.mjs` | The reading profile for the body of a decision card — the multi-line text a person reads before choosing, such as the plan on a plan-review card — exported as one frozen options object so that every shell reads the same one instead of minting its own. Ordinary typography is left alone: the space-class characters (the full-width space, the no-break space and the rest of that class) and the two-character CRLF line break are not marked, because plans written in Chinese use full-width spaces and Windows editors save CRLF. Everything that can pass for a space without being one, or that cannot be seen at all, is still marked: the Hangul filler characters, the blank Braille cell, zero-width and bidirectional controls, the other control characters and a lone carriage return. The profile needed only two narrow additions to the options, each a new accepted value of an existing switch (`blanks: 'fillers'` and `keepLayout: 'crlf'`), and every old value behaves exactly as before. The guard runs a matrix of what stays unmarked and what stays marked, in the plain form and in the injective backslash-paired form, which must carry the same set of marks; walks the whole Basic Multilingual Plane to prove the new blank-character value splits the existing table into exactly its two halves — the space class and the fillers — rather than keeping a second list of code points, and that the source holds each filler code point in one place only; pins that every old option combination (648 of them, over 104 texts that include credential-bearing strings) still yields byte-identical output and marks to the previous release; fuzzes the credential scanner with the new values to prove they read exactly like the old switch wherever the two overlap; and carries three negative controls inside the guard — the space class marked again, CRLF marked again, the fillers let through — each of which the matrix must turn red on, with the red reading printed, so an all-green run cannot mean the matrix measured nothing |
681
+ | `scripts/run-run-follow-projection-test.mjs` | One shape for "this screen is no longer following run X". The fact is minted only where the package's re-attach judges give up (count-based policy, wall-clock window, headless continuation), carries the run and the last break cause when known, and renders as one catalog sentence (English / Chinese) that never says failed, finished or resend and never mentions approval cards; the failure reading stops offering retry for it, the seat reader rejects a contradictory retry pair, and the headless idle line becomes one combined sentence (byte-identical when the fact is absent). An in-leg draining ladder that runs out is handed up exactly once through the ask-bridge `onBreak` dependency (same port on the root entry and the composer, carrying the run the bridge follows after a hand-off), and a lane that never re-attaches uses a zero budget so no leg is left without an upper layer. Also pins the `isStreamEndedWithoutTerminalError` predicate. |
682
+ | `scripts/run-plan-review-card-facts-projection-test.mjs` | Plan-review card facts. A plan-review card now has exactly one set of facts: which gate it asks about (task id and checkpoint id) and which plan text it would approve, or why that text cannot be shown. The package computes them when the stream stops at a plan gate, from the structured result of the main-lane plan tool call, and freezes them. The chrome event, the question frame the package publishes, the options on the card and the answer reader's domain are all derived from those facts, and only a plan that can be shown as written offers approval; otherwise the card offers reject and ask-for-the-plan-again. Each gate gets its own card instance with its own question id. The gate drives the real stream and the real arm, answer and reopen paths through the package root: clean, marked, oversized, missing and ambiguous plans, sub-agent plan calls, replays and reconnects, frames without a gate identity, malformed frames, the plan-file form, other review gates, an answer from a retired card, an approval label on a withheld card, ask-again with a note, a frame round-tripped through JSON or corrupted, a new gate on the same run with no plan call in between, a conflict on one gate followed by a replayed terminal frame, a run record pushed out of the bounded registry, a temporary reading after a stored one, and that the chrome facts and the frame facts are the same frozen object. Cards the package mints when it reopens or resumes a review take their facts by the same rule: the facts already stored for that gate when they are current, otherwise the run's ledger is read back (at most about three seconds; the run must be parked at the same gate before and after the read, two passes must agree, and the gate identity must match the one the caller asked for), and a reopened card offers approval only when that reading shows the plan. The gate also drives the self-heal reopen with the gate identity from a busy reply (no ledger read), concurrent reopens of one gate (one ledger read, one card), a stream that moves to a new gate while an older ledger read is in flight (the older reading is dropped and the new card stays), a retry after an unreadable reading, a conflicting reading that swaps the card instance, and the stored high-water mark. |
683
+ | `scripts/run-server-7141-pick-projection-test.mjs` | Readings that follow the server's 7.140 and 7.141 changes. The repin is checked against the installed type package (rule entries may be a string or a `{ rule, layer }` object, the memory backend word is `mysql`, the new keys are optional). The memory sync reading appears only when the server sent a well-formed `memorySync` and reports a parked capture as parked, and the sentence for a paused sync names the layers only when every name is printable and short, and names the operator's way out only when every layer reports it; an erasure refused because the personal store could not be judged reads as not erased; the engine lease on the health reading needs both numbers; a workflow completion carries its completion id through the stream, the adapter and the notification facts, while the engine's diagnostics text is offered for display only and never enters the model-facing notification body or the queued item; a subagent tool call the engine closed without running is shown as not executed rather than as a failure; the permission-mode write and MCP transport capability bits are read with absence kept apart from false, and an MCP server whose transport the engine says it does not support is held back with a sentence naming the transport. When the engine reports layered rule entries, deny and ask rules from settings files go out as `{ rule, layer }` (deduplicated within a layer, not across layers), request-level rules and allow rules stay plain strings, and with the bit absent the body is byte-identical to the previous release. Names the server or type package removed or that the package does not consume have no code-position reads in the package sources, and tasks are submitted only through the streaming call, so the reply body of a non-streaming submit is never parsed. An MCP entry's `sessionMetaKey` goes onto the request as written on both the local and the center config paths, and is left out when not set. |
684
+ | `scripts/run-notification-completion-dedup-test.mjs` | One completion reaching the client over several channels is shown once. A background completion can arrive as the live notification frame, as a parked-inbox entry replayed later, as a frame replayed after a reconnect, and as a fleet notification; the service now stamps the same completion id on all of them. The render de-duplication of task notifications reads that id: a notification is identified by its locally computed key, which is always present, plus the completion id when the notification is terminal, not external and carries one, and every key a notification carries is recorded as soon as it is seen. A notification whose local key was already seen is a replay of the same frame and is skipped entirely, as before; one whose only match is the completion id is another copy of the same completion and is not rendered again, while its other duties (withdrawing a still-queued card of the same run, closing the panel row, the terminal hint) still run exactly as they did before. So two copies of the same completion whose local keys disagree (for example one carries a sequence number and the other does not) render once, three mixed copies render once, and two completions that differ in both the id and the local key still render twice. The local key stays because three things never carry the id: external notifications, frames from servers that predate the id on inbox entries and replays, and records restored from a saved transcript or seeded by a host; a frame that carries the id is still matched against those records, and the exported ledger keeps both keys so a host reading it by the local key sees no change. The cross-channel checks that stop a card already shown from a status probe, a fleet notification or a poll result from being fed or shown a second time stay as they were, because some of those sources never carry the id; the gate pins each of them, and the queue withdrawal that removes a still-queued card when the engine's echo arrives. Ordering, concurrency and failure are each exercised: all 24 arrival orders of four channels show the completion exactly once, two interleaved streams on one adapter render it once, and an unreadable, empty or non-string id falls back to the local key without throwing |
680
685
  | `scripts/run-background-task-table-projection-test.mjs` | Background-task table rows after a compaction, read the way the engine mints them: a row is the `- [<id>] Task "<description>" ` prefix, a status phrase, and at most one optional tail that starts with ` — ` and runs to the end of the line. The tail is projected verbatim onto the optional `read` field of the task-status attachment (absent means the engine sent none — never an empty string), rows without a tail stay byte-identical to the previous release (a frozen copy of the old reader and a seeded random pool of 4000 batches prove it), a `parked` row is recognised with or without a tail, a second ` — ` inside the tail stays part of it, and unknown phrases and the `(+N more …)` footer line are still not recognised. The status phrase is always read after the last `" ` (quote plus space) on the line, so a description that itself contains a phrase never turns an unknown status into a known one; a table whose row description breaks across lines (an opening quote never closed on a line that is not the last non-empty one) is not read as rows at all and falls back to the generic row. The engine's own table renderer is driven for real, status by status and read form by read form. |
686
+ | `scripts/run-permission-mode-verb-projection-test.mjs` | Switching the permission mode of a running turn. `setRunPermissionMode()` sends the switch only when the permission-mode write capability reading is present and true; otherwise it answers `unavailable` without sending anything. The outcome is always a value, never a throw, and only `switched` carries a mode: the mode now in force is the engine receipt's `permissionMode.effective`, not the word that was sent. A fold by this deployment's floor or the task's settings is reported in `admission`, kept apart from the engine's own fold. `persisted: false` comes with a reason and a notice that a later resume returns to the earlier mode. Rejections are classified by status and machine code (not found / not running / refused / not available); a 500 `corrupt`, which the server sends before the switch reaches the engine, is a refusal, while other server errors and lost connections stay unconfirmed; an answer that does not match the documented seven-key shape, or contradicts itself, is unconfirmed rather than read as a switch. `runPermissionModeSwitchLine()` gives one sentence per outcome in English or Chinese. |
687
+ | `scripts/run-run-ledger-snapshot-test.mjs` | Run-ledger snapshot reader. `readRunLedgerSnapshot` reads, for a run parked at a gate, the highest ledger sequence number, the parked call's own `tool_start` frame and, optionally, a fold over the numbered frames. It answers only when three pieces of evidence agree — the reader proved the end of the stream, the tail sits next to a park marker, and two concurrent passes end on the same number — and the run is parked both before and after the read; otherwise it answers `null`. A pass cut short by the server's draining frame is not a completed read and is re-read on the package's draining ladder. A fold that depends on frame order rejects a pass whose numbers go backwards or repeat, or that carries an unnumbered frame the fold cares about, and both passes must fold to the same answer. All readers are injected, nothing throws, and the budget is a hard wall-clock limit: a parked-state read or a ledger iterator that hangs and ignores the abort signal still yields `null` when the budget runs out. |
681
688
 
682
689
  Each suite carries a floor that only moves up — a refactor that stops executing a group of
683
690
  assertions is a failure, not a quieter pass. Guards anchor on the **installed artefact's content**
@@ -2,7 +2,7 @@ import { ccToolDenialKindForToolEnd, gateOutcomeOf } from '../gateOutcome.js';
2
2
  import { terminalOwnerOf } from '../seam.js';
3
3
  import { isSubFlowSegmentEnd, SEGMENT_END_IDENTITY_KEYS, snapshotSegmentIdentity, snapshotIsSubFlow } from '../adapter/types.js';
4
4
  import { classifyPeerNotification, renderPeerFrameTranscriptText } from '../peerFrames.js';
5
- import { dropQueuedNotificationsForRun, freezeTaskNotificationFields, isEngineWorkflowNotified, isWorkflowCompletionCardEnqueued, markEngineWorkflowNotified, noteOwnWorkflowRun, normalizeTaskNotification, noteWorkflowCompletionCardEnqueued, renderTaskNotificationXml, taskNotificationDedupKey } from '../notifications.js';
5
+ import { dropQueuedNotificationsForRun, freezeTaskNotificationFields, isEngineWorkflowNotified, isWorkflowCompletionCardEnqueued, markEngineWorkflowNotified, noteOwnWorkflowRun, normalizeTaskNotification, noteWorkflowCompletionCardEnqueued, renderTaskNotificationXml, taskNotificationRenderKeys } from '../notifications.js';
6
6
  import { retryStatusFromArm } from '../retryStatus.js';
7
7
  import { steeringInjectedToAttachments } from '../steering.js';
8
8
  import { projectDiagnosticsFrame } from '../diagnostics.js';
@@ -15,7 +15,7 @@ import { readToolHistoryMismatchKey } from '../toolHistoryMismatch.js';
15
15
  import { isMintedRowCcError, readProviderRejectionResendKey } from '../providerRejection.js';
16
16
  import { rowKindOf } from '../rowKind.js';
17
17
  import { terminalCodedDetailOf } from '../runTerminal.js';
18
- import { contextUsageReadingOf, guardedRead, humanInputAttachmentsOf, ownSnapshotRecord, ownSnapshotWhenTyped, readOwnDataValue, promptManifestIdentityOfFrame, promptManifestSectionViewOf, readFrameStringKey, readFrameTrueMark } from '../ownKey.js';
18
+ import { contextUsageReadingOf, guardedRead, humanInputAttachmentsOf, ownSnapshotRecord, ownSnapshotWhenTyped, readOwnDataValue, promptManifestIdentityOfFrame, promptManifestSectionViewOf, readFrameStringKey, readFrameTrueMark, workflowCompletionExtrasOf } from '../ownKey.js';
19
19
  import { CORE_USER_ATTACHMENT_LABEL_MAX } from '../generated/engineFactTables.js';
20
20
  import { ESCAPE_FACE, paintAndCap, washCredentials } from '../displayUntrusted.js';
21
21
  import { resolveEnginePanelTaskId } from '../engineAgentPanelStore.js';
@@ -961,15 +961,19 @@ const taskNotificationArm = function* (m, { ctx, idOf, panel, inst }) {
961
961
  : {}),
962
962
  });
963
963
  }
964
- const dedupKey = taskNotificationDedupKey({
964
+ const dedupKeys = taskNotificationRenderKeys({
965
965
  taskId,
966
966
  status: rawStatus,
967
967
  ...(typeof n.seq === 'number' || typeof n.seq === 'string' ? { seq: n.seq } : {}),
968
968
  ...(typeof n.task_type === 'string' ? { taskType: n.task_type } : {}),
969
+ ...(fields.completionId !== undefined ? { completionId: fields.completionId } : {}),
969
970
  });
970
- if (inst.hasRenderedNotification(dedupKey))
971
+ const sameFrameSeen = inst.hasRenderedNotification(dedupKeys[0]);
972
+ const sameCompletionSeen = !sameFrameSeen && dedupKeys.slice(1).some((k) => inst.hasRenderedNotification(k));
973
+ for (const k of dedupKeys)
974
+ inst.addRenderedNotification(k);
975
+ if (sameFrameSeen)
971
976
  return;
972
- inst.addRenderedNotification(dedupKey);
973
977
  if (n.task_type !== 'external' && terminal) {
974
978
  yield chrome({
975
979
  kind: 'notification_terminal',
@@ -990,6 +994,8 @@ const taskNotificationArm = function* (m, { ctx, idOf, panel, inst }) {
990
994
  if (peerFrame === null && n.task_type !== 'external')
991
995
  markEngineWorkflowNotified(taskId);
992
996
  const fact = peerFrame === null ? freezeTaskNotificationFields(fields) : undefined;
997
+ if (sameCompletionSeen)
998
+ return;
993
999
  yield transcript({
994
1000
  type: 'user',
995
1001
  message: {
@@ -1019,7 +1025,8 @@ const workflowCompleteArm = function* (frame) {
1019
1025
  const wfStatus = m.status === 'failed' ? 'failed' : 'completed';
1020
1026
  const wfSummary = typeof m.summary === 'string' ? m.summary : '';
1021
1027
  noteWorkflowCompletionCardEnqueued(runId);
1022
- const notification = freezeTaskNotificationFields({ taskId: runId, taskType: 'workflow', status: wfStatus, summary: wfSummary });
1028
+ const extras = workflowCompletionExtrasOf(m);
1029
+ const notification = freezeTaskNotificationFields({ taskId: runId, taskType: 'workflow', status: wfStatus, summary: wfSummary, ...(extras.completionId !== undefined ? { completionId: extras.completionId } : {}) });
1023
1030
  yield chrome({
1024
1031
  kind: 'workflow_notification_enqueue',
1025
1032
  laneProof: mainLane(),
@@ -1028,6 +1035,7 @@ const workflowCompleteArm = function* (frame) {
1028
1035
  summary: wfSummary,
1029
1036
  message: renderTaskNotificationXml(notification),
1030
1037
  notification,
1038
+ ...(extras.diagnostics !== undefined ? { diagnostics: extras.diagnostics } : {}),
1031
1039
  });
1032
1040
  };
1033
1041
  const retryStatusArm = function* (m, { ctx, flags }) {
@@ -75,6 +75,7 @@ export interface ActiveRunSelfHealDeps {
75
75
  }) => Promise<number>;
76
76
  reopenPlanReview?: (taskId: string, opts?: {
77
77
  trigger?: PlanReviewReopenTrigger;
78
+ checkpointId?: string;
78
79
  }) => ReopenCardVerdict | Promise<ReopenCardVerdict>;
79
80
  reopenAskPark?: (taskId: string, opts?: {
80
81
  trigger?: PlanReviewReopenTrigger;
@@ -1,6 +1,8 @@
1
1
  import { abortableSleep } from '../abortableSleep.js';
2
2
  import { EN_MESSAGES } from '../i18n/messages.en.js';
3
3
  import { msg, renderMessage } from '../i18n/translate.js';
4
+ import { registeredPlanReviewCardFacts } from '../planReviewCardFacts.js';
5
+ import { DEFAULT_SESSION_KEY } from '../sessionSlot.js';
4
6
  import { ASK_PARK_AUTO_REOPEN_BLOCKS, askParkAutoReopenBlockedContent } from '../hitl/askParkAutoReopenBlock.js';
5
7
  import { corruptStoredRowContent, corruptStoredRowWhere, effectiveWireErrorCode } from '../wireErrorTriage.js';
6
8
  import { readThrownStatusSlot } from '../wireFailureShape.js';
@@ -522,8 +524,10 @@ export async function attemptActiveRunSelfHeal(signal, runs, deps) {
522
524
  }
523
525
  const gateKind = signal.pendingGate?.kind ?? null;
524
526
  const gateLeg = gateKind !== null ? durableDecideLegOf({ gateKind }) : 'absent';
527
+ const pendingCp = signal.pendingGate?.checkpointId;
528
+ const sameTaskCp = pendingCp !== undefined && registeredPlanReviewCardFacts(DEFAULT_SESSION_KEY, taskId, pendingCp) !== undefined ? pendingCp : undefined;
525
529
  if (gateLeg === 'plan_review')
526
- return planReviewArm(taskId, signal, deps);
530
+ return planReviewArm(taskId, signal, deps, sameTaskCp);
527
531
  if (gateLeg === 'approval')
528
532
  return askParkArm(taskId, signal, runs, deps);
529
533
  let status = signal.activeTaskStatus;
@@ -558,10 +562,11 @@ export async function attemptActiveRunSelfHeal(signal, runs, deps) {
558
562
  }
559
563
  return { kind: 'not-parked', taskId, status, ...triageGate };
560
564
  }
561
- async function planVerdict(taskId, deps) {
565
+ async function planVerdict(taskId, deps, checkpointId) {
562
566
  try {
563
567
  const trigger = planReviewReopenTriggerFor(deps?.submissionOrigin);
564
- const verdict = trigger === undefined ? deps?.reopenPlanReview?.(taskId) : deps?.reopenPlanReview?.(taskId, { trigger });
568
+ const second = { ...(trigger !== undefined ? { trigger } : {}), ...(checkpointId !== undefined && checkpointId.length > 0 ? { checkpointId } : {}) };
569
+ const verdict = Object.keys(second).length === 0 ? deps?.reopenPlanReview?.(taskId) : deps?.reopenPlanReview?.(taskId, second);
565
570
  return snapshotReopenVerdict(await verdict);
566
571
  }
567
572
  catch (e) {
@@ -581,8 +586,8 @@ async function askVerdict(taskId, deps) {
581
586
  return thrownReopenVerdict(e);
582
587
  }
583
588
  }
584
- async function planReviewArm(taskId, signal, deps) {
585
- const verdict = await planVerdict(taskId, deps);
589
+ async function planReviewArm(taskId, signal, deps, checkpointId) {
590
+ const verdict = await planVerdict(taskId, deps, checkpointId);
586
591
  if (reopenDelivered(verdict))
587
592
  return { kind: 'plan-review-reopened', taskId, firstSight: verdict.firstSight === true };
588
593
  const inFlight = reopenRefusedForDecisionInFlight(verdict);
@@ -5,7 +5,7 @@ import { ccToolDenialKindForToolEnd, gateOutcomeOf, toolInputRewriteOf } from '.
5
5
  import { projectToolRoster, projectToolRosterDelta } from '../../toolRoster.js';
6
6
  import { projectHandsSection } from '../../handsSeam.js';
7
7
  import { readMcpAuthorization, readMcpLiveness } from '../../mcpLiveness.js';
8
- import { contextUsageReadingOf, humanInputAttachmentsOf, guardedRead, markMcpSectionDroppedNamedRows, ownSnapshotTyped, promptManifestIdentityOfFrame, promptManifestSectionViewOf, readFrameStringKey, readFrameTrueMark, readOwnDataFiniteNumber, readOwnDataNonEmptyString } from '../../ownKey.js';
8
+ import { contextUsageReadingOf, humanInputAttachmentsOf, guardedRead, markMcpSectionDroppedNamedRows, ownSnapshotTyped, promptManifestIdentityOfFrame, promptManifestSectionViewOf, readFrameStringKey, readFrameTrueMark, readOwnDataFiniteNumber, readOwnDataNonEmptyString, workflowCompletionExtrasOf } from '../../ownKey.js';
9
9
  import { SEGMENT_END_IDENTITY_KEYS, eventSeq, stripToolUseSideChannels } from '../types.js';
10
10
  import { frozenSet } from '../../frozenSet.js';
11
11
  import { isCoreWorkflowRunTerminal } from '../../terminalStatuses.js';
@@ -166,6 +166,7 @@ function projectEvent(ev, ctx) {
166
166
  runId: wf.runId,
167
167
  status: wf.status,
168
168
  summary: typeof wf.summary === 'string' ? wf.summary : '',
169
+ ...workflowCompletionExtrasOf(wf),
169
170
  })));
170
171
  }
171
172
  case 'text':
@@ -283,10 +284,9 @@ function projectEvent(ev, ctx) {
283
284
  const nonEmptyStr = (v) => (typeof v === 'string' && v.length > 0 ? v : undefined);
284
285
  const providerCondition = nonEmptyStr(ev.providerCondition);
285
286
  const providerConditionSource = nonEmptyStr(ev.providerConditionSource);
286
- const stWait = ev;
287
- const sleptMsRaw = guardedRead(() => readOwnDataFiniteNumber(stWait, 'sleptMs'));
287
+ const sleptMsRaw = guardedRead(() => readOwnDataFiniteNumber(ev, 'sleptMs'));
288
288
  const sleptMs = sleptMsRaw !== undefined && sleptMsRaw >= 0 ? sleptMsRaw : undefined;
289
- const waitSegment = readFrameStringKey(stWait, 'waitSegment');
289
+ const waitSegment = readFrameStringKey(ev, 'waitSegment');
290
290
  const seat = readFrameStringKey(ev, 'seat');
291
291
  return projected(stamp(ctx, armBody({
292
292
  type: 'retry_status',
@@ -47,3 +47,9 @@ export declare function readInstructionsSegment(holder: object, key: 'instructio
47
47
  export declare function readFleetSegment(holder: object, key: 'fleet' | '_sema_fleet'): WiringManifestFleetView | undefined;
48
48
  export declare function wiringManifestViewOf(s: WiringManifestSections): WiringManifestView | undefined;
49
49
  export declare function modRowsOf(records: unknown): readonly WiringManifestModView[] | undefined;
50
+ export interface PermissionModeReceiptView {
51
+ readonly requested?: string;
52
+ readonly effective: string;
53
+ readonly foldedBy?: string;
54
+ }
55
+ export declare function readPermissionModeReceiptSegment(holder: object, key: 'permissionMode'): PermissionModeReceiptView | undefined;
@@ -208,3 +208,28 @@ export function modRowsOf(records) {
208
208
  return undefined;
209
209
  }
210
210
  }
211
+ export function readPermissionModeReceiptSegment(holder, key) {
212
+ try {
213
+ const seg = ownSlot(holder, key);
214
+ if (seg.state !== 'value' || !isModRecordObject(seg.value))
215
+ return undefined;
216
+ const raw = seg.value;
217
+ const effective = nonEmpty(ownSlot(raw, 'effective'));
218
+ if (effective === undefined)
219
+ return undefined;
220
+ const requestedSlot = ownSlot(raw, 'requested');
221
+ const foldedBySlot = ownSlot(raw, 'foldedBy');
222
+ const requested = nonEmpty(requestedSlot);
223
+ const foldedBy = nonEmpty(foldedBySlot);
224
+ if ((requestedSlot.state !== 'absent' && requested === undefined) || (foldedBySlot.state !== 'absent' && foldedBy === undefined))
225
+ return undefined;
226
+ return Object.freeze({
227
+ ...(requested !== undefined ? { requested } : {}),
228
+ effective,
229
+ ...(foldedBy !== undefined ? { foldedBy } : {}),
230
+ });
231
+ }
232
+ catch {
233
+ return undefined;
234
+ }
235
+ }