@sema-agent/client-core 0.68.0 → 0.68.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/adapt.js CHANGED
@@ -38,7 +38,10 @@ export const ADAPTER_COVERAGE = {
38
38
  'message_committed',
39
39
  // #318 件①(2026-08-21):`engine_notice` 引擎结构化通告 —— 投 chrome(开集,一个码都不判)。
40
40
  'engine_notice',
41
- // #323 / core #447:assistant 散文段边界 → chrome text_segment_end(子流断闸;本批**不**动文本缓冲)。
41
+ // #323 / core #447 + L-310(0.68.1 改口):assistant 散文段边界 → **整段替换文本缓冲** +
42
+ // chrome text_segment_end(子流断闸在替换之前)。修前这里写「本批**不**动文本缓冲」——
43
+ // server 7.75.3 起 `text_end.content` 经脱敏器而 `text_delta` 仍逐字,不动缓冲 = 未脱敏字节
44
+ // committed 进本地转录;替换语义见 `adapt/textStream.ts` 的 `replaceAnswerSegment` 头注。
42
45
  'text_end',
43
46
  'prompt_suggestions',
44
47
  'retry_status',
@@ -200,7 +203,20 @@ class WireToCcAdapterImpl {
200
203
  // 壳 B4 的队列端口回钩仍可调它 —— 三条入队链本来就写同一个集合,回钩因此变成幂等 no-op。
201
204
  noteWorkflowCompletionCardEnqueued(runId);
202
205
  }
206
+ /**
207
+ * CC-01(0.68.2):`adapt()` 的**出口单点** —— 每条产出过境时由 M1 盖段身份
208
+ * ({@link TextStream.stampSegmentIdentity};只盖 committed assistant 文本行,其余原样)。
209
+ * 放在出口而不放在各铸点:durable 整条消息(`assistant` 臂)与流式分段(M1 提交口)是两条腿,
210
+ * 归属键必须两条腿都盖,而「过境」是它们唯一的公共点(壳此前也正是在自己的过境口盖的)。
211
+ * 🔴 只包一层转发,不改内层的手动 `next()` 竞速与 abort 语义(T40/T58 全在内层)。
212
+ */
203
213
  async *adapt(frames, ctx) {
214
+ const slot = {};
215
+ for await (const out of this.adaptUnstamped(frames, ctx, slot)) {
216
+ yield slot.text === undefined ? out : slot.text.stampSegmentIdentity(out);
217
+ }
218
+ }
219
+ async *adaptUnstamped(frames, ctx, slot) {
204
220
  // FIX-05:实例台账走**唯一一条**显式路径(拆分前是 `self.` / `this.` 两条)。
205
221
  const inst = this.ledger;
206
222
  const turnStartAt = ctx.now();
@@ -208,6 +224,7 @@ class WireToCcAdapterImpl {
208
224
  const idOf = makeIdOf(ctx);
209
225
  // ── M1 流合并器(矩阵 §1.1 的 12 行状态整体落 adapt/textStream.ts)────────────────────────
210
226
  const text = createTextStream(ctx, idOf);
227
+ slot.text = text;
211
228
  // ── M2 工具卡台账(矩阵 §1.2 的 3 行状态整体落 adapt/toolCards.ts)────────────────────────
212
229
  const cards = createToolCardLedger(ctx, idOf);
213
230
  // ── M3 面板行台账(矩阵 §1.3 的 6 行状态 + 五个闭包整体落 adapt/panelTasks.ts)──────────
@@ -231,6 +231,25 @@ export interface WiringManifestMcpEntry {
231
231
  */
232
232
  httpStatus?: number;
233
233
  }
234
+ /**
235
+ * `wiring_manifest.mcp` 的形校验(S-124 / core 7.5.0,server ≥7.60.0)。
236
+ *
237
+ * 🔴 **空数组 ≠ 缺席,两者都要能被消费端分辨**(core 顶注逐字:"an empty array is *this leg
238
+ * declared no servers*, absence is an older mint or an external derivation")⇒ **真的**空数组
239
+ * 原样铸成 `[]`。把它折成缺席 = 把「我一台都没申报」这句**正面事实**说成「不知道」。
240
+ * 🔴 **但「过滤后为空」不是「原本就是空」**(异源对抗复审 r1 [medium] 采纳,真病):一份
241
+ * `[{name:'github', errorCode:'http_503'}]`(缺 `status`)进来时,唯一那一行被下面的逐行判据
242
+ * 丢掉 —— 若照样铸出 `[]`,消费端按本臂的义务④读到的是**「这条腿一台都没申报」这句肯定话**,
243
+ * 一份读不懂的回体就此被伪装成一个确定的零申报。⇒ **非空输入而零行幸存 ⇒ 整段缺席**
244
+ * (「我读不出来」不是「我知道是零」)。这与下面「逐条独立」并不矛盾:只要**还有幸存者**,
245
+ * 半张表仍是真读数,坏行照旧只丢自己。
246
+ * 🔴 **逐条独立**:一条坏行只丢自己(与 server 侧 `continue` 同判据),绝不因为一条坏行丢整表。
247
+ * 🔴 **逐键挑,禁 spread**:core 往条目上加新键时必须由人显式处置 —— 而它下一个加的很可能又是
248
+ * 一个像 `error` 那样的自由文本面。(0.60.0 按此显式处置了 S6-B 的 `delivered` / `httpStatus`。)
249
+ * ⚠️ `toolCount` 按**有限数**判(`Number.isFinite`),不按真值判:`0` 是合法读数(连上了、零工具),
250
+ * 折成缺席会让「连上了但没工具」与「没报」在消费端同形。
251
+ */
252
+ export declare function projectMcpSection(raw: unknown): WiringManifestMcpEntry[] | undefined;
234
253
  /**
235
254
  * CS-7 §2.7 — turn_end usage → CC `ModelUsage` (pinned name mapping;
236
255
  * costMicroUsd/1e6 → costUSD). Surfaced separately because the slice has no
@@ -877,8 +877,14 @@ export function eventToSdkMessage(ev, ctx) {
877
877
  *
878
878
  * ── 这是什么 ────────────────────────────────────────────────────────────────────────────────
879
879
  * 「assistant 的这一段散文写完了」—— 模型关掉了那个 text content block,而它的字节刚刚以
880
- * `text_delta` 流过。core 的臂注逐字:`content` = **该段的权威全文**(与那一段 delta 的拼接逐字节
881
- * 相等,取自 brain 自己的累加),消费方**据它提交这一段**,而不是信自己的 delta 缝合。
880
+ * `text_delta` 流过。core 的臂注逐字:`content` = **该段的权威全文**(取自 brain 自己的累加),
881
+ * 消费方**据它提交这一段**,而不是信自己的 delta 缝合。
882
+ *
883
+ * 🔴 **「与 delta 拼接逐字节相等」这句自 server 7.75.3 起不再成立**(L-310,0.68.1 订正;此前本注
884
+ * 照抄 core 臂注的那半句):`text_end.content` 与 `result`/账本走**同一只脱敏器**,而 `text_delta`
885
+ * 仍逐字(跨 chunk 的凭据无法就地判,流式脱敏是独立设计件)⇒ 含凭据形的段上两者**字节不同**。
886
+ * server `ASSISTANT-WIRE-CONTRACT` §5.1 live 面逐字要求消费端「在 `text_end` 到达时以它**整段
887
+ * 替换**已攒的 delta,而不是只当段界信号」。
882
888
  *
883
889
  * 🔴 **它的存在意义 = 让消费方撤掉 idle-flush 启发式**(core 臂注点名的那件事)。本包的
884
890
  * `adapt/textStream.ts` 至今用「静默 1.5s + 句末/段末边界 + 每段一刀」猜段边界(#323 症状① 的止血
@@ -893,11 +899,15 @@ export function eventToSdkMessage(ev, ctx) {
893
899
  * 信号(段边界 + 权威全文),静默丢 = 把一个真实能力缺口做成 fail-open,正是本文件对
894
900
  * `approval_request` 那段头注点名的病形。⇒ 投中性内部臂,宿主(壳 REPL 桥 / web / desktop 座位层)
895
901
  * 在臂上读它、决定何时提交段。
896
- * · 📋 **本包内的接线如实留白**(不在本批做):要让 `adapt/textStream.ts` 真的**撤掉** idle-flush,
897
- * 得先答一个行为面问题 —— core 臂注明写这是「诚实缺席」的位(只有会报块结束的 brain 才发它,
898
- * 自定义 brain 可能整条流一帧都没有),所以消费方必须按**每条流**判「这条流带不带边界帧」再决定
899
- * 退不退启发式。那是一条带状态的策略,属行为面改动,按宪法三问单独走,不在本提货批里顺手加。
900
- * 本批只把信号送到宿主手上(壳侧接线是下一棒),缺口写在这里,不留白。
902
+ * · **包内接线**(0.68.1 / L-310 起):`adapt/arms.ts` 的 `textSegmentEndArm` 拿这条内部臂调
903
+ * `adapt/textStream.ts` 的 `replaceAnswerSegment(content, frame)` —— **整段替换**段缓冲与活体尾巴,
904
+ * 再把 `diverged` / `committedPrefixDiverged` / `committedPrefixLen` 三个 never-false 键挂到
905
+ * chrome `text_segment_end` 上交给宿主。
906
+ * · 📋 **仍然留白的那一件**:让 `textStream` 真的**撤掉** idle-flush 启发式。core 臂注明写这是
907
+ * 「诚实缺席」的位(只有会报块结束的 brain 才发它,自定义 brain 可能整条流一帧都没有),所以
908
+ * 消费方必须按**每条流**判「这条流带不带边界帧」再决定退不退启发式 —— 那是一条带状态的策略,
909
+ * 属行为面改动,按宪法三问单独走。权威替换**兼容**启发式(半段已被 flush 那一形由两个前缀键
910
+ * 如实交代),不是它的继任。
901
911
  *
902
912
  * ── 畸形与空段(fail-closed 方向 + 对位 core 的「空段无帧」)────────────────────────────────
903
913
  * · `content` **非串** ⇒ `malformed`:承重位读不动(wire 是 JSON,SDK 只 JSON.parse 不校型)。
@@ -954,7 +964,7 @@ function textEndProjection(ev, ctx) {
954
964
  * ⚠️ `toolCount` 按**有限数**判(`Number.isFinite`),不按真值判:`0` 是合法读数(连上了、零工具),
955
965
  * 折成缺席会让「连上了但没工具」与「没报」在消费端同形。
956
966
  */
957
- function projectMcpSection(raw) {
967
+ export function projectMcpSection(raw) {
958
968
  if (!Array.isArray(raw))
959
969
  return undefined;
960
970
  const rows = [];
@@ -81,6 +81,10 @@ export interface ControlClientLike {
81
81
  * 可达其中 4 个:`not_running`(409)/ `invalid_content`(422)/ `queue_full`(409,core 5.14.0 队列
82
82
  * 化后新出)/ `duplicate_input_id`(409,调用方带 `Idempotency-Key` 时可达);另外 3 个来自子代
83
83
  * steer/resume 面与 workflow agent steer 面(`ambiguous_target` / `ambiguous_label` / `still_running`)。
84
+ * 🔴 **[7226] 包侧缺口 ①(0.68.1)**:码表补**第 8 码** `steering.blocked_by_hook`(422,部署
85
+ * `userPromptSubmit` 门拦下;live 腿可达)。它此前落开集兜底位 `steering_other` —— 兜底位的判词
86
+ * 逐字是「别按成员猜它的意思」,而这一码的处置恰恰是**明确的**(输入未受理、改内容自由重试)⇒
87
+ * 兜底在这一位上不是「安全降级」,是把一条能自救的拒绝渲成一条不知道怎么办的拒绝。
84
88
  * SDK 6.3.0 侧已把整族改成**前缀分派**(`SteeringError` 基类),所以「没认全」的后果不是崩溃,
85
89
  * 而是那些码原样裸抛给壳 —— 壳只 `catch (e instanceof ControlSafetyError)` 就漏在外面。
86
90
  *
@@ -101,6 +105,18 @@ export type ControlSafetyCode =
101
105
  | 'ambiguous_target'
102
106
  /** 对一个**还在飞**的子代调了 resume;处置=改调 steer,或等它 settle。`not_running` 的反面。 */
103
107
  | 'still_running'
108
+ /**
109
+ * 部署的 `userPromptSubmit` 门拦下了这条输入([7226] 包侧缺口 ①,0.68.1;server 契约 (2) 表
110
+ * 第 8 行;core 5.62 design/373 §4.3)。block / 超时 / 崩溃**同码 fail-closed**,成因由 message
111
+ * 判别(携 hook 自己的 bounded reason)。
112
+ * 🔴 **输入未被受理**:没有 `human_input` 帧、`inputId` 不入账 ⇒ 处置 = **改内容自由重试**。
113
+ * 🔴 它与 `invalid_content` / `steering_other` 都**不许合并**:前者是「正文违规,SURFACE 别
114
+ * strip-and-retry」,后者的判词逐字是「别按成员猜它的意思」—— 把一条**能自救**的拒绝塌进这两位
115
+ * 任何一位,UI 都会把「改一句话再发」说成「你没救了」。
116
+ * ⚠️ `/steer` 的 **park 腿结构性不可达本码**(hook 对 parked 转向的拦截发生在 resume 再投递时刻,
117
+ * 走 `steering.parked_input_blocked` 通告);本路由器调的是 live 腿,所以这一位在这里可达。
118
+ */
119
+ | 'blocked_by_hook'
104
120
  /** 开集兜底:`steering.` 前缀但本表不认得的**未来**码(SDK 前缀分派同款姿势)。
105
121
  * 处置=按「这条 steer 没落地」呈现,并把 `cause.errorCode` 原样打进日志,别按成员猜语义。 */
106
122
  | 'steering_other'
@@ -243,6 +243,11 @@ const STEERING_CODE_TO_SAFETY = new Map([
243
243
  ['steering.ambiguous_target', 'ambiguous_target'],
244
244
  ['steering.ambiguous_label', 'ambiguous_target'],
245
245
  ['steering.still_running', 'still_running'],
246
+ // [7226] 包侧缺口 ①(0.68.1):server 契约 (2) 表第 8 码(422)。SDK 8.8.0 **没有**专属子类
247
+ // (`errors.d.ts` 的 `Steering*Error` 只有六只)⇒ 只能按 `errorCode` 认;认不出时它会落开集位
248
+ // `steering_other`,而那一位的判词是「别猜它的意思」—— 恰好把一条「改内容重试即可」的拒绝
249
+ // 说成没救。所以码表必须点名它,不能靠前缀兜底。
250
+ ['steering.blocked_by_hook', 'blocked_by_hook'],
246
251
  ]);
247
252
  /** SDK 的 typed 子类名兜底(错误对象被传输层剥掉 `errorCode` 时仍认得族;`SteeringError` 基类本身
248
253
  * = 「是 steering 族但没有专属子类」⇒ 落开集臂)。 */
@@ -261,6 +266,7 @@ const STEERING_ADVICE = new Map([
261
266
  ['duplicate_input_id', 'this Idempotency-Key is already parked with DIFFERENT steering content — reissue with a fresh key, resending verbatim will never succeed'],
262
267
  ['ambiguous_target', 'more than one live target matches — address it uniquely (sub-agent: parentToolCallId; workflow: a more specific label); retrying verbatim yields the same result'],
263
268
  ['still_running', 'the target is STILL RUNNING — steer it instead of resuming, or wait for it to settle (this is the exact opposite of not_running: never collapse the two)'],
269
+ ['blocked_by_hook', "this deployment's userPromptSubmit gate refused the input (block, timeout and crash all report this one code, fail-closed) — the input was NOT accepted: no human_input frame, the inputId is not on the ledger, so editing the text and sending again is a normal retry, not a duplicate"],
264
270
  ['steering_other', 'an unrecognized steering.* refusal (open set) — the steer did NOT land; log the errorCode verbatim, do not guess its meaning'],
265
271
  ]);
266
272
  /**
@@ -0,0 +1,31 @@
1
+ /**
2
+ * src/engineHttpTools.ts — SDK **纯工具面**的转口口(L-61 / L-318 件④,0.68.2)。
3
+ *
4
+ * ── 病形(为什么要有这一只)──────────────────────────────────────────────────────────────────
5
+ * 端对引擎 wire 的一切消费必须经本包(sdk-isolation 门公约),而 SDK 里有两件**纯工具**至今没有
6
+ * 经本包的路:
7
+ * · `engineUrl(baseUrl, path)` —— 「URL 去尾斜杠 + 拼路径」的语义单真源(端自己拼字符串就会
8
+ * 在 `//v1/...` 这类形上各错各的);
9
+ * · `probeHealth(baseUrl, timeoutMs?, fetchImpl?)` —— 引擎存活探针的单真源。
10
+ * 于是 cli 有 5 个产品文件**直连** `@sema-agent/sdk` 只为取这两件(sdk-isolation 存量册
11
+ * `sdk-value` 桶里那几条,`_retirement` 段逐字:「多数只取 engineUrl builder 一件」),web /
12
+ * desktop 接同一条腿时会再各直连一次。
13
+ *
14
+ * ── 为什么这一只可以做**值级**转口(与 core 判官那一只的分界)──────────────────────────────
15
+ * 可移植门(`run-client-core-portability-test.mjs`)对包总入口做两件事:外部包**等值集**
16
+ * `EXPECTED_PACKAGES_INDEX` + esbuild `--platform=browser` 真打一次包。
17
+ * · `@sema-agent/sdk` **本来就在**那个等值集里(本包已有 5 处值级 SDK import)⇒ 加这一只
18
+ * 不改集合,只让闭包文件数 +1(棘轮按既有姿势逐件记账,见该门 `MAX_CLOSURE_FILES_INDEX` 头注);
19
+ * · SDK 的 `dist/health.js` **零 import**(实测:整文件一条 `import`/`require` 都没有,传输走
20
+ * 全局 `fetch` = web 标准,不是 Node 内建)⇒ 浏览器打包面零风险。
21
+ * 🔴 对照:`@sema-agent/core` 的同类件**不能**这么做(`hitl/editedRuleTextPrecheck.ts` 头注逐字:
22
+ * core 的 barrel 值级拉 `node:crypto`/`node:fs`/`node:path`,一条这样的边会把整台引擎焊进
23
+ * web/desktop 的产物)—— 那一族走**端口注入**形。两者的分界线是「这个外部包在不在等值集里」,
24
+ * 不是「它是不是纯函数」。
25
+ *
26
+ * 🔴 **原样转口,一个字节都不加工**:本模块不包装、不改签名、不补默认值 —— 包一层就是把「同一
27
+ * 函数体」这条唯一的抗漂移保证亲手拆掉(与 `editedRuleTextPrecheck` 的装口纪律同一条)。
28
+ * 要在这两件之上加判定的那天,新开一个具名模块,别往转口口里塞。
29
+ */
30
+ export { engineUrl, probeHealth } from '@sema-agent/sdk';
31
+ export type { ProbeHealthResult } from '@sema-agent/sdk';
@@ -0,0 +1,30 @@
1
+ /**
2
+ * src/engineHttpTools.ts — SDK **纯工具面**的转口口(L-61 / L-318 件④,0.68.2)。
3
+ *
4
+ * ── 病形(为什么要有这一只)──────────────────────────────────────────────────────────────────
5
+ * 端对引擎 wire 的一切消费必须经本包(sdk-isolation 门公约),而 SDK 里有两件**纯工具**至今没有
6
+ * 经本包的路:
7
+ * · `engineUrl(baseUrl, path)` —— 「URL 去尾斜杠 + 拼路径」的语义单真源(端自己拼字符串就会
8
+ * 在 `//v1/...` 这类形上各错各的);
9
+ * · `probeHealth(baseUrl, timeoutMs?, fetchImpl?)` —— 引擎存活探针的单真源。
10
+ * 于是 cli 有 5 个产品文件**直连** `@sema-agent/sdk` 只为取这两件(sdk-isolation 存量册
11
+ * `sdk-value` 桶里那几条,`_retirement` 段逐字:「多数只取 engineUrl builder 一件」),web /
12
+ * desktop 接同一条腿时会再各直连一次。
13
+ *
14
+ * ── 为什么这一只可以做**值级**转口(与 core 判官那一只的分界)──────────────────────────────
15
+ * 可移植门(`run-client-core-portability-test.mjs`)对包总入口做两件事:外部包**等值集**
16
+ * `EXPECTED_PACKAGES_INDEX` + esbuild `--platform=browser` 真打一次包。
17
+ * · `@sema-agent/sdk` **本来就在**那个等值集里(本包已有 5 处值级 SDK import)⇒ 加这一只
18
+ * 不改集合,只让闭包文件数 +1(棘轮按既有姿势逐件记账,见该门 `MAX_CLOSURE_FILES_INDEX` 头注);
19
+ * · SDK 的 `dist/health.js` **零 import**(实测:整文件一条 `import`/`require` 都没有,传输走
20
+ * 全局 `fetch` = web 标准,不是 Node 内建)⇒ 浏览器打包面零风险。
21
+ * 🔴 对照:`@sema-agent/core` 的同类件**不能**这么做(`hitl/editedRuleTextPrecheck.ts` 头注逐字:
22
+ * core 的 barrel 值级拉 `node:crypto`/`node:fs`/`node:path`,一条这样的边会把整台引擎焊进
23
+ * web/desktop 的产物)—— 那一族走**端口注入**形。两者的分界线是「这个外部包在不在等值集里」,
24
+ * 不是「它是不是纯函数」。
25
+ *
26
+ * 🔴 **原样转口,一个字节都不加工**:本模块不包装、不改签名、不补默认值 —— 包一层就是把「同一
27
+ * 函数体」这条唯一的抗漂移保证亲手拆掉(与 `editedRuleTextPrecheck` 的装口纪律同一条)。
28
+ * 要在这两件之上加判定的那天,新开一个具名模块,别往转口口里塞。
29
+ */
30
+ export { engineUrl, probeHealth } from '@sema-agent/sdk';
package/dist/index.d.ts CHANGED
@@ -141,6 +141,10 @@ export * from './diagnostics.js';
141
141
  export * from './retryStatus.js';
142
142
  export * from './sessionMemoryStatus.js';
143
143
  export * from './adapt.js';
144
+ export * from './adapt/textSegmentAuthority.js';
145
+ export * from './engineHttpTools.js';
146
+ export * from './sdkWireTransit.js';
147
+ export { SEMA_SEGMENT_ID_KEY } from './adapt/textStream.js';
144
148
  export * from './subagentContentStore.js';
145
149
  export * from './engineAgentPanelStore.js';
146
150
  export * from './fleetAgentPanelProjection.js';
@@ -152,6 +156,7 @@ export * from './sqlEngineCapability.js';
152
156
  export * from './writeProtectionCapability.js';
153
157
  export * from './runTerminal.js';
154
158
  export * from './readFacePosture.js';
159
+ export * from './mcpPanel.js';
155
160
  export * from './gateOutcome.js';
156
161
  export * from './postureKnob.js';
157
162
  export * from './engineIdentity.js';
package/dist/index.js CHANGED
@@ -152,6 +152,24 @@ export * from './retryStatus.js';
152
152
  // 合法缺席,一律读成「没有/关着/0」就是对用户下一个证不出的断言。
153
153
  export * from './sessionMemoryStatus.js';
154
154
  export * from './adapt.js';
155
+ // ── L-318(0.68.2):`text_end` 权威段替换的**判决单源** + print 形车道的段账状态机 ──────────
156
+ // 修前这套六形判据有两份实现:本包 `adapt/textStream.replaceAnswerSegment`(交互车道)与 cli
157
+ // 1.0.114 在壳里热修的 `PrintStreamProjector.onTextEnd`(`-p` 车道,B-122)。两条车道的**账本**
158
+ // 形状不同(消息划界 vs 帧划界,差别有理由、刻意不归一),但**判决**是同一件事 ⇒ 判决进包,
159
+ // 两条车道同吃;`-p` 车道的段账(已出门前缀累加 / 轮收口清账 / 被扣段判重)也归包,端只留
160
+ // 「把尾段写进哪一个 CC content block」这一步装配(§8-5:stream-json 帧序/SSE 重铸属端)。
161
+ export * from './adapt/textSegmentAuthority.js';
162
+ // ── L-61 / L-318 件④(0.68.2):SDK **纯工具面**转口口(engineUrl / probeHealth)。端直连 SDK
163
+ // 只为取这两件的存量(cli sdk-isolation `sdk-value` 桶)从此有「经包」的路;值级转口在可移植门
164
+ // 上零风险的理由(SDK 已在外部包等值集里 + `dist/health.js` 零 import)写在模块头注。
165
+ export * from './engineHttpTools.js';
166
+ // ── L-61 存量清零(clay 令 C-R43「所有欠账绝不延期,宁可红」):SDK **wire 面**转口口 ────────
167
+ // 客户端类 + 流内审批帧谓词 + 端上存量实际用到的那一小撮 wire 型面。**原样转口零包装**;
168
+ // 「端该不该直接 new AgentClient」是另一个(仍然欠着的)设计问题,不该继续挡着归层 —— 理由与
169
+ // 射程边界全在模块头注。新码一律走本包自有的 `makeEngineWireClient`,本转口口只给存量用。
170
+ export * from './sdkWireTransit.js';
171
+ // CC-01(0.68.2):段身份键 —— committed assistant 文本行顶层 `_sema_segment_id` 的唯一字面量出处(壳只读此常量)。
172
+ export { SEMA_SEGMENT_ID_KEY } from './adapt/textStream.js';
155
173
  // ── B1 批:纯函数 / 侧信道台账 / 投影闸(2026-07-27)──────────────────────────────────────────
156
174
  export * from './subagentContentStore.js';
157
175
  export * from './engineAgentPanelStore.js';
@@ -173,6 +191,8 @@ export * from './runTerminal.js';
173
191
  // (防御读 / 唯一措辞铸点 / UNTRUSTED-for-display);与租户面 `capabilities.readFace` 刻意不合流,
174
192
  // 只带一个纯比较函数,渲染归端。
175
193
  export * from './readFacePosture.js';
194
+ // CC-03(0.68.2):`GET /v1/sessions/:id/mcp` 面板体的防御读视图 + lastLegMcp 一行措辞铸点(三端共用)。
195
+ export * from './mcpPanel.js';
176
196
  export * from './gateOutcome.js';
177
197
  // 0.63.0(sdk 8.8.0 / engine ≥7.67.0 / S-178):`serverGates` 三根 posture 旋钮的读数窄读器
178
198
  // (值 + 谁定的 + 指路句)。四词来源表在本包只有这一份,`readFacePosture` 与它共用。
@@ -0,0 +1,60 @@
1
+ import { type WiringManifestMcpEntry } from './adapter/downstream/eventToSdkMessage.js';
2
+ /** 面板 `servers[]` 一行(sdk `McpServerStatus` 的窄读;开集键不透传,逐键挑)。 */
3
+ export interface McpPanelServerView {
4
+ /** 配置名(引擎产的标识,非用户内容)。 */
5
+ name: string;
6
+ /** `connected` / `failed`(sdk 声明的两词;**按开集读**,认不得的词照渲不丢行)。 */
7
+ status: string;
8
+ /** 服务器自报的名字与版本(仅 connected 时有)。缺席 = 没报。 */
9
+ serverInfo?: {
10
+ name: string;
11
+ version: string;
12
+ };
13
+ /** 挂上来的工具名(仅 connected 时有)。缺席 = 没报,**不是**空表。 */
14
+ toolNames?: string[];
15
+ /** 失败因由(仅 failed 时;server 已脱敏 + 封顶,本视图再消毒一次)。 */
16
+ error?: string;
17
+ }
18
+ /** `lastLegMcp{runId,at,mcp[]}`(server ≥7.77.0 S-297)的窄读。 */
19
+ export interface McpPanelLastLegView {
20
+ /** 该会话最近一条腿的 runId。 */
21
+ runId: string;
22
+ /** 写账本副本的钟(ISO;**不是** materialize 时刻 `asOf`,两者不比)。 */
23
+ at: string;
24
+ /** 该腿 `wiring_manifest.mcp[]` 逐字 —— 与活体腿的第三段同一只读器、同一形。 */
25
+ mcp: WiringManifestMcpEntry[];
26
+ }
27
+ /** `GET /v1/sessions/:id/mcp` 的读视图(缺席语义见文件顶注)。 */
28
+ export interface McpPanelView {
29
+ /** THIS materialize 时刻(ISO)。 */
30
+ asOf: string;
31
+ /** materialization-time 状态,不是 live 健康(壳渲「as of <asOf>」)。空表 = 一台都没配。 */
32
+ servers: McpPanelServerView[];
33
+ /** never false:在场 = materialize 超时/失败,`servers` 空但**不是**「没有 MCP」。 */
34
+ degraded?: true;
35
+ /** 最近一条腿的申报名册(server ≥7.77.0);缺席语义见顶注。🔴 禁与 `servers[]` 对账渲告警。 */
36
+ lastLegMcp?: McpPanelLastLegView;
37
+ /** never false:server 送了 `lastLegMcp` 但本视图读不出来(与整键缺席不是同一句话)。 */
38
+ lastLegMcpUnreadable?: true;
39
+ }
40
+ /**
41
+ * 面板体 → 读视图;**畸形一律 `undefined`**,绝不抛出(必填位 fail-closed,可选位只丢自己)。
42
+ *
43
+ * 🔴 `lastLegMcp` 只按「键在不在」判在场(`'lastLegMcp' in body`),不按真值判:server 的缺席形
44
+ * 是**键不出现**(LL-3),不是 `null`/`undefined` 在场 —— 后两者是坏形,走 `lastLegMcpUnreadable`。
45
+ * 🔴 本函数**不比** `servers[]` 与 `lastLegMcp.mcp[]`,视图上也没有任何「一致/不一致」位
46
+ * (契约 G.7 第 4 条:两面合法可不同)。
47
+ */
48
+ export declare function projectMcpPanel(body: unknown): McpPanelView | undefined;
49
+ /**
50
+ * `lastLegMcp` 那一行措辞的**唯一铸点**(三端共用;别在各端的行装配里另写一遍)。
51
+ *
52
+ * 🔴 四句刻意逐字互异(黑盒锚),且**没有一句**提到 `servers[]`:
53
+ * ① 在场 —— 名册 + runId + at;
54
+ * ② `opts.reachable === false` —— 「未观测」:这次进程没读到面板体;
55
+ * ③ 面板读到了但 `lastLegMcp` 整键缺席 —— 「不报」:三形同形 + 老引擎,**不武断咎为版本**;
56
+ * ④ 在场但读不懂 —— 「读不出」:与③是两句话。
57
+ */
58
+ export declare function mcpPanelLastLegDetail(view: McpPanelView | undefined, opts: {
59
+ reachable: boolean;
60
+ }): string;
@@ -0,0 +1,105 @@
1
+ import { projectMcpSection } from './adapter/downstream/eventToSdkMessage.js';
2
+ import { capForDisplay } from './fleetTaskDesc.js';
3
+ /** `error` 上屏前的封长(server 侧 `slice(200)`,同值)。 */
4
+ const MCP_PANEL_ERROR_MAX = 200;
5
+ /** 名字类词形的封长(与本包其余 detail 铸点同值同理由)。 */
6
+ const MCP_PANEL_WORD_MAX = 40;
7
+ /** `mcpPanelLastLegDetail` 里列出的名字上限(再多就是一行读不完的表,不是一句读数)。 */
8
+ const MCP_PANEL_NAMES_MAX = 8;
9
+ const isRecord = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
10
+ const nonEmpty = (v) => typeof v === 'string' && v.length > 0;
11
+ function projectServerRow(raw) {
12
+ if (!isRecord(raw))
13
+ return undefined;
14
+ if (!nonEmpty(raw.name) || !nonEmpty(raw.status))
15
+ return undefined;
16
+ const row = { name: raw.name, status: raw.status };
17
+ const si = raw.serverInfo;
18
+ if (isRecord(si) && typeof si.name === 'string' && typeof si.version === 'string') {
19
+ row.serverInfo = { name: si.name, version: si.version };
20
+ }
21
+ if (Array.isArray(raw.toolNames) && raw.toolNames.every((t) => typeof t === 'string')) {
22
+ row.toolNames = [...raw.toolNames];
23
+ }
24
+ if (typeof raw.error === 'string')
25
+ row.error = capForDisplay(raw.error, MCP_PANEL_ERROR_MAX);
26
+ return row;
27
+ }
28
+ function projectLastLeg(raw) {
29
+ if (!isRecord(raw))
30
+ return undefined;
31
+ if (!nonEmpty(raw.runId) || !nonEmpty(raw.at))
32
+ return undefined;
33
+ const mcp = projectMcpSection(raw.mcp);
34
+ if (mcp === undefined)
35
+ return undefined;
36
+ return { runId: raw.runId, at: raw.at, mcp };
37
+ }
38
+ /**
39
+ * 面板体 → 读视图;**畸形一律 `undefined`**,绝不抛出(必填位 fail-closed,可选位只丢自己)。
40
+ *
41
+ * 🔴 `lastLegMcp` 只按「键在不在」判在场(`'lastLegMcp' in body`),不按真值判:server 的缺席形
42
+ * 是**键不出现**(LL-3),不是 `null`/`undefined` 在场 —— 后两者是坏形,走 `lastLegMcpUnreadable`。
43
+ * 🔴 本函数**不比** `servers[]` 与 `lastLegMcp.mcp[]`,视图上也没有任何「一致/不一致」位
44
+ * (契约 G.7 第 4 条:两面合法可不同)。
45
+ */
46
+ export function projectMcpPanel(body) {
47
+ if (!isRecord(body))
48
+ return undefined;
49
+ if (!nonEmpty(body.asOf))
50
+ return undefined;
51
+ if (!Array.isArray(body.servers))
52
+ return undefined;
53
+ const servers = [];
54
+ for (const r of body.servers) {
55
+ const row = projectServerRow(r);
56
+ if (row !== undefined)
57
+ servers.push(row);
58
+ }
59
+ if (body.servers.length > 0 && servers.length === 0)
60
+ return undefined;
61
+ const view = { asOf: body.asOf, servers };
62
+ if (body.degraded === true)
63
+ view.degraded = true;
64
+ if ('lastLegMcp' in body) {
65
+ const leg = projectLastLeg(body.lastLegMcp);
66
+ if (leg !== undefined)
67
+ view.lastLegMcp = leg;
68
+ else
69
+ view.lastLegMcpUnreadable = true;
70
+ }
71
+ return view;
72
+ }
73
+ /**
74
+ * `lastLegMcp` 那一行措辞的**唯一铸点**(三端共用;别在各端的行装配里另写一遍)。
75
+ *
76
+ * 🔴 四句刻意逐字互异(黑盒锚),且**没有一句**提到 `servers[]`:
77
+ * ① 在场 —— 名册 + runId + at;
78
+ * ② `opts.reachable === false` —— 「未观测」:这次进程没读到面板体;
79
+ * ③ 面板读到了但 `lastLegMcp` 整键缺席 —— 「不报」:三形同形 + 老引擎,**不武断咎为版本**;
80
+ * ④ 在场但读不懂 —— 「读不出」:与③是两句话。
81
+ */
82
+ export function mcpPanelLastLegDetail(view, opts) {
83
+ if (!opts.reachable)
84
+ return "last leg mcp not observed (this end could not read the session's MCP panel)";
85
+ if (view === undefined)
86
+ return 'last leg mcp unreadable (the MCP panel body could not be read by this client)';
87
+ if (view.lastLegMcp !== undefined) {
88
+ const { runId, at, mcp } = view.lastLegMcp;
89
+ const names = mcp.slice(0, MCP_PANEL_NAMES_MAX).map((e) => capForDisplay(e.name, MCP_PANEL_WORD_MAX));
90
+ const more = mcp.length > MCP_PANEL_NAMES_MAX ? `, +${mcp.length - MCP_PANEL_NAMES_MAX} more` : '';
91
+ const roster = mcp.length === 0 ? 'none declared' : `${names.join(', ')}${more}`;
92
+ return `last leg mcp: ${roster} (run ${capForDisplay(runId, MCP_PANEL_WORD_MAX)}, at ${capForDisplay(at, MCP_PANEL_WORD_MAX)})`;
93
+ }
94
+ if (view.lastLegMcpUnreadable === true) {
95
+ return 'last leg mcp unreadable (the engine sent a shape this client cannot read)';
96
+ }
97
+ return 'last leg mcp not reported (no leg in the retention window, no manifest on the last leg, or the engine predates it)';
98
+ }
99
+ /**
100
+ * **编译期对账钉**(不出公面):sdk 的 `McpStatusPanel` 必须能赋给视图的必填半场 —— 名字在
101
+ * sdk ≥8.8.0 上存在(本包 peer 地板),`tsc` 在改名当天报「没有导出成员」。
102
+ * 反向刻意不钉(视图比 sdk 形窄:开集键不透传、`degraded` 收成 never-false)。
103
+ */
104
+ const _mcpPanelShapePin = (p) => p;
105
+ void _mcpPanelShapePin;
@@ -90,6 +90,12 @@ export interface TaskRequestInput {
90
90
  interactiveTools?: false;
91
91
  /** 一次性提交声明(`true|undefined` 型:opt-out=不传,绝不发 false——wire 上与缺席同义,发小的那个)。 */
92
92
  oneShot?: true;
93
+ /**
94
+ * 「本会话永远不进长期记忆」的**声明形**([7226] ⑤;server §12.4)。
95
+ * 🔴 型面就写成 `'off'`:上游是**单成员闭集**(没有 `"on"`),给它一个宽串型等于邀请调用方发
96
+ * 一个 400 回来。JS 调用方(本包是发出去的 npm 公开面)绕过型面时由构造器再判一次值。
97
+ */
98
+ memoryCapture?: 'off';
93
99
  /** rewind 三件(E18);端自己判空。 */
94
100
  rewind?: Record<string, unknown>;
95
101
  /** 仓库坐标串(scan/code-review 场景;今日唯一供给方 web 的 Repository 输入=url/`owner/name` 串。
@@ -118,6 +124,54 @@ export interface TaskRequestInput {
118
124
  * 🔴 行为纪律:本函数**不做任何统一**。print 没有 `ultracode` 就是没有(表里 `gap:true` 记着账),
119
125
  * 补齐要另立项 —— 在这里顺手加一行,就是把「合一」偷换成「行为改动」。
120
126
  */
127
+ /**
128
+ * ⑤ 的**响亮拒**([7226] 包侧缺口 ⑤;异源对抗复审轮五 finding① 采纳)。
129
+ *
130
+ * 🔴 修前这里是「不是 `'off'` 就整键不 stamp」——**静默删键**,而删掉的恰是一条**隐私声明**:
131
+ * 一个把 `/memory-capture off` 打成 `OFF` 的会话,请求照发、引擎照常采集,**没有任何人会知道**。
132
+ * 🔴 上游把这条写死了(装机 core `dist/core/memory.d.ts` 的 `capture?: "off"` 头注**逐字**):
133
+ * 「Any other value — `"on"`, `"OFF"`, booleans, garbage — is REFUSED loudly
134
+ * (`config.memory_capture_spelling`), never read as either state (**a privacy request must not be
135
+ * dropped by a typo**, and capture must not be switched off by one either)」;server 契约 §12.4
136
+ * 同样写明坏拼写 400 `request.field_invalid`,并点名「把一条隐私请求按打字错误静默丢掉恰是**禁的方向**」。
137
+ * ⇒ 提前删键 = 把上游那道响亮门**绕过去**,方向正好反了。
138
+ * 🔴 与本文件 `resolvedSnapshotForWire` 对畸形权限快照的处置**同一条纪律**(那里也是抛,理由逐字
139
+ * 是「降空 = 让请求带着被剥掉的权限面发出去」)——两处都是**声明方向**的位:丢了没人看得见。
140
+ * ⚠️ `undefined` / `null` = **合法缺席**(端没有这个入口 / 没有声明),照旧不 stamp,不拒。
141
+ * ⚠️ **不分车道、不受 live 门**:拒绝不是「stamp 一个键」,不改请求形状;而一条打错字的隐私声明
142
+ * 在哪条车道上都不该被默默放行。
143
+ * (本包是发出去的 npm 公开面:JS 调用方与版本偏斜的宿主都到得了这里,型面拦不住。)
144
+ */
145
+ /**
146
+ * wire 上那个**单成员闭集**的唯一字面(server §12.4)。导出它是为了让端的断言/门有一个机读锚,
147
+ * 🔴 **不是**为了让端拿它去自己拼请求 —— 拼请求走 {@link memoryCaptureDeclarationField}。
148
+ */
149
+ export declare const MEMORY_CAPTURE_OFF: "off";
150
+ /**
151
+ * 「本会话声明过 opt-out 没有」这个**意图位** → 提交腿的 spread-ready 片段(L-316,0.68.2)。
152
+ *
153
+ * ── 为什么这一只在包里(归层,不是搬家)────────────────────────────────────────────────────
154
+ * 端手里只有一个**布尔**:「这条会话敲过 `/memory-capture off` 没有」。而 wire 上那个值是
155
+ * **单成员闭集**的一个字面串 —— 谁铸这个串,谁就得同时承担「拼错了会被 400 响亮拒、而且这是
156
+ * 一条**隐私**声明、拼错即静默失效」这件事。cli 1.0.x 起这个串在壳里另有一处铸点
157
+ * (`memoryCaptureOptOut.memoryCaptureRequestField`),web / desktop 接这条腿时会各铸第三、第四处
158
+ * —— 三端各写一个字面量,正是本层存在的理由(与 `taskNotificationToPrintFrame` 同一条先例)。
159
+ * ⇒ 端交**意图位**,值由本层唯一铸出;拼写门与 {@link buildTaskRequest} 的响亮拒共用同一个字面。
160
+ *
161
+ * 🔴 **`false` ⇒ 整键缺席**,不是 `{ memoryCapture: undefined }`、更不是某个「on」值:wire 上
162
+ * 压根没有那个值(单成员闭集),**缺席就是「照常采集」的唯一写法**。所以未启用时本片段对
163
+ * wire 字节零影响。
164
+ * 🔴 **本层只答「下一条提交带不带这一键」,不答「采集关没关」**:后者只有引擎知道(老 worker
165
+ * 静默忽略本键 / 部署策略 403 / 控制面写失败),任何端都不许拿这一位去渲一句「已经关了」。
166
+ *
167
+ * 用法(端逐字照抄,别在外面再包一层字面量):
168
+ * ```ts
169
+ * buildTaskRequest({ …, ...memoryCaptureDeclarationField(declaredForThisSession) }, 'interactive')
170
+ * ```
171
+ */
172
+ export declare function memoryCaptureDeclarationField(declared: boolean): {
173
+ memoryCapture: 'off';
174
+ } | Record<string, never>;
121
175
  export declare function buildTaskRequest(input: TaskRequestInput, lane: RequestLane): TaskRequestLike;
122
176
  /** `applyLiveRequestDefaults` 的宿主输入(端解析好的值,同样零取值方式)。 */
123
177
  export interface LiveDefaultsInput {
@@ -42,6 +42,15 @@ export const REQUEST_FIELD_MATRIX = [
42
42
  { field: 'debate', lanes: ['interactive', 'print'], live: true, why: 'council 的 debate 变体开关(server 读 `=== true`)' },
43
43
  { field: 'rounds', lanes: ['interactive', 'print'], live: true, why: 'debate 轮数(server clamp [1,3],非有限数被 server 归 undefined——本层原样透传不预 clamp)' },
44
44
  { field: 'lenses', lanes: ['interactive', 'print'], live: true, why: 'code-review 镜头集(passthrough 同 repo;形状由 server 场景定义,本层不闭集)' },
45
+ // ── memoryCapture([7226] 包侧缺口 ⑤,0.68.1)──────────────────────────────────────────────
46
+ // 🔴 **单成员闭集**:server 契约 §12.4 逐字「`memoryCapture?: "off"`。单成员闭集(**没有**
47
+ // `"on"`)」—— 所以本层只在值**恰是** `'off'` 时 stamp;`'on'` / 坏拼写一律不上 wire
48
+ // (坏拼写 server 判 400 `request.field_invalid`,本层不预铸第二判官,但也不替它买路)。
49
+ // 🔴 与**中途翻转 verb** `POST /v1/runs/:id/memory/capture-optout`(§12.5,能力位
50
+ // `runMemoryCaptureOptOut`)是**同一条一次性单向记录的两个口**,不是两个轴:verb 管这一条
51
+ // 在飞的 run,本键管「本会话的下一次新提交」。非 live 的 verb(parked / 终态 / 他副本)回
52
+ // 409 `steering.not_running` 并**指路本键** —— 端没有本键就接不住那条指路。
53
+ { field: 'memoryCapture', lanes: ['interactive', 'print'], live: true, why: '本会话永远不进长期记忆的**声明形**(server §12.4 body 键,单成员闭集 "off");两条车道都是提交面 ⇒ 都能声明。与 memoryWrite:false 不是一个轴(那是「本 run 只读平面」)。⚠️ parked 续跑腿结构性拿不到声明(决策/续跑端点从持久 body 重建 spec),那是引擎侧事实,不是本层的漏' },
45
54
  // ── print 独有(§8-5 认定的**有理由**的差异)──────────────────────────────────────────────
46
55
  { field: 'finalVerification', lanes: ['print'], live: true, why: 'P1-1 终验:无人值守车道才需要引擎自证;交互 REPL 由人当场看结果。#106 裁 B(让位+告知)后交互面默认关' },
47
56
  { field: 'limits', lanes: ['print'], live: true, why: 'P2-3-b:`-p` 的预算护栏(--max-* flag 族),交互 REPL 由人随时 Esc' },
@@ -390,8 +399,70 @@ const resolvedSnapshotForWire = (resolved) => {
390
399
  * 🔴 行为纪律:本函数**不做任何统一**。print 没有 `ultracode` 就是没有(表里 `gap:true` 记着账),
391
400
  * 补齐要另立项 —— 在这里顺手加一行,就是把「合一」偷换成「行为改动」。
392
401
  */
402
+ /**
403
+ * ⑤ 的**响亮拒**([7226] 包侧缺口 ⑤;异源对抗复审轮五 finding① 采纳)。
404
+ *
405
+ * 🔴 修前这里是「不是 `'off'` 就整键不 stamp」——**静默删键**,而删掉的恰是一条**隐私声明**:
406
+ * 一个把 `/memory-capture off` 打成 `OFF` 的会话,请求照发、引擎照常采集,**没有任何人会知道**。
407
+ * 🔴 上游把这条写死了(装机 core `dist/core/memory.d.ts` 的 `capture?: "off"` 头注**逐字**):
408
+ * 「Any other value — `"on"`, `"OFF"`, booleans, garbage — is REFUSED loudly
409
+ * (`config.memory_capture_spelling`), never read as either state (**a privacy request must not be
410
+ * dropped by a typo**, and capture must not be switched off by one either)」;server 契约 §12.4
411
+ * 同样写明坏拼写 400 `request.field_invalid`,并点名「把一条隐私请求按打字错误静默丢掉恰是**禁的方向**」。
412
+ * ⇒ 提前删键 = 把上游那道响亮门**绕过去**,方向正好反了。
413
+ * 🔴 与本文件 `resolvedSnapshotForWire` 对畸形权限快照的处置**同一条纪律**(那里也是抛,理由逐字
414
+ * 是「降空 = 让请求带着被剥掉的权限面发出去」)——两处都是**声明方向**的位:丢了没人看得见。
415
+ * ⚠️ `undefined` / `null` = **合法缺席**(端没有这个入口 / 没有声明),照旧不 stamp,不拒。
416
+ * ⚠️ **不分车道、不受 live 门**:拒绝不是「stamp 一个键」,不改请求形状;而一条打错字的隐私声明
417
+ * 在哪条车道上都不该被默默放行。
418
+ * (本包是发出去的 npm 公开面:JS 调用方与版本偏斜的宿主都到得了这里,型面拦不住。)
419
+ */
420
+ /**
421
+ * wire 上那个**单成员闭集**的唯一字面(server §12.4)。导出它是为了让端的断言/门有一个机读锚,
422
+ * 🔴 **不是**为了让端拿它去自己拼请求 —— 拼请求走 {@link memoryCaptureDeclarationField}。
423
+ */
424
+ export const MEMORY_CAPTURE_OFF = 'off';
425
+ /**
426
+ * 「本会话声明过 opt-out 没有」这个**意图位** → 提交腿的 spread-ready 片段(L-316,0.68.2)。
427
+ *
428
+ * ── 为什么这一只在包里(归层,不是搬家)────────────────────────────────────────────────────
429
+ * 端手里只有一个**布尔**:「这条会话敲过 `/memory-capture off` 没有」。而 wire 上那个值是
430
+ * **单成员闭集**的一个字面串 —— 谁铸这个串,谁就得同时承担「拼错了会被 400 响亮拒、而且这是
431
+ * 一条**隐私**声明、拼错即静默失效」这件事。cli 1.0.x 起这个串在壳里另有一处铸点
432
+ * (`memoryCaptureOptOut.memoryCaptureRequestField`),web / desktop 接这条腿时会各铸第三、第四处
433
+ * —— 三端各写一个字面量,正是本层存在的理由(与 `taskNotificationToPrintFrame` 同一条先例)。
434
+ * ⇒ 端交**意图位**,值由本层唯一铸出;拼写门与 {@link buildTaskRequest} 的响亮拒共用同一个字面。
435
+ *
436
+ * 🔴 **`false` ⇒ 整键缺席**,不是 `{ memoryCapture: undefined }`、更不是某个「on」值:wire 上
437
+ * 压根没有那个值(单成员闭集),**缺席就是「照常采集」的唯一写法**。所以未启用时本片段对
438
+ * wire 字节零影响。
439
+ * 🔴 **本层只答「下一条提交带不带这一键」,不答「采集关没关」**:后者只有引擎知道(老 worker
440
+ * 静默忽略本键 / 部署策略 403 / 控制面写失败),任何端都不许拿这一位去渲一句「已经关了」。
441
+ *
442
+ * 用法(端逐字照抄,别在外面再包一层字面量):
443
+ * ```ts
444
+ * buildTaskRequest({ …, ...memoryCaptureDeclarationField(declaredForThisSession) }, 'interactive')
445
+ * ```
446
+ */
447
+ export function memoryCaptureDeclarationField(declared) {
448
+ return declared ? { memoryCapture: MEMORY_CAPTURE_OFF } : {};
449
+ }
450
+ const refuseBadMemoryCapture = (v) => {
451
+ if (v === undefined || v === null)
452
+ return;
453
+ if (v === 'off')
454
+ return;
455
+ throw new TypeError('buildTaskRequest: memoryCapture 只接受字面量 `\'off\'` 或缺席(undefined/null)—— ' +
456
+ `收到 ${shapeTag(v)}。这一位是**会话级隐私声明**(「本会话永远不进长期记忆」),` +
457
+ '上游是**单成员闭集**(没有 `"on"`)且对坏拼写**响亮拒**(core `config.memory_capture_spelling` / ' +
458
+ 'server 400 `request.field_invalid`)。本层若把坏值静默删键,请求会照发、引擎会照常采集,' +
459
+ '而声明人不会知道自己的 opt-out 丢了 —— 把一条隐私请求按打字错误静默丢掉是**禁的方向**。' +
460
+ '错误文本只报形状,不回显值。');
461
+ };
393
462
  export function buildTaskRequest(input, lane) {
394
463
  const live = input.live;
464
+ // ⑤ 响亮拒排在最前面:一条打错字的隐私声明绝不许被后面任何一条「不 stamp」路径吞掉。
465
+ refuseBadMemoryCapture(input.memoryCapture);
395
466
  /** stamp 门:字段在本车道的表里 ∧(非 live-gated ∨ 本次是 live 车道)∧ 值非空。 */
396
467
  const on = (field, value) => {
397
468
  if (value === undefined || value === null)
@@ -469,6 +540,10 @@ export function buildTaskRequest(input, lane) {
469
540
  ? { interactiveTools: false }
470
541
  : {}),
471
542
  ...(on('oneShot', input.oneShot) && input.oneShot === true ? { oneShot: true } : {}),
543
+ // ⑤ 单成员闭集:值**恰是** `'off'` 才 stamp。表外拼写在上面已经**响亮拒**(见 `refuseBadMemoryCapture`)。
544
+ ...(on('memoryCapture', input.memoryCapture) && input.memoryCapture === 'off'
545
+ ? { memoryCapture: 'off' }
546
+ : {}),
472
547
  ...(Object.keys(settingsOut).length > 0 ? { settings: settingsOut } : {}),
473
548
  };
474
549
  return out;