dsh-ptc-plus 0.5.4 → 0.5.7

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 (34) hide show
  1. package/README.en.md +8 -4
  2. package/README.md +8 -4
  3. package/client.cjs +13 -23
  4. package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +2 -2
  5. package/docs/adr/0017-track-the-latest-dsh-public-surface.md +4 -4
  6. package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +18 -12
  7. package/docs/adr/0044-coexist-with-the-official-persistent-repl.md +2 -0
  8. package/docs/adr/0051-persist-explicit-host-environment-resets.md +29 -0
  9. package/docs/adr/README.md +1 -0
  10. package/docs/architecture.md +3 -3
  11. package/docs/durability-design.md +10 -0
  12. package/docs/installation.md +9 -9
  13. package/docs/publishing.md +1 -1
  14. package/docs/release-notes.md +1 -1
  15. package/docs/releases/v0.5.4.md +10 -6
  16. package/docs/releases/v0.5.5.md +41 -0
  17. package/docs/releases/v0.5.6.md +45 -0
  18. package/docs/releases/v0.5.7.md +51 -0
  19. package/docs/runtime-reference.md +4 -4
  20. package/internal/config-spec.js +2 -2
  21. package/internal/cordis-tools-owner.js +32 -28
  22. package/internal/edit-transport-owner.js +3 -2
  23. package/internal/execution-seam-compat.js +13 -15
  24. package/internal/host-tool-sdk.js +17 -29
  25. package/internal/runtime-bridge-owner.js +11 -9
  26. package/internal/runtime-messages.js +8 -0
  27. package/internal/session-journal-recovery.js +68 -25
  28. package/internal/session-journal-schema.js +10 -0
  29. package/internal/session-journal.js +25 -13
  30. package/internal/session-log-view.js +15 -8
  31. package/internal/session-runtime.js +13 -0
  32. package/package.json +8 -8
  33. package/src/client-activity.js +4 -1
  34. package/src/client-settings-view.js +3 -10
@@ -2,7 +2,7 @@
2
2
 
3
3
  <h3 id="cn-v054">宿主适配</h3>
4
4
 
5
- * 目标仍是当前官方 DSH 发行:插件继续使用它的公共扩展面(设置卡片座位、会话投影、能力探测与公共 `Menu`),本版本不要求宿主侧改动,也不按宿主版本分支;`engines.dsh` 声明 `>=0.2.1-0 <0.2.3`
5
+ * 适配 DSH `0.2.2-alpha.3` 的公共扩展面,并保留上一代宿主调用方式;设置卡片、会话投影与公共 `Menu` 不要求宿主侧改动。兼容性按公共能力判定,不按版本号分支
6
6
  * 会话的能力决定与绑定取用改为写进会话日志本身,不再占用对话消息:重载或重启后恢复会话时,面板读到的仍是当时那次决定,模型也不会因此多收到一条消息
7
7
  * 支持哪些宿主仍由通道表达,而不是由版本号判定:`latest` 与 `next` 是阻塞通道,`alpha` 线只报告结果
8
8
 
@@ -15,6 +15,7 @@
15
15
 
16
16
  <h3>修复</h3>
17
17
 
18
+ * 新版 DSH 中,`edit_run_code` 接受的修订继续属于原调用,保留本次运行可用的工具范围,并遵循宿主的策略与取消决定;后续单元格能继续使用修订后的状态
18
19
  * 本会话关闭过的能力不再出现在任何模型可见处:`tools:sdk` 声明块、程序里真实的 `tools` 成员、`capabilities.*` 的探索结果,以及能力方案与说明段,都与这个会话真正可调的成员保持一致,关闭的能力不会只剩声明还在
19
20
  * 已保存的开关单独成列陈述,不再与本会话的记录混读:一列只回答「保存了什么」,另一列只回答「这个会话记录了什么」
20
21
  * 能力状态改用整块样式区分,而不是给控件里的文字染色:下拉控件保持正常文字颜色,只改它的填充与边框,行首的标记与那一格的词照旧陈述状态;只陈述文字的那一列改用同样底色的胶囊标签
@@ -27,8 +28,9 @@
27
28
 
28
29
  <h3>升级</h3>
29
30
 
