dsh-ptc-plus 0.5.3 → 0.5.4

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.
Files changed (40) hide show
  1. package/README.en.md +1 -1
  2. package/README.md +1 -1
  3. package/client.cjs +3326 -2881
  4. package/compiler-core.cjs +127 -127
  5. package/compiler-core.cjs.map +2 -2
  6. package/docs/README.md +1 -0
  7. package/docs/capability-planning-components.md +6 -6
  8. package/docs/capability-projection.md +1 -1
  9. package/docs/capability-surface.md +133 -0
  10. package/docs/client-ui.md +17 -15
  11. package/docs/client-visual-contract.md +7 -7
  12. package/docs/installation.md +9 -2
  13. package/docs/panel-concept.md +171 -0
  14. package/docs/releases/v0.5.4.md +75 -0
  15. package/docs/runtime-reference.md +14 -0
  16. package/docs/verification.md +1 -1
  17. package/index.js +117 -30
  18. package/internal/binding-adoption.js +103 -11
  19. package/internal/capability-command-receipt.js +99 -15
  20. package/internal/capability-environment.js +1108 -348
  21. package/internal/capability-plan.js +18 -2
  22. package/internal/capability-policy.js +13 -0
  23. package/internal/capability-selection.js +330 -0
  24. package/internal/direct-surface-owner.js +17 -4
  25. package/internal/message-sources.js +21 -0
  26. package/internal/program-bindings.js +13 -4
  27. package/internal/runtime-bridge-owner.js +5 -1
  28. package/internal/runtime-config.js +10 -0
  29. package/internal/session-log-view.js +47 -5
  30. package/internal/settings-compat.js +6 -0
  31. package/internal/user-bindings-owner.js +20 -3
  32. package/package.json +45 -31
  33. package/src/client-authoring-view.js +7 -8
  34. package/src/client-capabilities-view.js +871 -948
  35. package/src/client-capability-sort.js +22 -9
  36. package/src/client-copy.js +116 -294
  37. package/src/client-settings-view.js +90 -33
  38. package/src/client-styles.js +67 -19
  39. package/src/client.js +46 -21
  40. package/internal/capability-delivery-proof.js +0 -70
