@sema-agent/client-core 0.46.0 → 0.47.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.
@@ -0,0 +1,131 @@
1
+ /**
2
+ * interactiveHalt — 交互 **Esc** 的停止判定层(#363 件③,0.47.0;三端公共判定上收)。
3
+ *
4
+ * ── 收的是哪一件事 ────────────────────────────────────────────────────────────────────────
5
+ * 「用户按 Esc ⇒ 先发 **turn 级 halt**;升级成 **run 级 cancel** 恰有**两格**:
6
+ * ① 引擎自己回了升级闭集里的 **409**(它在说「这里没有在飞 turn 可切,run 级停止请用 cancel」);
7
+ * ② 这一发**连判决都没拿到**(传输失败/超时/未武装)**且**屏上确实挂着审批卡(`parked`)。
8
+ * 其余一律不升级。」—— 这条判定 TUI / desktop / web **三端都要**(三端都会 Esc、都会撞
9
+ * 同一个 parked 格),此前整条住在 cli 壳里(`src/sema/seamQuery.ts` 的 `bestEffortInteractiveHalt`
10
+ * + `src/sema/interruptWire.ts` 的 `needsRunLevelStop`)。本模块把**判据**搬进来;
11
+ * **发射**(裸 fetch / SDK verb)、台账、留痕、UI 反馈仍归各端。
12
+ * ⚠️ 别把①漏掉(异源复审 [medium] 采纳的原文歧义):只写「拿不到判决且 parked 才升级」的话,
13
+ * 端会漏接**引擎明确指路**那一格 —— parked run 的会话锁不放,下一条消息照样撞「Session busy」。
14
+ *
15
+ * ── 两个动词各自唯一能做到的格(**cancel 绝不删**,本模块存在的前提)──────────────────────
16
+ * · `interrupt`(turn 级,bare 形 = core `stream.halt()`「切 + 停」)—— 需要一条**活的、在飞的
17
+ * turn**。这是 Esc 的正题:切掉这一轮,run 在边界上终局、同 session 下一 submit 照常续。
18
+ * · `cancel`(run 级)—— 显式整 run 终局手势,而且它是**唯一**能把一条停在审批门上的
19
+ * `suspended`/`needs_review` run 就地终态化、把会话锁放开的动词(server [868] 起的语义)。
20
+ * ⇒ 无条件把 cancel 换成 interrupt = 审批卡挂着按 Esc 的主场景当场回归成「Session busy」病;
21
+ * 无条件补 cancel = 同一条 run 上别的在飞工具(后台 bash 等)被连坐拆掉(#324 的病形)。
22
+ * 本判定就是这两条之间那道**不对称**的闸。
23
+ *
24
+ * ── 🔴 不对称是刻意的(方向安全,两条代价不等价)────────────────────────────────────────
25
+ * 判**不**升级的代价 = 用户退回既有的「Session busy」卡,自己再选一次(**可恢复**);
26
+ * 判**错**升级的代价 = 拆掉一条其实还活着的 run,连坐它身上所有在飞工具(**不可恢复**)。
27
+ * ⇒ 闸往严的一侧设,宁可少升一次;凡「证不出来」一律落**不升级**侧。
28
+ *
29
+ * ── 判据锚在哪 ────────────────────────────────────────────────────────────────────────────
30
+ * · 升级的**第一判据 = 引擎自己的机器码**({@link RUN_LEVEL_STOP_ERROR_CODES} × 409),
31
+ * 不是壳对 UI 状态的猜测([anchor-on-the-deciding-quantity]);
32
+ * · 只有当**引擎连判决都没给**(传输失败/超时/根本没武装)时,才轮到壳自己独立知道的那个
33
+ * 事实(`parked` = 这一拍屏上确实挂着审批卡 ⇒ 那条 run 停在 pending 决断上 ⇒ 本来就没有
34
+ * 在飞 turn 可切 ⇒ 补 cancel 不构成连坐)。
35
+ *
36
+ * 🔴 **纯判定**:零 IO、零 import、零 module 级状态,同一入参恒同一出参。
37
+ */
38
+ /**
39
+ * server 在 interrupt 腿上表达「**这条 run 已经 parked、没有在飞 turn 可切**」的 409 机器码
40
+ * (engine 7.52.0 `dist/http/routes/runs.js` 真字节,`sendNoLiveTurn` 的 `isParkedRunStatus` 分支)。
41
+ * · `interrupt.nothing_in_flight` —— run 停在 pending 决断上。**审批卡挂着按 Esc 的主场景就是
42
+ * 这一格**,server 的原话直接指路 steer/cancel。
43
+ *
44
+ * 🔴 **闭集只收这一员**,另外两个 409 都刻意在外:
45
+ * · `interrupt.not_held` —— **不收**。语义是「**本副本**手上没有可切的 live turn face」,而 server
46
+ * 自己的两条原话把它拆得很清楚:一条是「run is live on **another replica**」,另一条是
47
+ * verify/cascade 车道不暴露 live stream。**两条都不证明「全局没有在飞 turn」** —— 尤其第一条,
48
+ * 那条 run 正在别的副本上跑得好好的。对它升级 = 把 **turn 级** Esc 放大成**整 run 终止**。
49
+ * server 提示「run-level stop 可用 cancel」是在告诉你**有这个动词**,不等于用户授权了 run 级停止。
50
+ * · `steering.not_running` —— 不收。run 已终局,没有任何东西要停,补一发 cancel 是纯噪声。
51
+ * ⇒ 只有「引擎结构化地证明了 run 已 parked」这一格才允许升级。等上游给出 owner/parked 判别位
52
+ * (或跨副本路由)之后,`not_held` 才谈得上有安全的处置。
53
+ *
54
+ * 🔴 **为什么是 `Object.freeze` 的数组而不是 `ReadonlySet`**(异源复审 [high] 采纳,真病):
55
+ * `ReadonlySet<string>` 只在**类型面**只读 —— 运行期它就是一只普通 `Set`,而判定查的是**同一个
56
+ * 实例**。任何 JS 消费者(或本包将来某处的一行手滑)`.add('interrupt.not_held')` 之后,同一份入参
57
+ * 就会从 `none` 变成 `escalate-cancel`,把一条**还活着**的 run 不可恢复地拆掉 —— 这正是本模块整段
58
+ * 头注在防的那个方向,却被自己的导出形留了后门;而「纯判定、零 module 级可变态」那句承诺也当场
59
+ * 变成假话。冻结数组在**运行期**真的改不动(ESM 恒 strict:`push`/下标赋值直接抛),于是「公开
60
+ * 面」与「判定源」可以安全地是同一个物,不必铸第二份(两份才会漂)。
61
+ * ⚠️ 判据形随之从 `.has()` 改成 `.includes()` —— 与同仓 `parkResolver.GATE_FAILURE_CODES` 的
62
+ * `as const` 数组 + `includes` 逐字同姿势。闭集只有一员,查找成本不是这里的量。
63
+ */
64
+ export const RUN_LEVEL_STOP_ERROR_CODES = Object.freeze([
65
+ 'interrupt.nothing_in_flight',
66
+ ]);
67
+ /** 升级闭集的码在 wire 上**只以 409 出现**(engine 7.52.0 `sendNoLiveTurn` 两分支逐字)。 */
68
+ const RUN_LEVEL_STOP_STATUS = 409;
69
+ /**
70
+ * 这一发 interrupt 的结局是不是在说「**改用 run 级停止**」。
71
+ *
72
+ * 判据 = **码闭集({@link RUN_LEVEL_STOP_ERROR_CODES})与 409 状态的合取**,两个条件都必要:
73
+ * · 只认码不认状态 ⇒ 一只 5xx(引擎内部错、代理改写体、老版本复用同名码)只要正文里带上那个码,
74
+ * 就能把我们骗去打一发 **run 级 cancel** —— 那是**破坏性**动作,而它当时其实没有任何判决依据。
75
+ * · 只认 409 不认码 ⇒ `steering.not_running`(run 已终局)也会被升级,对着一条已经结束的 run 补枪。
76
+ */
77
+ function enginePointsToRunLevelStop(outcome) {
78
+ const o = outcome;
79
+ return (o?.kind === 'refused' &&
80
+ o.status === RUN_LEVEL_STOP_STATUS &&
81
+ typeof o.errorCode === 'string' &&
82
+ RUN_LEVEL_STOP_ERROR_CODES.includes(o.errorCode));
83
+ }
84
+ /**
85
+ * Esc 停止弧的**唯一判定口**。
86
+ *
87
+ * ── 分支表(逐条 = 一条行为承诺)──────────────────────────────────────────────────────────
88
+ * | `interruptOutcome` | `parked` | 判决 | reason |
89
+ * |--------------------------------------------|----------|-------------------|--------|
90
+ * | 缺席(还没打) | 任意 | `interrupt` | `first-shot` |
91
+ * | `halted` | 任意 | `none` | `halted` |
92
+ * | `refused` + 409 + 升级闭集码 | 任意 | `escalate-cancel` | `engine-says-run-level` |
93
+ * | `refused`(其余:非 409 / 码不在闭集 / 码缺席)| 任意 | `none` | `refused-no-escalation` |
94
+ * | `transport` / `unarmed` | `true` | `escalate-cancel` | `no-verdict-on-parked-card` |
95
+ * | `transport` / `unarmed` | 其余 | `none` | `no-verdict-not-parked` |
96
+ * | 认不得的形 | 任意 | `none` | `unknown-outcome` |
97
+ *
98
+ * 🔴 **`parked` 只在「没有判决」那两格被读**:引擎给了判决时,判决说了算 —— 壳的 UI 状态不许覆盖
99
+ * 引擎的结构化答复(反过来也一样:引擎说 parked 时,`parked=false` 不阻止升级)。
100
+ * 🔴 **首发无条件**:`first-shot` 那一格刻意不看 `parked`。审批卡挂着时首发 interrupt 会吃一个
101
+ * 409,那正是升级闸要的**判决**;为了省一次往返而直接跳到 cancel,等于把判据从引擎搬回壳里猜。
102
+ * 🔴 **顺序契约(端必读,不是本函数能保证的那半)**:这一发必须排在「撕 SSE」**之前**。交互车道零
103
+ * `x-detach-on-disconnect`,先撕流 = server 按断连语义当场收尾那条 run,随后落地的 interrupt
104
+ * 只会拿到 409 `steering.not_running`(cli L-11 真机实测:同步撕流形每一轮都是它)。
105
+ */
106
+ export function planInteractiveHalt(input) {
107
+ const outcome = input.interruptOutcome;
108
+ // 首发恒行:一发都还没打的时候,没有任何东西需要判。
109
+ // 🔴 `null` 与 `undefined` **同判「还没打」**(两种缺席形;宿主用哪一种写「没有结局」都算数)——
110
+ // 而不是落 fail-closed 的 `unknown-outcome`:那会让 Esc 一发都不打(interrupt 是**非破坏性**
111
+ // 动词,把它扣下来比多打一发更坏)。fail-closed 守的是**升级**那一侧,不是首发。
112
+ if (outcome === undefined || outcome === null)
113
+ return { action: 'interrupt', reason: 'first-shot' };
114
+ // 防御读:wire/宿主形在运行期不存在类型(M0/M1),`kind` 可能是新词、可能压根不是对象。
115
+ const kind = outcome.kind;
116
+ if (kind === 'halted')
117
+ return { action: 'none', reason: 'halted' };
118
+ if (kind === 'refused') {
119
+ return enginePointsToRunLevelStop(outcome)
120
+ ? { action: 'escalate-cancel', reason: 'engine-says-run-level' }
121
+ : { action: 'none', reason: 'refused-no-escalation' };
122
+ }
123
+ if (kind === 'transport' || kind === 'unarmed') {
124
+ // 没拿到判决。只有壳能**独立证明** parked 的那一格才升级(理由见顶注的不对称段)。
125
+ return input.parked === true
126
+ ? { action: 'escalate-cancel', reason: 'no-verdict-on-parked-card' }
127
+ : { action: 'none', reason: 'no-verdict-not-parked' };
128
+ }
129
+ // 宿主传了本模块认不得的形(新 kind / 坏对象)。fail-closed:不对一个读不懂的结局动手。
130
+ return { action: 'none', reason: 'unknown-outcome' };
131
+ }
@@ -404,6 +404,15 @@ export function wireOutputToBody(toolName, output, isError, truncated, flatten,
404
404
  if (slots.exitCode === 137) {
405
405
  body += `\nCommand was killed (exit 137 — possibly out-of-memory; consider lowering build parallelism e.g. make -j2)`;
406
406
  }
407
+ // [5601]/#362(2026-08-29,live 帧直证 core 5.65.0):「跑过然后被掐」家族
408
+ // (timeout/aborted/callback_error)铸 isError:true + 三槽 {stdout:'',stderr:'',exitCode:null}
409
+ // + output 正文带完整说明——重建体在这形上恒为空串,引擎正文整段被丢,下游渲
410
+ // 「no error detail」空卡。回退判据锚**决定量本身**(重建 body 空而正文有话),
411
+ // 不锚 timedOut 等键名:同家族零输出三臂一次覆盖,且 exitCode 在场时 body 至少含
412
+ // `Exit code N` 恒非空 ⇒ 回退结构上只在 exitCode===null 且双槽空触发,权威臂地位不动。
413
+ // 双空(正文也空)⇒ 如实返回空串,消费端「no error detail」兜底句保持可达。
414
+ if (body.trim() === '' && text.trim() !== '')
415
+ body = text;
407
416
  return { content: truncated ? `${body}\n…(truncated)` : body, isError: true };
408
417
  }
