@sema-agent/client-core 0.70.1 → 0.71.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.
@@ -569,6 +569,8 @@ export const TOOL_APPROVAL_FRAME_KEYS_MIRROR = [
569
569
  // ⚠️ 与 `persistedRuleShadowed`/`probeCause` 同形:sdk 8.8.0 的运行期锚**尚无**本键 ⇒ **领先**一代,
570
570
  // 进对账门的 AHEAD_OF_ANCHOR 带退出条件登记(sdk 8.9.x 补上那天那条登记自红逼删,回到逐元素相等)。
571
571
  'ruleStoreUnreadable',
572
+ // CC-08(0.71.0):core 7.19.0 #803 根外读卡附读根候选;sdk 9.6.0 锚第 30 员。
573
+ 'readRootCandidate',
572
574
  // S-125③/#564(client-core 0.59.0):server 7.57.0 起真发 `origin`(core 7.5.0 `ASK_ORIGINS`
573
575
  // 八词;engine 7.60.0 fixture 直证)。同上:sdk 8.3.0 锚已含本键 ⇒ 追平,不进领先表。
574
576
  'origin',
@@ -13,6 +13,11 @@
13
13
  * · `lastLegMcp` **整键缺席** = server 的三形同形(新会话 / 最近腿的账本行在保留窗外 / 该腿没推
14
14
  * `wiring_manifest`)**或**引擎早于 7.77.0;本视图同样不铸该键。「为什么缺席」这一位答不出来,
15
15
  * 也不猜。
16
+ * 🔒 **定谳(0.70.1,三方闭环):这三形永远不会长出缺席原因伴生键**,本读器也不留位。理由 ①
17
+ * 后两形在上游是同一条代码路径(走完事件表就是没有那一帧),分不出是保留窗剪的还是压根没推,
18
+ * 启发式 = 把猜测穿成事实;② 该读面的属主门那一臂必须与其余缺席**同形**(非属主读别人的会话
19
+ * 时本键缺席、面板照常 200,反枚举),一旦带原因键,「不存在的会话答 no_leg、别人的会话不答」
20
+ * 就是一个**会话存在性 oracle** —— 可分辨性在这里让位给反枚举。⇒ 缺席只有一句话可说,不编第三句。
16
21
  * · `lastLegMcp` **在场但形坏**(`runId` / `at` 不是非空串、`mcp` 读不出一行)⇒ 视图不铸该键,
17
22
  * 改立 {@link McpPanelView.lastLegMcpUnreadable}(never false)。「引擎送了一份我读不懂的」与
18
23
  * 「引擎没送」对运维是两句话,不折成一个 `undefined`。
@@ -27,10 +32,10 @@
27
32
  * ── `mcp[]` 条目与 `wiring_manifest` 第三段**同一只读器**─────────────────────────────────────
28
33
  * `lastLegMcp.mcp[]` 是该腿 `wiring_manifest.mcp[]` 的逐字回放(server S-297),本视图直接调
29
34
  * {@link projectMcpSection} —— 同一形、同一判据、同一缺席律;三端渲这两处用同一个
30
- * {@link WiringManifestMcpEntry},不会出现「活体腿一种形、账本回放另一种形」。
35
+ * {@link WiringManifestMcpEntryView},不会出现「活体腿一种形、账本回放另一种形」。
31
36
  */
32
37
  import type { AgentClient } from '@sema-agent/sdk';
33
- import { type WiringManifestMcpEntry } from './adapter/downstream/eventToSdkMessage.js';
38
+ import { type WiringManifestMcpEntryView } from './adapter/downstream/eventToSdkMessage.js';
34
39
  /** 面板 `servers[]` 一行(sdk `McpServerStatus` 的窄读;开集键不透传,逐键挑)。 */
35
40
  export interface McpPanelServerView {
36
41
  /** 配置名(引擎产的标识,非用户内容)。 */
@@ -54,7 +59,7 @@ export interface McpPanelLastLegView {
54
59
  /** 写账本副本的钟(ISO;**不是** materialize 时刻 `asOf`,两者不比)。 */
55
60
  at: string;
56
61
  /** 该腿 `wiring_manifest.mcp[]` 逐字 —— 与活体腿的第三段同一只读器、同一形。 */
57
- mcp: WiringManifestMcpEntry[];
62
+ mcp: WiringManifestMcpEntryView[];
58
63
  }
59
64
  /** `GET /v1/sessions/:id/mcp` 的读视图(缺席语义见文件顶注)。 */
60
65
  export interface McpPanelView {
@@ -102,3 +107,16 @@ export declare function fetchMcpPanel(client: Pick<AgentClient, 'sessions'>, ses
102
107
  export declare function mcpPanelLastLegDetail(view: McpPanelView | undefined, opts: {
103
108
  reachable: boolean;
104
109
  }): string;
110
+ /**
111
+ * CC-19(0.71.0;cli L-335 归包):**引擎侧 MCP 在场三值**,只从本包的面板视图求(不自拼第二份判据、不发第二次请求)。
112
+ * · `'present'`:`servers[]` 非空(这台部署此刻 materialize 出的引擎侧连接有行)/ `lastLegMcp` 在场(最近一条腿申报过名册)/
113
+ * `lastLegMcpUnreadable`(引擎送了一份读不懂的 = 有东西)/ `degraded`(materialize 超时,`servers: []` 此时**不等于**没有);
114
+ * · `'absent'`:视图读得出 ∧ 以上全不成立 = **正面事实**「引擎侧没有 MCP」;
115
+ * · `'unknown'`:`view === undefined`(传输失败 / 体读不出 / 空 sessionId,`fetchMcpPanel` 刻意三形同形)—— 本包**说不出**。
116
+ * 🔴 宿主义务:`'unknown'` **不许**渲成 CC 那句「一台 MCP 都没配」(「不知道」不渲成 0);有引擎接线的端在 `'unknown'` 上
117
+ * 开面板,让 {@link mcpPanelLastLegDetail} 铸的「未观测」那一句自己说话;CC 逐字那句只在**壳侧零行 ∧ `'absent'`** 时出。
118
+ * 🔴 为什么不能拿 `mcpPanelLastLegDetail` 有没有出一行当在场判据:它对**任何**视图都出一句(在场 / 未观测 / 不报 / 读不出),
119
+ * 老引擎与新会话同样出「不报」—— 拿行数判在场恒真。在场只能问视图本身的键。
120
+ */
121
+ export type McpEngineLegPresence = 'present' | 'absent' | 'unknown';
122
+ export declare function mcpEngineLegPresence(view: McpPanelView | undefined): McpEngineLegPresence;
package/dist/mcpPanel.js CHANGED
@@ -127,3 +127,16 @@ export function mcpPanelLastLegDetail(view, opts) {
127
127
  */
128
128
  const _mcpPanelShapePin = (p) => p;
129
129
  void _mcpPanelShapePin;
130
+ export function mcpEngineLegPresence(view) {
131
+ if (view === undefined)
132
+ return 'unknown';
133
+ if (view.servers.length > 0)
134
+ return 'present';
135
+ if (view.lastLegMcp !== undefined)
136
+ return 'present';
137
+ if (view.lastLegMcpUnreadable === true)
138
+ return 'present';
139
+ if (view.degraded === true)
140
+ return 'present';
141
+ return 'absent';
142
+ }
@@ -49,7 +49,7 @@ export interface McpConfigLike {
49
49
  headers?: Record<string, string>;
50
50
  allowTools?: string[];
51
51
  /**
52
- * **透明键**(settings-schema 1.9.0 `McpServerSpec.toolFaces`,`z.unknown()`;core 7.8.0 design/388
52
+ * **透明键**(settings-schema 1.9.0 引入 `McpServerSpec.toolFaces`,`z.unknown()`,2.0.0 同形〔2026-09-16 核 dist/types.d.ts〕;core 7.8.0 design/388
53
53
  * 片 1 F7 / server ≥7.66.0)—— 这台 server 的 per-tool 面覆盖层,键 = 工具**裸名**,值形
54
54
  * `{ family?, pathTarget?, renderHints?, ruleFace? }`(表的属主是 core,见 sdk `McpToolFace`)。
55
55
  *
@@ -22,7 +22,7 @@
22
22
  * ⇒ 与 `engineNoticeCodes.ts` / `autoModeUnavailable.ts` 同一条处置:**按真字节镜像,把代价交给
23
23
  * 门** —— `run-model-capability-probe-test.mjs` 的 A 段对实装 devDep core 双向等值对账,
24
24
  * 上游加一个方言这里当天先红。
25
- * ⚠️ **settings-schema 那边取不到这张表,别去那儿找**:`ModelEntry.compat` 在 1.10.0 上是
25
+ * ⚠️ **settings-schema 那边取不到这张表,别去那儿找**:`ModelEntry.compat` 在 1.10.0 引入、2.0.0 仍是(2026-09-16 核 dist/types.d.ts)
26
26
  * **parse-transparent 的 `z.unknown()`**,它的 JSDoc 逐字写着「the SHAPE and the VOCABULARY …
27
27
  * are the consuming side's single point — this package deliberately does NOT restate that table」。
28
28
  * 那一格刻意不复述词表,所以「从 settings-schema import 闭集」这条路在型面上不存在。
@@ -22,7 +22,7 @@
22
22
  * ⇒ 与 `engineNoticeCodes.ts` / `autoModeUnavailable.ts` 同一条处置:**按真字节镜像,把代价交给
23
23
  * 门** —— `run-model-capability-probe-test.mjs` 的 A 段对实装 devDep core 双向等值对账,
24
24
  * 上游加一个方言这里当天先红。
25
- * ⚠️ **settings-schema 那边取不到这张表,别去那儿找**:`ModelEntry.compat` 在 1.10.0 上是
25
+ * ⚠️ **settings-schema 那边取不到这张表,别去那儿找**:`ModelEntry.compat` 在 1.10.0 引入、2.0.0 仍是(2026-09-16 核 dist/types.d.ts)
26
26
  * **parse-transparent 的 `z.unknown()`**,它的 JSDoc 逐字写着「the SHAPE and the VOCABULARY …
27
27
  * are the consuming side's single point — this package deliberately does NOT restate that table」。
28
28
  * 那一格刻意不复述词表,所以「从 settings-schema import 闭集」这条路在型面上不存在。
package/dist/seam.d.ts CHANGED
@@ -9,7 +9,8 @@
9
9
  import type { ModelUsage, SDKMessage } from '@sema-agent/agent-types';
10
10
  import type { EngineTurnUsage } from './adapter/downstream/turnUsageToModelUsage.js';
11
11
  import type { RunCostReconcile } from './adapter/downstream/terminalToSdkResult.js';
12
- import type { WiringManifestAutoMode, WiringManifestModelGate, WiringManifestMcpEntry } from './adapter/downstream/eventToSdkMessage.js';
12
+ import type { WiringManifestAutoMode, WiringManifestModelGate, WiringManifestMcpEntryView, WiringManifestHookEntryView, WiringManifestLspSeamView, ContextUsageSectionView } from './adapter/downstream/eventToSdkMessage.js';
13
+ import type { ToolRosterView, ToolRosterDeltaView } from './toolRoster.js';
13
14
  /**
14
15
  * `AbortSignal` 的结构型(B3 扩容,SEAM-GAP-4)。
15
16
  *
@@ -533,7 +534,7 @@ export type ChromeEvent = {
533
534
  * ⚠️ **只有 live 腿有本帧**(server 7.69.0 亲读:durable/bg 腿把它喂给 rewind 锚而不入账本)
534
535
  * ⇒ replay/resume 上映射表是空的;那时正解是**不画分割线**,不是回退成「画在最后一条」。
535
536
  */
536
- | MessageCommittedChromeEvent | EngineNoticeChromeEvent | TextSegmentEndChromeEvent | ThinkingSegmentEndChromeEvent | WiringManifestChromeEvent | ResultTextDivergedChromeEvent
537
+ | MessageCommittedChromeEvent | EngineNoticeChromeEvent | TextSegmentEndChromeEvent | ThinkingSegmentEndChromeEvent | ToolDisclosureChromeEvent | ToolProgressChromeEvent | ToolRosterDeltaChromeEvent | ContextUsageChromeEvent | WiringManifestChromeEvent | ResultTextDivergedChromeEvent
537
538
  /**
538
539
  * B-078 / L-208(0.65.0):design/172 流内审批协议的**两条帧**。修前它们在标准管线上落
539
540
  * `dropped('unsupported_arm')`,壳自己另接一份 ⇒ 只走包管线的宿主(desktop/web)拿不到
@@ -585,7 +586,13 @@ export interface WiringManifestChromeEvent {
585
586
  * 🔴 **`errorCode` 按开集分支**(十词 + `http_<status>` 形),`switch` 必须带 `default` 臂;
586
587
  * 远端错误**自由文本**(`error`)上游就不投,宿主也拿不到 —— 可操作的因由在 `errorCode`。
587
588
  */
588
- mcp?: readonly WiringManifestMcpEntry[];
589
+ mcp?: readonly WiringManifestMcpEntryView[];
590
+ /** CC-08 ①(0.71.0):第四段 —— 这条腿开跑那一刻的工具名册快照(`toolRoster.ts` 读器;之后的变更走 `tool_roster_delta`)。 */
591
+ tools?: ToolRosterView;
592
+ /** CC-08 ①:第五段 —— hook 族(family / owner / observational?)。 */
593
+ hooks?: readonly WiringManifestHookEntryView[];
594
+ /** CC-08 ①:第六段 —— LSP 接缝(在场 iff 解析出了 manager 席;`mounted` 与 `lane` 禁合读)。 */
595
+ lsp?: WiringManifestLspSeamView;
589
596
  /** core 铸的事件身份(uuidv7 形);wire 未必带 ⇒ 缺席时本键不在场。 */
590
597
  eventId?: string;
591
598
  }
@@ -665,6 +672,11 @@ export interface WiringManifestChromeEvent {
665
672
  * 「半段已被 idle-flush 提交」那一形由 `committedPrefixLen` / `committedPrefixDiverged` 如实
666
673
  * 交代,不是把启发式悄悄换掉了。
667
674
  */
675
+ /** {@link TextSegmentEndChromeEvent.committedRows} 的一行:该 uuid 的文本行替换后应有的正文(端逐行写回)。 */
676
+ export interface TextSegmentCommittedRow {
677
+ uuid: string;
678
+ text: string;
679
+ }
668
680
  export interface TextSegmentEndChromeEvent {
669
681
  kind: 'text_segment_end';
670
682
  laneProof: LaneProof;
@@ -704,6 +716,21 @@ export interface TextSegmentEndChromeEvent {
704
716
  * 恒在场(本包每条 leader 段边界都带);它**不是** wire 键 —— `eventId` 才是引擎铸的事件身份。
705
717
  */
706
718
  segmentId: string;
719
+ /**
720
+ * CC-16(0.71.0):本段身份窗口里、段边界**之前**已提交的文本行 uuid(按序;与 `ThinkingSegmentEndChromeEvent.committedUuids`
721
+ * 同律)。`[]` = 本包判定本窗口零已提交行(在场但空);整键缺席 = 本包零经手(形 f),说不出。
722
+ * 端义务:`committedPrefixDiverged` ⇒ **按 {@link committedRows} 逐行写回**(包已算好每行最终正文;别拿 `content` 整行换首行);
723
+ * 不再自己数窗口里过境的行、不再对多行段整发放弃。常态段的定稿行在轮换之后才提交 ⇒ 不在列(它的正文已是权威)。
724
+ */
725
+ committedUuids?: readonly string[];
726
+ /** 末条兼容位;零行时缺席。 */
727
+ committedUuid?: string;
728
+ /**
729
+ * CC-16(0.71.0):端要**写回**的最终正文,按行(与 `committedUuids` 同序)。只在 `committedPrefixDiverged` ∧ 本窗口有已提交行时在场:
730
+ * 首行 = 前段定稿前缀 + `content`,其余行 `''`。🔴 端按行写、零算术 —— 别自己拿 `content` 整行换首行:提交行可能是
731
+ * 「上一段已封存的定稿 + 本段那截」一条(封存只换字节不切消息),整行换会把上一段的权威正文一起删。
732
+ */
733
+ committedRows?: readonly TextSegmentCommittedRow[];
707
734
  /** core 铸的事件身份(uuidv7 形);wire 未必带 ⇒ 缺席时本键不在场。 */
708
735
  eventId?: string;
709
736
  }
@@ -731,6 +758,74 @@ export interface TextSegmentEndChromeEvent {
731
758
  * ③ 三键全缺席 ⇒ 照旧只当对账/定界,拿 `content` 再渲一行 = 同一段上屏两遍。
732
759
  * ④ 缺席 ≠ 段没结束:只有会报块结束的引擎才发它;按整条流判有没有边界帧。
733
760
  */
761
+ /**
762
+ * CC-08(0.71.0;sdk 9.6.0 `tool_disclosure`,core design/393 S3 第二缝):这条腿的工具**名字占位普查**。
763
+ * additive 臂(`required: false`):不接 = 看不见普查,不属「已发生的行为丢失」。
764
+ */
765
+ /**
766
+ * CC-08 ①(0.71.0;sdk `tool_roster_delta`,server ≥7.70.0 LIVE only):运行期工具名册**增量**。收到 = 收到一份完整名册
767
+ * (server 判不出形整帧不发);`fromDigest` 对不上不是拒绝,记 skew 后整只替换;durable/resume 不落账 ⇒ 重连按
768
+ * `wiring_manifest.tools` 快照重同步。additive 臂(`required: false`)。
769
+ */
770
+ export interface ToolRosterDeltaChromeEvent {
771
+ kind: 'tool_roster_delta';
772
+ laneProof: LaneProof;
773
+ /** 已窄读的增量;宿主拿它喂 `applyToolRosterDelta`(与手上名册比 digest)。 */
774
+ delta: ToolRosterDeltaView;
775
+ eventId?: string;
776
+ }
777
+ /**
778
+ * CC-08 ③(0.71.0;sdk `context_usage` + `sections` core 7.18.0 #790):每个压缩边界报一次,宿主按**最后一条**取当前值。
779
+ * 🔴 `usedTokens > compactAtTokens` 就是引擎自己喂 shouldCompact 的谓词,别重算;`windowTokens` 是 autocompact 窗不是模型上下文大小;
780
+ * 🔴 `sections[].tokens` 与 `usedTokens` **不是同一坐标系,禁相减**、禁拿一段比 `compactAtTokens`;`sections` 整键缺席 = 这条腿没装配出段 IR,不是各段为零。
781
+ */
782
+ export interface ContextUsageChromeEvent {
783
+ kind: 'context_usage';
784
+ laneProof: LaneProof;
785
+ usedTokens?: number;
786
+ windowTokens?: number;
787
+ compactAtTokens?: number;
788
+ sections?: readonly ContextUsageSectionView[];
789
+ eventId?: string;
790
+ }
791
+ export interface ToolDisclosureChromeEvent {
792
+ kind: 'tool_disclosure';
793
+ laneProof: LaneProof;
794
+ /** 哪条策略判的:core 闭三词 `always` / `never` / `auto`,**按开集读**(core 加一档要能穿过你的 default 臂)。 */
795
+ policy: string;
796
+ /** `auto` 档下候选集必须够到的窗口百分比。🔴 缺席 = 两档下根本没查过阈值,**不是**默认百分比。 */
797
+ thresholdPercent?: number;
798
+ /** 这条腿上每一个以名字占位的工具名(prepare 期冻结,已排序)。**全量快照:整只替换**;`[]` = 普查坏到只剩空壳。 */
799
+ deferred: readonly string[];
800
+ /** 此刻已被激活的延迟工具名(引擎当下状态;已排序;恒 ⊆ deferred)。**全量快照:整只替换**。 */
801
+ activated: readonly string[];
802
+ /** core 铸的事件身份;wire 未必带 ⇒ 缺席时本键不在场。 */
803
+ eventId?: string;
804
+ }
805
+ /**
806
+ * CC-08(0.71.0;sdk 9.6.0 `tool_progress`;DV-741 clay C-R50 裁「一帧两拍」入 divergence 册,本包按裁定形投):
807
+ * 一只**仍在跑**的工具调用的进度 tick。Bash 类每 ≥1 s 一拍带输出尾巴三键;其他工具 30 s 一拍只带 `elapsedSeconds`。
808
+ * additive 臂(`required: false`)。
809
+ */
810
+ export interface ToolProgressChromeEvent {
811
+ kind: 'tool_progress';
812
+ laneProof: LaneProof;
813
+ /** 与它自己的 `tool_start` / `tool_end` join 得上的 id。 */
814
+ toolCallId: string;
815
+ /** `tool_start` 的值逐字同源。 */
816
+ toolName: string;
817
+ /** 已经跑了多少整秒(向下取整、跨帧单调、一次调用一个原点)。🔴 **缺席不是 0**;🔴 不等于 `tool_end` 的 duration。 */
818
+ elapsedSeconds?: number;
819
+ /** 命令至今写出来的尾巴(stdout/stderr 交错,引擎按尾部行数封顶)—— 一块活面板,不是日志。
820
+ * 🔴 **UNTRUSTED RAW**(与 `tool_end.output` 同类;server 已脱敏一次):宿主可再封顶展示,**绝不**回喂模型。 */
821
+ output?: string;
822
+ /** 至今观察到的换行数(≥ 尾巴自己的行数)。只在 `output` 在场时铸。 */
823
+ totalLines?: number;
824
+ /** 至今观察到的字节数(尾巴封顶之前计)。只在 `output` 在场时铸。 */
825
+ totalBytes?: number;
826
+ /** core 铸的事件身份;wire 未必带 ⇒ 缺席时本键不在场。 */
827
+ eventId?: string;
828
+ }
734
829
  export interface ThinkingSegmentEndChromeEvent {
735
830
  kind: 'thinking_segment_end';
736
831
  laneProof: LaneProof;
package/dist/seam.js CHANGED
@@ -78,6 +78,28 @@ const CHROME_ARM_TABLE = {
78
78
  required: false,
79
79
  duty: '可选:渲引擎通告(按 code+detail,message 仅 fallback;未知 code 也必须渲不许丢;durable 重放按 eventId 幂等;harvest 的 moved/escalated 并列呈现绝不相减)',
80
80
  },
81
+ tool_roster_delta: {
82
+ required: false,
83
+ duty: 'CC-08(0.71.0):运行期名册增量。拿 `delta` 喂 `applyToolRosterDelta` 与手上那份比 digest:对不上记一次 skew 后**整只替换**(不是拒绝);' +
84
+ '本臂 LIVE only,重连后按 `wiring_manifest.tools` 快照重同步,别等补发的 delta。不接 = 名册停在快照,不属行为丢失。',
85
+ },
86
+ context_usage: {
87
+ required: false,
88
+ duty: 'CC-08(0.71.0):每个压缩边界一条,按**最后一条**取当前值;`usedTokens > compactAtTokens` 是引擎的 shouldCompact 谓词,别重算;`windowTokens` 是 autocompact 窗,' +
89
+ '别渲成模型上下文大小;🔴 `sections[].tokens` 与 `usedTokens` 不同坐标系,**禁相减**、禁拿一段比 `compactAtTokens`;`sections` 整键缺席 = 没装配出段 IR,不是各段为零。',
90
+ },
91
+ tool_disclosure: {
92
+ required: false,
93
+ duty: 'CC-08(0.71.0):这条腿的工具名字占位普查。`deferred` / `activated` 都是**全量快照,整只替换**(不做增量合并);' +
94
+ '`policy` 按开集读;`thresholdPercent` 缺席 = 两档下没查过阈值,**不是**默认百分比,别拿一个编出来的默认值渲成一条真的阈值;' +
95
+ '`[]` 的 deferred = 普查坏到只剩空壳,按「没有可用普查」处置。不接 = 看不见普查,不属行为丢失。',
96
+ },
97
+ tool_progress: {
98
+ required: false,
99
+ duty: 'CC-08(0.71.0,C-R50 一帧两拍):按 `toolCallId` 挂到那张工具卡上;`elapsedSeconds` 缺席不是 0、不等于 tool_end 的 duration;' +
100
+ '`output` 在场 = 命令尾巴窗口(活面板,不是日志;`totalLines`/`totalBytes` 与它同进同退),🔴 它是 UNTRUSTED RAW,可再封顶展示、**绝不回喂**模型;' +
101
+ '子代工具的进度不在这条臂上(三键断闸),fleet 面板另有口。不接 = 看不见进度,不属行为丢失。',
102
+ },
81
103
  thinking_segment_end: {
82
104
  // CC-02(0.68.3):与 text_segment_end 同一类目的后果 —— server 7.77.0 起不接 = 未脱敏的推理字节留在
83
105
  // 本地转录里(b2 形上那条明文思考行已经渲过 = 已发生的行为)⇒ `required: true`,理由同上臂。
@@ -133,7 +155,7 @@ const CHROME_ARM_TABLE = {
133
155
  '🔴 **单位 = JS 字符串长度(UTF-16 代码单元),不是 UTF-8 字节、也不是几条消息**' +
134
156
  '(按 UTF-8 字节截会在非 ASCII 正文上截错位置把凭据留在屏上;按消息丢会连合并在同一条消息里的上一段定稿一起删掉),' +
135
157
  '照抄 `已committed正文.slice(0, 长度 - committedPrefixLen) + content`;按**增量拼文**计长、不是 content 上的偏移;④三键全缺席 ⇒ 照旧只当对账/定界,拿 content 再渲一行 = 同一段上屏两遍;⑤`segmentId`(恒在场)= 段身份,与本段已过境的 committed 文本行顶层 `_sema_segment_id` 同值 —— 换掉已提交那截时认行的钥匙是**两把**:行 uuid ∧ 段身份(按字节相等找行会被同后缀的独立行冒充)。' +
136
- '🔴 缺席只表示「没报」,绝不等于「段没结束」——要退回自家启发式必须按整条流判、不按单帧判',
158
+ '🔴 缺席只表示「没报」,绝不等于「段没结束」——要退回自家启发式必须按整条流判、不按单帧判;⑥`committedUuids[]`(0.71.0 起,在场 = 本窗口已提交行按序、`[]` = 零行、整键缺席 = 本包说不出):`committedPrefixDiverged` ⇒ **按 `committedRows[]` 逐行写回正文**(包已算好:首行 = 前段定稿前缀 + content、其余行空;别整行换首行,提交行可能带上一段定稿),端不再自己数窗口里过境的行,也不再对多行段整发放弃;`committedUuid` = 末条兼容位。',
137
159
  },
138
160
  };
139
161
  /**
@@ -211,4 +211,25 @@ export interface ToolRosterDeltaSummary {
211
211
  * 🔴 帧本身判不出形 => **什么都没发生**:原样交回入参那一份(不清空 —— 清空会把一次读不懂的帧
212
212
  * 变成一次「所有工具都没挂」)。
213
213
  */
214
+ /** CC-08(0.71.0):一帧 `tool_roster_delta` 的**视图**(不应用、不比对 digest —— 那是持名册的宿主的事)。 */
215
+ export interface ToolRosterDeltaView {
216
+ /** 这一帧假定消费端手上那份名册的 digest。对不上 = 记 skew 后整只替换,不是拒绝。 */
217
+ fromDigest: string;
218
+ /** 变更**之后**的整只名册(位置信息一律从它读)。 */
219
+ roster: ToolRosterView;
220
+ /** 三个两两不相交的名字列表;读不出(半张名单)⇒ 整键缺席,名册照交。 */
221
+ summary?: ToolRosterSummaryView;
222
+ }
223
+ /** CC-08(0.71.0):`tool_roster_delta.summary` —— 三个两两不相交的名字列表(`changed` 里那个名字的新行在 `roster` 里)。 */
224
+ export interface ToolRosterSummaryView {
225
+ added: readonly string[];
226
+ removed: readonly string[];
227
+ changed: readonly string[];
228
+ }
229
+ /**
230
+ * CC-08(0.71.0):`tool_roster_delta.delta` → 视图;`fromDigest` 非空串 ∧ `roster` 读得出才成形,否则 `undefined`
231
+ * (server 判不出 core 的形就整帧不发,所以本层读不出 = 坏帧,由投影口报 malformed)。
232
+ * 宿主拿它喂 {@link applyToolRosterDelta}(那只吃 raw,这里给的是已窄读的同形;两只读法同源 `readRoster`)。
233
+ */
234
+ export declare function projectToolRosterDelta(delta: unknown): ToolRosterDeltaView | undefined;
214
235
  export declare function applyToolRosterDelta(held: ToolRosterView | undefined, delta: unknown): ToolRosterDeltaApplication;
@@ -224,15 +224,20 @@ function readSummary(raw) {
224
224
  return { added, removed, changed };
225
225
  }
226
226
  /**
227
- * `tool_roster_delta` → 新的持有状态。**纯函数**(不改入参)。
228
- *
229
- * 🔴 `fromDigest` 对不上**不是拒绝**(契约里唯一的硬话):携带的整只名册无论如何都是新状态,
230
- * 只有 `summary` 变得不可用。拒绝换会让消费端永远抱着一份过期名册 —— 比一次 skew 坏得多。
231
- * 🔴 手上**没有**名册(首帧就是 delta)同样算 `skew`:没有可比的 digest 就是比不出来,而
232
- * 「比不出来」不许被读成「对上了」。
233
- * 🔴 帧本身判不出形 => **什么都没发生**:原样交回入参那一份(不清空 —— 清空会把一次读不懂的帧
234
- * 变成一次「所有工具都没挂」)。
227
+ * CC-08(0.71.0):`tool_roster_delta.delta` → 视图;`fromDigest` 非空串 ∧ `roster` 读得出才成形,否则 `undefined`
228
+ * (server 判不出 core 的形就整帧不发,所以本层读不出 = 坏帧,由投影口报 malformed)。
229
+ * 宿主拿它喂 {@link applyToolRosterDelta}(那只吃 raw,这里给的是已窄读的同形;两只读法同源 `readRoster`)。
235
230
  */
231
+ export function projectToolRosterDelta(delta) {
232
+ const d = obj(delta);
233
+ if (d === undefined)
234
+ return undefined;
235
+ const roster = readRoster(d.roster);
236
+ if (roster === undefined || typeof d.fromDigest !== 'string' || d.fromDigest.length === 0)
237
+ return undefined;
238
+ const summary = readSummary(d.summary);
239
+ return { fromDigest: d.fromDigest, roster, ...(summary !== undefined ? { summary } : {}) };
240
+ }
236
241
  export function applyToolRosterDelta(held, delta) {
237
242
  const d = obj(delta);
238
243
  const next = d !== undefined ? readRoster(d.roster) : undefined;