30
- * 需要 Node.js `^22.19.0 || >=24.0.0`;升级插件后请重启 DSH
31
- * 不需要配置迁移:本版本不改配置字段;已有会话日志不需要清理或重写
31
+ * Node.js 需同时满足插件的 `^22.19.0 || >=24.0.0` 与所选 DSH 的要求;DSH `0.2.2-alpha.3` 要求 Node.js `>=24.9.0`。升级插件后请重启 DSH
32
+ * 不需要迁移插件配置
33
+ * 若旧会话因能力决定或绑定取用被写到受保护的会话头之前而无法导出或重新打开,请先停止使用该 profile 的宿主,再用仓库 checkout 中的 `scripts/repair-session-log.mjs` 修复;该脚本不随 npm 包发布,插件升级不会自动改写历史日志。用法见[安装指南](../installation.md#troubleshooting)
32
34
 
33
35
  ```sh
34
36
  dsh plugin --profile <profile> add dsh-ptc-plus@0.5.4
@@ -38,7 +40,7 @@ dsh plugin --profile <profile> add dsh-ptc-plus@0.5.4
38
40
 
39
41
  <h3 id="en-v054">Host Compatibility</h3>
40
42
 
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`
43
+ * Adapts to the public extension surfaces of DSH `0.2.2-alpha.3` while retaining the previous Host invocation contract. Settings cards, session projections and the public `Menu` require no Host-side changes. Compatibility follows public capabilities rather than version branches
42
44
  * 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
45
  * 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
46
 
@@ -51,6 +53,7 @@ dsh plugin --profile <profile> add dsh-ptc-plus@0.5.4
51
53
 
52
54
  <h3>Fixes</h3>
53
55
 
56
+ * On the new DSH, a revision accepted by `edit_run_code` stays within its original call, retains the tools admitted for that run, and follows Host policy and cancellation; later cells can continue using the revised state
54
57
  * 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
58
  * 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
59
  * 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
@@ -63,8 +66,9 @@ dsh plugin --profile <profile> add dsh-ptc-plus@0.5.4
63
66
 
64
67
  <h3>Upgrade</h3>
65
68
 
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
69
+ * Node.js must satisfy both the plugin's `^22.19.0 || >=24.0.0` range and the selected DSH requirements; DSH `0.2.2-alpha.3` requires Node.js `>=24.9.0`. Restart DSH after upgrading the plugin
70
+ * No plugin configuration migration is needed
71
+ * If an older session cannot be exported or reopened because capability decisions or binding adoption were written before its protected session head, stop the Host using that profile and repair the log with `scripts/repair-session-log.mjs` from the repository checkout. The script is not shipped in the npm package, and a plugin upgrade does not rewrite historical logs automatically. See the [installation guide](../installation.md#troubleshooting) for usage
68
72
 
69
73
  ```sh
70
74
  dsh plugin --profile <profile> add dsh-ptc-plus@0.5.4
@@ -0,0 +1,41 @@
1
+ [中文](#cn-v055) | [English](#en-v055)
2
+
3
+ <h3 id="cn-v055">宿主适配</h3>
4
+
5
+ * 适配 DSH `0.2.2-rc.1` 的公共工具执行接口,保留上一代宿主的调用方式;兼容性按宿主提供的公共能力判定
6
+
7
+ <h3>修复</h3>
8
+
9
+ * 新版 DSH 中,`edit_run_code` 接受的修订保留原调用关系和本次运行可用的工具范围,继续遵循宿主策略与取消决定;后续单元格能继续使用修订后的状态
10
+
11
+ <h3>升级</h3>
12
+
13
+ * Node.js 需同时满足插件的 `^22.19.0 || >=24.0.0` 与所选 DSH 的要求;DSH `0.2.2-rc.1` 要求 Node.js `>=24.9.0`
14
+ * 无需迁移插件配置;升级后重启 DSH。插件升级不会自动改写历史会话日志
15
+
16
+ ```sh
17
+ dsh plugin --profile <profile> add dsh-ptc-plus@0.5.5
18
+ ```
19
+
20
+ ---
21
+
22
+ <h3 id="en-v055">Host Compatibility</h3>
23
+
24
+ * Adapts to the public tool execution interface of DSH `0.2.2-rc.1` while retaining the previous Host invocation contract; compatibility follows the public capabilities the Host provides
25
+
26
+ <h3>Fixes</h3>
27
+
28
+ * On the new DSH, a revision accepted by `edit_run_code` retains its original call relationship and the tools admitted for that run, and continues to follow Host policy and cancellation; later cells can continue using the revised state
29
+
30
+ <h3>Upgrade</h3>
31
+
32
+ * Node.js must satisfy both the plugin's `^22.19.0 || >=24.0.0` range and the selected DSH requirements; DSH `0.2.2-rc.1` requires Node.js `>=24.9.0`
33
+ * No plugin configuration migration is needed; restart DSH after upgrading. A plugin upgrade does not rewrite historical session logs automatically
34
+
35
+ ```sh
36
+ dsh plugin --profile <profile> add dsh-ptc-plus@0.5.5
37
+ ```
38
+
39
+ ---
40
+
41
+ Full Changelog: https://github.com/muyuanjin/dsh-ptc-plus/compare/v0.5.4...v0.5.5
@@ -0,0 +1,45 @@
1
+ [中文](#cn-v056) | [English](#en-v056)
2
+
3
+ <h3 id="cn-v056">宿主适配</h3>
4
+
5
+ * 适配 DSH `0.2.2-rc.1` 的公共工具执行接口,保留上一代宿主的调用方式;运行时接口按宿主提供的公共能力选择
6
+ * 声明 DSH 兼容范围 `>=0.2.1-0 <0.2.3`,由宿主按官方版本预检规则判断安装与启动资格;`0.2.2-rc.1` 在该范围内
7
+
8
+ <h3>修复</h3>
9
+
10
+ * 新版 DSH 中,`edit_run_code` 接受的修订保留原调用关系和本次运行可用的工具范围,继续遵循宿主策略与取消决定;后续单元格能继续使用修订后的状态
11
+ * 修复 0.5.5 将 DSH 兼容范围意外改成 `*` 的问题,恢复 0.5.4 的上下界;范围内的运行时接口仍按宿主公共能力选择
12
+
13
+ <h3>升级</h3>
14
+
15
+ * Node.js 需同时满足插件的 `^22.19.0 || >=24.0.0` 与所选 DSH 的要求;DSH `0.2.2-rc.1` 要求 Node.js `>=24.9.0`
16
+ * 无需迁移插件配置;升级后重启 DSH。插件升级不会自动改写历史会话日志
17
+
18
+ ```sh
19
+ dsh plugin --profile <profile> add dsh-ptc-plus@0.5.6
20
+ ```
21
+
22
+ ---
23
+
24
+ <h3 id="en-v056">Host Compatibility</h3>
25
+
26
+ * Adapts to the public tool execution interface of DSH `0.2.2-rc.1` while retaining the previous Host invocation contract; runtime interfaces follow the Host's public capabilities
27
+ * Declares the DSH compatibility range `>=0.2.1-0 <0.2.3`; the Host applies its official version preflight to installation and startup. This range includes `0.2.2-rc.1`
28
+
29
+ <h3>Fixes</h3>
30
+
31
+ * On the new DSH, a revision accepted by `edit_run_code` retains its original call relationship and the tools admitted for that run, and continues to follow Host policy and cancellation; later cells can continue using the revised state
32
+ * Fixes 0.5.5 unintentionally changing the DSH compatibility range to `*`, restoring the bounds from 0.5.4; runtime interfaces within that range continue to follow the Host's public capabilities
33
+
34
+ <h3>Upgrade</h3>
35
+
36
+ * Node.js must satisfy both the plugin's `^22.19.0 || >=24.0.0` range and the selected DSH requirements; DSH `0.2.2-rc.1` requires Node.js `>=24.9.0`
37
+ * No plugin configuration migration is needed; restart DSH after upgrading. A plugin upgrade does not rewrite historical session logs automatically
38
+
39
+ ```sh
40
+ dsh plugin --profile <profile> add dsh-ptc-plus@0.5.6
41
+ ```
42
+
43
+ ---
44
+
45
+ Full Changelog: https://github.com/muyuanjin/dsh-ptc-plus/compare/v0.5.4...v0.5.6
@@ -0,0 +1,51 @@
1
+ [中文](#cn-v057) | [English](#en-v057)
2
+
3
+ <h3 id="cn-v057">宿主适配</h3>
4
+
5
+ * 适配 DSH `0.2.2-rc.2` 的官方 Cordis 开发 Skill;启用 `cordisToolsEnabled` 后加载 `dsh-doc`,同时保留旧版宿主的兼容入口
6
+ * DSH 安装准入范围保持 `>=0.2.1-0 <0.2.3`;运行时接口继续按宿主提供的公共能力选择
7
+
8
+ <h3>修复</h3>
9
+
10
+ * 宿主显式重置程序环境后,会话变量、命名 checkpoint 和可编辑目标会被清除,重启或 fork 后也不会恢复;普通 Agent 释放仍保留可验证的会话恢复
11
+ * 历史重置记录缺失或包含不支持的版本时,只收缩一次无法验证的状态;后续已确认的新计算可跨冷恢复和 fork 保留,不重复执行历史外部操作
12
+ * 包含多次重置的会话日志可正常迁移,并保留重置后计算的恢复关系
13
+ * Windows 开发启动器使用实际 Windows Node.js 检查监听端口,自动替换被占用或拒绝访问的缓存端口;手动指定的端口保持原值,探测失败时保留原缓存并停止启动
14
+ * “绑定更新语义”设置直接显示当前策略,可选择“允许重声明和覆盖”(`stateful`)或“启用名称保护”(`protected`);已有配置值保持不变
15
+
16
+ <h3>升级</h3>
17
+
18
+ * Node.js 需同时满足插件的 `^22.19.0 || >=24.0.0` 与所选 DSH 的要求;DSH `0.2.2-rc.2` 要求 Node.js `>=24.9.0`
19
+ * 无需迁移插件配置;升级后重启 DSH。插件升级不会自动改写历史会话日志
20
+
21
+ ```sh
22
+ dsh plugin --profile <profile> add dsh-ptc-plus@0.5.7
23
+ ```
24
+
25
+ ---
26
+
27
+ <h3 id="en-v057">Host Compatibility</h3>
28
+
29
+ * Adapts to the official Cordis development Skill in DSH `0.2.2-rc.2`: enabling `cordisToolsEnabled` loads `dsh-doc`, while retaining the compatibility entry for older Hosts
30
+ * The DSH admission range remains `>=0.2.1-0 <0.2.3`; runtime interfaces continue to follow the Host's public capabilities
31
+
32
+ <h3>Fixes</h3>
33
+
34
+ * An explicit Host environment reset clears session variables, named checkpoints and edit targets across restart and fork; ordinary Agent disposal still preserves verifiable session recovery
35
+ * Missing or unsupported versions in historical reset records contract unproved state once; subsequently confirmed computation survives cold recovery and fork without repeating historical external operations
36
+ * Session logs containing multiple resets migrate successfully while preserving recovery relationships for post-reset computation
37
+ * The Windows development launcher probes listening ports with the actual Windows Node.js runtime and replaces occupied or denied cached ports; explicit ports remain unchanged, and failed probes preserve the cache and stop startup
38
+ * The “Binding update semantics” setting displays the selected policy by name: “Allow redeclarations and overrides” (`stateful`) or “Protect names” (`protected`); existing configuration values remain unchanged
39
+
40
+ <h3>Upgrade</h3>
41
+
42
+ * Node.js must satisfy both the plugin's `^22.19.0 || >=24.0.0` range and the selected DSH requirements; DSH `0.2.2-rc.2` requires Node.js `>=24.9.0`
43
+ * No plugin configuration migration is needed; restart DSH after upgrading. A plugin upgrade does not rewrite historical session logs automatically
44
+
45
+ ```sh
46
+ dsh plugin --profile <profile> add dsh-ptc-plus@0.5.7
47
+ ```
48
+
49
+ ---
50
+
51
+ Full Changelog: https://github.com/muyuanjin/dsh-ptc-plus/compare/v0.5.6...v0.5.7
@@ -148,17 +148,17 @@ Missing-description diagnostics append a complete DSH user message to `additiona
148
148
  tipEscalationFailures: 2
149
149
  ```
150
150
 
151
- The five legacy binding and module switches remain migration inputs. Complete enabled/protected choices map to the corresponding new policy; mixed old choices retain their effective controls until the user explicitly selects the new switch. The Host schema preserves an omitted `bindingUpdates` value until migration, and the settings card resolves the same policy. Selecting the switch also clears the compatibility marker.
151
+ The five legacy binding and module switches remain migration inputs. Complete enabled/protected choices map to the corresponding new policy; mixed old choices retain their effective controls until the user explicitly selects a policy. The Host schema preserves an omitted `bindingUpdates` value until migration, and the settings card resolves the same policy. The **Binding update semantics** selector displays **Legacy settings (not migrated)** until a policy is selected, then shows **Allow redeclarations and overrides** (`stateful`) or **Protect names** (`protected`). Selecting a policy also clears the compatibility marker.
152
152
 
153
153
  `enabled: false` is the settings-based kill switch: the Host keeps the settings namespace, client card, and passive cleanup that can withdraw old PTC declarations at the next permitted accepted step. It removes execution hooks, prompt sections, and tool surfaces without reading binding storage. Settings are applied live when their owner can reconcile them, except the capability inputs frozen per session above and the activation-only seam wait `executionSeamTimeoutMs`, which is read when the plugin activates or reloads. A submitted cell retains one configuration snapshot for its budgets and policies through preflight, execution, binding calls, completion validation, and diagnostics; an update accepted while it runs applies to cells submitted afterward. Worker lifecycle bounds are not cell budgets: `maxOldGenerationSizeMb` is fixed at creation, and `workerReclaimMs` and `startupProbeMs` are read from the current configuration when a worker is created or closed. Node fixes a worker's V8 old-generation limit at creation, so changing `maxOldGenerationSizeMb` while a session worker is active or a submitted cell is preparing to execute is rejected and rolled back rather than reported as applied. Other non-`enabled` updates reconfigure existing owners without replacing session-bound bindings. The settings card renders in whichever settings seat the installed DSH declares — the sidebar **Plugins → Installed** page per-bundle configuration (`plugins.row.config`), or the preceding Host's **Settings → Plugin configuration** card (`settings.plugin.item`) ([ADR 0019](adr/0019-plugin-settings-and-kill-switch.md)) — places the main switch beside the PTC Plus title and disables every other control while the plugin is off. The card does not register in General settings; the sparkle menu opens the same complete form in an application dialog. If a live enable or reconfiguration cannot install a complete runtime, PTC Plus unwinds or restores every owner and persists the last applied settings; a failed compensating settings write is surfaced as an activation diagnostic. The TypeScript language check is deferred until runtime activation, so a non-TypeScript host can load the plugin in disabled mode but cannot enable it.
154
154
 
155
155
  `startupProbeMs` bounds the worker startup conformance probe, which must settle before the kernel reports ready. The worker reads the value from its startup data and falls back to 5000 ms when it is absent; a healthy probe settles inside the worker's own microtasks, so the bound can only fire when startup itself yields or blocks, and the failure is reported as a startup diagnostic without executing any cell. That worker-side read is verified by inspection rather than by a timer test, because a healthy probe never reaches the bound.
156
156
 
157
- `officialReplPriority` defaults to `false` and is evaluated against the live provider capability rather than a release number: a provider that declares `retainsState: true` keeps a program environment per state owner, and a provider that predates that declaration answers through the one retained source form (`programFormat: 'repl'`). With the setting enabled and such a provider live, this host attachment installs no session runtime, so `run_code` and its descriptors — `programFormat`, `retainsState`, `executionInstructions`, `sandboxMode` and `timeout` — stay with the official provider, and the plugin's REPL guidance, session-state semantics, recovery, editor tool, capability SDK, REPL observation and binding workbench are suspended until ownership changes back. The settings card, enablement and stored global bindings remain available so the switch can be reversed. Changing the setting or the host mode re-evaluates ownership as an installation change: the retired attachment disposes its live kernels before the other shape installs, so no committed cell mixes owners. Calls executed under the official mode produce no plugin journal records and are never recovery evidence. The plugin's provider surface forwards `programFormat` to the live original, answers `retainsState` for itself (an installed session kernel retains a per-session environment; otherwise the original declaration decides) and retains weak owner-to-original dispatch provenance for `release(owner)` — captured Agent cleanup reaches the original instances that served that owner even after adapter removal or source replacement, and a failed cleanup remains available for explicit retry. Owner-keyed retained environments and the executed program format belong to the provider that actually runs the program, and non-session consumers such as the workflow adapter keep executing through it — while `executionInstructions`, `sandboxMode` and `timeout` stay withheld while PTC Plus owns session execution. The plugin's own session kernel state is keyed by session and released through the plugin lifecycle, not by `release(owner)`.
157
+ `officialReplPriority` defaults to `false` and is evaluated against the live provider capability rather than a release number: a provider that declares `retainsState: true` keeps a program environment per state owner, and a provider that predates that declaration answers through the one retained source form (`programFormat: 'repl'`). With the setting enabled and such a provider live, this host attachment installs no session runtime, so `run_code` and its descriptors — `programFormat`, `retainsState`, `executionInstructions`, `sandboxMode` and `timeout` — stay with the official provider, and the plugin's REPL guidance, session-state semantics, recovery, editor tool, capability SDK, REPL observation and binding workbench are suspended until ownership changes back. The settings card, enablement and stored global bindings remain available so the switch can be reversed. Changing the setting or the host mode re-evaluates ownership as an installation change: the retired attachment disposes its live kernels before the other shape installs, so no committed cell mixes owners. Calls executed under the official mode produce no plugin journal records and are never recovery evidence. The plugin's provider surface forwards `programFormat` to the live original, answers `retainsState` for itself (an installed session kernel retains a per-session environment; otherwise the original declaration decides) and retains weak owner-to-original dispatch provenance for `release(owner)` — captured Agent cleanup reaches the original instances that served that owner even after adapter removal or source replacement, and a failed cleanup remains available for explicit retry. Owner-keyed retained environments and the executed program format belong to the provider that actually runs the program, and non-session consumers such as the workflow adapter keep executing through it — while `executionInstructions`, `sandboxMode` and `timeout` stay withheld while PTC Plus owns session execution. The plugin's own kernel is keyed by session. For a live session Agent, `release(owner)` implements the Host's explicit reset: it releases the kernel and appends a durable reset boundary, clearing earlier bindings, named checkpoints and edit targets from subsequent computation. For an Agent scope whose public Cordis fiber uid is already null, release is lifecycle cleanup and preserves proven cold recovery. Both routes also clean up original-provider state; failures propagate for Host-managed retry. Non-session owners retain original-provider delegation.
158
158
 
159
- `cordisToolsEnabled: true` atomically mounts the official `@deepseek-ai/dsh-tool-cordis` plugin and exactly the shipped `cordis-plugin-development` companion Skill in agent scopes that expose the `run_code` and `skill` tools plus the public preset and Skill services. PTC Plus resolves the official `cordis` preset through DSH's public preset service, lets the maintained filesystem provider read its Skill root as an immutable bundled Host resource, and filters that provider at the public registration boundary so sibling Skills are never published to the PTC scope. It does not copy the Skill or switch the agent's preset. The first request waits until the Cordis tool fiber, owner guidance, exact Skill provider, and scoped Skill load are all ready. An agent scope that omits one of those surfaces is left for later: the owner publishes nothing there, reports nothing, and retries when the tool surface changes or the next prompt is assembled. A scope whose exposed preset, Skill/tool service, declared Cordis service, inspect manifest, or activation contradicts the host contract fails the setting change at installation and removes every provisional contribution; reached through the host's own `agent/created` or assemble call it withdraws from that scope with one warning instead, so this plugin's unavailability never fails an agent another plugin created ([ADR 0029](adr/0029-keep-plugin-unavailability-out-of-the-host-agent-lifecycle.md)). Disable and agent/runtime disposal remove both fibers. Native agents inherit neither the resulting `tools.*` members nor the Skill, and the code-only direct-tool projection remains `[run_code]`. Cordis can evaluate model-written plugins against the live runtime, so enabling it grants shell-equivalent trust.
159
+ `cordisToolsEnabled: true` atomically mounts the official `@deepseek-ai/dsh-tool-cordis` plugin and exactly the selected official companion Skill in agent scopes that expose the `run_code` and `skill` tools plus the public preset and Skill services. PTC Plus resolves the official `cordis` preset through DSH's public preset service, lets the maintained filesystem provider read its Skill root as an immutable bundled Host resource, and filters that provider at the public registration boundary so sibling Skills are never published to the PTC scope. It does not copy the Skill or switch the agent's preset. The first request waits until the Cordis tool fiber, owner guidance, exact Skill provider, and scoped Skill load are all ready. An agent scope that omits one of those surfaces is left for later: the owner publishes nothing there, reports nothing, and retries when the tool surface changes or the next prompt is assembled. A scope whose exposed preset, Skill/tool service, declared Cordis service, inspect manifest, or activation contradicts the host contract fails the setting change at installation and removes every provisional contribution; reached through the host's own `agent/created` or assemble call it withdraws from that scope with one warning instead, so this plugin's unavailability never fails an agent another plugin created ([ADR 0029](adr/0029-keep-plugin-unavailability-out-of-the-host-agent-lifecycle.md)). Disable and agent/runtime disposal remove both fibers. Native agents inherit neither the resulting `tools.*` members nor the Skill, and the code-only direct-tool projection remains `[run_code]`. Cordis can evaluate model-written plugins against the live runtime, so enabling it grants shell-equivalent trust.
160
160
 
161
- The Skill root uses the preset's absolute composition path when that generation publishes one. A pathless generation is resolved through the active Host's public `pluginPackages.packageOf` service, which returns the official singular or plural preset package from that installation rather than an ambient package reachable from the plugin profile. An older generation with no such service may resolve only its historical plural package relative to the Host's `ctx.baseUrl`; the fallback is disabled whenever `pluginPackages` exists. The exact `cordis-plugin-development/SKILL.md` must exist before the mount begins. An invalid published path remains authoritative and fails instead of falling through to another generation's package layout.
161
+ The Skill root uses the preset's absolute composition path when that generation publishes one. A pathless generation is resolved through the active Host's public `pluginPackages.packageOf` service, which returns the official singular or plural preset package from that installation rather than an ambient package reachable from the plugin profile. An older generation with no such service may resolve only its historical plural package relative to the Host's `ctx.baseUrl`; the fallback is disabled whenever `pluginPackages` exists. The root must contain `dsh-doc/SKILL.md` or the historical `cordis-plugin-development/SKILL.md`; `dsh-doc` takes precedence when both exist. The selected official name and resource base remain intact through discovery, loading and readiness validation. An invalid published path remains authoritative and fails instead of falling through to another generation's package layout.
162
162
 
163
163
  Keep large Cordis host or client source in a top-level binding before calling a Cordis tool. If that call rejects while the worker stays live, an initialized binding can be reused in a short continuation. Decide whether to retry from the operation owner's retry/idempotence contract and available execution facts; a thrown call or a smaller cell does not prove that no effect occurred.
164
164
 
@@ -130,8 +130,8 @@ export const CONFIG_FIELDS = Object.freeze([
130
130
  type: 'enum',
131
131
  options: Object.freeze(['stateful', 'protected']),
132
132
  default: 'stateful',
133
- label: '跨 cell 声明策略',
134
- labelEn: 'Cross-cell declaration policy',
133
+ label: '绑定更新语义',
134
+ labelEn: 'Binding update semantics',
135
135
  description: '同一作用域的声明和赋值可更新变量、函数、类及导入。选“启用名称保护”时改为检测跨 cell 重声明,并保护新建的 const 和 import;静态 import/export 始终可用。',
136
136
  descriptionEn: 'Declarations and assignments update variables, functions, classes and imports in the same scope. With “Protect names” selected, cross-cell redeclarations are detected instead and newly created const and import bindings are protected; static import/export remains available.',
137
137
  },
@@ -8,7 +8,7 @@ import { RUN_CODE } from './runtime-bridge-owner.js'
8
8
  import { sessionExecutionAvailable } from './host-tool-sdk.js'
9
9
 
10
10
  const CORDIS_PRESET_ID = 'cordis'
11
- const CORDIS_SKILL_NAME = 'cordis-plugin-development'
11
+ const COMPANION_SKILL_NAMES = Object.freeze(['dsh-doc', 'cordis-plugin-development'])
12
12
  const CORDIS_SKILL_PROVIDER = 'ptc-plus-cordis'
13
13
  const COMPANION_SKILL_TOOL = 'skill'
14
14
  const COMPANION_PACKAGE_LAYOUTS = Object.freeze([
@@ -22,19 +22,19 @@ const COMPANION_PACKAGE_LAYOUTS = Object.freeze([
22
22
  }),
23
23
  ])
24
24
 
25
- function exactCompanionCandidates(observation) {
25
+ function exactCompanionCandidates(observation, skillName) {
26
26
  if (Array.isArray(observation)) {
27
- return observation.filter(candidate => candidate?.name === CORDIS_SKILL_NAME)
27
+ return observation.filter(candidate => candidate?.name === skillName)
28
28
  }
29
29
  if (observation === null || typeof observation !== 'object'
30
30
  || !Array.isArray(observation.candidates)) return observation
31
31
  return {
32
32
  ...observation,
33
- candidates: observation.candidates.filter(candidate => candidate?.name === CORDIS_SKILL_NAME),
33
+ candidates: observation.candidates.filter(candidate => candidate?.name === skillName),
34
34
  }
35
35
  }
36
36
 
37
- function exactCompanionProvider(provider) {
37
+ function exactCompanionProvider(provider, skillName) {
38
38
  if (provider === null || typeof provider !== 'object'
39
39
  || typeof provider.name !== 'string'
40
40
  || typeof provider.list !== 'function'
@@ -44,17 +44,17 @@ function exactCompanionProvider(provider) {
44
44
  return {
45
45
  ...provider,
46
46
  async list(options) {
47
- return exactCompanionCandidates(await Reflect.apply(provider.list, provider, [options]))
47
+ return exactCompanionCandidates(await Reflect.apply(provider.list, provider, [options]), skillName)
48
48
  },
49
49
  async get(candidate, options) {
50
- if (candidate?.name !== CORDIS_SKILL_NAME) return undefined
50
+ if (candidate?.name !== skillName) return undefined
51
51
  const skill = await Reflect.apply(provider.get, provider, [candidate, options])
52
- return skill?.name === CORDIS_SKILL_NAME ? skill : undefined
52
+ return skill?.name === skillName ? skill : undefined
53
53
  },
54
54
  }
55
55
  }
56
56
 
57
- function exactCompanionSkillPlugin(skillFilesystemPlugin) {
57
+ function exactCompanionSkillPlugin(skillFilesystemPlugin, skillName) {
58
58
  return {
59
59
  ...skillFilesystemPlugin,
60
60
  apply(pluginCtx, config) {
@@ -68,7 +68,7 @@ function exactCompanionSkillPlugin(skillFilesystemPlugin) {
68
68
  const facade = new Proxy(skills, {
69
69
  get(target, property) {
70
70
  if (property === 'registerProvider') {
71
- return create => target.registerProvider(control => exactCompanionProvider(create(control)))
71
+ return create => target.registerProvider(control => exactCompanionProvider(create(control), skillName))
72
72
  }
73
73
  const value = Reflect.get(target, property, target)
74
74
  return typeof value === 'function' ? value.bind(target) : value
@@ -144,12 +144,13 @@ function requireCompanionServices(agent) {
144
144
  return { agentPresets, skills }
145
145
  }
146
146
 
147
- function containsCompanionSkill(directory, fileExists) {
148
- return fileExists(join(directory, CORDIS_SKILL_NAME, 'SKILL.md'))
147
+ function companionSkill(directory, fileExists) {
148
+ const skillName = COMPANION_SKILL_NAMES.find(name => fileExists(join(directory, name, 'SKILL.md')))
149
+ return skillName === undefined ? undefined : { skillRoot: directory, skillName }
149
150
  }
150
151
 
151
152
  /** Resolve the official Cordis companion Skill across DSH preset generations. */
152
- export async function resolveCompanionSkillDirectory(agentPresets, options = {}) {
153
+ export async function resolveCompanionSkillResource(agentPresets, options = {}) {
153
154
  const preset = await agentPresets.resolve(CORDIS_PRESET_ID)
154
155
  const resolution = typeof options === 'function' ? options() : options
155
156
  const fileExists = resolution.fileExists ?? existsSync
@@ -161,10 +162,11 @@ export async function resolveCompanionSkillDirectory(agentPresets, options = {})
161
162
  throw new Error('ptc-plus: DSH cordis preset did not expose an absolute composition path')
162
163
  }
163
164
  const directory = join(dirname(preset.path), 'skills')
164
- if (!containsCompanionSkill(directory, fileExists)) {
165
- throw new Error(`ptc-plus: DSH cordis preset does not contain ${CORDIS_SKILL_NAME}/SKILL.md beside its composition`)
165
+ const resource = companionSkill(directory, fileExists)
166
+ if (resource === undefined) {
167
+ throw new Error(`ptc-plus: DSH cordis preset does not contain a supported companion SKILL.md beside its composition`)
166
168
  }
167
- return directory
169
+ return resource
168
170
  }
169
171
 
170
172
  for (const layout of COMPANION_PACKAGE_LAYOUTS) {
@@ -174,7 +176,8 @@ export async function resolveCompanionSkillDirectory(agentPresets, options = {})
174
176
  throw new Error(`ptc-plus: DSH pluginPackages returned an invalid ${layout.name} package resource`)
175
177
  }
176
178
  const directory = layout.skillRoot(pkg.dir)
177
- if (containsCompanionSkill(directory, fileExists)) return directory
179
+ const resource = companionSkill(directory, fileExists)
180
+ if (resource !== undefined) return resource
178
181
  }
179
182
 
180
183
  if (resolution.packageOf === undefined && resolution.resolveLegacyPackageManifest !== undefined) {
@@ -186,29 +189,30 @@ export async function resolveCompanionSkillDirectory(agentPresets, options = {})
186
189
  }
187
190
  if (manifest !== undefined) {
188
191
  const directory = join(dirname(manifest), 'presets', CORDIS_PRESET_ID, 'skills')
189
- if (containsCompanionSkill(directory, fileExists)) return directory
192
+ const resource = companionSkill(directory, fileExists)
193
+ if (resource !== undefined) return resource
190
194
  }
191
195
  }
192
- throw new Error(`ptc-plus: the active DSH installation does not publish a loadable ${CORDIS_SKILL_NAME} companion Skill through its package-resolution surface`)
196
+ throw new Error(`ptc-plus: the active DSH installation does not publish a loadable official companion Skill through its package-resolution surface`)
193
197
  }
194
198
 
195
- async function requireCompanionSkill(agent, skills) {
199
+ async function requireCompanionSkill(agent, skills, skillName) {
196
200
  const lookup = {
197
201
  cwd: agent.session?.header?.cwd,
198
202
  scope: agent,
199
203
  }
200
204
  const owned = (await skills.list(lookup)).filter(skill => skill?.provider === CORDIS_SKILL_PROVIDER)
201
205
  const summary = owned[0]
202
- if (owned.length !== 1 || summary?.name !== CORDIS_SKILL_NAME
206
+ if (owned.length !== 1 || summary?.name !== skillName
203
207
  || summary.invocation?.modelInvocable !== true) {
204
- throw new Error(`ptc-plus: official Cordis Skill provider must publish exactly ${JSON.stringify(CORDIS_SKILL_NAME)} as model-invocable`)
208
+ throw new Error(`ptc-plus: official Cordis Skill provider must publish exactly ${JSON.stringify(skillName)} as model-invocable`)
205
209
  }
206
- const skill = await skills.get(CORDIS_SKILL_NAME, lookup)
210
+ const skill = await skills.get(skillName, lookup)
207
211
  if (skill?.provider !== CORDIS_SKILL_PROVIDER
208
212
  || skill.invocation?.modelInvocable !== true
209
213
  || typeof skill.content !== 'string'
210
214
  || skill.content.length === 0) {
211
- throw new Error(`ptc-plus: official Cordis companion Skill ${JSON.stringify(CORDIS_SKILL_NAME)} is not loadable`)
215
+ throw new Error(`ptc-plus: official Cordis companion Skill ${JSON.stringify(skillName)} is not loadable`)
212
216
  }
213
217
  }
214
218
 
@@ -321,7 +325,6 @@ export function createCordisToolsOwner(
321
325
  const withdrawn = new Map()
322
326
  const cordisInspectLeases = createCordisInspectLeases(ctx)
323
327
  const scopedCordisPlugin = cordisInspectLeases.plugin(cordisPlugin)
324
- const scopedSkillPlugin = exactCompanionSkillPlugin(skillFilesystemPlugin)
325
328
  const packageBaseUrl = ctx.baseUrl
326
329
  const skillResolution = companionResolution ?? (() => {
327
330
  const pluginPackages = ctx.get?.('pluginPackages')
@@ -423,13 +426,14 @@ export function createCordisToolsOwner(
423
426
  mounts.set(agent, mount)
424
427
  mount.activation = Promise.resolve().then(async () => {
425
428
  const { agentPresets, skills } = requireCompanionServices(agent)
426
- const skillDirectory = await resolveCompanionSkillDirectory(agentPresets, skillResolution)
429
+ const { skillRoot, skillName } = await resolveCompanionSkillResource(agentPresets, skillResolution)
430
+ const scopedSkillPlugin = exactCompanionSkillPlugin(skillFilesystemPlugin, skillName)
427
431
  if (mount.disposed) return
428
432
 
429
433
  const skillFiber = agent.ctx.plugin(scopedSkillPlugin, {
430
434
  providerName: CORDIS_SKILL_PROVIDER,
431
435
  includeDefaultRoots: false,
432
- bundledSkillDir: skillDirectory,
436
+ bundledSkillDir: skillRoot,
433
437
  watch: false,
434
438
  })
435
439
  if (typeof skillFiber?.dispose !== 'function') {
@@ -449,7 +453,7 @@ export function createCordisToolsOwner(
449
453
  await Promise.resolve(cordisFiber)
450
454
  if (mount.disposed) return
451
455
 
452
- await requireCompanionSkill(agent, skills)
456
+ await requireCompanionSkill(agent, skills, skillName)
453
457
  if (mount.disposed) return
454
458
  mount.activated = true
455
459
  }).catch(async error => {
@@ -310,8 +310,9 @@ export function createEditTransportOwner(ctx, {
310
310
  subCallId: `${String(exec.callId)}:derived`, name: RUN_CODE, arguments: argumentsValue }
311
311
  const executionStart = agent.session.append('tool/ptc-dispatch-start', data)
312
312
  const derivedExec = { callId: data.subCallId, rootCallId: data.rootCallId,
313
- parent: rootExec.token, name: RUN_CODE, arguments: argumentsValue, agent, signal: rootSignal }
314
- const dispatch = () => dispatchToolCall(ctx.tools, derivedExec)
313
+ parent: rootExec.token, name: RUN_CODE, arguments: argumentsValue, agent, signal: rootSignal,
314
+ ...(rootExec.toolSet === undefined ? {} : { toolSet: rootExec.toolSet }) }
315
+ const dispatch = () => dispatchToolCall(ctx.tools, derivedExec, ctx)
315
316
  const tentative = await executeTentative(executionStart.seq, start.rootCallSeq,
316
317
  dispatch, executionLog, derivedExec)
317
318
  const inner = tentative.result
@@ -94,19 +94,16 @@ export function createExecutionSeam(service, serviceName) {
94
94
  requireCompatible()
95
95
  return installed === undefined ? upstreamRun.call(service, input) : installed.execute(input)
96
96
  },
97
- /**
98
- * Release one owner's retained environment. The resources live in the
99
- * original generation (the session kernel releases its state through its
100
- * own lifecycle). Adapter retirement only closes execution; the original
101
- * still owns cleanup for references retained by its consumers.
102
- */
103
- release(owner) {
104
- if (typeof service.release !== 'function') return Promise.resolve()
105
- try {
106
- return Promise.resolve(service.release.call(service, owner))
107
- } catch (error) {
108
- return Promise.reject(error)
109
- }
97
+ /** Release both the installed environment and any original-provider state. */
98
+ async release(owner) {
99
+ const entry = installed
100
+ const results = await Promise.allSettled([
101
+ Promise.resolve().then(() => entry?.release?.(owner)),
102
+ Promise.resolve().then(() => service.release?.call(service, owner)),
103
+ ])
104
+ const failures = results.filter(result => result.status === 'rejected').map(result => result.reason)
105
+ if (failures.length === 1) throw failures[0]
106
+ if (failures.length > 1) throw new AggregateError(failures, 'ptc-plus: execution environment release failed')
110
107
  },
111
108
  [PLUGIN_EXECUTION_SEAM]() { return seam },
112
109
  })
@@ -143,11 +140,12 @@ export function createExecutionSeam(service, serviceName) {
143
140
  requireCompatible()
144
141
  return upstreamRun.call(service, spec)
145
142
  },
146
- installExecute(execute) {
143
+ installExecute(execute, release) {
147
144
  requireAvailable()
148
145
  if (typeof execute !== 'function') throw new TypeError('ptc-plus: execution entry must be a function')
149
146
  if (installed !== undefined) throw new Error('ptc-plus: execution seam already installed')
150
- const entry = { execute, language: service.language }
147
+ if (release !== undefined && typeof release !== 'function') throw new TypeError('ptc-plus: release entry must be a function')
148
+ const entry = { execute, release, language: service.language }
151
149
  installed = entry
152
150
  const restore = () => {
153
151
  if (installed !== entry) return
@@ -260,41 +260,29 @@ export function sessionExecutionAvailable(tools, scope, name = 'run_code') {
260
260
  }
261
261
 
262
262
  /**
263
- * Executions this plugin submitted under the host's batch dispatch contract.
263
+ * Dispatch one tool call through the Host's invocation policy.
264
264
  *
265
- * That contract assigns the execution identity itself, so the execution object a
266
- * host runs is not the one the caller built and carries no parent token the caller
267
- * could compare. The submission record is what remains: the call id this plugin
268
- * chose, which the host matched its result back to.
269
- */
270
- const batchSubmissions = new WeakSet()
271
-
272
- /**
273
- * Whether this execution was submitted as a host batch call.
274
- * @param {object} execution - an execution object this plugin built.
275
- * @returns {boolean} true when the batch contract dispatched it.
276
- */
277
- export function isBatchSubmission(execution) {
278
- return execution !== null && typeof execution === 'object' && batchSubmissions.has(execution)
279
- }
280
-
281
- /**
282
- * Dispatch one tool call through the host registry.
283
- *
284
- * Two dispatch contracts are published. A registry host accepts one call and
285
- * resolves to that call's settled result. An executor host accepts a complete
286
- * model batch, assigns the execution identity itself, and resolves to the
287
- * ordered outcomes — so the caller supplies only the call it wants run, and the
288
- * executor's own token is the one its observers see.
265
+ * Registry Hosts accept an execution input directly. Executor Hosts reserve
266
+ * batches for root calls; nested inputs use the published invocation pipeline
267
+ * so the Host retains their actual parent token and enclosing root identity.
268
+ * The Host owns validation, policy, cancellation and result materialization.
289
269
  * @param {object} tools - the host tool registry (`ctx.tools`).
290
270
  * @param {object} execution - call identity, arguments, agent and signal.
271
+ * @param {object} [ctx] - the owning plugin context, required for nested execution.
291
272
  * @returns {Promise<object>} this call's settled result.
292
273
  */
293
- export async function dispatchToolCall(tools, execution) {
274
+ export async function dispatchToolCall(tools, execution, ctx) {
294
275
  if (typeof tools?.selectExecutor !== 'function') return tools.execute(execution)
295
- // Recorded before the host runs the batch: it calls back into this plugin during
296
- // the dispatch, and the relation has to be provable then.
297
- batchSubmissions.add(execution)
276
+ if (execution.parent !== undefined) {
277
+ if (ctx === undefined) throw new Error('ptc-plus: nested Host execution requires its owning context')
278
+ const { ToolExecutionPipeline } = await import('@deepseek-ai/dsh-tools/pipeline')
279
+ const pipeline = new ToolExecutionPipeline(ctx, {
280
+ get: (name, scope) => hostTransportDefinition(tools, scope, name),
281
+ })
282
+ const prepared = await pipeline.prepare(execution)
283
+ const outcome = prepared.kind === 'dispatch' ? await pipeline.dispatch(prepared.exec) : prepared
284
+ return pipeline.complete(prepared.exec, outcome)
285
+ }
298
286
  const settled = await tools.execute({
299
287
  calls: [{ id: execution.callId, name: execution.name, arguments: JSON.stringify(execution.arguments) }],
300
288
  signal: execution.signal,
@@ -25,7 +25,7 @@ import {
25
25
  } from './program-bindings.js'
26
26
  import { deepFreeze } from './record-utils.js'
27
27
  import { adaptRunCodeSchema } from './direct-surface-owner.js'
28
- import { hostTransportDefinition, isBatchSubmission } from './host-tool-sdk.js'
28
+ import { hostTransportDefinition } from './host-tool-sdk.js'
29
29
  import { REPL_MEMORY_META_KEY, withReplMemorySnapshot } from './repl-memory-projection.js'
30
30
  import { withUserBindingDraftCapability } from './user-binding-draft-projection.js'
31
31
  import { POST_EDIT_KEY } from './session-journal-schema.js'
@@ -697,22 +697,24 @@ function applyEditableSlots(definition, promptText, slotPatches) {
697
697
  .finally(projected.release)
698
698
  }
699
699
 
700
- const releaseSeam = seam.installExecute(patchedRun)
700
+ const releaseSeam = seam.installExecute(patchedRun, owner => {
701
+ if (owner?.session === undefined || sessionId(owner) === undefined) return
702
+ // Host uses release for explicit reset and for its retained-environment effect.
703
+ // Cordis clears the public Agent scope uid before invoking disposal effects.
704
+ if (owner.ctx?.fiber?.uid === null) return sessions.disposeAgent(sessionId(owner))
705
+ return sessions.resetEnvironment(owner.session)
706
+ })
701
707
 
702
708
  /**
703
709
  * Whether this execution is the derived execution the edit transport submitted.
704
710
  *
705
- * A registry host runs the very object the plugin built, so the call id, the root
706
- * call and the parent token it carries all prove the relation. The batch contract
707
- * assigns its own identity instead: the parent token is the host's own and the
708
- * root call is whichever call that host treats as direct. What survives there is
709
- * the call id this plugin chose, which the host matched the batch result back to,
710
- * together with the dispatch record; the derived source is compared beside it.
711
+ * Both Host invocation contracts preserve the submitted call id, enclosing
712
+ * root and actual parent token. All three must match; a root batch submission
713
+ * cannot establish a nested execution merely by choosing the same call id.
711
714
  */
712
715
  const isDerivedExecution = (derived, exec) => {
713
716
  if (derived === null || typeof derived !== 'object' || exec === null || typeof exec !== 'object') return false
714
717
  if (derived.callId !== exec.callId) return false
715
- if (isBatchSubmission(derived)) return true
716
718
  return exec.parent !== undefined
717
719
  && derived.rootCallId === exec.rootCallId && derived.parent === exec.parent
718
720
  }
@@ -1,3 +1,4 @@
1
+ import { ENVIRONMENT_RESET_VERSION } from './session-journal-schema.js'
1
2
  import { createUserMessage } from '@deepseek-ai/dsh-llm'
2
3
  import {
3
4
  CATALOG_PREFIX, EMPTY_CATALOG, LEGACY_STATE_PREFIX, PREVIOUS_EMPTY_CATALOG, PREVIOUS_STATE_PREFIX,
@@ -185,3 +186,10 @@ export function runtimeNoticeMessage(tip) {
185
186
  if (readRuntimeMessage(message) === undefined) throw new Error('invalid PTC recovery notice')
186
187
  return message
187
188
  }
189
+
190
+ /** A persisted computation boundary; the Host separately owns its reset receipt. */
191
+ export function environmentResetMessage() {
192
+ return createUserMessage({ source: { kind: PTC_MESSAGE_SOURCE_KIND, form: 'environment-reset', version: ENVIRONMENT_RESET_VERSION },
193
+ content: [{ type: 'text', text: 'PTC Plus computation reset: earlier session bindings, named checkpoints and edit targets are retired. New computation starts empty with the configured global bindings. Completed external effects remain.' }],
194
+ })
195
+ }