@sema-agent/client-core 0.41.0 → 0.43.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.
@@ -29,7 +29,7 @@
29
29
  * false」—— 对这一族,发 `false` 是违约(核心根本没有「显式关」这个语义位),所以本模块对
30
30
  * 它们**永不产出 false**:OFF ⇒ 键不出现。
31
31
  *
32
- * ② **explicit-false 族**(2 员:agentListing / skillsListing):
32
+ * ② **explicit-false 族**(3 员:agentListing / skillsListing / **backgroundTasks**):
33
33
  * 它们在 core 里是 **DEFAULT-ON**,SDK 头注写死「`agentListing`/`skillsListing` 是 core
34
34
  * DEFAULT-ON —— **explicit false 才关**(1.254 起 false 真透传;更老 server 静默丢)」。
35
35
  * 于是「删键」在这一族上的含义是**保持开着**,而不是关掉。旧的 `AttachmentsSpec` 把全部键
@@ -37,6 +37,29 @@
37
37
  * 路径可以关闭**(不是「难关」,是类型层封死)。本批把这两键的类型放宽成 `boolean` 并给出
38
38
  * 产出 `false` 的路径。
39
39
  *
40
+ * ══ 🔴 [4982] 候选①(P0-KPI,0.42.0)—— **第三员漏族:`backgroundTasks`** ════════════════════
41
+ *
42
+ * core **5.12.0 (BREAKING)** 把 `attachments.backgroundTasks` 与 `agentListing`/`skillsListing`
43
+ * **同批**翻成 DEFAULT-ON(core `types.d.ts` 逐字:「Post-compact background-task restatement —
44
+ * DEFAULT ON since 5.12.0 (**boolean, not `true`**: explicit `false` is the opt-out; **same contract
45
+ * as the listing family below**)」)。三兄弟同批翻转,本模块**跟修了后两个、漏了第一个** ——
46
+ * `backgroundTasks` 一直留在 ① 族(off = 删键,永不发 false)。
47
+ * 后果是这个旋钮上最坏的一种失效:`SEMA_ATTACHMENTS=off`(文件自称「真·全关」)对这一位与
48
+ * **什么都不配**逐字节同效(两条路都不发这个键 = 都是「开」),**且零报错** —— 用户以为关掉了,
49
+ * 附件照进上下文,没有任何一处会说出来。与 ② 族当初那个洞是同一个病形的第三例。
50
+ *
51
+ * ── 修法:把「能不能显式打开」与「能不能显式关掉」拆成**两条正交的轴** ────────────────────────
52
+ * `backgroundTasks` 同时住两族,这不是分类含糊,这**就是**它的契约:
53
+ * · **轴 A(可显式开)** = `full` 会全开的那 7 员 —— `backgroundTasks` 在内,**membership 不动**;
54
+ * · **轴 B(可显式关)** = core DEFAULT-ON 的 3 员 —— `backgroundTasks` 新入。
55
+ * 只把它整个搬进 ② 族(= 从轴 A 里删掉)是**错的修法**,两条理由都是硬的:
56
+ * ① `full` / `default` / `+backgroundTasks` 三条既有路会当场少发一个 `backgroundTasks: true`
57
+ * —— 对 core ≥5.12.0 行为等价(默认就是开),但对**更老引擎**那是真的从「开」变成「关」,
58
+ * 而本模块手里没有 server 版本这个量(见下面 §能力边界),没资格替用户做那个降级;
59
+ * ② 空 env 的默认产出 `{backgroundTasks: true, toolsDelta: true}` 是 [487]② CC-parity 那一对的
60
+ * 字面承诺,删掉它就是把「默认逐字节不变」这条本模块的立身纪律自己破了。
61
+ * ⇒ 本批**只加一条出路**(`off` 与 `-backgroundTasks` 产出 `false`),其余取值逐字节不变。
62
+ *
40
63
  * ── env 面(`SEMA_ATTACHMENTS`)的取值语法,以及为什么选这一种 ────────────────────────────────
41
64
  * 目标是「**默认行为逐字节不变** + 有路可关 DEFAULT-ON 两键」,所以选了**在既有值上加逗号 token
42
65
  * 列表**,而不是新开一个 env:
@@ -45,8 +68,9 @@
45
68
  * · 逗号 token 列表对**存量取值零影响**:`unset` / `full` / `all` / off 拼法四种老写法解析结果
46
69
  * 逐字节不变(FIX7 段 ⑤ 有正面钉),新语法只在用户真写了逗号 token 时才被触发。
47
70
  * 语法(逗号分隔,大小写不敏感,允许空白):
48
- * · `off|0|false|no|none`(整值) ⇒ **真·全关**:literal-true 族一个不发(那一族的 off 就是
49
- * 删键)+ DEFAULT-ON 两键发 `false`。
71
+ * · `off|0|false|no|none`(整值) ⇒ **真·全关**:只可显式开的那一族一个不发(它们的 off 就是
72
+ * 删键)+ DEFAULT-ON **三键**(agentListing / skillsListing /
73
+ * backgroundTasks)发 `false`。
50
74
  * 🔴 对抗复审 [medium](2026-08-07)判的真病:这一支此前直接 `return undefined`(不发字段),
51
75
  * 而「不发字段」对 DEFAULT-ON 两键的含义**恰恰是保持开着** —— 一个叫 off 的总开关关不掉十件里