@@ -0,0 +1,171 @@
1
+ # 能力面板:施工契约(已被取代)
2
+
3
+ ## 0. 权威
4
+
5
+ **本文的页面形状已被 [能力面:概念契约](capability-surface.md) 取代。** 现行列集、取值、回执、入口与唯一渲染器由那份契约决定;本文继续生效的部分是 §1 的红线、§5 的写通道(行上的决定走的仍是同一条命令)与 §9 的落地位置,以及读路径的一致性要求。下文出现的七列、证据列、设置页清单投影都随该契约失效,保留在这里作为历史来源,不再是缺陷判据。
6
+
7
+
8
+ 非目标:不跨会话检索能力;不改冻结语义与 `/capabilities refresh` 的同步含义;不展示 prompt 原文、恢复快照或 worker 内部;不改 REPL 控制台与工作台的分工;**本面板不陈述物化**——方案由运行时在执行前构建,面板只陈述供给、记录与证据;不新增写入器、RPC 写操作或第二写通道。
9
+
10
+ ## 1. 红线:取值不得由面板之外的开关决定
11
+
12
+ 一个字段、一句文案、一个控件状态,若它的取值能被面板之外的任何来源改变——某个进程/服务/运行时/命令没有在运行,或任何全局开关没有启用——它就是缺陷。
13
+
14
+ 判定只有一句:**把那个来源摘掉,这个值会变吗?会变,它就不许存在。**
15
+
16
+ 这条覆盖两类来源,不是一类。「没有在跑」和「没有启用」是同一个错误的两种写法:都让面板去陈述一件它没有资格陈述的事。屏幕上的「未观察/尚未清点/暂不可用/未启用」之所以禁止,是因为它们都是同一个翻译动作的产物:把面板之外的缺失,翻译成面板之内的状态。
17
+
18
+ 推论:面板不需要知道有没有 PTC 运行时,也不需要知道某个开关是开是关;它一旦知道,就一定会在某处把这个知识画到屏幕上。行不存在,是唯一合法的「不能做这件事」的写法。
19
+
20
+ **这条分界线不落在运行时的有无上,落在「声明面」与「运行时」上。** 两者只差一句:
21
+
22
+ - **会话的声明面属于会话,必须读。** 这个会话作用域里的工具注册表视图、该作用域解析出的 Skill 层、prompt 装配在该作用域贡献的段,都是宿主对这个会话的供给。会话还有 agent 时,这三样都在它的 scope 上;`agent` 在这里只是取 scope 的句柄,不是「会话可用」的判断,也不是运行时。把它当成运行时来回避,就是把会话自己的声明面误记成 §1 禁止的来源。
23
+ - **PTC 运行时不属于声明面,一律不读。** worker、`canRefresh`、`run_code` 是否存在、方案投递(`prepare`/`refresh` 的重建与投递)、`session.append` 回答的是「这个会话现在能不能执行」,面板从不问这个问题。它们一旦被读,§1 的翻译动作就会以另一种拼写回来。
24
+ - **会话的 agent 已不在时,作用域随之消失**,面板读这个会话自己记录的冻结方案(§7)。冻结方案是会话的事实,不是运行时状态;拿宿主平面顶替它就是把这个缺陷换了方向——把「这个会话的声明面读不到」画成一张更小的表。记录里本来就没有面时(没有方案、方案损坏、或组合时就没有要求库存)这条读失败并点名缺的那条记录,而不是退回宿主平面、也不是画一张空表。
25
+
26
+ 分界只在读哪一处声明:有没有 agent 决定读 scope 还是读那份记录,不决定任何一格怎么写。
27
+
28
+ ## 2. 概念
29
+
30
+ | 概念 | 回答的问题 | 边界(属于 / 不属于) | 属于它的后果 | 不得混淆的邻居 |
31
+ | --- | --- | --- | --- | --- |
32
+ | **供给** | 宿主对这个会话声明存在什么 | 属于:该会话作用域里的工具注册表视图(含 preset 在 agent 作用域注册的工具)、该作用域解析出的 Skill 层、prompt 装配在该作用域贡献的 MCP/资源服务声明;这个会话的 agent 已不在时,属于它自己记录的冻结面。不属于:这个会话选了它没有、它有没有被证明用过 | 一条供给至少一行;供给没有,行就没有 | 记录:供给说「存在」,记录说「选了什么」 |
33
+ | **记录** | 这个会话写下过哪些决定 | 属于:绑定取用事件、能力选择事件。不属于:运行时构建出的方案、worker 装了什么 | 没有记录 ⇒ 该格取跟随保存的值;只有最后一条已投递方案带 `sessionOverrides` 的旧会话按那条方案折算出的同一条记录作答;绑定取用记录损坏 ⇒ 该行报记录损坏 | 供给:记录不能说某条目存在 |
34
+ | **证据** | 这个会话里有没有一条被接受的执行证明它被拿到过 | 属于:`tool/result` 的 `meta.dshPtcPlusCapabilities.used` 含该 id。不属于:方案、安装、模型是否知道 | 有 ⇒ 一个标记;没有 ⇒ 空格子,不是否定的词 | 记录:记录说「选了」,证据说「拿到过」 |
35
+ | **行** | 哪些对象各自构成一个可决定的单位 | 属于:宿主声明的能力、声明的 MCP 服务、全局绑定文档里 `enabled` 的条目。不属于:没有声明的、没有被供给的、统计聚合出来的 | 一行至少一个控件;对象不能被单独决定,它就不是行 | 组与批量:批量是「对已存在的行同时做同一个决定」,不产生行 |
36
+ | **控件** | 把一条新记录写进这个会话 | 属于:本会话的决定。不属于:全局配置(在设置页)、运行时动作 | 一行必有,永远可点,没有失败分支;写不进去说明该行不该存在 | 设置页的全局开关:它不决定这一行 |
37
+
38
+ 被删除的概念:清单可用性、就绪、处境、投递、`scope`、`plan`、`management`、`live`、`unreadableSupply`。它们不回答任何别的决定,只把面板之外的缺失翻译成界面。**没有这个概念,就没有它的词。**
39
+
40
+ ## 3. 行
41
+
42
+ - **能力行**:每个宿主声明的能力一条,恒在。声明没有的条目不会因为配置、记录或运行时有无而出现或消失。
43
+ - **服务行**:每个被声明的 MCP 服务一条,`mcpServers.<server>` 是它自己的决定。它是行,不是一批行:整台服务关掉是这个对象自己的决定,不是「筛选后批量」的替代品。
44
+ - **绑定行**:全局绑定文档里 `enabled` 的条目一条。未被供给的条目不进本面板——把它画成一行只会得到一个按不动的按钮。
45
+ - 行集只由供给决定;配置、记录、证据都不增减行。
46
+
47
+ 现行列集是 `对象 | 类型 | 来源 | 保存·启用 | 保存·披露 | 本会话·启用 | 本会话·披露` 加 `回执`(会话入口),由 [能力面:概念契约](capability-surface.md) §4 决定。下面这段是历史形状,不是缺陷判据:
48
+
49
+ - **保存**:当前配置里的值。能力行曾两维并写(`自动 · 自动`)——「自动」不是取值,这正是契约把它换掉的原因。
50
+ - **可用性/披露**:本会话记录的值,控件就在各自那一格,一格一个控件。
51
+ - **证据**:有执行报告时出现标记。这一列已随裁决四移出决定页,换成点名记录的回执列。
52
+
53
+ ## 4. 词表
54
+
55
+ 下面这张表是历史词表,不是缺陷判据:现行列名、取值与回执由 [能力面:概念契约](capability-surface.md) §4 与 §6 决定,本文的 `可用性`/`证据` 列名与 `开启`/`宿主`/`完整`/`按需`/`自适应`/`已证明` 已随该契约退场。
56
+
57
+ 没有否定词。每个词陈述一件存在的事实。
58
+
59
+ | 列 | 取值 |
60
+ | --- | --- |
61
+ | 来源 | `DSH 工具`|`MCP`|声明的 MCP 服务器名(全部列出,以 `、` 连接)|`多个服务器`|Skill 来源桶|`技能`|`全局绑定` |
62
+ | 保存·可用性 | `开启`|`关闭`|`自动` |
63
+ | 可用性 | `开启`|`关闭`|`自动`|`继承` |
64
+ | 保存·披露 | `宿主`|`完整`|`按需`|`自适应`|`自动` |
65
+ | 披露 | `宿主`|`完整`|`按需`|`自适应`|`自动`|`继承` |
66
+ | 可用性·取用 | `使用`|`不使用` |
67
+ | 证据 | `已证明` |
68
+
69
+ `自动` 是「这份记录没有为这个对象写下这一维」,`继承` 是「跟随保存列」,两者不互答。没有记录时可用性列与披露列写 `继承`,不写空、不写「未记录」。日志没有任何选择记录、而最后一条已投递方案带着 `sessionOverrides` 时,这两列陈述的是那条方案折算出的同一条记录(`decided`,标记 `legacy`);那条方案没有带任何选择时仍是 `继承`。方案的选择表读不出来时这条读是 `unreadable`:能力行没有表达它的词,这两列照常写 `继承`,`记录损坏` 只属于绑定组那条记录(见 §9 的未交付项)。
70
+
71
+ 唯一的例外,且必须同时满足三条:**只由 `classifyBindingAdoption` 返回 `unreadable` 触发**(记录存在但无法解析;记录不存在返回 `none`,不产生这个词);**陈述的是那条记录,不是面板的状态**;**同一行的控件仍然可点,写一条新记录即取代它**——`记录损坏`。它存在的理由只有一个:另一种做法是替会话报告一个它从未做过的决定。这个词只在面板的行上出现:模型可见的绑定目录没有位置表达「记录不可读」,它对同一条记录发布整份供给,所以 `记录损坏` 不能被读成模型目录的陈述(见 §9 的未交付项)。
72
+
73
+ 一个工具名同时被两个 server 命名空间命名时(`mcp__docs__deep__search` 同时以 `docs`、`docs__deep` 开头),来源列列出这两个 server;历史载荷只声明「有多个 server」而没有名单时写 `多个服务器`。两种写法都陈述存在的事实,来源列不写「未确认/未知/unknown」。
74
+
75
+ ## 5. 写通道
76
+
77
+ 一个意图:把一条新记录写进这个会话。写者只有一个,回执只有一种。
78
+
79
+ - 绑定 → 取用事件。能力/服务 → 选择事件(携带 id 与选择)。两者同形,都是这个会话的日志事件:`user/message` 携带 PTC 的 `source.form`,不新增事件类型(未知类型会让整份日志拒读)。
80
+ - 写者是**这个会话当前的写所有权**,不是「有没有 PTC 运行时」:
81
+ - 会话在 DSH 里活着(`sessions.get(id)` 有它,即使没有 PTC 运行时):用它的 `session.append`。
82
+ - 会话是冷的(不在 `sessions.get`):用持久化写句柄 `open(id, 'write')` → `append` → `flush` → **close**,并照运行时交还路径的做法先 `snapshotSessionEvent` 再 `foldSurface` 验证,再 append。
83
+ - 两个分支写的是同一条记录,回执由写者统一给出。**新会话不进入 PTC 模式时,它是活着的会话,走 append**——这正是今天会抛「select an active PTC session」的那一格。
84
+ - 命令事务(`/binding`、`/capabilities set`)与面板控件走同一个 owner、写同一条记录、给同一种回执。人类输入与面板不是两条路。
85
+ - 记录的物化在运行时:`preStep` 发现记录摘要与现有方案摘要不同时重建方案;`reconcilePendingChoice`、`pendingCommand`/`pendingPrevious` 失去依据,删除。
86
+ - **控件没有失败分支。** 没有禁用、没有预检、没有「先启用某开关」。凡是「这一行现在不能改」的情形,都说明该行不该存在。
87
+
88
+ 一个必然的推论:会话记录了选择,这个会话就是被管理的。全局开关决定这份记录**是否被应用**,不改变页面的任何一格:关掉时记录照旧保留、两列照常陈述,新的决定要到重新启用后才改变运行结果(`src/client-capabilities-view.js` 的格子不读这个开关;它决定的是方案的 `enabled`,以及只由管理开关决定的那种配置是否被发布)。要它成为 §1 禁止的那个来源,除非它开始改变某一格的值——那正是这条推论禁止的。
89
+
90
+ ## 6. 面板形状(出稿依据)
91
+
92
+ 一个 Modal(`管理会话能力`),五区,顺序固定:
93
+
94
+ 1. **头**:标题 + 当前会话标识 + 关闭。
95
+ 2. **工具栏**:搜索、类型筛选、来源筛选、批量(对已勾选的行做同一个决定)。
96
+ 3. **行区**:表格,§3 的列。
97
+ 4. **绑定组**:表格下方,同一套行与列语义;组永远在,行可以为零,零行就是空表,不配一句解释。
98
+ 5. **底部**:最近一次写入的回执。能力写入按当前界面语言陈述这次写了哪条记录、什么选择、写进哪个会话或全局设置;绑定组的写入回执是 `/binding` 命令返回的原文(英文),面板不改写它。回执不陈述证明程度,原始载荷留在命令自己的结果里。
99
+
100
+ 行不可展开:面板不陈述第六件事。七列已经分别陈述供给、记录与证据,行里没有可再展开的陈述。
101
+
102
+ 禁止出现在截图上:侧栏、产品名、版本号、服务状态、整块 `disabled` 的区域、空表头配一句解释的整块区域、任何否定词。
103
+
104
+ **图与词的分工**:稿决定版式、层级、控件形态与密度;**版式以本节文字为准,稿是它的实现目标**;文字一律按 §4 的词表写,图片模型生成的文字不作为契约。两者冲突时以本节与 §4 为准。
105
+
106
+ 本版设计稿:`artifacts/panel-mock/panel-0.5.5.png`(生成物,不入库)。
107
+
108
+ ## 7. 读契约
109
+
110
+ 一次读,三个来源,全成功或全失败:
111
+
112
+ 1. **会话头与事件**:同一次日志取用(`sessionLogOf`:`readSessionLog({id}, query)`;宿主没有 projection 服务时取该会话自己的 `snapshotEvents()`)同时给出 header(含工作目录)与事件。会话头与事件来自同一次日志取用;该次取用失败则整次读失败,不返回部分行集——没有 header 就没有目录,日志读不出时目录也没有。事件只用于取记录与证据。
113
+ 2. **供给**:读这个会话自己的声明面。两条分支,都不碰 PTC 运行时:
114
+ - **会话还有 agent**(任何 DSH agent;不看 `canRefresh`、不看 `run_code`、不看 worker):`assemble({ agent, scope: agent, ptcPlusCapabilityObservation: true })`、`toolSchemasForAgent(agent)`、`observeSkillsAt({ scope: agent, cwd })`。观察标志让这次公共装配保持被动:它贡献该作用域的段,不准备、不投递,也不重贴 model-facing 传输投影。`agent` 只作作用域句柄——用它判断会话是否可用、调用 `change`/`prepare`/`refresh` 的运行时分支、或 `session.append`,都是缺陷。
115
+ - **没有 agent 的冷会话**:从会话日志里最新一条方案消息的 `source.snapshot.capabilities` 取面(id/category/name/presentation),交给同一条行合成路径。这是这个会话记录的冻结面,不是宿主平面。这条记录能陈述面的前提是它真的冻结过一份库存:一份记录的 `settings` 经 `requiresCapabilityInventory` 判定为不要求库存时(能力与上下文总开关关闭,或能力管理关闭且记录的策略未启用统计与恢复——判据与组合时同一个 owner,`effectiveCapabilitySettings` 先按总开关压掉管理、统计与恢复)`capabilities` 是空数组,它陈述的是「这次组合没有把任何能力供给模型」,不是「宿主没有声明」。没有方案、方案损坏、或这种没有库存的记录,都让这条读失败并点名缺失的那条记录:不拿宿主平面顶替,也不返回空表——宿主平面只回答「宿主声明了什么」,不回答「这个会话被供给过什么」。
116
+ 配置:`capabilityConfiguration(currentConfig)`。
117
+ 3. **绑定文档**:`bindingRows` 读的全局绑定供给(`readUserBindings`),只为绑定组那几行,且只在全局绑定开关打开时读;它读不出时整次读同样失败,不返回部分行集。
118
+
119
+ 来源在实现里各自读,便于定位错误;但**任何一步失败就是整次读失败**:不做部分成功的响应。少了几行而屏幕不说,就是谎。
120
+
121
+ 这条读只有一个入口:`describeSessionCatalog`,两个入口都读它。请求指名会话时读该会话自己的日志与供给(同一个 scope,或同一份冻结方案);不指名会话时读 `hostSupply({})`,即宿主自己的进程级声明(不含 preset 在 agent 作用域注册的条目),不是任何会话的陈述,因此不充作会话的供给,且这样一次读不列出任何绑定行(绑定采纳没有保存地址)。读路径不得调用 `change`/`prepare`/`refresh` 的运行时分支、不得写(`session.append`)、不得 reconcile 待处理提案,也不得用 `canRefresh`/`run_code`/worker 判断这个会话能不能读:读不产生写,不改变进程态。
122
+
123
+ 读要么给出完整行集,要么整次失败。不存在部分成功的响应:少了几行而屏幕不说,就是谎。失败不是状态,是缺陷——面板给出原始错误、出错的文件或来源,以及重试;空表只表示「宿主真的没有声明」。
124
+
125
+ 响应(`ptc-capability-session-catalog/v2`):
126
+
127
+ ```
128
+ { schema: 'ptc-capability-session-catalog/v2', sessionId,
129
+ rows: [ { id, kind: 'capability'|'server'|'binding',
130
+ name, category, source,
131
+ saved, // 每行都有:能力/服务行取自己的规则,绑定行取绑定文档
132
+ session, // 本会话记录的值;绑定行另带 recorded 标明这条采纳是否被写下
133
+ record, // 能力/服务行:记录自己的字,layers 标明每一维由哪一层写下
134
+ evidence } ], // 能力行:boolean
135
+ config: { bindingsEnabled } }
136
+ ```
137
+
138
+ - 没有 `live`、没有 `scope`、没有 `plan`、没有 `management`、没有 `unreadableSupply`、没有 `capabilities: null`、没有 `defect` 字段。
139
+ - 失败走 RPC 错误通道,带原始错误;错误不分类来源枚举——来源枚举正是运行时重新溜回来的入口。
140
+
141
+ ## 8. 验收
142
+
143
+ 截图必须能判定:
144
+
145
+ 1. 五区齐备且顺序正确;没有 §6 的禁止项。
146
+ 2. 行区有真实的能力行与工具行(`tools.*`,含 `tools.mcp__<server>__*`)与 Skill 行(`skills.*`),条数可数;有被声明的 MCP 服务行。
147
+ 3. 每一行都有控件,形态是下拉或按钮,不是开关。
148
+ 4. 绑定组在表格下方,每行一个取用控件;组存在,即使零行。
149
+ 5. 全屏文字不含否定词族:`未观察`、`尚未清点`、`不可用`、`暂不可用`、`未记录`、`未证明`、`未启用`、`未确认`、`未知`、`unknown`、`unconfirmed`。
150
+ 6. 没有整块 `disabled` 区域。
151
+ 7. 新会话、不进入 PTC 模式打开面板:1–6 全部成立;进入 PTC 模式再打开:行集、行数与控件数不变。
152
+ 8. 点任一控件都能得到一种回执,底栏文字,与有无运行时无关。
153
+
154
+ **证伪方式**:把运行时摘掉重开面板,行集改为这个会话记录的冻结面:它与活读的差异只允许是方案冻结之后新注册的条目(集合差 ⊆ 新注册);该记录没有冻结过面时(没有方案、方案损坏、组合时没有要求库存)读失败并说明缺的是哪条记录,既不给一张更小的表,也不给一张空表。把任一全局开关关掉重开,行集必须只按供给变化(开关关掉的是供给,不是行里的值)。若变而本文没有授权,实现即缺陷。
155
+
156
+ ## 9. 实施落点
157
+
158
+ 本节只陈述已交付的落点;未交付的部分单列在末尾,读到它们不算读到授权。
159
+
160
+ - `internal/capability-selection.js`:`ptc-capability-selection/v1` 的正文与词表、`CAPABILITY_ENTRY_ID`(条目 id 的唯一词表)、`normalizeCapabilitySelection`;`capabilitySelectionText` 是一份记录的唯一正文字面;`capabilityRuleOfSelection` 是一个整选择与它陈述的维度规则相遇的唯一位置;`classifyCapabilitySelection` 与 `capabilitySelectionOf` 是启用与披露两列和组合入口共用的三态读(`none`/`decided`/`unreadable`);`capabilitySelectionOf` 在日志没有任何选择记录时把最新一条已投递方案的 `sessionOverrides` 折算成同一形态的 `decided`(带 `legacy` 标记):整选择词与维度规则写的同一个决定读成同一条记录,`prepare` 因此原地恢复那条方案而不是重建;有记录时以记录为准;该方案没有带任何选择时仍是 `none`(这两列照常写 `继承`),方案的选择表读不出来时是 `unreadable`,方案的 `source.sessionId` 不是本会话时不作为本会话的陈述。
161
+ - `internal/capability-environment.js`:`change()` 是唯一写者,按词表判断后写一条语句,不比较方案、不查运行时、不跑预检;`recordSelection` 通过宿主的 `appendPluginRecord` 把这条语句写成 `plugin:ptc-plus/capability-selection` 记录:决定做出的那一刻就落盘,不占 surface、不需要受保护 head、也不等待任何 step,因此新会话开始前的改动在下一次组合里就生效;记录不进入模型可见 surface,`capabilitySelectionOf` 直接读它(`user/message` 形态只作为 0.5.4 及更早会话的历史载体保留,`scripts/repair-session-log.mjs` 会把被旧版本写在 head 之前的语句转成同样的记录);写入后把这条事件并进本次观察的切面,紧接着的读不会回到被取代的那条;`prepare()` 在记录与已交付组合的 choices 不一致时重建(比较的是两者陈述的决定,不是拼写),重建沿用该组合的 `sourceSettings`(普通刷新仍以冻结来源为准,只有 `refresh` 读当前保存配置);`describeSessionCatalog` 按 §7 三源读:`sessionCatalogSupply` 决定读哪处声明(`liveAgentFor` 找到 agent 时由 `hostSupply` 以该 agent 为作用域取供给,否则读 `frozenSupply` 重放的冻结面),并在记录没有冻结过面时失败(没有方案、方案损坏、组合时没有要求库存——最后一种由记录自己的 `settings` 经 `requiresCapabilityInventory` 判定,与组合时的判据同一个 owner),`inventory` 接受 `access` 提供的注册表视图与 Skill 观察结果;server 行在 `capabilityRows` 合成且没有自己的披露;`bindingRows` 用 `bindingAdoptionOf` 的三态写绑定行的启用列(取用)。
162
+ - `internal/capability-command-receipt.js`:回执只校验它自己那句话需要的形式(无空白、有界长度)。条目 id 的词表由记录 owner 决定,回执不再保留第二个更窄的词表——更窄的那一个会拒掉会话已经写下的记录,于是写成功而回执成文为失败。
163
+ - `src/client-capabilities-view.js`:八列行模型、常驻绑定组、行内控件直接走命令通道、底栏与聊天卡片共用 `capabilityReceiptNotice`;`src/client-copy.js` 拥有页面自己的标签、区域标题与唯一一份取值词表(`capabilities.word.*`,两种语言各一份),词表的形状与退场词由 [能力面:概念契约](capability-surface.md) §4/§6 决定,接线与本地化由它自己的结构用例校对;`client.cjs` 是 `src/` 的构建产物,与 `scripts/client-fallback-smoke.mjs` 的断言在同一次改动里一起走。
164
+
165
+ 未交付,不得读成已交付:
166
+
167
+ - **模型可见的绑定目录没有「记录不可读」的表达。** 不可读的最新取用记录只在面板的行上读作 `记录损坏`;同一条记录交给模型目录时按「没有否决」处理,因此整份供给仍然发布给模型。要让模型也知道这条记录不可读,需要一个新增的模型可见陈述,本文没有授权它,也就没有它的词。
168
+ - **能力选择读不可读时面板没有词。** 一条无法解析的能力选择记录(或方案里读不出的选择表)让这条读成为 `unreadable`,而 §4 的例外只授权绑定组写 `记录损坏`:能力行的启用列与披露列没有词陈述它,仍按「没有记录」写 `继承`。要让能力行也陈述这条记录,需要一个本文没有授权的词,因此这里只把它记作未交付。
169
+ - **旧形态只覆盖以 `user/message` 投递的方案。** 一条只存在于命令回执提案里的方案由环境的恢复路径读(`recordedPlan`),不由这条陈述读覆盖:它不是会话的陈述,`prepare` 只把记录读到的选择与那条方案的选择逐项比较,相等时原地恢复那条方案,否则按该组合自己的 `sourceSettings` 与记录重算,那条提案自己的会话选择因此不被恢复(`docs/capability-planning-components.md` 记录了这一边界)。
170
+ - **冷会话的 Skill 来源桶没有记录。** 冻结方案里的 Skill 能力带 id、name、description 与调用资格,不带 provider/source 桶(它们不进入冻结能力快照),因此冷会话的 Skill 行来源列只写「技能」,不写活读能写出的桶名。要让冻结面也陈述桶,需要一个新增的冻结字段,本文没有授权它。这是两个面唯一按设计不同的列,`test/capability-environment.test.js` 的「the replayed Skill row states every column the live row states except the provider bucket」把这条边界钉成断言:重放行除来源桶外每一列都必须与活读相同。
171
+ - **绑定写所有权只有活会话这一条分支。** `internal/user-bindings-owner.js` 的 `recordAdoption` 要求活的 `session.append`;§5 的冷会话持久化写分支没有实现,冷会话写不了取用记录。
@@ -0,0 +1,75 @@
1
+ [中文](#cn-v054) | [English](#en-v054)
2
+
3
+ <h3 id="cn-v054">宿主适配</h3>
4
+
5
+ * 目标仍是当前官方 DSH 发行:插件继续使用它的公共扩展面(设置卡片座位、会话投影、能力探测与公共 `Menu`),本版本不要求宿主侧改动,也不按宿主版本分支;`engines.dsh` 声明 `>=0.2.1-0 <0.2.3`
6
+ * 会话的能力决定与绑定取用改为写进会话日志本身,不再占用对话消息:重载或重启后恢复会话时,面板读到的仍是当时那次决定,模型也不会因此多收到一条消息
7
+ * 支持哪些宿主仍由通道表达,而不是由版本号判定:`latest` 与 `next` 是阻塞通道,`alpha` 线只报告结果
8
+
9
+ <h3>新增</h3>
10
+
11
+ * 会话能力面板按「宿主提供了什么、这个会话记录了哪些决定、证据落在哪一层」重排:每行分列陈述对象、类型、来源、已保存的启用与披露、本会话的启用与披露,以及这一行的回执;点击列头可按该列排序
12
+ * 会话里直接列出全局绑定条目,并在面板里控制这个会话用不用它:每行按本会话的采纳记录读作「使用」「不使用」或「记录损坏」,点这一格就写下这个会话的取用;全局已停用的条目不出现在清单里。点击提交的就是人类输入的同一条 `/binding use|skip <id>` 命令,面板不新增写通道,结果按同一份会话能力目录回读
13
+ * 面板同时出现在会话入口与设置页:设置页编辑保存的规则,会话入口只写本会话的记录,两个入口各自固定自己的地址,不互相代替
14
+ * 支持一次选择多行、批量写下同一个决定,每行仍然各写一条记录,回执列逐行说明这次决定落在哪一层
15
+
16
+ <h3>修复</h3>
17
+
18
+ * 本会话关闭过的能力不再出现在任何模型可见处:`tools:sdk` 声明块、程序里真实的 `tools` 成员、`capabilities.*` 的探索结果,以及能力方案与说明段,都与这个会话真正可调的成员保持一致,关闭的能力不会只剩声明还在
19
+ * 已保存的开关单独成列陈述,不再与本会话的记录混读:一列只回答「保存了什么」,另一列只回答「这个会话记录了什么」
20
+ * 能力状态改用整块样式区分,而不是给控件里的文字染色:下拉控件保持正常文字颜色,只改它的填充与边框,行首的标记与那一格的词照旧陈述状态;只陈述文字的那一列改用同样底色的胶囊标签
21
+ * 能力变更的回执在会话里常驻:它不再因为会话里还没有人类消息而在发出第一句话时消失,连续几次写入合并成一张卡片、逐行列出这次改了哪些条目
22
+ * REPL 页签改排在宿主自己的页签之后,不再插在原生页签之间:对话、轨迹与实验性统计仍按宿主的顺序排列,REPL 尾随它们
23
+ * 设置页的能力表格恢复滚轮滚动:鼠标停在表格上时页面照常滚动;名称与来源单行省略,完整文字保留为悬浮提示,长名字不再把整行撑高
24
+ * 本版本写入的能力决定不会再让会话在重载时读不出来;旧版本留下的这类日志不再由新版写入
25
+ * 没有活动运行时的会话不再被读成「读不出来」:面板读它日志里最后一版方案,列出的行与有运行时时一致;真的读不出来时,错误与重试就写在行旁
26
+ * 绑定组永远在:读失败在行旁报出并可重试;可读但没有任何条目时它就是一张空表,不另配一句解释
27
+
28
+ <h3>升级</h3>
29
+
30
+ * 需要 Node.js `^22.19.0 || >=24.0.0`;升级插件后请重启 DSH
31
+ * 不需要配置迁移:本版本不改配置字段;已有会话日志不需要清理或重写
32
+
33
+ ```sh
34
+ dsh plugin --profile <profile> add dsh-ptc-plus@0.5.4
35
+ ```
36
+
37
+ ---
38
+
39
+ <h3 id="en-v054">Host Compatibility</h3>
40
+
41
+ * It still targets the current official DSH release and keeps using its public extension surfaces (the settings card seat, the session projection, capability probing and the public `Menu`); no host-side change is required, and no behaviour branches on a host version. `engines.dsh` declares `>=0.2.1-0 <0.2.3`
42
+ * A session's capability decisions and binding adoption are now written into the session log itself instead of taking up conversation messages: when a session is reloaded or resumed after a restart, the panel still reads the decision that was made, and the model receives no extra message for it
43
+ * Which hosts are supported is still expressed as channels rather than decided by a version number: `latest` and `next` are the blocking channels, and the `alpha` line is reported on without blocking a release
44
+
45
+ <h3>New Features</h3>
46
+
47
+ * The session capability panel is organised around what this host supplies, which decisions this session recorded, and which layer the evidence belongs to: each row states its object, type, source, the saved availability and disclosure, this session's availability and disclosure, and that row's receipt; a column header sorts by its own column
48
+ * The panel lists the global binding entries and controls, for this session, whether it uses one: every row reads as used in this session, not used in this session, or a damaged record, and its cell writes this session's adoption when it is clicked; an entry disabled globally is not listed at all. Clicking submits the same `/binding use|skip <id>` command a human types, the panel adds no write channel, and it reads the result back from the same session capability catalog
49
+ * The panel appears at both entrances: the settings page edits saved rules, the session entrance only writes this session's record, and each entrance fixes its own address instead of standing in for the other
50
+ * Several rows can be selected and given the same decision at once, each still writing its own record, and the receipt column says per row which layer the decision reached
51
+
52
+ <h3>Fixes</h3>
53
+
54
+ * A capability this session closed no longer appears anywhere the model can see: the `tools:sdk` declaration block, the real `tools` members of a program, what `capabilities.*` can explore, and the capability plans and guidance sections all agree with the members this session can actually call, so a closed capability is never left declared on its own
55
+ * The saved switch is stated in its own column instead of being read together with this session's record: one column answers only "what is saved", the other only "what this session recorded"
56
+ * A capability state is told apart by the whole control instead of by tinting its words: a dropdown keeps the normal label colour and only its fill and border change, the row mark and the cell word still state the state, and a word-only cell states it as a chip of the same tone
57
+ * The receipt of a capability change now stays in the conversation: it no longer disappears when the first message gives the window a starting point, and several writes that arrive together are stated once, as one card listing what each one changed
58
+ * The REPL tab now trails the Host's own tabs instead of sorting between them: Conversation, Trajectory and the experimental statistics tab keep the Host's order, and REPL follows them
59
+ * The settings capability table scrolls with the wheel again: the page scrolls while the pointer is over the table, the name and source clip to one line with the full text kept as a tooltip, and a long name no longer makes its whole row taller
60
+ * A capability decision written by this version no longer makes a session unreadable when it is reloaded, and the logs older releases left behind are no longer written by this one
61
+ * A session with no live runtime is no longer read as unreadable: the panel reads the last plan its log carries and lists the same rows a live session lists, and a read that really fails states the error and its retry beside the rows
62
+ * The binding group is always present: a failed read is reported beside the rows with its retry, and a readable group with no entries is an empty table rather than a sentence explaining it
63
+
64
+ <h3>Upgrade</h3>
65
+
66
+ * Requires Node.js `^22.19.0 || >=24.0.0`; restart DSH after upgrading the plugin
67
+ * No configuration migration is needed: no configuration field changes in this version, and existing session logs need no cleanup or rewrite
68
+
69
+ ```sh
70
+ dsh plugin --profile <profile> add dsh-ptc-plus@0.5.4
71
+ ```
72
+
73
+ ---
74
+
75
+ Full Changelog: https://github.com/muyuanjin/dsh-ptc-plus/compare/v0.5.3...v0.5.4
@@ -49,6 +49,20 @@ The default SDK declares `capabilities.tree/find/inspect/search` directly. `sear
49
49
 
50
50
  The compatible `find` uses case-insensitive lexical matching, prioritizing exact `namespace.member` symbols, namespace names, and member names before complete identifier or description tokens. CamelCase and punctuation separate tokens; multiple query tokens must be contiguous. A miss means no lexical match, not proof that a capability is unavailable. `tree` and item-budgeted `inspect` keep their existing unbounded-byte interfaces. With capability management enabled, the SDK section is re-rendered from the frozen session plan: Host-declared members keep their Host schemas, complete and on-demand contracts arrive through session messages, and the always-callable `edit_run_code` transport keeps its Host signature in every render. Settings changes affect new sessions; `/capabilities refresh` synchronizes an existing session. See [public integration evidence](context-public-interface-coverage.md).
51
51
 
52
+ The declaration states the delivered capability set and nothing else. That set is the step's admission: the definitions the Host renders its own `tools:sdk` block from and builds a program's `tools` members from. A registered definition this step does not admit is therefore declared nowhere, and one session record that closes a capability removes it from the declaration, the program members and the explorable metadata at once instead of waiting for the next composition. A record is durable when it is made, so the surfaces that decide what can be called read it directly; the delivered plan catches up at the next accepted step and states the rest. The panel is a different read — it lists the rows a session can decide about, closed ones included.
53
+
54
+ A switch that is off delegates the Host surface, so the record is retained but not applied. A paused capability context and a disabled management switch both compose nothing and filter nothing: the declaration is the Host's own text, the program members are the Host's, and a closure the session recorded earlier applies again once management is on. Reading the record there would enforce a closure the same switch suspended, and enforce it only halfway — the switch also lets the Host execute that member. A session the Host still holds keeps the composition it froze, so its own snapshot decides whether this environment still applies; an agent with no composition yet answers from the configuration it would compose under.
55
+
56
+ A closure is one fact whichever layer of the record states it, so a projection never asks which document recorded the decision. A saved policy rule closes a capability, and so does the legacy overrides map of a plan composed before the policy generation that the session has not refreshed; one predicate reads both. A closure also reaches the rows whose contract names the closed member as their only model path: closing the `skill` loader keeps a Skill the user can still invoke with that remaining path stated and removes one that has no path left, and a resource server row states only the operations this session still has. A row may therefore never instruct a call the same session closed. A closure written for an MCP server states the server, not any one member of it, so it resolves through that one name to everything the server owns: the `mcp__<server>__*` tool rows, the derived `mcp.<server>` row, the `mcp:<server>` instructions that row depends on, and the `mcp-resource-servers` entry that names the server as the argument of the three resource operations. A registry view that still carries those members does not keep them on a surface, and the plan composed after the closure states the decision — its row is `closed` — rather than restating a calling contract.
57
+
58
+ Stating that path is a projection rule, not a composition rule. A composition states the path it was built under, and a decision recorded afterwards — `/capabilities` writes a record, never a plan — is already the session's newest statement, so an explorable row states the intersection of three statements: what the composition delivered, what the decision left, and what this step hands the program. The third is the same admission the declaration and the program members are made of: a composition is built once and reused while a step may deliver less, and a session that decided nothing does not recompose it, so a derived row's own contract is read against the members the cell really has. Absent that observation the composition's answer stands, because a caller that published no admission is not asking to state less than the session was composed from. A Skill row only ever loses the model path there, never gains one a composition did not state; a resource server row stays explorable only while an operation remains for its contract to name, and the guidance section and `mcp-resource-servers` directory entry it owns follow that same answer. One function states the resource sentence for both the composition and the read, so an unchanged session reads the same bytes.
59
+
60
+ The assembly observes that admission instead of inferring it. The Host resolves one admission per step and publishes it with the prompt context, and the registry read takes it as its limit, so the declaration, the plan and the program describe the step that is being assembled rather than every definition registered at that scope. The admission narrows this plugin's own contract view rather than replacing it: the definitions the Host publishes are model-facing and carry no output schema, so a capability recorded from them would promise an `unknown` result and a revision no live inventory agrees with. The same set bounds the repair of a top-level call into a program call: a model that names a member this step delivers is rewritten into `tools.<name>(...)`, and a name the registry merely carries is left as a top-level call for the Host to reject as unknown. A host that publishes no admission leaves the registry view as the only evidence, and that step's declaration is then as broad as its registry.
61
+
62
+ Guidance sections follow the same admission. A host that registers instructions for a tool states them as a `tool:<name>` section, and that text is a model-visible statement about a capability: a step that does not admit the definition delivers no member for it to instruct, so the section is withheld together with the declaration, the member and the metadata. The composition is not the admission — it is composed once and reused until the session decides something else — so a composition broader than the step (built from the registry or from an earlier step's admission) may not keep the section; a later step that delivers the definition states it again, which is why this is a statement about the step rather than a section deleted once. A projection that observed no admission keeps the registry view, as for the declaration. The two call transports are exempt: `run_code` and `edit_run_code` are the channel a program is called through, not capabilities.
63
+
64
+ The `mcp-resource-servers` directory follows the same rule, and it is the one Host section whose text *is* a list of capability names: its entries are the servers a program may pass as `server`, and the three resource operations are the only members that take one. A step that delivers none of them lists no server — the section leaves instead of naming a server no member of the program can name — and it returns on the step that delivers an operation again. The rows it is rewritten from are the resource server rows this step still states, so a server's own `mcp:<server>` instructions leave with the last row that named it and stay while a tool of that server is still delivered, because that text describes the server rather than one call's signature. A session with no composition states the directory from the admission alone, and a caller that observed no admission keeps the Host's own list.
65
+
52
66
  `tree()` returns namespace entries whose `members` are strings, not a flat symbol list. Form symbols with `tree.flatMap(entry => entry.members.map(member => `${entry.namespace}.${member}`))`. Reuse an observed tree within the current capability view and inspect only relevant symbols; do not infer completeness or effect guarantees from discovery. Exact symbols in examples are conditional on the live view. The advanced `repl.state` and `code.run` schemas remain behind inspection:
53
67
 
54
68
  ```ts
@@ -49,7 +49,7 @@ After the shared ledger, channel, semantic, build and syntax prerequisites pass,
49
49
 
50
50
  Both verification scopes produce a review-bound proof. Coverage runs only before a new version is released, as specified by [ADR 0035](adr/0035-run-coverage-only-before-a-new-release.md). CI starts from a clean lockfile install and runs `verify:ci` on every supported Node and operating-system entry: channel-manifest validation, semantic ownership, generated builds, Client tests, and focused runtime contracts covering compiler startup, helper-process lifecycle, plugin takeover, native-call transport, and durable argument diagnostics. A separate job per host channel packs the plugin and runs the host contracts against that channel's installed release. Normal CI therefore proves portable installation and runtime integration without running coverage-specific suites, collecting coverage, or running the exhaustive semantic suite on hosted runners.
51
51
 
52
- `host:channels:verify`, which is part of deterministic verification, is offline: it validates the tracked channel manifest — schema, per-channel dist-tag and blocking role, full coverage of the probed host packages, one release per pinned cohort, and a recorded integrity for each — without resolving a tag or installing a host. CI runs `test:host-contract` as one job per channel. Each job installs its channel on demand from the manifest into an ignored cache, proves the installed versions against the manifest, packs the plugin, and runs the host contracts through a repository-external consumer; the `alpha` job reports without failing the workflow. Registry resolution belongs only to `host:channels:update` and the scheduled channel-refresh workflow. That workflow publishes a changed manifest as a pull request and explicitly dispatches this CI workflow once because its `GITHUB_TOKEN`-authored events cannot start another run; ordinary gates never install a floating channel.
52
+ `host:channels:verify`, which is part of deterministic verification, is offline: it validates the tracked channel manifest — schema, per-channel dist-tag and blocking role, full coverage of the probed host packages, one release per pinned cohort, and a recorded integrity for each — without resolving a tag or installing a host. It also reports which channel the development graph declares and whether the lock materializes that record, so the checkout's own graph is read against the manifest the CI jobs install from. CI runs `test:host-contract` as one job per channel. Each job installs its channel on demand from the manifest into an ignored cache, proves the installed versions against the manifest, packs the plugin, and runs the host contracts through a repository-external consumer; the `alpha` job reports without failing the workflow. Registry resolution belongs only to `host:channels:update` and the scheduled channel-refresh workflow. That workflow publishes a changed manifest as a pull request and explicitly dispatches this CI workflow once because its `GITHUB_TOKEN`-authored events cannot start another run; ordinary gates never install a floating channel.
53
53
 
54
54
  ## Release coverage
55
55
 
package/index.js CHANGED
@@ -123,6 +123,47 @@ function runCodeDefinitionFor(tools, scope) {
123
123
  return hostTransportDefinition(tools, scope, RUN_CODE)
124
124
  }
125
125
 
126
+ /**
127
+ * One catalogue row in the shape `/capabilities list` publishes.
128
+ *
129
+ * The command states the panel's two columns, so an entry carries the same
130
+ * statement the panel's column carries: exactly the dimensions that row decides,
131
+ * each naming the answer its own source gave. A dimension this session recorded
132
+ * no choice for stays stated as `inherit`. The saved column states the words the
133
+ * configuration wrote for that object: its own entry, or `automatic` when it wrote
134
+ * none — a category, server or bottom layer that would answer at runtime is
135
+ * behaviour, so it reaches no cell (capability-surface §8.1c). A server row states
136
+ * `automatic` unless its own `mcpServers.<server>` rule writes availability. Compressing a
137
+ * row into one word would
138
+ * answer for the dimension it dropped — a record that decided disclosure but not
139
+ * availability would read as if the session had decided nothing. No column
140
+ * reports the composition: the panel states supply, record and the receipt, and
141
+ * this listing is the same read rather than a second one that could answer for
142
+ * what a runtime built.
143
+ */
144
+ function capabilityListingEntry(row) {
145
+ // A server decides availability only: its members carry disclosure, so the panel
146
+ // states one value for such a row and this listing states the same one.
147
+ const statement = column => row.kind === 'server' ? { availability: column.availability }
148
+ : { availability: column.availability, disclosure: column.disclosure }
149
+ return { id: row.id, name: row.name, category: row.category, source: row.source,
150
+ ...(typeof row.description === 'string' ? { description: row.description } : {}),
151
+ saved: statement(row.saved), temporary: statement(row.session) }
152
+ }
153
+
154
+ /**
155
+ * The composition a pending capability command made, in the shape a receipt carries.
156
+ *
157
+ * A command that composed a plan before the Host accepted a step has to carry it in its
158
+ * receipt: the plan is not in the log yet, so a restart would otherwise have no way to know
159
+ * which composition the command made. `recordedPlan` reads it back by matching the command's
160
+ * own id and raw input, which is why the proposal states both.
161
+ */
162
+ function pendingProposal(capabilityEnvironment, commandId, rawInput, agent) {
163
+ const message = capabilityEnvironment.pendingChoiceMessage(agent)
164
+ return message === undefined ? undefined : { commandId, rawInput, message }
165
+ }
166
+
126
167
  /** Register the session-bound REPL runtime. */
127
168
  function installPtCRuntime(ctx, resolvedConfig, seam, toolSchemasForAgent, toolSdkSchemasForAgent, sessionId, sessionLog, settingsForCommand, readPromptText) {
128
169
  const presentationGeneration = randomUUID()
@@ -375,7 +416,10 @@ function installPtCRuntime(ctx, resolvedConfig, seam, toolSchemasForAgent, toolS
375
416
  valueLimits: valueLimitsFromConfig(activeConfig),
376
417
  })
377
418
  capabilityEnvironment = createCapabilityEnvironment({ ctx, config: activeConfig,
378
- toolSchemasForAgent: toolSdkSchemasForAgent, modelSchemasForAgent: toolSchemasForAgent,
419
+ toolSchemasForAgent: toolSdkSchemasForAgent,
420
+ // The panel's binding rows report the same catalog `/binding` acts on, so the
421
+ // read borrows the bindings owner's `list()` instead of opening a second store.
422
+ readUserBindings: () => userBindings.list(),
379
423
  store: new CapabilityStatisticsStore({ ctx }) })
380
424
  runtimeBridge = createRuntimeBridgeOwner({
381
425
  ctx,
@@ -430,6 +474,10 @@ function installPtCRuntime(ctx, resolvedConfig, seam, toolSchemasForAgent, toolS
430
474
  let unreadableTransportReported = false
431
475
  disposers.push(ctx.on('system-prompt/assemble', async (assembly, context, next) => {
432
476
  const projected = await directSurface.assemble(assembly, context, next)
477
+ // A capability observation states what the Host declares. It prepares no session,
478
+ // so it neither re-applies the model-facing slots nor reports a transport the
479
+ // operator can do nothing about from a read.
480
+ if (context?.ptcPlusCapabilityObservation === true) return projected
433
481
  if (runCodeDefinitionFor(ctx.tools, context?.agent) !== undefined) runtimeBridge.prepareAgent(context?.agent)
434
482
  else if (!unreadableTransportReported && hostTransportUnreadable(ctx.tools, context?.agent)) {
435
483
  // The host declares the reserved transport and this plugin cannot read it.
@@ -462,7 +510,7 @@ function installPtCRuntime(ctx, resolvedConfig, seam, toolSchemasForAgent, toolS
462
510
  const capabilitiesRpc = ctx.inject(['ptcPlusRpc'], scope => {
463
511
  scope.effect(() => scope.ptcPlusRpc.register(RPC_CONTRACTS.capabilities, async (operation, payload, signal) => {
464
512
  signal.throwIfAborted()
465
- if (operation === 'catalog') return { ok: true, value: await capabilityEnvironment.describeCatalog(signal) }
513
+ if (operation === 'catalog') return { ok: true, value: await capabilityEnvironment.describeCatalog(payload, signal) }
466
514
  if (operation === 'targets') return { ok: true, value: capabilityEnvironment.describeTargets() }
467
515
  if (operation === 'session-catalog') return { ok: true, value: await capabilityEnvironment.describeSessionCatalog(payload, signal) }
468
516
  if (operation === 'allocation') return { ok: true, value: await capabilityEnvironment.observeAllocation(payload, signal) }
@@ -478,41 +526,58 @@ function installPtCRuntime(ctx, resolvedConfig, seam, toolSchemasForAgent, toolS
478
526
  input: { hint: 'refresh | list [all|skills|tools|mcp|mcp_tools] [query] | set session|global <id> [availability|disclosure] <choice>' },
479
527
  handler: async ({ commandId, agent, rawInput, signal }) => {
480
528
  const input = rawInput.trim()
481
- const receipt = (state, error, proof) => capabilityCommandReceipt(state, String(agent.session.id), error,
482
- state === 'pending' && capabilityEnvironment.pendingChoiceMessage(agent)
483
- ? { commandId, rawInput, message: capabilityEnvironment.pendingChoiceMessage(agent) } : undefined,
484
- proof)
529
+ // A composition the Host has not committed yet is not in the log, so a pending
530
+ // receipt carries it as the proposal it is: a restart reads that proposal back as
531
+ // the plan the command made. A record needs no proposal — it is in the log already.
532
+ const receipt = (state, error, record) => capabilityCommandReceipt(state, String(agent.session.id), error,
533
+ state === 'pending' ? pendingProposal(capabilityEnvironment, commandId, rawInput, agent) : undefined, record)
485
534
  const selection = /^set (session|global) (\S+) (?:(availability|disclosure) )?(full|on-demand|closed|automatic|inherit|enabled|host|adaptive)$/.exec(input)
486
535
  if (selection) {
487
- if (!capabilityEnvironment.canRefresh(agent)) return { kind: 'error', text: receipt('unavailable') }
536
+ // The write is stated before it happens: the receipt names the record
537
+ // this command asked for, and whether that record exists is the answer
538
+ // the command itself returns.
539
+ const record = { scope: selection[1], id: decodeURIComponent(selection[2]),
540
+ ...(selection[3] === undefined ? {} : { dimension: selection[3] }), selection: selection[4] }
488
541
  const changed = await capabilityEnvironment.change(agent, {
489
- scope: selection[1], id: decodeURIComponent(selection[2]), dimension: selection[3], selection: selection[4], commandId,
542
+ scope: record.scope, id: record.id, dimension: selection[3], selection: record.selection,
490
543
  }, signal, settingsForCommand?.())
491
- // A save states what it changed and what it did not prove: the
492
- // model-visible catalog and a cell that reached the capability are
493
- // separate facts, reported by the layer that owns each one.
494
- const proof = capabilityEnvironment.describeCapabilityProof(agent, decodeURIComponent(selection[2]))
544
+ // A receipt states what this command wrote — which entry, which choice
545
+ // and which session or global configuration now records it. What a cell
546
+ // later obtained is that row's own `evidence` field, not a sentence here.
547
+ // A session choice is a record of this session's log, so it is durable the
548
+ // moment the command returns and needs no waiting state.
549
+ const state = changed.sessionApplied ? 'recorded' : 'saved-only'
495
550
  return { kind: changed.sessionApplied ? 'success' : 'error',
496
- text: receipt(changed.sessionApplied ? changed.publication === 'pending' ? 'pending' : 'recorded' : 'saved-only', changed.error, proof) }
551
+ text: receipt(state, changed.error, state === 'recorded' ? record : undefined) }
497
552
  }
498
553
  const listing = /^list(?: (all|skills|tools|mcp|mcp_tools))?(?: (.*))?$/.exec(input)
499
554
  if (listing) {
500
- const entries = await capabilityEnvironment.catalogFor(agent, signal)
555
+ // The listing is the panel's own read: one session catalogue, projected
556
+ // into the columns this command states. The session's scope supplies the
557
+ // declared face while the Host holds its agent, and the plan its log froze
558
+ // supplies it afterwards; the saved configuration states the saved column.
559
+ // Nothing about the PTC runtime enters it, so the two surfaces cannot
560
+ // disagree about which ids exist.
561
+ const catalog = await capabilityEnvironment.describeSessionCatalog({
562
+ sessionId: String(agent.session.id ?? agent.id),
563
+ }, signal)
501
564
  const query = (listing[2] ?? '').toLocaleLowerCase()
502
565
  const kind = listing[1] ?? 'all'
503
- return { kind: 'success', text: JSON.stringify({ schema: 'ptc-capability-catalog/v1', entries: entries.filter(entry => (
504
- kind === 'all' || kind === 'skills' && entry.category === 'skill'
505
- || kind === 'tools' && entry.category === 'tool' && entry.source?.kind !== 'mcp'
506
- || kind === 'mcp' && ['server', 'resource'].includes(entry.category)
507
- || kind === 'mcp_tools' && entry.category === 'tool' && entry.source?.kind === 'mcp'
508
- ) && (!query || `${entry.id} ${entry.description}`.toLocaleLowerCase().includes(query))) }) }
566
+ const entries = catalog.rows
567
+ .filter(row => row.kind !== 'binding')
568
+ .filter(row => kind === 'all' || kind === 'skills' && row.category === 'skill'
569
+ || kind === 'tools' && row.category === 'tool' && row.source?.kind !== 'mcp'
570
+ || kind === 'mcp' && ['server', 'resource'].includes(row.category)
571
+ || kind === 'mcp_tools' && row.category === 'tool' && row.source?.kind === 'mcp')
572
+ .filter(row => !query || `${row.id} ${row.name ?? ''} ${row.description ?? ''}`.toLocaleLowerCase().includes(query))
573
+ .map(capabilityListingEntry)
574
+ return { kind: 'success', text: JSON.stringify({ schema: 'ptc-capability-catalog/v1', entries }) }
509
575
  }
510
576
  if (input !== '' && input !== 'refresh') return { kind: 'error', text: receipt('usage') }
511
577
  if (input === 'refresh') {
512
- if (!capabilityEnvironment.canRefresh(agent)) {
513
- return { kind: 'error', text: receipt('unavailable') }
514
- }
515
- await capabilityEnvironment.refresh(agent, undefined, signal, commandId)
578
+ // The command acts on the session it was invoked in; the agent is the
579
+ // argument that states which composition to replace.
580
+ await capabilityEnvironment.refresh(agent, undefined, signal)
516
581
  // A refresh always republishes the saved configuration; when that
517
582
  // configuration has capability management or context off, report
518
583
  // the off state instead of claiming an updated environment.
@@ -684,11 +749,16 @@ export const apply = (ctx, config = {}) => {
684
749
  function applyWithExecutionSeam(ctx, resolvedConfig, seam, volatileHost, volatileConfig) {
685
750
  let settingsWriter
686
751
  const sessionLog = createSessionLogOwner(ctx)
687
- const toolSchemasForAgent = agent => typeof ctx.tools.schemas === 'function'
688
- ? ctx.tools.schemas(agent)
752
+ // The second argument is the step's admission. Naming it is how a caller states the set
753
+ // the Host actually delivers for that step; omitting it reads every definition
754
+ // registered at this scope, which is the only view a call outside a model step has.
755
+ const toolSchemasForAgent = (agent, toolSet = undefined) => typeof ctx.tools.schemas === 'function'
756
+ ? ctx.tools.schemas(agent, toolSet)
689
757
  : []
690
- // SDK contracts include output schemas; model-facing schemas remain unchanged.
691
- const toolSdkSchemasForAgent = agent => toolSdkSchemas(ctx.tools, agent)
758
+ // SDK contracts include output schemas; model-facing schemas remain unchanged. The
759
+ // admission is read through the same registry, so a contract describes the definition
760
+ // this step delivers rather than one the registry would resolve.
761
+ const toolSdkSchemasForAgent = (agent, toolSet = undefined) => toolSdkSchemas(ctx.tools, agent, toolSet)
692
762
  const sessionId = agent => {
693
763
  const id = agent?.session?.id ?? agent?.id
694
764
  return id === undefined ? undefined : String(id)
@@ -985,11 +1055,28 @@ function applyWithExecutionSeam(ctx, resolvedConfig, seam, volatileHost, volatil
985
1055
  try {
986
1056
  if (settingsWriter?.update === undefined) return
987
1057
  const previousConfig = controller.committedConfig()
1058
+ // A policy this process synthesized is not a record the settings document holds: write it
1059
+ // back and the next resolution reports it as saved, so the rollback removes the field with
1060
+ // the service's own `unset` op when it offers one. The two switches derived beside it are
1061
+ // restated to the committed values; a service without `unset` keeps the previous, narrower
1062
+ // behaviour and the residual is stated in capability-surface §8.7.
1063
+ const keepDerived = previousConfig?.capabilityPolicySaved === true
1064
+ // Only a committed configuration that reports it saved no policy lets the rollback remove
1065
+ // one: with no committed record this process does not know what the document holds, and
1066
+ // deleting a policy it never wrote would erase rules the user saved.
1067
+ const removable = previousConfig !== undefined && previousConfig.capabilityPolicySaved === false
1068
+ && typeof settingsWriter.mutate === 'function'
988
1069
  const patch = previousConfig === undefined
989
1070
  ? { enabled: false }
990
- : Object.fromEntries(CONFIG_FIELDS.map(field => [field.key, previousConfig[field.key]]))
1071
+ : Object.fromEntries(CONFIG_FIELDS
1072
+ // Only the policy is synthesized with the two switches; a writer without `mutate` cannot
1073
+ // remove it, so it omits that one field and still restates everything else it derived.
1074
+ .filter(field => keepDerived || field.key !== 'capabilityPolicy')
1075
+ .map(field => [field.key, previousConfig[field.key]]))
991
1076
  try {
992
- await settingsWriter.update(patch)
1077
+ if (removable && Object.keys(patch).length > 0) await settingsWriter.update(patch)
1078
+ if (removable) await settingsWriter.mutate([{ op: 'unset', path: ['capabilityPolicy'] }])
1079
+ else await settingsWriter.update(patch)
993
1080
  } catch (rollbackError) {
994
1081
  reportActivationFailure(new Error(
995
1082
  `ptc-plus: failed to roll back runtime configuration: ${rollbackError.message}`,