409
418
  const stdout = truncated ? `${slots.stdout}\n…(truncated)` : slots.stdout;
@@ -15,24 +15,26 @@
15
15
 
16
16
  ## §0 版本锚与重扫纪律
17
17
 
18
- ### 0a. 版本锚(2026-08-21)
18
+ ### 0a. 版本锚(2026-08-31)
19
19
 
20
20
  | 项 | 值 | 真源 |
21
21
  |---|---|---|
22
- | 本包 | `@sema-agent/client-core` **0.38.0** | `package.json` `version` |
22
+ | 本包 | `@sema-agent/client-core` **0.47.0**(工作树;发布前 npm 最新是 `0.46.0`) | `package.json` `version` |
23
23
  | peer:wire 契约 | `@sema-agent/sdk` **>=7.2.0**(value-level,非 type-only) | `package.json` `peerDependencies` |
24
24
  | peer:会话词汇表 | `@sema-agent/agent-types` **>=0.2.0**(type-only,零运行时) | 同上 |
25
25
  | runtime dep | `diff` ^9.0.0(**唯一**一条;portability 门按**等值**钉死) | `package.json` `dependencies` |
26
- | 公开导出面 | **787** 个运行期符号(+ 41 个测试钩;= 未发 design/285 批 0+1+2+3 的值,npm `0.44.0` 是 **783**,`0.43.1`/`0.43.0` 是 **776**,`0.42.0` 是 **771**,`0.41.0` 是 **767**,`0.39.0` 是 **766**,`0.38.0` 是 **764**;`0.37.0` 是 **753**,见 `CHANGELOG.md`) | `scripts/public-export-baseline.json` 的 `count` / `testHookCount` —— **别手抄进别处,以该文件为准** |
26
+ | 公开导出面 | **790** 个运行期符号(+ 41 个测试钩;= 未发 0.47.0 的值,npm `0.46.0` 是 **787**,`0.44.0` 是 **783**,`0.43.1`/`0.43.0` 是 **776**,`0.42.0` 是 **771**,`0.41.0` 是 **767**,`0.39.0` 是 **766**,`0.38.0` 是 **764**;`0.37.0` 是 **753**,见 `CHANGELOG.md`) | `scripts/public-export-baseline.json` 的 `count` / `testHookCount` —— **别手抄进别处,以该文件为准** |
27
27
  | 常驻门 | 以 `scripts/gates-manifest.json` 的 `suites` 长度为准(**本档不抄这个数**) | `scripts/gates-manifest.json`;`npm test` 的名单等值门与它逐名对账 |
28
28
  | 沿革档 | 0.29.0 起建 `CHANGELOG.md`;更早批次记账在 `src/index.ts` 文件头 + `docs/REFACTOR-LEDGER.md` | — |
29
29
 
30
- ⚠️ 0.38.0 已于 2026-08-21 发布,本表与 npm 最新版重新对齐(工作树 = npm)。装旧版(≤0.37.0)的
31
- 端注意:0.38.0 新增的 **11 个 additive 导出**(`engineCapState` + `EngineCapState`、
32
- `resolveEntryVision` / `computeDeleteBlockers` / `computeDeleteWarnings`、`CONFIG_DELEGATION_ENTRY_CAPS` /
33
- `DELEGATION_CONCURRENCY_CAP` / `DELEGATION_SESSION_CAP` / `DELEGATION_CAP_CODES` / `isDelegationCapCode`、
34
- `wireCycleSeq` / `wireRetiredBy`)在旧版上按名 import 会**在 ESM 实例化当场炸**(具名导出不存在)——
35
- 提货前先抬依赖。旧版对表以 `CHANGELOG.md` 对应版本段为准。
30
+ ⚠️ **工作树 ≠ npm**:本表记的是**工作树**的 0.47.0,npm 上最新仍是 0.46.0(冻结账里 0.47.0
31
+ `pending` 行)。装 ≤0.46.0 的端注意:0.47.0 新增的 **3 个 additive 导出**
32
+ (`planInteractiveHalt` / `RUN_LEVEL_STOP_ERROR_CODES` / `readDecideCurrentPending`)在旧版上按名
33
+ import 会**在 ESM 实例化当场炸**(具名导出不存在)—— 提货前先抬依赖。同一条对 0.38.0 那 11 个
34
+ additive 导出成立(`engineCapState` + `EngineCapState`、`resolveEntryVision` /
35
+ `computeDeleteBlockers` / `computeDeleteWarnings`、`CONFIG_DELEGATION_ENTRY_CAPS` /
36
+ `DELEGATION_CONCURRENCY_CAP` / `DELEGATION_SESSION_CAP` / `DELEGATION_CAP_CODES` /
37
+ `isDelegationCapCode`、`wireCycleSeq` / `wireRetiredBy`)。旧版对表以 `CHANGELOG.md` 对应版本段为准。
36
38
 