52
76
  * 的两件,且用户没有任何办法察觉。旧行为在 <1.254 的 server 上与新行为**逐字节等价**(那些
@@ -60,8 +84,9 @@
60
84
  * · `full` / `all` ⇒ literal-true 族 7 员全开(**不含** DEFAULT-ON 两键 ——
61
85
  * 不发 = 保持 core 默认开,壳不替 core 做那个决定)
62
86
  * · `default` ⇒ CC-parity 那一对(显式写出「默认」这个意思)
63
- * · `<key>` / `+<key>` ⇒ 单点打开一个 literal-true 族成员
64
- * · `-agentListing` / `-skillsListing` ⇒ 产出 **`false`**(explicit-false 族唯一的关法)
87
+ * · `<key>` / `+<key>` ⇒ 单点打开一个「可显式开」族成员(轴 A 的 7 员)
88
+ * · `-agentListing` / `-skillsListing` / `-backgroundTasks` ⇒ 产出 **`false`**(DEFAULT-ON 三键
89
+ * 唯一的关法;`-backgroundTasks` 是 [4982] 候选① 补的那条)
65
90
  * · `todoReminderMode=baseline|off` ⇒ 取值型键
66
91
  * 🔴 未登记 token / 对 literal-true 族用 `-` / todoReminderMode 取闭集外的值 ⇒ **fail-loud 抛错**。
67
92
  * 静默忽略是这条链上最坏的失效形:用户以为自己关掉了 agentListing,而请求照发、附件照进上下文,
@@ -88,7 +113,12 @@ export interface AttachmentsSpec {
88
113
  };
89
114
  planModeReminder?: true;
90
115
  budgetUsd?: true;
91
- backgroundTasks?: true;
116
+ /**
117
+ * 🔴 **两族兼属**([4982] 候选①,0.42.0):它在轴 A(`full` 会开)也在轴 B(core ≥5.12.0
118
+ * DEFAULT-ON,`false` 才是关)。类型因此是 `boolean` 而不是 `true` —— 钉成 `true` 正是
119
+ * 「关不掉」那个洞的类型层根因。
120
+ */
121
+ backgroundTasks?: boolean;
92
122
  toolsDelta?: true;
93
123
  mcpInstructions?: true;
94
124
  agentListing?: boolean;
@@ -96,8 +126,13 @@ export interface AttachmentsSpec {
96
126
  }
97
127
  /** 键面台账(机读位):加键必须同批登记,否则下方 `Covers` 双向钉编译红。 */
98
128
  export declare const ATTACHMENTS_SPEC_KEYS: readonly ["todoReminder", "todoReminderMode", "changedFiles", "planModeReminder", "budgetUsd", "backgroundTasks", "toolsDelta", "mcpInstructions", "agentListing", "skillsListing"];
99
- /** ② 族(core DEFAULT-ON,explicit false 才关)。 */
100
- export declare const ATTACHMENTS_DEFAULT_ON_KEYS: readonly ["agentListing", "skillsListing"];
129
+ /**
130
+ * **轴 B —— 可显式关**(core DEFAULT-ON,`false` 才是关;`-<key>` 与总开关 `off` 的作用面)。
131
+ * 🔴 [4982] 候选①(0.42.0):`backgroundTasks` 补入 —— core 5.12.0 与另两员**同批**翻 DEFAULT-ON,
132
+ * 本模块当时只跟修了两员。这张表**不是** ① 族的补集(`backgroundTasks` 两族兼属,见 AttachmentsSpec
133
+ * 上的键注与文件头注的「两条正交的轴」段)。
134
+ */
135
+ export declare const ATTACHMENTS_DEFAULT_ON_KEYS: readonly ["agentListing", "skillsListing", "backgroundTasks"];
101
136
  /**
102
137
  * ENV source → the `TaskRequest.attachments` stamp.
103
138
  *
@@ -29,7 +29,7 @@
29
29
  * false」—— 对这一族,发 `false` 是违约(核心根本没有「显式关」这个语义位),所以本模块对
30
30
  * 它们**永不产出 false**:OFF ⇒ 键不出现。
31
31
  *
32
- * ② **explicit-false 族**(2 员:agentListing / skillsListing):
32
+ * ② **explicit-false 族**(3 员:agentListing / skillsListing / **backgroundTasks**):
33
33
  * 它们在 core 里是 **DEFAULT-ON**,SDK 头注写死「`agentListing`/`skillsListing` 是 core
34
34
  * DEFAULT-ON —— **explicit false 才关**(1.254 起 false 真透传;更老 server 静默丢)」。
35
35
  * 于是「删键」在这一族上的含义是**保持开着**,而不是关掉。旧的 `AttachmentsSpec` 把全部键
@@ -37,6 +37,29 @@
37
37
  * 路径可以关闭**(不是「难关」,是类型层封死)。本批把这两键的类型放宽成 `boolean` 并给出
38
38
  * 产出 `false` 的路径。
39
39
  *
40
+ * ══ 🔴 [4982] 候选①(P0-KPI,0.42.0)—— **第三员漏族:`backgroundTasks`** ════════════════════
41
+ *
42
+ * core **5.12.0 (BREAKING)** 把 `attachments.backgroundTasks` 与 `agentListing`/`skillsListing`
43
+ * **同批**翻成 DEFAULT-ON(core `types.d.ts` 逐字:「Post-compact background-task restatement —
44
+ * DEFAULT ON since 5.12.0 (**boolean, not `true`**: explicit `false` is the opt-out; **same contract
45
+ * as the listing family below**)」)。三兄弟同批翻转,本模块**跟修了后两个、漏了第一个** ——
46
+ * `backgroundTasks` 一直留在 ① 族(off = 删键,永不发 false)。
47
+ * 后果是这个旋钮上最坏的一种失效:`SEMA_ATTACHMENTS=off`(文件自称「真·全关」)对这一位与
48
+ * **什么都不配**逐字节同效(两条路都不发这个键 = 都是「开」),**且零报错** —— 用户以为关掉了,
49
+ * 附件照进上下文,没有任何一处会说出来。与 ② 族当初那个洞是同一个病形的第三例。
50
+ *
51
+ * ── 修法:把「能不能显式打开」与「能不能显式关掉」拆成**两条正交的轴** ────────────────────────
52
+ * `backgroundTasks` 同时住两族,这不是分类含糊,这**就是**它的契约:
53
+ * · **轴 A(可显式开)** = `full` 会全开的那 7 员 —— `backgroundTasks` 在内,**membership 不动**;
54
+ * · **轴 B(可显式关)** = core DEFAULT-ON 的 3 员 —— `backgroundTasks` 新入。
55
+ * 只把它整个搬进 ② 族(= 从轴 A 里删掉)是**错的修法**,两条理由都是硬的:
56
+ * ① `full` / `default` / `+backgroundTasks` 三条既有路会当场少发一个 `backgroundTasks: true`
57
+ * —— 对 core ≥5.12.0 行为等价(默认就是开),但对**更老引擎**那是真的从「开」变成「关」,
58
+ * 而本模块手里没有 server 版本这个量(见下面 §能力边界),没资格替用户做那个降级;
59
+ * ② 空 env 的默认产出 `{backgroundTasks: true, toolsDelta: true}` 是 [487]② CC-parity 那一对的
60
+ * 字面承诺,删掉它就是把「默认逐字节不变」这条本模块的立身纪律自己破了。
61
+ * ⇒ 本批**只加一条出路**(`off` 与 `-backgroundTasks` 产出 `false`),其余取值逐字节不变。
62
+ *
40
63
  * ── env 面(`SEMA_ATTACHMENTS`)的取值语法,以及为什么选这一种 ────────────────────────────────
41
64
  * 目标是「**默认行为逐字节不变** + 有路可关 DEFAULT-ON 两键」,所以选了**在既有值上加逗号 token
42
65
  * 列表**,而不是新开一个 env:
@@ -45,8 +68,9 @@
45
68
  * · 逗号 token 列表对**存量取值零影响**:`unset` / `full` / `all` / off 拼法四种老写法解析结果
46
69
  * 逐字节不变(FIX7 段 ⑤ 有正面钉),新语法只在用户真写了逗号 token 时才被触发。
47
70
  * 语法(逗号分隔,大小写不敏感,允许空白):
48
- * · `off|0|false|no|none`(整值) ⇒ **真·全关**:literal-true 族一个不发(那一族的 off 就是
49
- * 删键)+ DEFAULT-ON 两键发 `false`。
71
+ * · `off|0|false|no|none`(整值) ⇒ **真·全关**:只可显式开的那一族一个不发(它们的 off 就是
72
+ * 删键)+ DEFAULT-ON **三键**(agentListing / skillsListing /
73
+ * backgroundTasks)发 `false`。
50
74
  * 🔴 对抗复审 [medium](2026-08-07)判的真病:这一支此前直接 `return undefined`(不发字段),
51
75
  * 而「不发字段」对 DEFAULT-ON 两键的含义**恰恰是保持开着** —— 一个叫 off 的总开关关不掉十件里
52
76
  * 的两件,且用户没有任何办法察觉。旧行为在 <1.254 的 server 上与新行为**逐字节等价**(那些
@@ -60,8 +84,9 @@
60
84
  * · `full` / `all` ⇒ literal-true 族 7 员全开(**不含** DEFAULT-ON 两键 ——
61
85
  * 不发 = 保持 core 默认开,壳不替 core 做那个决定)
62
86
  * · `default` ⇒ CC-parity 那一对(显式写出「默认」这个意思)
63
- * · `<key>` / `+<key>` ⇒ 单点打开一个 literal-true 族成员
64
- * · `-agentListing` / `-skillsListing` ⇒ 产出 **`false`**(explicit-false 族唯一的关法)
87
+ * · `<key>` / `+<key>` ⇒ 单点打开一个「可显式开」族成员(轴 A 的 7 员)
88
+ * · `-agentListing` / `-skillsListing` / `-backgroundTasks` ⇒ 产出 **`false`**(DEFAULT-ON 三键
89
+ * 唯一的关法;`-backgroundTasks` 是 [4982] 候选① 补的那条)
65
90
  * · `todoReminderMode=baseline|off` ⇒ 取值型键
66
91
  * 🔴 未登记 token / 对 literal-true 族用 `-` / todoReminderMode 取闭集外的值 ⇒ **fail-loud 抛错**。
67
92
  * 静默忽略是这条链上最坏的失效形:用户以为自己关掉了 agentListing,而请求照发、附件照进上下文,
@@ -88,10 +113,20 @@ export const ATTACHMENTS_SPEC_KEYS = [
88
113
  'agentListing',
89
114
  'skillsListing',
90
115
  ];
91
- /** ② 族(core DEFAULT-ON,explicit false 才关)。 */
92
- export const ATTACHMENTS_DEFAULT_ON_KEYS = ['agentListing', 'skillsListing'];
93
- /** 族里 `full` 会全开的那 7 员(`todoReminderMode` 是取值型,不搭 full 的车)。 */
94
- const LITERAL_TRUE_KEYS = [
116
+ /**
117
+ * **轴 B —— 可显式关**(core DEFAULT-ON,`false` 才是关;`-<key>` 与总开关 `off` 的作用面)。
118
+ * 🔴 [4982] 候选①(0.42.0):`backgroundTasks` 补入 —— core 5.12.0 与另两员**同批**翻 DEFAULT-ON,
119
+ * 本模块当时只跟修了两员。这张表**不是** 族的补集(`backgroundTasks` 两族兼属,见 AttachmentsSpec
120
+ * 上的键注与文件头注的「两条正交的轴」段)。
121
+ */
122
+ export const ATTACHMENTS_DEFAULT_ON_KEYS = ['agentListing', 'skillsListing', 'backgroundTasks'];
123
+ /**
124
+ * **轴 A —— 可显式开**:`full` 会全开的那 7 员(`todoReminderMode` 是取值型,不搭 full 的车)。
125
+ * 🔴 `backgroundTasks` **留在本表**([4982] 候选① 的取舍,理由见文件头注):把它从这里删掉会让
126
+ * `full`/`default`/`+backgroundTasks` 三条既有路少发一个 `true`,对 core <5.12.0 是真的从
127
+ * 「开」变成「关」,而本模块手里没有 server 版本这个量。
128
+ */
129
+ const OPT_IN_KEYS = [
95
130
  'todoReminder',
96
131
  'changedFiles',
97
132
  'planModeReminder',
@@ -100,9 +135,15 @@ const LITERAL_TRUE_KEYS = [
100
135
  'toolsDelta',
101
136
  'mcpInstructions',
102
137
  ];
138
+ /**
139
+ * **只可显式关、不可显式开**的那两员(轴 B ∖ 轴 A)。`+agentListing` 这类写法的 fail-loud 判据
140
+ * 锚在**本表**而不是整个轴 B —— 锚轴 B 会把 `+backgroundTasks` / `full` 一起误拒([4982] 候选①
141
+ * 修复批的判别力所在:两条轴各判各的,不许拿一条去代另一条)。
142
+ */
143
+ const DEFAULT_ON_ONLY_KEYS = ['agentListing', 'skillsListing'];
103
144
  /** CC-parity 默认对(TOC live 路径,[487]②)。 */
104
145
  const DEFAULT_PAIR = ['backgroundTasks', 'toolsDelta'];
105
- const _attachmentsKeyPins = [true, true, true, true];
146
+ const _attachmentsKeyPins = [true, true, true, true, true, true];
106
147
  void _attachmentsKeyPins;
107
148
  const TODO_REMINDER_MODES = ['baseline', 'off'];
108
149
  /** fail-loud 的统一措辞:说清**哪个 token 坏了**、以及合法形是什么(判词要指得出下一步)。 */
@@ -156,7 +197,7 @@ export function attachmentsForRequest(env = hostEnv()) {
156
197
  if (!isDefaultOnKey(key)) {
157
198
  refuse(token, `"${key}" belongs to the literal-true family whose OFF is "delete the key", not "send false" ` +
158
199
  '(core wire contract, board [479] ask-2) — simply do not turn it on. ' +
159
- 'Only the core DEFAULT-ON keys (agentListing / skillsListing) accept an explicit false');
200
+ `Only the core DEFAULT-ON keys (${ATTACHMENTS_DEFAULT_ON_KEYS.join(' / ')}) accept an explicit false`);
160
201
  }
161
202
  spec[key] = false;
162
203
  continue;
@@ -174,7 +215,10 @@ export function attachmentsForRequest(env = hostEnv()) {
174
215
  const key = canonicalKey(lhs.startsWith('+') ? lhs.slice(1) : lhs);
175
216
  if (key === undefined)
176
217
  refuse(token, 'unknown attachments key');
177
- if (isDefaultOnKey(key)) {
218
+ // 🔴 判据锚 `DEFAULT_ON_ONLY_KEYS`(轴 B ∖ 轴 A),**不是**整个轴 B:0.42.0 起
219
+ // `backgroundTasks` 两族兼属,拿轴 B 判会把 `+backgroundTasks` 一起误拒(而它在
220
+ // `full` 里本来就开得出来,拒它等于同一件事有两个答案)。
221
+ if (isDefaultOnOnlyKey(key)) {
178
222
  refuse(token, `"${key}" is core DEFAULT-ON — turning it "on" is a no-op that would send a redundant true; ` +
179
223
  `use "-${key}" to turn it OFF`);
180
224
  }
@@ -184,7 +228,8 @@ export function attachmentsForRequest(env = hostEnv()) {
184
228
  }
185
229
  return spec;
186
230
  }
187
- /** 总开关 off 的产出:literal-true 族一个不发(删键即关)+ DEFAULT-ON 两键显式 `false`。 */
231
+ /** 总开关 off 的产出:只可显式开的那族一个不发(删键即关)+ DEFAULT-ON **三键**显式 `false`
232
+ * ([4982] 候选①:`backgroundTasks` 从 0.42.0 起真的在这条路上被关掉)。 */
188
233
  function allDefaultOnOff() {
189
234
  const out = {};
190
235
  for (const k of ATTACHMENTS_DEFAULT_ON_KEYS)
@@ -193,7 +238,7 @@ function allDefaultOnOff() {
193
238
  }
194
239
  function allLiteralTrue() {
195
240
  const out = {};
196
- for (const k of LITERAL_TRUE_KEYS)
241
+ for (const k of OPT_IN_KEYS)
197
242
  out[k] = true;
198
243
  return out;
199
244
  }
@@ -206,9 +251,14 @@ function splitAssign(token) {
206
251
  function canonicalKey(lower) {
207
252
  return ATTACHMENTS_SPEC_KEYS.find(k => k.toLowerCase() === lower);
208
253
  }
254
+ /** 轴 B:可显式关(`-<key>` 的合法作用面)。 */
209
255
  function isDefaultOnKey(k) {
210
256
  return ATTACHMENTS_DEFAULT_ON_KEYS.includes(k);
211
257
  }
258
+ /** 轴 B ∖ 轴 A:只可关不可开(`+<key>` 的 fail-loud 作用面)。 */
259
+ function isDefaultOnOnlyKey(k) {
260
+ return DEFAULT_ON_ONLY_KEYS.includes(k);
261
+ }
212
262
  /** 单 token 形的历史回落判据(见 `attachmentsForRequest` 里那段注释)。 */
213
263
  function isKnownToken(token) {
214
264
  if (token === 'full' || token === 'all' || token === 'default')
@@ -52,8 +52,10 @@
52
52
  * 从 `GET /v1/approvals` 拿 pending 行(input=questions + boundCallId/boundInputHash 绑定)、
53
53
  * 合成 QuestionFrame 借既有 AskUserQuestion overlay(planReviewWire 同款 local-responder 姿势,
54
54
  * CC 原生对话框,零新 UI)、等用户作答。
55
- * 3. 决断走 T23 HitlBridge(答案 ride `ApprovalDecision.answer`,D-1 绑定 verbatim 回显;拒答=
56
- * cancel-by-deny,contract/04 §2.4)。decide SYNC 驱动的:引擎跑到下一个 park 或终态才返
55
+ * 3. 决断走 T23 HitlBridge(答案 ride `ApprovalDecision.answer`,D-1 绑定 verbatim 回显;拒答 =
56
+ * 一次 **TOOL 级 deny** 应答,把这只 ask 结算掉 —— server `ASSISTANT-WIRE-CONTRACT.md` §4a。
57
+ * 🔴 **0.42.0 撤稿**:原文写的是「拒答=cancel-by-deny,contract/04 §2.4」;§4a 逐字反对把
58
+ * deny 读成 run kill,逐条撤稿见 `hitlBridge.decideTool` 头注)。decide 是 SYNC 驱动的:引擎跑到下一个 park 或终态才返
57
59
  * (实测 4-5s+),返回体 `{status}` 即下一状态。
58
60
  * 4. 续流:attach `GET /v1/runs/:taskId/events`(durable leg;实证 durable log 从 park 点才开始,
59
61
  * 无挂起前重放)。重放的 tool_start/tool_end 按 toolCallId 去重;被 gate 的 call 在重放里带来
@@ -71,6 +73,7 @@
71
73
  import type { AgentEvent } from '@sema-agent/sdk';
72
74
  import { type AskGateWireDeps } from './frameRouter.js';
73
75
  export { HITL_REJECT_MESSAGE, ENGINE_ABORT_TOOL_RESULT } from './frameRouter.js';
76
+ export { HITL_INTERRUPT_MESSAGE_FOR_TOOL_USE } from './frameRouter.js';
74
77
  export type { AskGateWireDeps } from './frameRouter.js';
75
78
  export type { AskAnsweredOutput } from './gateLedger.js';
76
79
  export { SEMA_COLLATERAL_ABORT_KEY } from './gateLedger.js';
@@ -1,5 +1,5 @@
1
1
  import { createGateLedger } from './gateLedger.js';
2
- import { isHostProgressFrame, routeFrame, } from './frameRouter.js';
2
+ import { flushHeldWithInterruptRewrite, isHostProgressFrame, routeFrame, } from './frameRouter.js';
3
3
  import { resolvePark } from './parkResolver.js';
4
4
  // ── 宿主面口(搬迁差分 3)────────────────────────────────────────────────────────────────────
5
5
  //
@@ -19,6 +19,9 @@ import { resolvePark } from './parkResolver.js';
19
19
  // `toAnsweredOutput` / 下方 `bridgeAskUserQuestionGates`)、类型四个。定义搬去了实现所在的
20
20
  // 那一刀,本文件是它们对外的**唯一门牌**——下游 import 路径不变,public-export 基线不变。
21
21
  export { HITL_REJECT_MESSAGE, ENGINE_ABORT_TOOL_RESULT } from './frameRouter.js';
22
+ // 件 B(#323 症状②,2026-08-25):CC 中断形的**单一真源**导出(与 SEMA_COLLATERAL_ABORT_KEY 同款
23
+ // 理由 —— 三端各自手抄同一句 CC 文案 = 漂移温床)。壳/web/desktop 认这个常量。
24
+ export { HITL_INTERRUPT_MESSAGE_FOR_TOOL_USE } from './frameRouter.js';
22
25
  // #324 / [4907]:连坐 abort 机读位的**单一真源**导出([C93] `toolEndOutputText` 同款理由 ——
23
26
  // 三端各自手抄键名 = 漂移温床)。壳/web/desktop 认这个常量,不认文案。
24
27
  export { SEMA_COLLATERAL_ABORT_KEY } from './gateLedger.js';
@@ -58,8 +61,14 @@ export async function* bridgeAskUserQuestionGates(source, deps, opts) {
58
61
  led.noteSeq(ev);
59
62
  // HOLD 只护 park 窗口:host lane 的模型推进帧一到就放行扣留帧(触发集的来龙去脉见
60
63
  // `frameRouter.isHostProgressFrame` 上方长注)。
61
- if (led.heldCount() > 0 && isHostProgressFrame(ev))
62
- yield* led.flushHeld();
64
+ // 件 B(异源复审 finding 采纳):这个中途出口**也**要走中断感知的那一个。病形 = 用户按了
65
+ // Esc(signal 已 aborted)之后,源流里还有一条**已缓冲**的推进帧到达 ⇒ 扣留帧从这里放行,
66
+ // 绕开收口出口的改写 ⇒ 用户照旧看到红 `Error: Operation aborted`。
67
+ // 🔴 这里**没有终帧**可读,所以只有宿主腿(signal)能命中;引擎还在推进而 signal 没 abort 的
68
+ // 正常情形下,判据两腿全不命中 ⇒ 与本件之前逐字节相同。
69
+ if (led.heldCount() > 0 && isHostProgressFrame(ev)) {
70
+ yield* flushHeldWithInterruptRewrite(led, { signal: opts?.signal });
71
+ }
63
72
  const action = await routeFrame(ev, ctx);
64
73
  if (action.kind === 'skip')
65
74
  continue;
@@ -83,7 +92,9 @@ export async function* bridgeAskUserQuestionGates(source, deps, opts) {
83
92
  }
84
93
  if (!park) {
85
94
  // 源流走完没终帧(disconnect/abort)——flush 后结束,与现状一致。
86
- yield* led.flushHeld();
95
+ // 件 B(#323 症状②):没有终帧 ⇒ 出口证据只剩宿主腿(`signal.aborted` = 用户按了 Esc);
96
+ // 缺席时包装恒走原路,与本行修前逐字节相同。
97
+ yield* flushHeldWithInterruptRewrite(led, { signal: opts?.signal });
87
98
  return;
88
99
  }
89
100
  hops++;
@@ -0,0 +1,102 @@
1
+ /**
2
+ * editedRuleTextPrecheck.ts — 编辑臂的「边打字边校验」判官**转出口**(#225 / [5076] 自领件,0.42.0)。
3
+ *
4
+ * ## 这一格要解决的问题
5
+ * 卡上的规则编辑框想在人还在打字的时候就说「这条能不能提交」。唯一合法的判官是引擎自己那只 ——
6
+ * core 5.57.0 导出的纯函数 `precheckEditedRuleText(text, command)`,它与 `confirmRuleApproval` 的
7
+ * fresh-edit 臂**跑同一个函数体**(`checkEditedRuleText`),所以预检面与真裁判**结构性不可分歧**。
8
+ * 在边界上复读一遍 `parseAllowRuleText` 是**装第二个更严的判官**:`Bash(adb *)` 这类肌肉记忆形
9
+ * 会被它当场误杀,而编辑面本来先跑拼写归一(`normalizeEditedSpelling`)再接受
10
+ * ——[5071] 定界②的原话,core [5075] 复核确认。
11
+ *
12
+ * ## 🔴 为什么是**端口注入**而不是 `export { precheckEditedRuleText } from '@sema-agent/core'`
13
+ * [5076] 的自领件原话是「re-export + 类型面」。**逐字照做在本包里是不可能的**,而且不是口味问题,
14
+ * 是本仓门族的硬约束(0.42.0 施工时实测,证据两条):
15
+ * · `scripts/run-client-core-portability-test.mjs` 的 `EXPECTED_PACKAGES_INDEX` 是**等值门**
16
+ * ——包总入口闭包的外部包集合恒等于 `{diff, @sema-agent/sdk}`,多一个当场红;
17
+ * · 同门 ③ 段拿 esbuild `--platform=browser` **真打一次包**。`@sema-agent/core` 的 barrel 值级
18
+ * 拉进 `node:crypto`(`engine/session/log-digest.js`)、`node:fs`/`node:path`
19
+ * (`core/skills-directory.js`)…… 浏览器腿当场打不成。
20
+ * ⇒ 一个 value 级 re-export 会把**整台引擎**焊进每一个装 client-core 的端(web/desktop 首当其冲),
21
+ * 换来一只纯函数。本包的存在理由正是「三端共用的**客户端**运行时」,这条边不能连。
22
+ *
23
+ * 于是转出口取**同等效力的第二形**:
24
+ * ① **类型面**逐形转出({@link EditedRuleTextPrecheck} / {@link EditedRuleTextPrechecker}),
25
+ * 三端从此对着同一份形写代码,谁都不用自己抄一份返回型;
26
+ * ② **注入口**({@link installEditedRuleTextPrechecker}):Node 宿主(TUI / desktop 主进程 /
27
+ * server 侧渲染)在启动时把 core 那只函数原样装进来 —— **原样装,不许包一层判断**;
28
+ * ③ **读口**({@link precheckEditedRuleText}):没装 ⇒ 返 `undefined`(**诚实缺席**),
29
+ * 绝不返一个编出来的 `{ok:true}`。缺席时呈现面的正解是退「提交后才知道」的往返形
30
+ * ——[5076] 施工要点③ 已经把这两段写成互不阻塞的两腿。
31
+ * 🔴 浏览器 lane(web)结构上装不了这只函数(引擎不在那一侧),它**本就该**留缺席走往返形;
32
+ * 本口的缺席语义因此不是「宿主忘了装」的同义词,别拿它去判部署形态。
33
+ *
34
+ * ## 🔴 两点语义预披露(core [5075],消费侧写文案前必读;[5076] 已落账为 #225 施工要点)
35
+ * · `ok` = **可提交**,不是「confirm 必成」。记录级闸(binding 回声 / 记录 owner 与 state /
36
+ * scope 继承 / 部署开关)**不在**预检覆盖面,由 `confirmRuleApproval` 对着记录重判。
37
+ * ⇒ UI 文案只许写「可提交」,**绝不**写「将被批准」。
38
+ * · `canonicalRule` 可能与输入**字节不同**(拼写归一)。它是真正会落库的那一形,所以内联反馈
39
+ * 应当显示 **canonical 形**,而不是把人的输入原样回显。
40
+ *
41
+ * ## 调用面契约(core 逐字,注入口不许替它软化)
42
+ * · `text` 是**人**的输入 ⇒ 任何畸形拼写都是一个**答案**(refusal),永不抛;
43
+ * · `command` 是**调用方**的上下文(卡所依据的被裁决命令)⇒ 缺席/非串是**调用方 bug**,
44
+ * core 响亮抛(`config.invalid_argument`)。本包的读口**不吞这个抛**(见 {@link precheckEditedRuleText}
45
+ * 的 `@throws`):把它折成一个 refusal 会让人读到「你的规则不接受空命令」——把调用方的错
46
+ * 误归到人头上,正是 core 那段头注点名要避免的事。
47
+ * · 卡上**压根没有被裁决命令**(老记录 / 店丢了这一列)⇒ 那是**记录级**拒绝,不该问本函数;
48
+ * 宿主此时的正解是**根本不给编辑框**。
49
+ */
50
+ /**
51
+ * `text × command` 三步门(拼写归一 → 语法门 → coverage 闸)的判决 —— core 5.57.0
52
+ * `EditedRuleTextPrecheck` 的**逐形镜像**(结构等价,故 core 那只函数可直接装进
53
+ * {@link EditedRuleTextPrechecker} 而无需任何适配)。
54
+ *
55
+ * 🔴 `code` 的**在场规则**与提交面的 `edit_rejected.detail` 同源:验证器自己拒时**在场**,
56
+ * coverage 闸拒时**缺席** —— 同一具函数体出来的同一批值。缺席不许被读成「没有理由」。
57
+ * 🔴 `code` 是**开集串**:core 的 `RuleRejectCode` 词表属主是引擎,本包**刻意不镜像那张枚举**
58
+ * (镜像 = 引擎加员当天把一个合法值判没,#157 词表纪律的反面)。要分支就按串比,未知值原样呈现。
59
+ */
60
+ export type EditedRuleTextPrecheck = {
61
+ ok: true;
62
+ /** 真正会落库的那一形(可能与输入字节不同 —— 拼写归一)。内联反馈显示**这一形**。 */
63
+ canonicalRule: string;
64
+ } | {
65
+ ok: false;
66
+ /** 验证器自己拒时在场;coverage 闸拒时缺席(开集串,见类型注)。 */
67
+ code?: string;
68
+ /** 拒绝措辞(与 `edit_rejected.detail` 同词汇)。原样呈现,包边界零加工。 */
69
+ message: string;
70
+ };
71
+ /**
72
+ * 判官的形:`(text, command) => 判决`。**唯一合法的实参** = core 5.57.0 导出的
73
+ * `precheckEditedRuleText`,原样装,不许在外面包一层自己的判断
74
+ * ——包一层就是把「同一函数体」这条唯一的抗漂移保证亲手拆掉。
75
+ */
76
+ export type EditedRuleTextPrechecker = (text: string, command: string) => EditedRuleTextPrecheck;
77
+ /**
78
+ * 装/卸判官。返回**还原函数**(装口族统一姿势:还原到装之前那一只,不是无脑清空 —— 嵌套装载
79
+ * 时无脑清空会把外层那只一起抹掉)。
80
+ *
81
+ * Node 宿主的装法(**逐字**,别加工):
82
+ * ```ts
83
+ * import { precheckEditedRuleText } from '@sema-agent/core'
84
+ * installEditedRuleTextPrechecker(precheckEditedRuleText)
85
+ * ```
86
+ */
87
+ export declare function installEditedRuleTextPrechecker(fn: EditedRuleTextPrechecker | null): () => void;
88
+ /**
89
+ * 装没装(存在性读口哨兵:`boolean` 形 —— 见 `docs/INTEGRATION-CLIENTS.md` §5a 的三形分野)。
90
+ * 呈现面据它决定**给不给编辑框的内联反馈**;`false` 时编辑框本身仍然可用(走往返形)。
91
+ */
92
+ export declare function hasEditedRuleTextPrechecker(): boolean;
93
+ /**
94
+ * 预检一段编辑过的规则文本。**没装判官 ⇒ 返 `undefined`**(诚实缺席,不是 `{ok:true}`
95
+ * 也不是 `{ok:false}` —— 两者都是在替一只不在场的判官发言)。
96
+ *
97
+ * @throws core 判官对**调用方 bug**(`command` 缺席/非串/不可渲染)的响亮拒**原样上抛**
98
+ * ——本包不吞它:吞掉会让一个调用方错误变成一句对人的判决(见模块头注「调用面契约」)。
99
+ */
100
+ export declare function precheckEditedRuleText(text: string, command: string): EditedRuleTextPrecheck | undefined;
101
+ /** 测试钩:卸口。 */
102
+ export declare function _resetEditedRuleTextPrecheckerForTest(): void;
@@ -0,0 +1,91 @@
1
+ /**
2
+ * editedRuleTextPrecheck.ts — 编辑臂的「边打字边校验」判官**转出口**(#225 / [5076] 自领件,0.42.0)。
3
+ *
4
+ * ## 这一格要解决的问题
5
+ * 卡上的规则编辑框想在人还在打字的时候就说「这条能不能提交」。唯一合法的判官是引擎自己那只 ——
6
+ * core 5.57.0 导出的纯函数 `precheckEditedRuleText(text, command)`,它与 `confirmRuleApproval` 的
7
+ * fresh-edit 臂**跑同一个函数体**(`checkEditedRuleText`),所以预检面与真裁判**结构性不可分歧**。
8
+ * 在边界上复读一遍 `parseAllowRuleText` 是**装第二个更严的判官**:`Bash(adb *)` 这类肌肉记忆形
9
+ * 会被它当场误杀,而编辑面本来先跑拼写归一(`normalizeEditedSpelling`)再接受
10
+ * ——[5071] 定界②的原话,core [5075] 复核确认。
11
+ *
12
+ * ## 🔴 为什么是**端口注入**而不是 `export { precheckEditedRuleText } from '@sema-agent/core'`
13
+ * [5076] 的自领件原话是「re-export + 类型面」。**逐字照做在本包里是不可能的**,而且不是口味问题,
14
+ * 是本仓门族的硬约束(0.42.0 施工时实测,证据两条):
15
+ * · `scripts/run-client-core-portability-test.mjs` 的 `EXPECTED_PACKAGES_INDEX` 是**等值门**
16
+ * ——包总入口闭包的外部包集合恒等于 `{diff, @sema-agent/sdk}`,多一个当场红;
17
+ * · 同门 ③ 段拿 esbuild `--platform=browser` **真打一次包**。`@sema-agent/core` 的 barrel 值级
18
+ * 拉进 `node:crypto`(`engine/session/log-digest.js`)、`node:fs`/`node:path`
19
+ * (`core/skills-directory.js`)…… 浏览器腿当场打不成。
20
+ * ⇒ 一个 value 级 re-export 会把**整台引擎**焊进每一个装 client-core 的端(web/desktop 首当其冲),
21
+ * 换来一只纯函数。本包的存在理由正是「三端共用的**客户端**运行时」,这条边不能连。
22
+ *
23
+ * 于是转出口取**同等效力的第二形**:
24
+ * ① **类型面**逐形转出({@link EditedRuleTextPrecheck} / {@link EditedRuleTextPrechecker}),
25
+ * 三端从此对着同一份形写代码,谁都不用自己抄一份返回型;
26
+ * ② **注入口**({@link installEditedRuleTextPrechecker}):Node 宿主(TUI / desktop 主进程 /
27
+ * server 侧渲染)在启动时把 core 那只函数原样装进来 —— **原样装,不许包一层判断**;
28
+ * ③ **读口**({@link precheckEditedRuleText}):没装 ⇒ 返 `undefined`(**诚实缺席**),
29
+ * 绝不返一个编出来的 `{ok:true}`。缺席时呈现面的正解是退「提交后才知道」的往返形
30
+ * ——[5076] 施工要点③ 已经把这两段写成互不阻塞的两腿。
31
+ * 🔴 浏览器 lane(web)结构上装不了这只函数(引擎不在那一侧),它**本就该**留缺席走往返形;
32
+ * 本口的缺席语义因此不是「宿主忘了装」的同义词,别拿它去判部署形态。
33
+ *
34
+ * ## 🔴 两点语义预披露(core [5075],消费侧写文案前必读;[5076] 已落账为 #225 施工要点)
35
+ * · `ok` = **可提交**,不是「confirm 必成」。记录级闸(binding 回声 / 记录 owner 与 state /
36
+ * scope 继承 / 部署开关)**不在**预检覆盖面,由 `confirmRuleApproval` 对着记录重判。
37
+ * ⇒ UI 文案只许写「可提交」,**绝不**写「将被批准」。
38
+ * · `canonicalRule` 可能与输入**字节不同**(拼写归一)。它是真正会落库的那一形,所以内联反馈
39
+ * 应当显示 **canonical 形**,而不是把人的输入原样回显。
40
+ *
41
+ * ## 调用面契约(core 逐字,注入口不许替它软化)
42
+ * · `text` 是**人**的输入 ⇒ 任何畸形拼写都是一个**答案**(refusal),永不抛;
43
+ * · `command` 是**调用方**的上下文(卡所依据的被裁决命令)⇒ 缺席/非串是**调用方 bug**,
44
+ * core 响亮抛(`config.invalid_argument`)。本包的读口**不吞这个抛**(见 {@link precheckEditedRuleText}
45
+ * 的 `@throws`):把它折成一个 refusal 会让人读到「你的规则不接受空命令」——把调用方的错
46
+ * 误归到人头上,正是 core 那段头注点名要避免的事。
47
+ * · 卡上**压根没有被裁决命令**(老记录 / 店丢了这一列)⇒ 那是**记录级**拒绝,不该问本函数;
48
+ * 宿主此时的正解是**根本不给编辑框**。
49
+ */
50
+ let installed = null;
51
+ /**
52
+ * 装/卸判官。返回**还原函数**(装口族统一姿势:还原到装之前那一只,不是无脑清空 —— 嵌套装载
53
+ * 时无脑清空会把外层那只一起抹掉)。
54
+ *
55
+ * Node 宿主的装法(**逐字**,别加工):
56
+ * ```ts
57
+ * import { precheckEditedRuleText } from '@sema-agent/core'
58
+ * installEditedRuleTextPrechecker(precheckEditedRuleText)
59
+ * ```
60
+ */
61
+ export function installEditedRuleTextPrechecker(fn) {
62
+ const prev = installed;
63
+ installed = fn;
64
+ return () => {
65
+ installed = prev;
66
+ };
67
+ }
68
+ /**
69
+ * 装没装(存在性读口哨兵:`boolean` 形 —— 见 `docs/INTEGRATION-CLIENTS.md` §5a 的三形分野)。
70
+ * 呈现面据它决定**给不给编辑框的内联反馈**;`false` 时编辑框本身仍然可用(走往返形)。
71
+ */
72
+ export function hasEditedRuleTextPrechecker() {
73
+ return installed !== null;
74
+ }
75
+ /**
76
+ * 预检一段编辑过的规则文本。**没装判官 ⇒ 返 `undefined`**(诚实缺席,不是 `{ok:true}`
77
+ * 也不是 `{ok:false}` —— 两者都是在替一只不在场的判官发言)。
78
+ *
79
+ * @throws core 判官对**调用方 bug**(`command` 缺席/非串/不可渲染)的响亮拒**原样上抛**
80
+ * ——本包不吞它:吞掉会让一个调用方错误变成一句对人的判决(见模块头注「调用面契约」)。
81
+ */
82
+ export function precheckEditedRuleText(text, command) {
83
+ const fn = installed;
84
+ if (fn === null)
85
+ return undefined;
86
+ return fn(text, command);
87
+ }
88
+ /** 测试钩:卸口。 */
89
+ export function _resetEditedRuleTextPrecheckerForTest() {
90
+ installed = null;
91
+ }
@@ -24,6 +24,20 @@ import type { GateLedger } from './gateLedger.js';
24
24
  * 🔴 pure 门 B7 段:包内冻结字面量(无条件)+ 壳树全树扫描「每一处声明都逐字节相同」(壳树缺席=跳过)。
25
25
  */
26
26
  export declare const HITL_REJECT_MESSAGE = "The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). STOP what you are doing and wait for the user to tell you how to proceed.";
27
+ /**
28
+ * CC `utils/messages.ts` 的 `INTERRUPT_MESSAGE_FOR_TOOL_USE` **逐字**(件 B,#323 症状②,2026-08-25)。
29
+ *
30
+ * 🔴 CC 语料直证的两条事实(壳侧勘察,cli `src/utils/messages.ts:216-217` 逐字节同):
31
+ * ① `"Operation aborted"` 在 CC 只在 transport 层 throw,**从不上屏** —— 它是我们这条引擎链
32
+ * 特有的产物,渲成红 `Error: Operation aborted` 是 CC 里根本不存在的形;
33
+ * ② CC 的**唯一**中断呈现形就是本串:壳的 `UserToolErrorMessage` 对
34
+ * `content.includes(INTERRUPT_MESSAGE_FOR_TOOL_USE)` 渲 dim `Interrupted`(CC 2.1.223 逐字判据)。
35
+ * ⇒ 用户中断批的 tool_result 文案在**呈现向**归一到本串,三端就天然拿到 CC 的 Interrupted 形,
36
+ * 不必各自再抄一份「Operation aborted 也算中断」的手工判据(那正是漂移温床)。
37
+ * 🔴 **呈现向,不是模型面**:本包这条链是转录/渲染链;喂回模型的那一份由引擎自己的 wire 承载,
38
+ * 这里一个字节都不碰它。
39
+ */
40
+ export declare const HITL_INTERRUPT_MESSAGE_FOR_TOOL_USE = "[Request interrupted by user for tool use]";
27
41
  /**
28
42
  * core 对**被 gate park 的那个 call**(gate 主角)铸的 tool_end 载体(逐字;desktop session-host
29
43
  * 真引擎实测同款)。HOLD 谓词锚它做**精确等值** —— 普通工具错的输出是各自的错误文案,永不进 HOLD。
@@ -60,7 +74,14 @@ export interface AskGateWireDeps {
60
74
  respondToolApproval?: RespondToolApprovalFn;
61
75
  /** #229 respond-note 供给链(0.29.0 发包扫描门修,2026-08-12):帧腿车道参数(能力位读数),
62
76
  * 宿主从自己的 caps 缓存供给。缺席 = note 门 fail-closed 到「不发」侧(决断照常,现状字节
63
- * 不变)—— 此前包内唯一生产调用点(routeFrame 的 tool_approval 臂)无此位,note 恒不发。 */
77
+ * 不变)—— 此前包内唯一生产调用点(routeFrame 的 tool_approval 臂)无此位,note 恒不发。
78
+ * ⚠️ **本位早已不只管 note**(0.43.0 订正,异源对抗复审五轮 [medium]:上句的「note 门」措辞
79
+ * 是 0.29.0 的历史射程,再留着会让宿主以为不接这个字段只损失一条备注)。`ToolApprovalFrameLaneOpts`
80
+ * 今天携带**四位**,缺席各有各的后果:`approvalDecisionNoteCapable`(备注不发)·
81
+ * `respondFreeFormRulesCapable`(**编辑臂**整条不发)· `respondBatchRuleOffersCapable`
82
+ * (**批臂**整条不发)· `windowIsCurrent`(窗三键不过境 ⇒ 卡上无倒计时)。后两位缺席时,人在卡上
83
+ * 按下的「不再询问」会被整条丢弃(决断照送 + 一条 `surfaceRuleArmNotSent` 通知)—— 逐位语义见
84
+ * 各自的 JSDoc 与 `docs/INTEGRATION-CLIENTS.md` §5b。 */
64
85
  approvalLane?: ToolApprovalFrameLaneOpts;
65
86
  }
66
87
  /** 两族 gate:AskUserQuestion 走问答 overlay(原路);其余(fs 写三件 / Bash / 一等
@@ -113,6 +134,43 @@ export interface FrameRouterCtx {
113
134
  export declare function isAskTool(toolName: string | undefined): boolean;
114
135
  /** 这一帧是不是「host lane 的模型推进」= 扣留帧的放行信号。 */
115
136
  export declare function isHostProgressFrame(ev: AgentEvent): boolean;
137
+ /** {@link flushHeldWithInterruptRewrite} 的出口证据(三腿,全是结构事实;缺席即不改写)。 */
138
+ export interface HeldFlushInterruptEvidence {
139
+ /** 本出口手上的终帧(有就读它的 cancel 码);中途出口/无终帧路径传 `undefined`。 */
140
+ terminal?: AgentEvent | undefined;
141
+ /** 宿主的 turn 中断信号。 */
142
+ signal?: AbortSignal | undefined;
143
+ /**
144
+ * **这张 park 是被用户中断掉的**(`GateOutcome.kind === 'aborted'`)—— 唯一写者 =
145
+ * `parkResolver` 的 fail-soft 出口。该 outcome 在包内的语义就是逐字的「turn 被中断(Esc/Ctrl+C)」
146
+ * (见 `toolApprovalWire` 同臂:它顺带用一次 TOOL 级 deny `reason:'Interrupted by user'` 把
147
+ * 挂着的门结算掉),所以它是**关于这张 park 本身**的一手中断证据,不是推断。
148
+ */
149
+ gateAbortedByUser?: boolean | undefined;
150
+ }
151
+ /**
152
+ * `led.flushHeld()` 的**唯一**排水出口包装:用户中断批把毒化帧的 `output` 归一成 CC 的中断形
153
+ * ({@link HITL_INTERRUPT_MESSAGE_FOR_TOOL_USE}),其余一切原样。
154
+ *
155
+ * ── 判词(两模,别合并;方向一律 fail-safe:证据不足就走原路)──────────────────────────────
156
+ * · **模 A —— 纯用户中断批**(证据 = cancel 终帧码 / 宿主 `signal.aborted`):要求 **本批零 park
157
+ * 登记**(`hasBatchPark() === false`),且帧上 `errorCode !== "gate.parked"`。两道门都是同一件事
158
+ * 的两个面 ——「这一批背后没有任何门」才谈得上纯中断;有门而我们只看见 abort 帧 = 判不出,不改。
159
+ * · **模 B —— 门被用户中断**(证据 = {@link HeldFlushInterruptEvidence.gateAbortedByUser}):
160
+ * park 在场是**前提**而不是障碍,所以模 A 的两道门在这一模下**都让位** —— 包括
161
+ * `gate.parked` 那一票。理由:该码回答的是「这次调用为什么中止 =被门 park 了」,回答不了
162
+ * 「这一批为什么收场 = 用户按了 Esc」,两者正交;拿它做否决会把这一格里**最该显形的那张
163
+ * 主角卡**留成红 `Error: Operation aborted`(真机复现:审批卡挂着按 Esc,主角与连坐帧同文案)。
164
+ * 🔴 **gate deny 语义零变**:deny 走的是 `decided` 分支(→ 续流重放 → `denied-call`/`deny-stamp-next`
165
+ * 两臂 stamp CC REJECT 文案),那两臂排在 hold-poison **之前**、帧根本不进扣留表,本包装够不着。
166
+ * fail-soft 的其余原因(no_pending / 传输失败 / hop 用尽)三腿全不命中 ⇒ 诚实红照旧。
167
+ *
168
+ * 🔴 **只改 `output` 一个键**:`isError` / `errorCode` / `toolCallId` / `toolName` / `settledBy` /
169
+ * `approver` / `resolution` / `structured` 以及 `flushHeld` 自己盖的
170
+ * `_sema_collateral_abort` 机读位全部原样过境(展开赋值只覆盖 `output`)—— 端侧的连坐折叠锚的是
171
+ * 机读位不是文案,所以折叠语义不受影响,只是被折那一行的正文换成了 CC 中断形。
172
+ */
173
+ export declare function flushHeldWithInterruptRewrite(led: GateLedger, evidence: HeldFlushInterruptEvidence): Generator<AgentEvent>;
116
174
  /**
117
175
  * 一帧 → 一个处置。手柄的**先后**与原文逐条同序:tool_approval 帧 → tool_start → tool_end →
118
176
  * suspended → done → failed → 透传。