@sema-agent/client-core 0.68.1 → 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.
@@ -1,5 +1,32 @@
1
1
  import { chrome, MAIN, transcript, messageIdentityOf } from './ids.js';
2
2
  import { estimateCjkTokens, FLUSH_INTERVAL_MS } from './wireShapes.js';
3
+ // L-318:六形判决单源(两条车道同吃;落账各归各层,见 textSegmentAuthority.ts 头注)。
4
+ import { resolveTextSegmentAuthority } from './textSegmentAuthority.js';
5
+ /**
6
+ * **段身份键**(CC-01,0.68.2)—— 打在 committed assistant **文本**转录行**顶层**的 `_sema_` 超集键。
7
+ *
8
+ * ── 为什么有它(归层修,不是新功能)──────────────────────────────────────────────────────────
9
+ * `text_segment_end` 到达时若已提交前缀自己也过期(`committedPrefixDiverged`),宿主要在**自己持有的**
10
+ * 转录里找出「这一段交出去的那一行」把它换掉。按字节相等去找会被**同后缀的独立行**冒充(实撞过);
11
+ * 按 uuid 找只证明「是这一行」,不证明「是这一段的」。⇒ 归属要两把钥匙:行身份(`uuid`)∧ **段身份**。
12
+ * 段身份此前由壳在自己的过境口铸(先壳后包);段的边界(开场 / 收口 / 轮换)本来就只有本模块知道,
13
+ * 铸点归这里,壳只读。
14
+ *
15
+ * ── 语义(一句话)────────────────────────────────────────────────────────────────────────────
16
+ * **一个身份覆盖「上一次段收口 → 本次段收口」之间过境的全部 committed 文本行**:流开场一个初值;
17
+ * idle-flush 提交的半段与终态提交的同段共享一个身份(归属正要这个);`text_segment_end` 臂处理完之后
18
+ * 轮换(帧上带的是轮换**前**的那一个,与它盖过的行同值);思考→回答边界**不**轮换;子流(带
19
+ * `parentToolCallId`)的段边界不轮换(它们不产 leader 事件,见臂头注)。
20
+ *
21
+ * ── 铸法与形 ──────────────────────────────────────────────────────────────────────────────
22
+ * 派生自本窗口的**第一帧**锚 + 窗口序号(`idOf(anchor, 'segment<n>')`)⇒ **同流重放同身份**
23
+ * (与 committed 行的 uuid 同一条确定性纪律;宿主的双跑对拍门靠它)。🔴 它是**转录格式**上的键,
24
+ * 不是 wire 键:只出现在宿主落盘的转录行上,不进 provider 请求(顶层自铸键不外溢,与
25
+ * `_sema_degraded` 同律)。🔴 **只盖尾块是 `text` 的 assistant 行**:只带 `tool_use` 的行永远不可能是
26
+ * 替换目标,盖了只是往转录里加噪;思考块同理。缺席 = 「这条不属于任何引擎段」,不是「属于某个未知段」。
27
+ * 🔴 **只加不减**:非本形消息一个字节不动。
28
+ */
29
+ export const SEMA_SEGMENT_ID_KEY = '_sema_segment_id';
3
30
  /**
4
31
  * 件 A(#323 症状①)判据 (b) 的句末字符集 —— 中英文各一套,**闭集**(开集匹配会把「代码里的点」
5
32
  * 之外的一切标点都当句末,等于没收紧)。换行单独在 {@link endsAtSentenceBoundary} 里判(段末)。
@@ -109,6 +136,18 @@ export function createTextStream(ctx, idOf) {
109
136
  /** committed 消息的 id 锚 —— 该段/该思考块的**第一帧**(重放确定性:同流同 id)。 */
110
137
  let segmentAnchor = null;
111
138
  let thinkingAnchor = null;
139
+ /**
140
+ * 段身份窗口(见 {@link SEMA_SEGMENT_ID_KEY} 头注):窗口的第一帧锚 + 序号 + 派生结果缓存。
141
+ * 🔴 缓存不是优化:锚缺席那一形派生落到 `ctx.uuid()`,不缓存的话同一窗口两次读会得到两个身份。
142
+ */
143
+ let segmentWindowAnchor = null;
144
+ let segmentWindowSerial = 0;
145
+ let segmentIdCache = null;
146
+ const segmentIdNow = () => {
147
+ if (segmentIdCache === null)
148
+ segmentIdCache = idOf(segmentWindowAnchor ?? {}, `segment${segmentWindowSerial}`);
149
+ return segmentIdCache;
150
+ };
112
151
  /** 提交累积的思考块(cli takeThinking:committed 形 + 关活体块 + 收活动行)。 */