37
39
  🔴 **本表里仍然手抄的数字都有门看着**(#252,2026-08-14):`scripts/run-integration-doc-freshness-test.mjs`
38
40
  ① 段把 638 / 32 / §2b 十六域名数之和 / 191 / 4 / 33 逐个对 `public-export-baseline.json` 算出来的值,
@@ -101,7 +103,7 @@ SDK 核对「本包 import 的每个值级符号仍然导出」「`TaskStats.cos
101
103
 
102
104
  ## §2 公共导出面地图(按域)
103
105
 
104
- > 全集真源 = `scripts/public-export-baseline.json` 的 `names`(**787** 项)。
106
+ > 全集真源 = `scripts/public-export-baseline.json` 的 `names`(**790** 项)。
105
107
  > 本节**不逐名抄**,只给「域 → 承重导出 → 用途 → 实现锚」。承重导出 = 一个端为了让这个域干活
106
108
  > **必须**直接调到的那几个符号;其余是它们的类型、变体与辅助位。
107
109
  > 单一入口:`import { … } from '@sema-agent/client-core'`(`exports` 只有 `.` 一个;
@@ -111,7 +113,7 @@ SDK 核对「本包 import 的每个值级符号仍然导出」「`TaskStats.cos
111
113
 
112
114
  `public-export-baseline.json` 由 **`dist/index.js` 的运行期导出**生成(生成口径自述见
113
115
  `scripts/run-client-core-typeshape-test.mjs`,双向精确集合门在 `scripts/run-public-surface-test.mjs`)。
114
- 实测:787 项 **100% 是运行期导出,零 type-only**。
116
+ 实测:790 项 **100% 是运行期导出,零 type-only**。
115
117
 
116
118
  **推论(端必须知道)**:
117
119
  - barrel 导出的**类型**面比 707 大得多,且**不被这道门看守** —— `AdapterContext` / `SeamEvent` /
@@ -120,18 +122,18 @@ SDK 核对「本包 import 的每个值级符号仍然导出」「`TaskStats.cos
120
122
  端依赖这些类型是合法的,但**不要**拿基线 diff 当"类型面没变"的证据。
121
123
  - `src/agentSession/contract.ts` 对基线贡献 **0** 项(纯类型模块,`export *` 在 dist 里是空转发)。
122
124
 
123
- 787 项的内部构成(帮助端估读表大小):**232** 项是 `SCREAMING_SNAKE` 常量数据表/词汇表
125
+ 790 项的内部构成(帮助端估读表大小):**233** 项是 `SCREAMING_SNAKE` 常量数据表/词汇表
124
126
  (矩阵、键集、env 名、锚串)而非可调用物;**5** 项是 PascalCase 运行期值
125
127
  (`ControlRouter` / `ControlSafetyError` / `HitlBridge` / `HitlSafetyError` / `DecideTransportRetryExhaustedError`);
126
128
  **41** 项是 `*For(sessionKey, …)` 的 per-session 变体(§6;其中 `engineNamespaceKeyFor` 是命名巧合 —— 参数是 baseUrl 不是 sessionKey,见域 14)。
127
129
 
128
- ### 2b. 域图(16 域,逐域计数之和 = 787)
130
+ ### 2b. 域图(16 域,逐域计数之和 = 790)
129
131
 
130
132
  | # | 域 | 名数 | 承重导出 | 用途 | 实现锚 |
131
133
  |---|---|---|---|---|---|
132
134
  | 1 | **适配内核(下行主链)** | 33 | `adapt` · `createWireToCcAdapter` · `runStream` · `eventToSdkMessage` · `terminalToSdkResult` · `turnUsageToModelUsage` · `isRunStreamActive` · `ADAPTER_DIVERGENCES` | 引擎 SSE `AgentEvent` → 端要渲的**双面输出**:transcript(`SDKMessage`)+ chrome(瞬态 `ChromeEvent`)。**本包存在的理由** | `src/adapt.ts`、`src/adapt/{arms,wireShapes,panelTasks}.ts`(经 `adapt.ts` 再导出)、`src/adapter/runStream.ts`、`src/adapter/downstream/*`、`src/adapter/types.ts` |
133
135
  | 2 | **seam 公共契约** | 2(其余为 type-only) | `CHROME_ARMS` · `deriveTranscriptId` | 公共词汇 + **id 确定性不变量**(同一条流重放 ⇒ 同一串 id)。`CHROME_ARMS` = 端「我要消费哪些 chrome 臂」的对照清单 | `src/seam.ts` |
134
- | 3 | **HITL 决断卡链**(§4/§5 主战场) | 131 | `makeHitlCanUseTool` · `HitlBridge` · `findPendingForTask` · `HitlSafetyError` · `bridgeAskUserQuestionGates` · `surfaceToolApprovalFrameAndRespond` / `surfaceFsApprovalAndDecide` · `readToolApprovalRespondAck` · `installApprovalCardPort(For)` · `installHitlHostSurface(For)` · `armPlanReviewApproval` · `reopenPlanReviewCard` · `decidePlanReview` · `startApprovalsFeed` · `pendingRowIsOwnedByThisSession` · `approvalCallKey`/`liveFrameCallKey`/`planReviewQuestionId` · `registerArmedGateFor`/`wasGateArmedFor`/`clearArmedGateFor` · `waitForGateArmed(For)`/`onGateArmed(For)`/`gateArmedWaitMs`(#244 F1 呈现回执事件源) · `planReviewArmedKey(For)`/`notePlanReviewAnswered(For)`/`notePlanReviewAnsweredIfDecisive(For)`(A-024.4 plan 呈现分代) · `toolEndOutputText` · `isAskTool` · `waitForParkRowBirth` · `classifyAskParkRows` / `askParkRowArm` / `classifyAskParkChainFailure` · `readDecisionNoteAudit` / `decisionNoteAuditLine` · `resumeRunningOptions` / `resumeChoiceFromLabels` · `persistedRulesLaneAvailable`/`persistedRulesGovernanceAvailable` · `classifyRulesFailure` · `listAllPersistedRules` · `classifySkippedReason` · `readRulePersistOutcome`(#244 F2:persist-ack 读口与 `readToolApprovalRespondAck` 合成一处) · `parseLocalAllowRule`(durable 腿本地落规则窄化骨架,谓词经 `LocalAllowRuleDeps` 注入) · `readToolApprovalRespondRefusal`(#225 件5,0.42.0:respond 抛错的结构化原文读口 —— 三位各自防御读、各自缺席不铸、**三位皆缺席时整只返 `undefined`**;原样交还零加工,UNTRUSTED-for-display)· `installEditedRuleTextPrechecker` / `hasEditedRuleTextPrechecker` / `precheckEditedRuleText`([5076] 转出口,0.42.0:core 5.57.0 `precheckEditedRuleText` 的**端口注入形** —— 类型面 + 注入口 + 诚实缺席读口。🔴 **不是** value 级 re-export,理由见 §7 缺口 **P-34**;未装 ⇒ 返 `undefined`,绝不编一个 `{ok:true}`)· `surfaceRuleArmNotSent` / `RULE_NOT_SENT_WARN_TEXT` + `surfaceRuleArmRejected` / `RULE_NOT_SENT_REJECTED_WARN_TEXT`(#334,0.43.0:人在卡上按下的「不再询问」被整条丢弃时的诚实告知——**两条刻意分开**:前者=**引擎能力位未确认**(换台引擎/等探测就好),后者=**这次选择没过包内表核/互斥核**(表外文本/坏下标/两臂同场,换引擎也不会变) —— 编辑臂 `respondFreeFormRules` 与批臂 `respondBatchRuleOffers` 两条同形存量共用一条,与 `surfaceRememberNotApplied` 同族纪律:决断照送、只是规则没存,静默丢掉用户明确意图 = 让人以为功能坏了)· `DecideTransportRetryExhaustedError`(Inkglow-1085 P0a:decide 出站瞬断重试耗尽的 typed 判别 —— HitlBridge 内建单次退避重试,耗尽走重呈臂不判死 turn;端一般只消费行为,不需要 instanceof) | suspended→decide→resume 环。🔴 **D-1 两元组 verbatim 回显**是字节级断言的安全不变量,端**不许重实现它的任何一段**。🔴 键空间边界(web [C1] d3 拦截):`gateIdentity` 四常量两函数只覆盖 HITL questionId/callKey 空间;seat 的 `TOOL_PERMISSION_REQUEST_ID_DOMAINS`(`plan:` 等)是另一键空间,**两者绝不合并**(合并=座位校验器静默拒全部 plan-review 卡) | `src/hitl/hitlBridge.ts`、`toolApprovalWire.ts`、`askGateWire.ts`、`planReviewWire.ts`、`hitlHostSurface.ts`、`gateIdentity.ts`、`armedGateRegistry.ts`、`parkOwnership.ts`、`parkResolver.ts`、`approvalsFeed.ts`、`frameRouter.ts`(**只挑名导出** `toolEndOutputText`/`ENGINE_ABORT_TOOL_RESULT`/`isAskTool`/`HITL_REJECT_MESSAGE`/`HITL_INTERRUPT_MESSAGE_FOR_TOOL_USE`)、`parkRowBirthWait.ts`、`approvalDecisionNoteAudit.ts`、`askParkRowRouting.ts`、`resumeRunningCard.ts`(#265 上收的判定层)、`persistedRulesWire.ts`、`localAllowRule.ts`(#244 F2 规则侧) |
136
+ | 3 | **HITL 决断卡链**(§4/§5 主战场) | 132 | `makeHitlCanUseTool` · `HitlBridge` · `findPendingForTask` · `HitlSafetyError` · `bridgeAskUserQuestionGates` · `surfaceToolApprovalFrameAndRespond` / `surfaceFsApprovalAndDecide` · `readToolApprovalRespondAck` · `installApprovalCardPort(For)` · `installHitlHostSurface(For)` · `armPlanReviewApproval` · `reopenPlanReviewCard` · `decidePlanReview` · `startApprovalsFeed` · `pendingRowIsOwnedByThisSession` · `approvalCallKey`/`liveFrameCallKey`/`planReviewQuestionId` · `registerArmedGateFor`/`wasGateArmedFor`/`clearArmedGateFor` · `waitForGateArmed(For)`/`onGateArmed(For)`/`gateArmedWaitMs`(#244 F1 呈现回执事件源) · `planReviewArmedKey(For)`/`notePlanReviewAnswered(For)`/`notePlanReviewAnsweredIfDecisive(For)`(A-024.4 plan 呈现分代) · `toolEndOutputText` · `isAskTool` · `waitForParkRowBirth` · `classifyAskParkRows` / `askParkRowArm` / `classifyAskParkChainFailure` · `readDecisionNoteAudit` / `decisionNoteAuditLine` · `resumeRunningOptions` / `resumeChoiceFromLabels` · `persistedRulesLaneAvailable`/`persistedRulesGovernanceAvailable` · `classifyRulesFailure` · `listAllPersistedRules` · `classifySkippedReason` · `readRulePersistOutcome`(#244 F2:persist-ack 读口与 `readToolApprovalRespondAck` 合成一处) · `parseLocalAllowRule`(durable 腿本地落规则窄化骨架,谓词经 `LocalAllowRuleDeps` 注入) · `readToolApprovalRespondRefusal`(#225 件5,0.42.0:respond 抛错的结构化原文读口 —— 三位各自防御读、各自缺席不铸、**三位皆缺席时整只返 `undefined`**;原样交还零加工,UNTRUSTED-for-display)· `installEditedRuleTextPrechecker` / `hasEditedRuleTextPrechecker` / `precheckEditedRuleText`([5076] 转出口,0.42.0:core 5.57.0 `precheckEditedRuleText` 的**端口注入形** —— 类型面 + 注入口 + 诚实缺席读口。🔴 **不是** value 级 re-export,理由见 §7 缺口 **P-34**;未装 ⇒ 返 `undefined`,绝不编一个 `{ok:true}`)· `surfaceRuleArmNotSent` / `RULE_NOT_SENT_WARN_TEXT` + `surfaceRuleArmRejected` / `RULE_NOT_SENT_REJECTED_WARN_TEXT`(#334,0.43.0:人在卡上按下的「不再询问」被整条丢弃时的诚实告知——**两条刻意分开**:前者=**引擎能力位未确认**(换台引擎/等探测就好),后者=**这次选择没过包内表核/互斥核**(表外文本/坏下标/两臂同场,换引擎也不会变) —— 编辑臂 `respondFreeFormRules` 与批臂 `respondBatchRuleOffers` 两条同形存量共用一条,与 `surfaceRememberNotApplied` 同族纪律:决断照送、只是规则没存,静默丢掉用户明确意图 = 让人以为功能坏了)· `DecideTransportRetryExhaustedError`(Inkglow-1085 P0a:decide 出站瞬断重试耗尽的 typed 判别 —— HitlBridge 内建单次退避重试,耗尽走重呈臂不判死 turn;端一般只消费行为,不需要 instanceof) | suspended→decide→resume 环。🔴 **D-1 两元组 verbatim 回显**是字节级断言的安全不变量,端**不许重实现它的任何一段**。🔴 键空间边界(web [C1] d3 拦截):`gateIdentity` 四常量两函数只覆盖 HITL questionId/callKey 空间;seat 的 `TOOL_PERMISSION_REQUEST_ID_DOMAINS`(`plan:` 等)是另一键空间,**两者绝不合并**(合并=座位校验器静默拒全部 plan-review 卡) | `src/hitl/hitlBridge.ts`、`toolApprovalWire.ts`、`askGateWire.ts`、`planReviewWire.ts`、`hitlHostSurface.ts`、`gateIdentity.ts`、`armedGateRegistry.ts`、`parkOwnership.ts`、`parkResolver.ts`、`approvalsFeed.ts`、`frameRouter.ts`(**只挑名导出** `toolEndOutputText`/`ENGINE_ABORT_TOOL_RESULT`/`isAskTool`/`HITL_REJECT_MESSAGE`/`HITL_INTERRUPT_MESSAGE_FOR_TOOL_USE`)、`parkRowBirthWait.ts`、`approvalDecisionNoteAudit.ts`、`askParkRowRouting.ts`、`resumeRunningCard.ts`(#265 上收的判定层)、`persistedRulesWire.ts`、`localAllowRule.ts`(#244 F2 规则侧) |
135
137
  | 4 | **子代 wire + 面板侧信道台账** | 84 | `tailEngineSubagent` · `installSubagentActivitySink` · `installSubagentTailMetaSink`(#280 件2:tail meta 帧发布口,`contentFrames` 判别位载体)· `stopEngineTask` + `classifyTaskStopConflict` · `fetchEngineSubagentReport` · `steerEngineSubagent`(0.32.0 未发布 #280 件A:additive 第三参 `childTaskId` —— 端有行上下文时**应当**传,传了就走「台账优先 / 缺席即诚实缺席 + `noteBgOwnerAbsence` 留痕」的 Q3 口径,与 tail·taskOutput·subagentOutput 三腿同姿势、与孪生 resume 腿共用同一个 `resolveOwnerRunId` 判据;**不传**则逐字维持旧行为=回落在飞 run)· `resumeSettledSubagent` + `resolveSubagentResumeContext` + `resolveOwnerRunId` + `classifySubagentResumeFailure` + `subagentResumeAvailable`(#242 批 2 A-028.7:resume 判定半场上收,与 steer 孪生同居;取址三态 = 台账有行用行值 / 指名了行但台账缺席则**诚实缺席绝不回落在飞 run** / 没指名行才回落。出路文案归端)· `recordSubagentOwnerFromProgress` + `getBgParentRunOwner`(A-028.6:「子代 → 宿主 run」**单表**,宿主 run 必须由持 stream-local 值的调用方显式传入,包内绝不从 `activeEngineRunId()` 推断)· `noteBgOwnerAbsence`(#242 批 3 [4000] Q3=B:tail/taskOutput·taskStop/subagentOutput 三腿台账缺席即诚实缺席**绝不回落在飞 run**,缺席 warn 留痕每 (腿,taskId) 一条)· `clearBgTerminalFacts`(#242 批 3 扫码修:复活=新周期,旧周期终态事实作废——fleetLedger 复活两腿按尾段清账,factsAccepted 方向核不再拿上周期终态当先例)· `auditRetainWithoutWake`([4000] Q5:引擎宣示 `subagentResume` + 本端在付 `retainSubagentSessions` + 端未实现 `wakeSubagent` ⇒ 响亮一条;`CLIENT_VERBS.wakeSubagent` 维持 fail-soft)· `subscribeSubagentContent` · `subscribeEngineAgentPanel` · `publishQuestionFrame` / `respondToQuestion` | 驱动与观测委派子代;经 module 级台账喂活体 agent/task 面板。全部**能力位 gate**(§5b) | `src/subagent/*.ts`、`src/subagentContentStore.ts`、`src/engineAgentPanelStore.ts`、`src/engineInlineTaskStats.ts`、`src/engineToolLabelStore.ts`、`src/liveQuestionStore.ts` |
136
138
  | 5 | **fleet 投影** | 46 | `createFleetLedger` · `projectTasks` · `projectWorkflows` · `projectFleetAgentRows` · `readEngineActiveBgTasks` · `FLEET_TASK_VIEW_KEYS` · `escapeDisplayControlChars`(不可见字符可见化,行标签/描述消毒的共享底座)· `wireCycleSeq` / `wireRetiredBy`(0.38.0 提货补投的 #261 §2 两位:代际号 = SendMessage 复活即 +1,**缺席 ≠ 第一代**;`retiredBy` 在场 = 这条终态是对账腿从 durable run 行投影出来的、**不是**发布方亲报 —— 幽灵行与正常收尾唯一的 wire 判据。两位都只在场才落键) | 老 `fleetClient` 那一刀的成品:**帧体归库、连接归端** —— 端持 SSE 连接,库做行投影 + 保留台账 | `src/fleet/fleetProjection.ts`、`src/fleet/fleetLedger.ts`、`src/fleetAgentPanelProjection.ts`、`src/fleetTaskDesc.ts` |
137
139
  | 6 | **请求装配(上行唯一构造口)** | 8 | `buildTaskRequest` · `REQUEST_FIELD_MATRIX` · `unregisteredRequestKeys` · `applyLiveRequestDefaults` · `taskNotificationToPrintFrame` | 两条车道(`interactive`/`print`)出站请求的**唯一**构造器;`unregisteredRequestKeys` 是可执行门 —— 端偷带一个未登记键上 wire 就红 | `src/request/taskRequest.ts`、`src/request/printNotification.ts` |
@@ -143,7 +145,7 @@ SDK 核对「本包 import 的每个值级符号仍然导出」「`TaskStats.cos
143
145
  | 12 | **workflow 与后台工作视图** | 19 | `projectWorkflowRun` · `createLiveWorkflowSource` · `ensureWorkflowActivityLedger` · `readWorkflowActivityLedger` · `stopWorkflowActivityLedger` · `resetWorkflowActivityLedgers` · `createBackgroundView` · `projectBackgroundView` · `recordWorkflowAgentTaskId` · `agentDisplayStatus` | 活过一个 turn 的长任务读面:workflow run + 跨 session 后台任务归一表(`assistant.tasks` 与 fleet SSE **两源独立降级**) | `src/workflow.ts`、`src/workflowClient.ts`、`src/workflowMonitor.ts`、`src/agentSession/backgroundView.ts`(+ 纯类型 `src/agentSession/contract.ts`) |
144
146
  | 13 | **座位 IPC 契约** | 33 | `LOCAL_SESSIONS_SPEC` · `SEAT_METHOD_NAMES` · `SEAT_EVENT_TYPES` · `isLocalSessionEvent` · `isToolPermissionRequest` · `toolPermissionRequestId` · `SEAT_VALIDATOR_KEY_COVERAGE` | desktop↔web 座位 IPC 契约的**单一真源**(此前两边各一份、名字零重合 ⇒ 编译器永远不会告诉你它们漂了)。🔴 加 verb 忘了加 `LOCAL_SESSIONS_SPEC` **不报错**:preload 不注册 channel、渲染端读到 `undefined` | `src/seatContract.ts`(**零 import**,纯类型 + 常量 + 纯谓词)。🔴 **证据等级标注(0.42.0,test [5087] 的「语料**种类**缺口」/ cli [5088] 认领件)**:该文件里所有以「CC 如何如何」为形的断言(`212 methods` / `854-channel census` / 方法名逐字保留 / `fQe` 逐字段对照 / 一切 `.vite/build/index.chunk-*.js` 坐标)**证据等级 = 桌面 unpack,本地语料库不可复验** —— 本仓手边可复验的参照语料**只覆盖终端 CLI 形态**的静态产物,拿它去 grep 桌面壳里的符号只会零命中,而零命中在这里**既不证真也不证伪**。复核这些断言**不得**拿本仓语料当反证 |
145
147
  | 14 | **宿主端口与会话槽** | 26 | `installHost` · `installHostFor` · `hostPortMisses(For)` · `DEFAULT_SESSION_KEY` · `hostEnv` · `unrefTimer` · `parseLocaleTag` / `pickUiLanguage`(#244 F4 族D A-028.20:locale tag 手术单源 + UI 语言判定;与 `resolveRegionHint` 双出口成文 —— 语言偏好域 en/zh ≠ 地址可达域 cn/intl/unknown,`zh-Hant` 前者 zh 后者 intl 是设计)· `engineNamespaceKeyFor` / `mergeSessionMapRecord` / `mergeEngineEntry`(A-028.12:会话 id 映射单一键形 + merge 判定;存储经 `SessionMapStorePort` 归端 —— cli 文件锁/原子写,web localStorage)| 进程/端级装配层(settings/fs/queue/timers/session/log/probe),与 per-turn 的 `AdapterContext` **分层**。头注的判定规则:**这个能力每 turn 都会变吗?** 会 ⇒ `ctx`;不会 ⇒ `installHost` | `src/host.ts`、`src/hostEnv.ts`、`src/sessionSlot.ts`、`src/unrefTimer.ts`、`src/env/{localeGeo,localeTag,uiLanguage}.ts`、`src/sessionMap.ts` |
146
- | 15 | **控制面与传输** | 76 | `ControlRouter`(+ `ControlSafetyError`)· `makeEngineWireClient` + `resolveWireAuth` · `installEngineWireTarget` · `diagnoseSseIdleTear` / `isSseIdleError` · `attemptActiveRunSelfHeal` + `activeRunBusySignal` + `activeRunSelfHealRow` / `activeRunBusyHeadlessRow` · `kickEngineCapsProbe` / `engineCapTrue` / `invalidateEngineCaps(baseUrl, probe?)`(#307 S25:引擎温切后的 caps 生产失效口 —— kick 自带幂等闸,同 baseUrl 重启后不显式失效就永远读到旧引擎那一版能力位;调用方 = 壳的 respawn/restartEngine。🔴 **推荐两参形**:第二参给替代探测则「推进代际 + 注册新探测」在同一同步块内完成,失效与下一次 kick 之间那个「等待者读到未判」的窗按构造不存在;单参形保留给「只丢缓存、这一刻没有替代探测」的调用方,那种情形下读到未判是诚实结局) · `mapBrainStatusToRetry` · `waitForClaimRelease` + `CLAIM_RELEASED_STATES` / `CLAIM_HELD_STATES` · `atMostOnceFailureClass` / `readSteerDelivery` · `clearRunningChoiceOffer`(Inkglow-1085 P0b①:「Do nothing」登记的清口 —— 端的「重新打开操作菜单」入口;登记在场时 attemptActiveRunSelfHeal 不整卡重弹,not-parked 结局带 `alreadyOffered: true` 判别位,端据此降级渲一行)· `INTERACTIVE_WAY_OUT`(默认出路串单源)· `engineSessionParamFor`(design/285 批 0:`?session=` 派生的 **per-key** 形 —— `hostSessionFor(sessionKey)?.currentSessionId()` + [1501] 空串归一;零参 `engineSessionParam()` = 默认槽兼容层,取值链逐字等价)· `normalizeWirePrincipal`(A-028.10:principal 在场性 trim 原语 —— 全空白=缺席不发头,engineWireTarget 两臂/makeEngineWireClient/壳 livePrincipal 同尺)· `classifyTurnWireError` / `isWireTransportError` / `isPreStreamDrainingReject` / `isResumeAtRejection` / `drainingRetryDelayMs` / `scenarioDenyFromError` + `WIRE_NETWORK_ERROR_PATTERN`(A-028.11/.13:turn 错误分型判定半场,人话文案与渲染归端) | 上行通道的**监管**半场(submit / steer / kill / 队列命令定序)+ 传输构造、caps 探测、SSE 断流分诊、**409 active-run 自愈** | `src/controlRouter.ts`、`steering.ts`、`sseIdleTriage.ts`、`retryStatus.ts`、`diagnostics.ts`、`engineWireSdk.ts`、`engineWireTarget.ts`、`src/principalWire.ts`、`src/wireErrorTriage.ts`、`engineSessionParam.ts`、`engineCapsCache.ts`、`liveInitToolFace.ts`、`adapter/activeRunSelfHeal.ts` |
148
+ | 15 | **控制面与传输** | 78 | `ControlRouter`(+ `ControlSafetyError`)· `makeEngineWireClient` + `resolveWireAuth` · `installEngineWireTarget` · `diagnoseSseIdleTear` / `isSseIdleError` · `attemptActiveRunSelfHeal` + `activeRunBusySignal` + `activeRunSelfHealRow` / `activeRunBusyHeadlessRow` · `kickEngineCapsProbe` / `engineCapTrue` / `invalidateEngineCaps(baseUrl, probe?)`(#307 S25:引擎温切后的 caps 生产失效口 —— kick 自带幂等闸,同 baseUrl 重启后不显式失效就永远读到旧引擎那一版能力位;调用方 = 壳的 respawn/restartEngine。🔴 **推荐两参形**:第二参给替代探测则「推进代际 + 注册新探测」在同一同步块内完成,失效与下一次 kick 之间那个「等待者读到未判」的窗按构造不存在;单参形保留给「只丢缓存、这一刻没有替代探测」的调用方,那种情形下读到未判是诚实结局) · `mapBrainStatusToRetry` · `waitForClaimRelease` + `CLAIM_RELEASED_STATES` / `CLAIM_HELD_STATES` · `atMostOnceFailureClass` / `readSteerDelivery` · `clearRunningChoiceOffer`(Inkglow-1085 P0b①:「Do nothing」登记的清口 —— 端的「重新打开操作菜单」入口;登记在场时 attemptActiveRunSelfHeal 不整卡重弹,not-parked 结局带 `alreadyOffered: true` 判别位,端据此降级渲一行)· `INTERACTIVE_WAY_OUT`(默认出路串单源)· `engineSessionParamFor`(design/285 批 0:`?session=` 派生的 **per-key** 形 —— `hostSessionFor(sessionKey)?.currentSessionId()` + [1501] 空串归一;零参 `engineSessionParam()` = 默认槽兼容层,取值链逐字等价)· `normalizeWirePrincipal`(A-028.10:principal 在场性 trim 原语 —— 全空白=缺席不发头,engineWireTarget 两臂/makeEngineWireClient/壳 livePrincipal 同尺)· `classifyTurnWireError` / `isWireTransportError` / `isPreStreamDrainingReject` / `isResumeAtRejection` / `drainingRetryDelayMs` / `scenarioDenyFromError` + `WIRE_NETWORK_ERROR_PATTERN`(A-028.11/.13:turn 错误分型判定半场,人话文案与渲染归端) | 上行通道的**监管**半场(submit / steer / kill / 队列命令定序)+ 传输构造、caps 探测、SSE 断流分诊、**409 active-run 自愈** | `src/controlRouter.ts`、`steering.ts`、`sseIdleTriage.ts`、`retryStatus.ts`、`diagnostics.ts`、`engineWireSdk.ts`、`engineWireTarget.ts`、`src/principalWire.ts`、`src/wireErrorTriage.ts`、`engineSessionParam.ts`、`engineCapsCache.ts`、`liveInitToolFace.ts`、`adapter/activeRunSelfHeal.ts` |
147
149
  | 16 | **引擎词汇表与包自检** | 48 | `CONFIG_REFUSAL_CODES` / `isConfigRefusalCode` · `STOP_CONFLICT_CODES` · `isInterruptedToolEndCode` · `isRewindFamilyCode` · `CLIENT_VERBS` · `compensationSplitViolations` · `DELEGATION_CAP_CODES` / `isDelegationCapCode` / `DELEGATION_CONCURRENCY_CAP` / `DELEGATION_SESSION_CAP`(0.38.0 #318 件④:core 5.48.0 design/323 委派席位到限**两码,处置不对称禁合并** —— 并发帽=**可等**(兄弟结束即有位)/ 会话累计帽=**等也没用**(这条会话的配额用尽))· `CONFIG_DELEGATION_ENTRY_CAPS`(同批入 `CONFIG_REFUSAL_CODES` 识别表)· `delegationCapDispositionOf` / `MCP_SERVER_REVOKED`(0.39.0 载体到货消费件:core 5.50.0 补 `{ error: code, code }` 孪生拼法后两码真上 `tool_end.errorCode`,0.38.0「先立词不落消费分支」的已知局限自此解除;处置轴 `wait-for-slot` / `reuse-existing-or-await-reap` 机器可读(累计帽=retained-window 帐,行回收配额即回,处置=SendMessage 复用,**非**「换会话/永久耗尽」——0.38.0 段该句系勘误),未知 `delegation.*` 码 ⇒ `undefined`;`mcp.server_revoked` = 操作员 mid-session 吊销 server 后的工具面本地闸(被吊销的 server **名**今天不过 wire 境:detail.server 是进程内位,抬升腿只 lift code——归因渲染候 core 补 typed detail,已点名);载体门 = engine-vocab G3 腿锚 core dist 铸点) | 三端分臂共用的**去字面化** `errorCode` 词表(病根正是三端各抄一份字面);编译期 verb 闭合门;搬迁补偿登记表 | `src/engineErrorCodes.ts`(计数以 `scripts/public-export-baseline.json` 为准,别手抄;A-028.11/.13 补 `DRAINING_ERROR_CODE`/`SCENARIO_NOT_ALLOWED_ERROR_CODE`/`RESUME_AT_ERROR_CODE_PREFIX`;#318 件④ 补 `delegation.*` 族四位 + `config.delegation_entry_caps`;0.39.0 补三新码消费件三位)、`src/classifierVerdictWire.ts`、`src/compensations.ts`、`src/clientSlice.ts` |
148
150
 
149
151
  🔴 **`engineErrorCodes` 的开集纪律**(该文件头注逐字):这些 `ReadonlySet` / 前缀谓词一律是**识别表**,
@@ -239,14 +241,29 @@ type EventProjection =
239
241
 
240
242
  ### 3d. 键级剥离(帧投影了,但键被投影边界剥掉)
241
243
 
242
- `case 'task_progress'` 的白名单**逐字**只 stamp 七键:
244
+ `case 'task_progress'` 的白名单**逐字**只 stamp 八键(0.47.0 起,#363 补 `model`):
243
245
 
244
246
  ```
245
- taskId · name · usage · currentAction · workflowRunId · workflowAgentLabel · parentToolCallId
247
+ taskId · name · usage · model · currentAction · workflowRunId · workflowAgentLabel · parentToolCallId
246
248
  ```
247
249
 
248
- **`taskType` / `status` / `parentTaskId` 三键在本层被整体剥掉** —— §7 缺口 **P-1**。
249
- lane 归属改用 id 形状 / `workflowRunId` 启发式判(`src/adapt/arms.ts`)。
250
+ 🆕 **`model`(0.47.0;server ≥7.52.1)** = 这条 tick 所属**子 run 的模型 id**(core 铸点
251
+ `prepared.model.id`;server `trace/project.js` `taskProgressEventData` 里条件 spread:
252
+ `string` 且非空才发)。本层同条件透传 —— **空串/缺席一律整键不铸**(空串既不是模型 id 也不是
253
+ 「不知道」)。补它之前是 `workflowRunId` 的**同形第二例**:上游真发、白名单剥掉、两边代码看着都对。
254
+ ⚠️ sdk **7.3.0** 的 `task_progress` 臂**尚未声明**这一位(与 `requiresRealApproval`/`ruleOffers`
255
+ 同形:server 已真发、SDK 锚未跟),故本层是**结构视图读**;SDK 补上当天那处 cast 可整条删。
256
+
257
+ **仍然被本层剥掉的五键**(`seq` / `taskType` / `parentTaskId` / `status` / `eventId`):
258
+ - `taskType` / `status` / `parentTaskId` —— 在册的 §7 缺口 **P-1**;lane 归属今天改用 id 形状 /
259
+ `workflowRunId` 启发式判(`src/adapt/arms.ts`)。
260
+ - `seq` —— core #258 的 stop-cycle 代际号(复活即 +1)。fleet 面已有同轴的 `wireCycleSeq`(0.38.0),
261
+ **tick 这条腿今天没有消费方**;SDK 7.3.0 连声明都没有(core/server 两侧都有)。
262
+ - `eventId` —— EventIdentity 的另一半;本臂只补了 `parentToolCallId`(lane 判据要它),`eventId` 至今无消费方。
263
+
264
+ 🔴 **透一位的前置条件**(键账的维护规矩,不是修辞):说得出**谁读它、读来干什么**,并同批更新
265
+ 本节的逐字表。这张表由常驻门 `scripts/run-additive-key-passthrough-test.mjs` 的 G1 段与**真产物**
266
+ 逐名对账(双向:多透一个红、少透一个也红;档与码不一致同样红)。
250
267
 
251
268
  **实现锚**:`src/adapter/downstream/eventToSdkMessage.ts` 的 `case 'task_progress'`。
252
269
 
@@ -807,6 +824,56 @@ durable park 腿走 `HitlBridge.decideTool(outcome, toolUseID, opts, preResolved
807
824
  **实现锚**:`src/hitl/hitlBridge.ts`(`HitlSafetyError`)、`src/hitl/hitlHostSurface.ts`(`isNoPendingError`)、
808
825
  `src/hitl/parkOwnership.ts`(`isHitlSafetyErrorLike`)。
809
826
 
827
+ ### 4f. 🆕 decide 失败上的 `currentPending` 指路键(0.47.0;server S-02 ≥7.52)
828
+
829
+ server 的 409 `approval_stale` 拒体带一个 **additive 指路键** `currentPending` —— 本会话**当前**那条
830
+ pending 的三件 D-1 坐标。本包把它从 decide 失败原样搬到调用方拿得到的 outcome 上。
831
+
832
+ | 位 | 语义 |
833
+ |---|---|
834
+ | `toolName` | 当前 pending 被门住的工具名(与 pending 行同源同值)。**UNTRUSTED-for-display** |
835
+ | `boundCallId` | 当前 pending 的 `pendingAction.toolCallId` —— 重定位后 decide 要**逐字回显**的 D-1 锚 |
836
+ | `boundInputHash?` | server 铸的绑定 hash(行上有才带)。🔴 **逐字回显,绝不本地重算**(§4d 同一条铁律) |
837
+
838
+ **端怎么接**(三件,逐条是纪律不是建议):
839
+
840
+ 1. **读口**:`readDecideCurrentPending(err)` —— 从任意抛出物读出 `GateCurrentPending | undefined`。
841
+ 宿主若自己接 decide,用这一个读口,**别再铸第二份窄读器**。经本包 durable fs 审批腿
842
+ (`surfaceFsApprovalAndDecide`)时它已经落在 `FsApprovalOutcome.currentPending` 上,直接读。
843
+ 2. **它是指路,不是裁决**:server 自己的登记原话逐字 ——「纯指路/便利面,**不参与任何门/CAS/resume
844
+ 判定**」。合法用法只有一个:拿 `boundCallId`(+`boundInputHash`)**一跳重定位**到当前那条 pending,
845
+ 重新呈卡、重新 decide。🔴 **绝不**拿它当「可以自动重决」的凭据:上一张卡的答案是人对**另一件事**
846
+ 给的,自动搬过去 = 替人对他没看过的事按 Yes(与 `binding_mismatch` 同一条铁律,§4d/§4e)。
847
+ 3. **缺席什么都不证明**:老 server / 非工具门 / server 侧行读失败(它自己的 F 类留痕臂)/ 本次失败
848
+ 根本不是 stale 臂 —— 四种情形在 wire 上**同形**。⇒ 缺席时退回既有姿势(重拉 `GET /v1/approvals`
849
+ 自行重定位),**不许**把缺席读成「没有别的 pending 了」。
850
+ 4. 🔴 **`checkpointToken` 永不过境**:server 侧 resume 凭证不外发,本形也刻意没有那一位;
851
+ 读口对表外键一律不搬运(常驻门逐条钉)。
852
+
853
+ 5. **它同时是一道「绝不自动重决」闸**(#363,异源复审 [high] 采纳):`allowSession`(accept-session)
854
+ 那条腿会先发 `approve + remember:'session'`,失败时回退一发**纯 approve**。修前那条回退臂除两个
855
+ 本地错误类外**全吞** —— 引擎回的 stale(带 `currentPending`)会被当成「老 server 不识别
856
+ `remember`」,然后**用人对旧卡给的答案再发一次 decide**,正是 §4d/§4e 那条铁律禁的动作;
857
+ 顺带首发的指路键还会被第二发的错误顶掉。现在:**拒体带指路键 ⇒ 立即上抛**(只发一次 decide,
858
+ 指路键原样进 outcome),人重新决断。老 server 的 400 未知键**照旧回退**(既有兼容腿宽度不变)。
859
+
860
+ ⚠️ **可达性如实登记(端接之前必读)**:sdk 的 `ApprovalDecision` 自 1.0.0 起**刻意无**
861
+ `checkpointToken`,而 server 的 stale 臂**只在调用方回显该 token 时触发** ⇒ **经 SDK client 的
862
+ decide 今天拿不到这枚 409**。本位是给「注入自有传输层 / 读别人写的 wire」的宿主与将来上游放行准备的
863
+ 通路 —— 今天在标准 SDK 路径上它恒缺席。这不是缺陷,是如实的射程边界(见 §7b)。
864
+
865
+ ⚠️ **类型形自铸的记账**:sdk **7.3.0** 有逐字同形的 `ApprovalStaleCurrentPending` +
866
+ `ApprovalStaleError.currentPending`,但本包 peer 地板是 **>=7.2.0**(那一版上两个名字都不存在),
867
+ `import type` 会让装 7.2.0 的端**当场编不过**,而抬地板对所有消费方都是提要求、不是 additive。
868
+ ⇒ 与 `RuleOffer`(#334)同款处置:**自铸形 + 记账**,名字刻意**不同名**(`GateCurrentPending`)。
869
+ **退役条件**:peer 地板抬到 `>=7.3.0` 的那一批换成上游类型别名。
870
+
871
+ **实现锚**:`src/hitl/hitlBridge.ts`(`GateCurrentPending` / `readDecideCurrentPending`)、
872
+ `src/hitl/toolApprovalWire.ts`(`FsApprovalOutcome.currentPending`,allow + deny 两条 decide 失败臂)、
873
+ `src/hitl/parkResolver.ts`(ask 腿同形);常驻门 `scripts/run-additive-key-passthrough-test.mjs` G2 段。
874
+
875
+ ---
876
+
810
877
  ---
811
878
 
812
879
  ## §5 能力位 gate 义务与端口缺席语义
@@ -1256,7 +1323,7 @@ CHANGELOG 0.29.0「已知局限」段与相应 JSDoc 都有成文。**别在读
1256
1323
 
1257
1324
  | ID | 级别 | 一句话现状 | 实现锚 | 端**今天**必须怎么办 |
1258
1325
  |---|---|---|---|---|
1259
- | **P-1** | med | `task_progress` 白名单**只 stamp 七键**(`taskId`/`name`/`usage`/`currentAction`/`workflowRunId`/`workflowAgentLabel`/`parentToolCallId`);SDK `events.d.ts` `task_progress` 臂上声明的 **`taskType` / `status` / `parentTaskId` 三键被整体剥掉**(`eventId` 也从不 stamp)。⚠️ 码里的理由注释只覆盖 `status` + EventIdentity(「service wire whitelist strips `status` + EventIdentity」)—— **`taskType` 与 `parentTaskId` 被剥掉,码里没有任何说法**,而 SDK 明写 `parentTaskId` 正是同一张 server 白名单**产出**的 | `src/adapter/downstream/eventToSdkMessage.ts` 的 `case 'task_progress'`;lane 启发式在 `src/adapt/arms.ts`(`recordWorkflowAgentTaskId` 三级门) | 别指望从一条 progress tick 上拿到委派类别(`taskType`)或嵌套归属(`parentTaskId`)。嵌套要从 `bg_notification.parentTaskId` 经 `src/fleet/fleetLedger.ts` 的 `recordBgParentRun` 恢复;workflow lane 归属走 `src/adapt/arms.ts` 的三级门。🔴 **别在端侧自己从别处补进投影**(那是绕过唯一投影口) |
1326
+ | **P-1** | med | `task_progress` 白名单**只 stamp 八键**(`taskId`/`name`/`usage`/`model`/`currentAction`/`workflowRunId`/`workflowAgentLabel`/`parentToolCallId`;`model` 是 0.47.0 补的,#363)。server 投影**发 13 键**,**仍被剥掉五键**:`taskType` / `status` / `parentTaskId`(本条原本的三件)+ `seq`(core #258 stop-cycle 代际号,tick 这条腿今天无消费方;SDK 7.3.0 连声明都没有)+ `eventId`(EventIdentity 的另一半,至今无消费方)。✅ **0.47.0 销掉的那半**:原文说「`taskType` 与 `parentTaskId` 被剥掉,码里没有任何说法」—— 现在该臂头注有**逐条族扫账**(放行 8 / 剥离 5,各带「谁没在读它」),且档与码由常驻门 `scripts/run-additive-key-passthrough-test.mjs` G1 段**双向对账** | `src/adapter/downstream/eventToSdkMessage.ts` 的 `case 'task_progress'`;lane 启发式在 `src/adapt/arms.ts`(`recordWorkflowAgentTaskId` 三级门) | 别指望从一条 progress tick 上拿到委派类别(`taskType`)或嵌套归属(`parentTaskId`)。嵌套要从 `bg_notification.parentTaskId` 经 `src/fleet/fleetLedger.ts` 的 `recordBgParentRun` 恢复;workflow lane 归属走 `src/adapt/arms.ts` 的三级门。🔴 **别在端侧自己从别处补进投影**(那是绕过唯一投影口);要透一位先走 §3d 的前置条件 |
1260
1327
  | **P-2** | med | 包内注释断言「**子代从不发终态 tick**」(`src/adapt/arms.ts` 的 `toolEndResultArm` ② MF-10 段逐字;`src/adapt/panelTasks.ts` 与补偿 T36/T34 复述),而 pin 的 SDK 逐字说 **server ≥1.258 会转发 core 的 SETTLE 终态 tick**(`"completed"`/`"failed"`)。**且这个矛盾自我维持** —— P-1 的白名单删掉了 `status`,所以终态 tick 就算上了 wire,在包内也**观测不到**。源码里**没有任何一处**把它记为假断言 | `src/adapt/arms.ts`(MF-10 段)、`src/adapt/panelTasks.ts`、`src/compensations.ts`(T36 `retireOn: 'W6(引擎为每条子代发终态 tick)后…'` / T34 `retireOn: null`) | 把四处防御 sweep 当成子代行 settle 的**唯一**机制;**不要**在端侧建「等子代终态 tick」的状态机 —— 包永远不会交给你一条 |
1261
1328
  | **P-3** | med | `approval_request`(design/172 流内审批开卡帧)在本包**零消费口** ⇒ `dropped('unsupported_arm')`(有痕、没人接) | `src/adapter/downstream/eventToSdkMessage.ts` 的 `case 'approval_request'` | 存量路径今天仍能决断(legacy `tool_approval` 腿在,同一只 ask 出两帧,顺序钉死「先 legacy、后 `approval_request`」)。🔴 **但降级路径不保证**(逐字):legacy respond 是 **live-only + same-replica**(错副本 404),重连时 pending 卡靠 `approval_request` preamble 对账 —— 「收到 legacy 帧后断线、按 `Last-Event-ID` 重连只再看到 `approval_request`」以及**非粘性多副本部署**这两条路上,今天只能等窗口到期 → park/deny。**多副本 worker 上这是已知缺口不是环境问题** |
1262
1329
  | **P-4** | med | **`approval_revoke` 在 SDK union 里连成员都没有**(SDK 顶注:known asymmetry,`Registering it is an open item for the next batch`)⇒ 本包不可能有 case ⇒ 运行期落 `dropped('unknown_arm')`;审批链也看不见它(`isToolApprovalFrame` 只认两帧)。全仓 `grep -rn "revoke"` = **0** | `src/adapter/downstream/eventToSdkMessage.ts` 的 `default` 臂;`src/hitl/toolApprovalWire.ts` 的 `isToolApprovalFrame` | 🔴 **引擎撤卡时本包不会替你撤那张卡** —— 被撤的 ask 会一直留在屏上,直到它自己的 5 分钟 TTL / deny 路径触发。要 revoke 语义的端只能自己接 raw named-SSE 腿并撤自己的卡(`unknown_arm` 的 drop 至少留了一行痕) |
@@ -1281,6 +1348,10 @@ CHANGELOG 0.29.0「已知局限」段与相应 JSDoc 都有成文。**别在读
1281
1348
  | **P-36** | low | **中断文案归一只覆盖 `Operation aborted` 这一串**(#323 症状②,0.43.0;clay 裁定的**明确边界**,不是漏做):同一次用户中断里,**执行前被连坐**的旁观者拿的是 core 的另一串 `operation aborted before execution`,`interrupted_never_started` 族又是第三种;这两族**刻意不并入**中断改写臂 —— core 显式拒绝合并两串向([4973]),两串各承真语义(`Operation aborted` = 执行中被中止 / 该串 = 从未执行),而且它们**各有自己的文案与折叠腿**(端侧的 interrupted-batch 折叠 + `TOOL_END_INTERRUPTED_CODES` 词表)。⇒ 纯取消批里,那两族的 tool_end 今天仍按各自原文呈现 | `src/hitl/frameRouter.ts`(`isUserInterruptRewritable` 的判据①头注 + `isEngineAbortToolEnd` 的两串族说明)· 负控 = `scripts/run-hitl-gate-honesty-test.mjs` F13-d | 端**不要**假定「用户中断 ⇒ 这一批 tool_end 文案全是 CC 中断串」;两族按各自既有腿归因(机读码优先,文案兜底)。要不要并成一形是**语义裁定**不是实现细节,需 clay 先裁 |
1282
1349
  | **P-34** | low | **编辑臂预检判官在浏览器 lane 结构上装不了**(#225 / [5076],0.42.0):`precheckEditedRuleText` 的唯一合法实参是 core 5.57.0 那只纯函数,而 `@sema-agent/core` 的 barrel 值级拉 `node:crypto`/`node:fs`/`node:path` —— 本包**不能**做 value 级 re-export(portability 门 `EXPECTED_PACKAGES_INDEX` 等值门 + esbuild 浏览器腿双重否决,施工时实打验证) | `src/hitl/editedRuleTextPrecheck.ts`(模块头注的「为什么是端口注入」段) | Node 宿主(TUI / desktop 主进程)装上即得内联即时校验;**浏览器 lane 留缺席走「提交后才知道」的往返形**,这是设计不是漏装。🔴 缺席**不可**据以判断部署形态 |
1283
1350
 
1351
+ | **P-41** | low | 🆕 **decide 的 `currentPending` 指路键今天在标准 SDK 路径上恒缺席**(0.47.0 件②,如实登记的射程边界不是缺陷):server 的 409 `approval_stale` 臂**只在调用方回显 `checkpointToken` 时触发**(engine 7.52.1 `http/server.js` 的 `resumeCheckpoint`,`if (binding?.checkpointToken && …)` 真字节),而 sdk 的 `ApprovalDecision` 自 1.0.0 起**刻意删掉**了那一位 ⇒ 经 SDK client 的 decide 拿不到这枚 409。另:ask 腿(`GateOutcome.currentPending`)**包内无消费方** —— `GateOutcome` 是包内型、决断结局不出包;它在场的理由是**同形存量清剿**(两条 decide 失败腿一次改齐) | `src/hitl/hitlBridge.ts`(`readDecideCurrentPending`)、`src/hitl/toolApprovalWire.ts`、`src/hitl/parkResolver.ts` | 按 §4f 接:读得到就一跳重定位,**缺席时退回重拉 `GET /v1/approvals`**,绝不把缺席读成「没有别的 pending 了」。自己注入传输层(非 SDK client)的宿主今天就拿得到 |
1352
+
1353
+ | **P-43** | med(**存量、非本批引入** —— 0.42.0 基线上逐字相同) @cli @web @desktop | 🆕 **accept-session 回退臂的错误分类比它自己的注释宽**(#363 异源复审 [high] 的**未收窄那一半**,如实登记):`allowSession` 腿先发 `approve + remember:'session'`,失败时回退一发纯 approve;那条 catch 的注释写的是「**只**兜『老 server 不识别 remember ⇒ 400 未知键』这一形」,而实际形是 **catch-all 减去三条具名再抛**(`HitlSafetyError` / `DecideTransportRetryExhaustedError` / 0.47.0 新加的『拒体带 `currentPending`』)。⇒ 一个 **404 / 5xx / 宿主自抛的无 status 错误**今天仍会被当成「老 server 不识别 remember」并**自动重发**一次纯 approve。收窄成「只认 400」是**行为改动**,不属于 0.47.0 这个 additive 批的射程 | `src/hitl/toolApprovalWire.ts`(`case 'allow'` 的内层 catch) | 端今天不需要做什么(两发都是 approve,不构成跨门的 double-act);**属主批**:下一个愿意改老引擎兼容腿宽度的批把它收窄成精确的 legacy-400,并同批给回退臂补正控/负控 |
1354
+
1284
1355
  ### 7c. 多会话(sessionKey)面在册局限 —— 多会话端**接之前必读**
1285
1356
 
1286
1357
  | ID | 级别 | 一句话现状 | 实现锚 | 端**今天**必须怎么办 |
@@ -1316,6 +1387,8 @@ CHANGELOG 0.29.0「已知局限」段与相应 JSDoc 都有成文。**别在读
1316
1387
  | **P-29** | low(自检面) | **通知队列口没有存在性读口**:审批卡口有 `hasApprovalCardPort(For)`、HITL 面有 `hitlHostSurfaceFor`、宿主端口族有 `hostSettings()` 等无副作用读口(见 §5a 的 (a) 表),**唯独 `installNotificationQueuePort()` 没有对偶谓词**。而它的 `notificationQueuePortMisses()` 与同族几个 miss 计数一样**初值为 0**,只有真发生过一次「用到了但没装」才递增 ⇒ 「完全没装 + 还没有任何投递」照样是 0。拿它做**启动装配自检**必然假绿 —— §5a 此前正是这么写的(#252 复审 R3/R4 命中,已按端口拆成「存在性读口」与「回归探针」两类) | `src/notifications.ts`(`installNotificationQueuePort` 无对偶读口;`queuePortMisses` 初值与 `port()` 的 null 分支) | 队列口:按 §8-B 真调 `installNotificationQueuePort()`,miss 计数只当**跑过真流量之后**的回归探针用;其余端口按 §5a (a) 表用各自的存在性读口做启动校验。要队列口的读口按 [C162] 令④ 回 C 板提(正位解在包侧:补一个 `hasNotificationQueuePort()` 谓词) |
1317
1388
  | **P-30** | med(HITL 路由面) | **durable 审批腿不按 `gateKind` 路由,且取行有「同 taskId 任意行」回落** (0.30.0 发包扫描 对抗复审 finding① 坐实,**非本窗引入**):`findPendingForTask` 在工具名谓词无命中时走 `?? rows.find(r => r.taskId === taskId)`,而 `surfaceFsApprovalAndDecide` 拿到行之后**不校 `gateKind`** ⇒ 同一 task 上同时停着 `plan_review` / `resource_limit` 行时,会弹出一张 `toolName` 为空串的**工具审批卡**。⚠️ **不会误批**(server 侧 fail-closed):本腿打的是 `POST /v1/approvals/:sessionId/decide`,非工具门在该端点上回 **409 `gate_not_tool_approval`**(SDK `dist/errors.d.ts`;⚠️ **不是** `gate_not_resumable` / `gate_not_plan_review` —— 那两个分别是 `/resume` 与 plan-review 端点的守卫,2026-08-14 对抗复审 R2 订正本条初稿的错码)。🔴 **但后果不止「一次失败的决断」**:decide 抛错 ⇒ `surfaceFsApprovalAndDecide` 折成 `{kind:'failed'}` ⇒ `parkResolver` 走 fail-soft 结束**本次客户端 turn**;而 server 侧 checkpoint 因为 fail-closed **没被消费**,run/session 仍 suspended、仍持 claim ⇒ 重试还会再撞一次。⚠️ **终帧按入口分两形,排障别只等一个码**(2026-08-14 对抗复审 R3 订正本条初稿的单一描述):① **初始 park 入口**(`done{…park…}` 经 `frameRouter.routeDone` 进来,`park.pendingDone` **在场**)⇒ 先 `led.flushHeld()` 吐出 park 期被 HOLD 的**毒化帧**(`tool_end{isError:true, output:'Operation aborted'}`,`frameRouter.ENGINE_ABORT_TOOL_RESULT`),再原样回吐那条 `done` —— **没有**合成 `failed` 终帧、**没有** `hitl_unanswered` 错误码,可观察到的失败信号只有那条 isError 的 `tool_end`。⚠️ **它与「用户真按了拒绝」可以分辨,按 `output` 分**(2026-08-14 对抗复审 R5 订正本条初稿的「同形不可分」;**0.43.0 起是三分不是两分**,见下):fail-soft 这条是 `flushHeld()` 吐出的**毒化帧**,`output` 逐字是 `ENGINE_ABORT_TOOL_RESULT`(`'Operation aborted'`);真 deny 走 `frameRouter` 的 `denied-call` / `deny-stamp-next` 臂,`output` 被改写成 `HITL_REJECT_MESSAGE`(CC `REJECT_MESSAGE` 逐字)。🆕 **0.43.0 新增第三形(#323 症状②)**:fail-soft 的原因若是**用户在门卡上中断**(`GateOutcome.kind === 'aborted'`,= 用户按 Esc/Ctrl+C),同一批毒化帧的 `output` 被归一成 `HITL_INTERRUPT_MESSAGE_FOR_TOOL_USE`(CC `[Request interrupted by user for tool use]` 逐字,公面导出)——`isError` / `errorCode`(含 `gate.parked`)/ `_sema_collateral_abort` 等机读位**一个都不改**。⇒ 端做归因的 `output` 三分:`'Operation aborted'` = 非中断原因的 fail-soft(卡面不可用 / no_pending / 传输失败)· CC 中断串 = 用户中断 · CC REJECT 串 = 用户真拒绝。端**不要**再假定「fail-soft ⇒ 必是引擎原文」;② **续流 / durable re-attach 入口**(`suspended` 进来,无 `pendingDone`)⇒ 才合成 `failed{errorCode:'hitl_unanswered'}`。⇒ 端做告警/埋点时**不要**只锚 `hitl_unanswered`,①那条路径上它根本不出现。⚠️ 定性要分清:这条 fail-soft 链是 durable 腿**通用**的失败路径(设计如此 —— 替代方案是谎报成功,更坏),**不是**本缺口独有;本缺口的**增量**是「弹了一张 `toolName` 为空的卡 + 发了一次注定 409 的 decide + 把用户的一次表态浪费掉」 | `src/hitl/hitlBridge.ts`(`findPendingForTask` 的第二条 `rows.find`)· `src/hitl/toolApprovalWire.ts`(`surfaceFsApprovalAndDecide` 全程零 `gateKind` 读)· 常驻登记见 `scripts/run-durable-card-display-keys-test.mjs` ⑦ 段 `gateKind` 那条未投影理由 | 端**不要**把「durable 卡弹出来了」读成「这一定是个工具门」;拿到 `toolName` 为空串的卡按异常处置、别渲成可决断卡。🔴 正位解在包侧(本腿按 `gateKind` 严格路由 + 回落收窄),要同批想好 pre-`gate_kind` 历史行 `gateKind` 缺席时的降级 —— 属独立设计件,按 [C162] 令④ 回 C 板提 |
1318
1389
 
1390
+ | **P-42** | low @cli @web @desktop | 🆕 **Esc halt 只上收了判定,发射面仍在各端**(0.47.0 件③,刻意的分工不是半成品):`planInteractiveHalt` 给判据与升级码闭集,`POST /v1/runs/:id/interrupt` 的**发射**(以及 `?session=` 供给、超时窗、台账、留痕)仍归端。cli 侧那条「裸 fetch 直拨 interrupt」的网络面豁免,**退役条件就是端接上这个口子**(壳换装不在 0.47.0 批内) | `src/interactiveHalt.ts`;§10 | 按 §10b 的分支表接:判定用本包,发射用端自己的传输腿;🔴 halt 必须排在撕 SSE **之前**(§10c 第 1 条) |
1391
+
1319
1392
  ### 7e. 缺口的共同形状(值得单独说)
1320
1393
 
1321
1394
  **P-1 / P-2 / P-3 / P-4 / P-5 / P-6 / P-7 是同一类**:上游(server / SDK)已经把材料铸到 wire 上了,
@@ -1466,3 +1539,70 @@ reason 里写明「枚举器盲区形」。已知两形:
1466
1539
  1. 公面有没有**新导出 / 撤回**(→ §2 域图 + 基线 diff);
1467
1540
  2. 有没有**新回执键或缺席语义变更**(→ §4 三列表;这一类改动最容易被当成 additive 而漏接);
1468
1541
  3. 有没有**新端口 / 新能力位 gate**(→ §5 + §8 checklist)。
1542
+
1543
+ ---
1544
+
1545
+ ## §10 🆕 上行停止动词的判定层 —— 交互 Esc 的 halt(0.47.0)
1546
+
1547
+ 「用户按 **Esc** ⇒ 先发 **turn 级** halt;升级成 **run 级** cancel 恰有**两格**——
1548
+ ① 引擎自己回了升级闭集里的 **409**(它在说「这里没有在飞 turn 可切,run 级停止请用 cancel」),
1549
+ 或 ② 这一发**连判决都没拿到**(传输失败/超时/未武装)**且**屏上确实挂着审批卡;其余一律不升级」
1550
+ —— 三端(TUI / desktop / web)都会 Esc、都会撞同一个 parked 格,所以判定归本包。
1551
+ **发射**(裸 fetch / SDK verb)、台账、留痕、UI 反馈仍归各端。
1552
+ ⚠️ **别把①漏掉**:只按「拿不到判决且 parked 才升级」接线,会漏掉**引擎明确指路**那一格 ——
1553
+ parked run 的会话锁不放,用户下一条消息照样撞「Session busy」(正是本判定要消灭的病)。
1554
+
1555
+ ### 10a. 两个动词各自唯一能做到的格(**cancel 绝不删**)
1556
+
1557
+ | 动词 | 语义 | 它唯一能做到的事 |
1558
+ |---|---|---|
1559
+ | `POST /v1/runs/:id/interrupt`(**bare 体 `{}`**) | turn 级「切 + 停」(core `stream.halt()`) | 切掉在飞那一轮,run 在边界上终局、**同 session 下一 submit 照常续**。这是 Esc 的正题 |
1560
+ | `POST /v1/runs/:id/cancel` | run 级终局 | **唯一**能把一条停在审批门上的 `suspended`/`needs_review` run 就地终态化、把会话锁放开的动词(server [868] 语义) |
1561
+
1562
+ ⇒ 无条件把 cancel 换成 interrupt = 审批卡挂着按 Esc 的主场景当场回归成「Session busy」病;
1563
+ 无条件补 cancel = 同一条 run 上别的在飞工具(后台 bash 等)被**连坐**拆掉。本判定就是这两条之间那道闸。
1564
+ ⚠️ 带 `text` 的 interrupt 是 **steer** 语义(切 + 转向),**Esc 不用**;未知键的对象体 server 400
1565
+ `request.body_shape`(它刻意不把 malformed 折成换动词)。
1566
+
1567
+ ### 10b. 端怎么接
1568
+
1569
+ ```ts
1570
+ import { planInteractiveHalt, RUN_LEVEL_STOP_ERROR_CODES } from '@sema-agent/client-core'
1571
+
1572
+ // ① 还没打过 ⇒ 恒判 interrupt(这一格刻意不看 parked)
1573
+ planInteractiveHalt({}) // → { action: 'interrupt', reason: 'first-shot' }
1574
+ // ② 端自己发射,把结局折成 InteractiveHaltInterruptOutcome 再问第二次
1575
+ planInteractiveHalt({ parked, interruptOutcome })
1576
+ // → { action: 'escalate-cancel' | 'none', reason: <闭集机读词> }
1577
+ ```
1578
+
1579
+ | `interruptOutcome` | `parked` | 判决 | `reason` |
1580
+ |---|---|---|---|
1581
+ | 缺席 / `null` | 任意 | `interrupt` | `first-shot` |
1582
+ | `{kind:'halted'}` | 任意 | `none` | `halted` |
1583
+ | `{kind:'refused', status:409, errorCode ∈ 闭集}` | 任意 | `escalate-cancel` | `engine-says-run-level` |
1584
+ | `{kind:'refused', 其余}` | 任意 | `none` | `refused-no-escalation` |
1585
+ | `{kind:'transport'}` / `{kind:'unarmed'}` | `true` | `escalate-cancel` | `no-verdict-on-parked-card` |
1586
+ | `{kind:'transport'}` / `{kind:'unarmed'}` | 其余 | `none` | `no-verdict-not-parked` |
1587
+ | 认不得的形 | 任意 | `none` | `unknown-outcome` |
1588
+
1589
+ 🔴 **不对称是刻意的**:判**不**升级 = 用户退回「Session busy」卡再选一次(**可恢复**);判**错**升级
1590
+ = 拆掉一条其实还活着的 run 并连坐它身上的在飞工具(**不可恢复**)。凡「证不出来」一律落不升级侧。
1591
+ 🔴 **升级码闭集恰一员** `interrupt.nothing_in_flight`(`RUN_LEVEL_STOP_ERROR_CODES`)。
1592
+ `interrupt.not_held` **刻意在外** —— 它的语义是「**本副本**手上没有可切的 live turn face」,server
1593
+ 自己的原话之一是「run is live on **another replica**」:那不证明全局没有在飞 turn。
1594
+ `steering.not_running` 也在外(run 已终局,补枪是纯噪声)。
1595
+ 🔴 **码与 409 是合取**:只认码不认状态 ⇒ 一只 5xx 只要正文里带上那个码就能骗来一发破坏性 cancel。
1596
+ 🔴 `parked` 只在**没有判决**那两格被读,且**只认严格 `true`**;引擎给了判决时判决说了算(两个方向都是)。
1597
+
1598
+ ### 10c. 端仍然要自己做的三件(本包**不做**)
1599
+
1600
+ 1. **发射与顺序**:🔴 halt 必须排在「撕 SSE」**之前**。交互车道零 `x-detach-on-disconnect`,先撕流 =
1601
+ server 按断连语义当场收尾那条 run,随后落地的 interrupt 只会拿到 409 `steering.not_running`
1602
+ (cli L-11 真机实测:同步撕流形每一轮都是它)。
1603
+ 2. **不挡 UI**:Esc 的本地即时反馈一拍都不许被网络往返推迟;判定本身是同步纯函数,发射走
1604
+ fire-and-forget + 端侧本地兜底窗。
1605
+ 3. **台账与留痕**:「这条 run 被怎么处理了」(切一轮 vs 拆整条)是端的诊断面;`reason` 是**机读闭集词**,
1606
+ 人话文案归端(别把它当展示串)。
1607
+
1608
+ **实现锚**:`src/interactiveHalt.ts`;常驻门 `scripts/run-esc-halt-plan-test.mjs`。
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sema-agent/client-core",
3
- "version": "0.46.0",
4
- "description": "Client-side session runtime shared by every sema human client (TUI / web / desktop): sema wire frames (AgentEvent) -> CC session vocabulary (SDKMessage) with dual-plane output (transcript/chrome), deterministic transcript ids, lane discipline as a type, and the notification/dedup ledgers. Every CC-skin shape is collected here so the wire itself stays neutral. Blackboard [1832] design axioms; [1651]/[1652]/[1653] signed seam design. Renamed from @sema-agent/wire-cc-adapter (0.1.x).",
3
+ "version": "0.47.0",
4
+ "description": "Client-side session runtime shared by every sema human client (TUI / web / desktop): sema wire frames (AgentEvent) -> CC session vocabulary (SDKMessage) with dual-plane output (transcript/chrome), deterministic transcript ids, lane discipline as a type, and the notification/dedup ledgers. Every CC-skin shape is collected here so the wire itself stays neutral. Renamed from @sema-agent/wire-cc-adapter (0.1.x).",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "main": "./dist/index.js",
@@ -18,7 +18,7 @@
18
18
  "README.md",
19
19
  "CHANGELOG.md",
20
20
  "docs/INTEGRATION-CLIENTS.md",
21
- "docs/REFACTOR-LEDGER.md"
21
+ "LICENSE"
22
22
  ],
23
23
  "sideEffects": false,
24
24
  "scripts": {