113
152
  function* takeThinking() {
114
153
  if (thinking.length === 0)
@@ -225,11 +264,15 @@ export function createTextStream(ctx, idOf) {
225
264
  // L-310:已封存的定稿段也算「这条消息已经开了头」⇒ 锚不重置(同流重放同 id)。
226
265
  if (answerSegment.length === 0 && segmentSealed.length === 0)
227
266
  segmentAnchor = frame;
267
+ if (segmentWindowAnchor === null)
268
+ segmentWindowAnchor = frame;
228
269
  answer += delta;
229
270
  answerSegment += delta;
230
271
  textPending += delta;
231
272
  },
232
273
  *feedThinking(delta, frame) {
274
+ if (segmentWindowAnchor === null)
275
+ segmentWindowAnchor = frame;
233
276
  if (thinking.length === 0) {
234
277
  thinkingAnchor = frame;
235
278
  // P2d:elapsed 锚在**首条** leader 推理增量(token 累计由 stream_delta.estimatedTokens
@@ -255,20 +298,29 @@ export function createTextStream(ctx, idOf) {
255
298
  * 而权威全文又被完整提交一次(同一段话上屏两遍 + 明文没人清)。实测复现过。
256
299
  */
257
300
  const livePrefix = previousSegmentCommitted + segmentCommitted;
258
- // 🔴 **本包一个字节都没经手这一段 ⇒ 什么都不做**(形 (f);#323-g 那条正控守的就是这一形)。
259
- // 可达形两条,都不是「流式段边界」:① durable 内容腿(`assistant` 臂的整块 `text`)——
260
- // 那一帧本身就是一整条 assistant 消息、已经铸过 transcript 行,随后到的 `text_end` 若在这里
261
- // 把 `content` 塞进段缓冲,turn 收口就会**再铸一条一模一样的**;② 宿主只喂 `text_end`、
262
- // 不喂 `text_delta` 的自建管线。共同事实 = 没有可替换的对象,而替换的语义是**换掉本包自己
263
- // 缝合出来的那一段**,不是「凭空铸一段」。
264
- // (durable 腿本身不缺脱敏:server 的 `text` 行在写口与读口各脱一次,见契约 §5.1。)
265
- if (livePrefix.length === 0 && prevSegment.length === 0) {
301
+ /**
302
+ * 🔴 **判决单源**(L-318,0.68.2):六形判据整只让位给 `adapt/textSegmentAuthority.ts` 的
303
+ * {@link resolveTextSegmentAuthority} —— 与 `-p` 形车道(宿主的 CC stdout 投影器)吃的是
304
+ * **同一只**。修前这里是本包这一份,壳里另有逐形对照的第二份(cli 1.0.114 B-122 热修),
305
+ * 上游改口时两边各走各的。判据语义**逐字未变**:
306
+ * · 形 (f)「本层一个字节都没经手」⇒ `form: 'untouched'`,三位全报否;
307
+ * · `diverged` = 缝出来的那一段 ≠ 权威全文;
308
+ * · `committedPrefixLen` = 已撤不回那一截的 UTF-16 长度;
309
+ * · `committedPrefixDiverged` 锚 `content.startsWith(前缀)`(不锚「有没有 flush 过」)。
310
+ * **落账仍归本模块**:下面的 handOffOnly / 活体尾巴重算 / 封存 / 补差账同步,一行未动
311
+ * (两条车道的账本形状不同,理由见 textSegmentAuthority.ts 头注「判决与落账刻意分家」)。
312
+ */
313
+ const verdict = resolveTextSegmentAuthority(content, {
314
+ committedPrefix: livePrefix,
315
+ openSegment: prevSegment,
316
+ });
317
+ if (verdict.form === 'untouched') {
266
318
  return { diverged: false, committedPrefixLen: 0, committedPrefixDiverged: false };
267
319
  }
268
320
  const liveSegment = livePrefix + prevSegment;
269
- const diverged = liveSegment !== content;
270
- const committedPrefixLen = livePrefix.length;
271
- const committedPrefixDiverged = committedPrefixLen > 0 && !content.startsWith(livePrefix);
321
+ const diverged = verdict.diverged;
322
+ const committedPrefixLen = verdict.committedPrefixLen;
323
+ const committedPrefixDiverged = verdict.committedPrefixDiverged;
272
324
  /**
273
325
  * 🔴 **段跨过包侧边界、而且已提交那截自己也过期** ⇒ 本模块**一个字节都不动**(只发信号)。
274
326
  *
@@ -326,7 +378,9 @@ export function createTextStream(ctx, idOf) {
326
378
  }
327
379
  }
328
380
  else {
329
- segmentSealed += content.slice(committedPrefixLen);
381
+ // L-318:尾段切片也走判决那一份(`content.slice(committedPrefixLen)` 逐字等价),
382
+ // 免得「怎么切」这件事在两条车道上各留一个算式。
383
+ segmentSealed += verdict.tail;
330
384
  }
331
385
  answerSegment = '';
332
386
  // ── ③ `answer`(D8 调试行的长度读位)跟着换尾 —— 只在尾巴真对得上时换 ──────────────────
@@ -366,6 +420,29 @@ export function createTextStream(ctx, idOf) {
366
420
  if (typeof committed === 'string')
367
421
  committedText += committed;
368
422
  },
423
+ segmentId: segmentIdNow,
424
+ rotateSegmentIdentity: (anchor) => {
425
+ segmentWindowSerial += 1;
426
+ segmentWindowAnchor = anchor;
427
+ segmentIdCache = null;
428
+ },
429
+ stampSegmentIdentity: (out) => {
430
+ if (out.plane !== 'transcript')
431
+ return out;
432
+ // ⚠️ 局部名刻意不叫 `msg`/`content`:message-branching 门把 `msg.message` 读成「错误文案」,
433
+ // 并按名字把污染扩到整个文件 —— 同名的 `content.startsWith` 会被误判成按文案分支。
434
+ const row = out.message;
435
+ if (row.type !== 'assistant')
436
+ return out;
437
+ const blocks = row.message?.content;
438
+ if (!Array.isArray(blocks) || blocks.length === 0)
439
+ return out;
440
+ const last = blocks[blocks.length - 1];
441
+ if (last === undefined || last.type !== 'text' || typeof last.text !== 'string')
442
+ return out;
443
+ out.message[SEMA_SEGMENT_ID_KEY] = segmentIdNow();
444
+ return out;
445
+ },
369
446
  beginAssistantMessage: () => {
370
447
  if (committedText.length > 0)
371
448
  previousCommittedText = committedText;
package/dist/adapt.js CHANGED
@@ -203,7 +203,20 @@ class WireToCcAdapterImpl {
203
203
  // 壳 B4 的队列端口回钩仍可调它 —— 三条入队链本来就写同一个集合,回钩因此变成幂等 no-op。
204
204
  noteWorkflowCompletionCardEnqueued(runId);
205
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
+ */
206
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) {
207
220
  // FIX-05:实例台账走**唯一一条**显式路径(拆分前是 `self.` / `this.` 两条)。
208
221
  const inst = this.ledger;
209
222
  const turnStartAt = ctx.now();
@@ -211,6 +224,7 @@ class WireToCcAdapterImpl {
211
224
  const idOf = makeIdOf(ctx);
212
225
  // ── M1 流合并器(矩阵 §1.1 的 12 行状态整体落 adapt/textStream.ts)────────────────────────
213
226
  const text = createTextStream(ctx, idOf);
227
+ slot.text = text;
214
228
  // ── M2 工具卡台账(矩阵 §1.2 的 3 行状态整体落 adapt/toolCards.ts)────────────────────────
215
229
  const cards = createToolCardLedger(ctx, idOf);
216
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
@@ -964,7 +964,7 @@ function textEndProjection(ev, ctx) {
964
964
  * ⚠️ `toolCount` 按**有限数**判(`Number.isFinite`),不按真值判:`0` 是合法读数(连上了、零工具),
965
965
  * 折成缺席会让「连上了但没工具」与「没报」在消费端同形。
966
966
  */
967
- function projectMcpSection(raw) {
967
+ export function projectMcpSection(raw) {
968
968
  if (!Array.isArray(raw))
969
969
  return undefined;
970
970
  const rows = [];
@@ -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;
@@ -118,6 +118,60 @@ export interface TaskRequestInput {
118
118
  outputStyle?: string;
119
119
  };
120
120
  }
121
+ /**
122
+ * 合一后的请求构造器 —— **按车道出两形**,字段集差异全部由 `REQUEST_FIELD_MATRIX` 决定。
123
+ *
124
+ * 🔴 行为纪律:本函数**不做任何统一**。print 没有 `ultracode` 就是没有(表里 `gap:true` 记着账),
125
+ * 补齐要另立项 —— 在这里顺手加一行,就是把「合一」偷换成「行为改动」。
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 {
@@ -417,6 +417,36 @@ const resolvedSnapshotForWire = (resolved) => {
417
417
  * 在哪条车道上都不该被默默放行。
418
418
  * (本包是发出去的 npm 公开面:JS 调用方与版本偏斜的宿主都到得了这里,型面拦不住。)
419
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
+ }
420
450
  const refuseBadMemoryCapture = (v) => {
421
451
  if (v === undefined || v === null)
422
452
  return;