dsh-ptc-plus 0.5.1 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +10 -4
- package/README.md +10 -4
- package/client.cjs +1623 -104
- package/compiler-core.cjs +132 -132
- package/compiler-core.cjs.map +3 -3
- package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +2 -0
- package/docs/adr/0017-track-the-latest-dsh-public-surface.md +4 -2
- package/docs/adr/0019-plugin-settings-and-kill-switch.md +2 -2
- package/docs/adr/0023-global-user-bindings.md +12 -0
- package/docs/adr/0027-run-the-session-kernel-in-a-killable-helper-process.md +11 -1
- package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +1 -1
- package/docs/adr/0030-serve-both-typert-codec-generations-from-one-codec.md +2 -2
- package/docs/adr/0032-own-session-log-facts-in-a-host-only-projection.md +31 -1
- package/docs/adr/0044-coexist-with-the-official-persistent-repl.md +4 -4
- package/docs/adr/0045-customize-model-visible-prompt-text.md +38 -0
- package/docs/adr/0046-gate-the-commit-with-the-semantic-evidence-tier.md +35 -0
- package/docs/adr/0047-store-journal-argument-digests-in-the-projection.md +88 -0
- package/docs/adr/0048-supersede-a-rewritten-cell.md +48 -0
- package/docs/adr/0049-prove-a-settlement-against-its-published-record.md +53 -0
- package/docs/adr/0050-read-host-surfaces-through-their-published-contract.md +48 -0
- package/docs/adr/README.md +7 -1
- package/docs/architecture.md +3 -3
- package/docs/client-ui.md +5 -2
- package/docs/durability-design.md +15 -2
- package/docs/installation.md +9 -9
- package/docs/releases/v0.5.2.md +91 -0
- package/docs/releases/v0.5.3.md +77 -0
- package/docs/runtime-reference.md +43 -3
- package/docs/unmeasured-cell-recovery-repair-proposal.md +387 -0
- package/docs/user-bindings.en.md +15 -0
- package/docs/user-bindings.md +15 -0
- package/docs/v0.5.3-goals.md +117 -0
- package/docs/verification.md +7 -3
- package/index.js +100 -31
- package/internal/binding-adoption.js +136 -0
- package/internal/bounded-literal.js +85 -0
- package/internal/capability-command-receipt.js +18 -4
- package/internal/capability-delivery-proof.js +70 -0
- package/internal/capability-environment.js +200 -11
- package/internal/capability-plan.js +11 -8
- package/internal/cell-error.js +13 -0
- package/internal/config-spec.js +126 -6
- package/internal/cordis-tools-owner.js +2 -1
- package/internal/direct-surface-owner.js +39 -21
- package/internal/edit-transport-owner.js +81 -15
- package/internal/execution-provider-owner.js +6 -0
- package/internal/execution-seam-compat.js +25 -0
- package/internal/host-tool-sdk.js +308 -0
- package/internal/isolated-worker.js +134 -17
- package/internal/kernel-worker.js +167 -3
- package/internal/legacy-cell-analysis.js +14 -2
- package/internal/legacy-repl-convenience.js +16 -3
- package/internal/official-repl-priority.js +31 -10
- package/internal/program-bindings.js +12 -1
- package/internal/projection-retention.js +238 -0
- package/internal/prompt-text.js +858 -0
- package/internal/recovery-tips.js +50 -11
- package/internal/rejected-cell-editor.js +3 -2
- package/internal/repl-memory-projection.js +21 -2
- package/internal/runtime-bridge-owner.js +114 -9
- package/internal/runtime-config.js +2 -0
- package/internal/runtime-contexts.js +14 -5
- package/internal/runtime-messages.js +48 -15
- package/internal/session-cell-executor.js +132 -11
- package/internal/session-journal-recovery.js +1204 -116
- package/internal/session-journal-schema.js +10 -5
- package/internal/session-journal.js +214 -14
- package/internal/session-log-view.js +133 -26
- package/internal/session-runtime.js +746 -38
- package/internal/session-state.js +27 -0
- package/internal/stateful-root-compiler.js +11 -0
- package/internal/stateful-root-runtime.js +17 -0
- package/internal/user-binding-console.js +7 -0
- package/internal/user-bindings-owner.js +112 -6
- package/internal/user-bindings.js +2 -2
- package/internal/validated-parse-repair.js +6 -1
- package/internal/worker-client.js +27 -4
- package/package.json +16 -6
- package/src/client-activity.js +24 -4
- package/src/client-capabilities-view.js +504 -27
- package/src/client-copy.js +187 -2
- package/src/client-indicator-view.js +2 -1
- package/src/client-repl-view.js +35 -11
- package/src/client-styles.js +64 -2
- package/src/client.js +3 -1
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# v0.5.3 目标:可证明的状态
|
|
2
|
+
|
|
3
|
+
## 0.5.2 留下了什么
|
|
4
|
+
|
|
5
|
+
0.5.2 解决的是**状态连续**:视图变化不再重建内核,绑定不会因为一次重渲染而丢失,编辑是追加而不是改写历史。它把"REPL 环境活着的时候算数"这件事做扎实了。
|
|
6
|
+
|
|
7
|
+
但它留下一个未回答的问题:**插件对模型说的那些话,凭什么算数?**"这个绑定可用""这条记录没被改过""这个能力已经生效"——这些都只是**声明**,不是**证明**。0.5.2 里已经能看到三种破口:
|
|
8
|
+
|
|
9
|
+
- 恢复提示说某个名字 live,下一格却 `not defined`(`PTC-R002` 收口了一部分);
|
|
10
|
+
- 保存了全局绑定开关,面板只能说"已保存",说不出"已投递""已可用";
|
|
11
|
+
- 历史记录被事后改动,插件从记录本身看不出来,因为记录与它的副本永远自洽。
|
|
12
|
+
|
|
13
|
+
所以 0.5.3 的主题不是再加功能,而是**把声明变成证明**:
|
|
14
|
+
|
|
15
|
+
> **0.5.3:可证明的状态。** 插件对模型陈述的每一条事实,都要有它能产出、模型能检验的证据;并且永远区分"已配置""已投递""已证明"。
|
|
16
|
+
|
|
17
|
+
## 概念基元
|
|
18
|
+
|
|
19
|
+
四个词贯穿本版全部目标,先定义再谈功能。
|
|
20
|
+
|
|
21
|
+
| 概念 | 回答的问题 | 谁拥有 |
|
|
22
|
+
| --- | --- | --- |
|
|
23
|
+
| **供给 supply** | 产品声明存在哪些能力、绑定、能力开关 | 全局配置与目录 |
|
|
24
|
+
| **取用 adoption** | 这个会话的环境选择用其中哪些 | 会话 |
|
|
25
|
+
| **物化 materialization** | worker 真的把它装进环境了吗 | 运行时与 worker |
|
|
26
|
+
| **证明 proof** | 有什么可被模型检验的证据 | 日志、探测、诊断 |
|
|
27
|
+
|
|
28
|
+
三条不变式:
|
|
29
|
+
|
|
30
|
+
- **I1 层次不可互相代答。** 供给不能代取用,取用不能代物化,物化不能代证明。
|
|
31
|
+
- **I2 只有证明可以宣称"可用"。** 没有证明时如实说"不可确认",而不是默认可用。
|
|
32
|
+
- **I3 可证明的事实必须能从会话日志重建。** 重建不出来的,要显式标注为不可重建,而不是假装它不存在。
|
|
33
|
+
|
|
34
|
+
0.5.2 的教训正好对应 I1:全局开关是供给,会话要不要用是取用,装了没有是物化——三层混成一个"开关",就会出现"我改了全局,为什么这个会话不变"和"配置关了但绑定还在"这类说不清的状态。
|
|
35
|
+
|
|
36
|
+
## 目标
|
|
37
|
+
|
|
38
|
+
### G1 绑定:从"开关"到"环境成员"
|
|
39
|
+
|
|
40
|
+
**概念纠正。** 绑定不是配置项,是**环境成员**。全局决定目录供给,会话决定取用,worker 决定可用。
|
|
41
|
+
|
|
42
|
+
- 全局关闭一个条目 = 它不在目录里,会话无从取用。**"覆盖复活已删除条目"这个概念上不存在**,不需要额外的冲突规则去禁止。
|
|
43
|
+
- 会话取用是二态(用 / 不用)加一个缺省(未表态即全取),不需要三态哨兵,也不借用能力侧的 `automatic`——那个词的含义是"由宿主推导",绑定没有可推导的决策。
|
|
44
|
+
- 取用 ≠ 物化成功:装失败的名字必须点名(接 `PTC-R002`),曾经 live 后消失的必须点名(`PTC-R006`)。
|
|
45
|
+
|
|
46
|
+
**验收**:不设会话取用时行为与 0.5.2 完全一致;A 会话不用某绑定、B 会话仍可用;全局关闭后会话不可取用且无"复活"路径;重启后取用关系随日志重建;物化失败在模型可见处点出名字。
|
|
47
|
+
|
|
48
|
+
### G2 能力与上下文:把"已保存"升级为"已投递"
|
|
49
|
+
|
|
50
|
+
**概念。** 保存设置只改变供给,不证明模型看得到、也不证明 worker 初始化过。面板与提示必须分开说这三件事。
|
|
51
|
+
|
|
52
|
+
**验收**:保存后能报告目录是否出现该能力、新单元格能否真的取到它;三种状态(已保存 / 已投递 / 已可用)在面板与模型可见提示中区分;任一层失败都指名,而不是笼统"已生效"。
|
|
53
|
+
|
|
54
|
+
### G3 历史证据:记录必须能自证未被改动
|
|
55
|
+
|
|
56
|
+
**概念。** 一份可以被事后编辑、且编辑后仍与自身副本一致的记录,不是证据。执行时要写入**不可重算的快照**,折叠时用它比对。
|
|
57
|
+
|
|
58
|
+
现有的半成品:`completion` 与诊断已经互相校验(0.5.2),但 `diagnostics` 被改后再重算 `completion` 就自洽了——因为事件里没有第三份独立副本(`message.content` 为空)。
|
|
59
|
+
|
|
60
|
+
**契约**:快照字段随 journal schema 一起版本化;不一致即判该格不可信;**否决必须是重放终态**,不能进入重试循环(上一次实现因此挂死)。
|
|
61
|
+
|
|
62
|
+
**验收**:篡改 completion 或 diagnostic 都使该格不可信;恢复矩阵不退化;journal 迁移对旧记录保持兼容。
|
|
63
|
+
|
|
64
|
+
### G4 改写历史:`editRewritesHistory`
|
|
65
|
+
|
|
66
|
+
**概念纠正。** "改写历史"不是删除,而是**取代关系**。旧单元格仍然留在日志里,只是不再参与环境构建;它声明的名字以"被取代"报告,而不是以"丢失"报告。
|
|
67
|
+
|
|
68
|
+
这比"追加"更诚实:不假装旧执行没发生过,而是显式声明谁取代了谁(日志里记为 `supersedes`:这条记录取代了哪条)。默认关闭,开启时改写才真正替换目标单元格。
|
|
69
|
+
|
|
70
|
+
**验收**:关闭时全部既有用例不变;开启时"改写 → 后续格子读到新值"成立;被取代单元格的绑定以专门诊断点名;折叠能从日志重建取代关系。
|
|
71
|
+
|
|
72
|
+
### G5 快照归属:REPL 面板的 generation
|
|
73
|
+
|
|
74
|
+
**概念。** 面板显示的不是"当前状态",而是**某个运行期的一次观察**。需要分开两件事:
|
|
75
|
+
|
|
76
|
+
- **新鲜度**:同一运行期内的投影刷新——清单仍然有效,不该闪成"不可确认";
|
|
77
|
+
- **归属**:跨运行期的快照——属于上一个进程,必须判为不可确认(既有契约,`repl-memory-projection` 的用例钉着它)。
|
|
78
|
+
|
|
79
|
+
0.5.2 的客户端兜底只解决了新鲜度,没有表达归属。0.5.3 要在数据里表达"这份清单来自哪个运行期",而不是靠随机 UUID 与对象身份比较去猜。
|
|
80
|
+
|
|
81
|
+
**验收**:两个会话并行、其中一个结束时,另一个面板不闪;真正重建后显示不可确认并说明原因;既有契约用例保持绿。
|
|
82
|
+
|
|
83
|
+
### G6 覆盖率:证据,不是仪式
|
|
84
|
+
|
|
85
|
+
**概念。** 覆盖率是"这段代码被执行过"的证据;阈值是策略,不是证据。0.5.2 为发版引入了"按改动文件 100% + 历史欠账白名单",那是策略例外。
|
|
86
|
+
|
|
87
|
+
**验收**:全局 `lines 100 / functions 100`,白名单清空;`review:finalize` 记录对应证明;发布流程不再需要"按改动文件"的特殊分支。
|
|
88
|
+
|
|
89
|
+
## 非目标
|
|
90
|
+
|
|
91
|
+
- 不新增 PTC 语言特性,不改 DSH 的公共契约;
|
|
92
|
+
- 不做跨会话共享 worker 或跨会话共享 REPL 状态;
|
|
93
|
+
- 不为通过门槛而放宽断言或增加 coverage ignore(除非证明不可达并写入提交信息)。
|
|
94
|
+
|
|
95
|
+
## 发布判据
|
|
96
|
+
|
|
97
|
+
1. 六个目标各自的验收项全部有可执行证据;
|
|
98
|
+
2. `npm run check`、`npm run coverage`(全局 100%)、`semantic:check`(unknown 0)全绿;
|
|
99
|
+
3. `docs/` 中受影响的 owner 文档与 ADR 同步更新;
|
|
100
|
+
4. Release 工作流 `stage` 成功,npm 进入 staged 状态;
|
|
101
|
+
5. 发布说明按 [release-notes.md](release-notes.md) 的用户视角重写,不以功能清单代替变化说明。
|
|
102
|
+
|
|
103
|
+
## 发布时机
|
|
104
|
+
|
|
105
|
+
0.5.2 已经把"提前适配"这条路的代价演示过一遍:为一个尚未发布到 GitHub 的 alpha 改代码,等对应 rc 出来又要再改一轮,而 alpha 的接口形状在正式发布前并不稳定。
|
|
106
|
+
|
|
107
|
+
因此 0.5.3 的发布以**通道**为准,不以"某天适配好了"为准:
|
|
108
|
+
|
|
109
|
+
| 通道 | 含义 | 对发布的作用 |
|
|
110
|
+
| --- | --- | --- |
|
|
111
|
+
| `latest` | 最新的已发布 rc | 必须通过;它决定能不能发 |
|
|
112
|
+
| `next` | 正在推进的下一条 rc 线,GitHub 已有发布说明 | 必须通过 |
|
|
113
|
+
| `alpha` | 提前适配,正式发布尚未出现 | 只报告,不阻塞 |
|
|
114
|
+
|
|
115
|
+
规则:**插件可以提前适配 alpha(那是有价值的预警),但版本发布等待包含该形状的正式 rc。** 已经 staged 的包如果对应的是 alpha 形状的实现,就撤掉重新准备,而不是"先发出去再说"。
|
|
116
|
+
|
|
117
|
+
通道的具体版本由 `npm run host:channels:update` 从 dist-tag 解析并写入 [compat/channels.json](../compat/channels.json),不手工编辑版本号;行为代码只做形状探测,由 `npm run host:shape-check` 守住。
|
package/docs/verification.md
CHANGED
|
@@ -13,6 +13,10 @@ Backend and browser entries accept `--list`, and selections can repeat. Omitting
|
|
|
13
13
|
|
|
14
14
|
`npm run check` classifies the complete source candidate against HEAD, runs the appropriate checks below, checks source/ledger stability, and records the verified tree and verification scope for the commit hook. Run `check` alone when the ledger is absent or resolved; while findings remain unresolved, use `verify`. Do not run complete component suites around the same final command: that duplicates its work. Focused evidence is not a source-bound commit proof. Inspect a failed gate's stage/case, reproduce it with a focused entry, repair it, and rerun the final command only after the candidate is stable.
|
|
15
15
|
|
|
16
|
+
The full branch of `check` runs `npm run verify:check`: `review:validate`, `host:channels:verify`, `host:shape-check`, `semantic:check`, `build:check`, the syntax checks and `scripts/deterministic-test-groups.mjs --check`, which selects `npm run test:client` and `npm run test:backend:fast`. The Client units run in the gate because a change can be covered only by them; the backend tier is the set of test files `semantic-obligations.json` names as evidence for at least one obligation, excluding coverage-only paths and the verification-tooling probes that fail by construction outside their harness. It targets a few minutes so the commit gate is actually run; [ADR 0046](adr/0046-gate-the-commit-with-the-semantic-evidence-tier.md) owns this split and its evidence.
|
|
17
|
+
|
|
18
|
+
`npm run verify` keeps the complete behavioral gate: `test:client:visual` plus the whole ordinary backend inventory. CI runs `verify:ci`; release preparation runs `verify`, and coverage stays release-only. A change that touches a semantic owner missing from the graph must extend the graph, which widens the fast tier at the same time. The browser suites are not part of the commit gate: they are the complete visual contract and run in `verify`, in CI, and on demand.
|
|
19
|
+
|
|
16
20
|
Stage runners print inventory, concurrency and elapsed times. Start a permitted long gate with a short tool yield and retain its process/session ID. Tell the user the command, start time and expected range before waiting. Keep verbose diagnostics in ignored logs and wait on the same process in intervals no longer than 60 seconds. Report completed stages, failures or changed timing expectations. Inspect process activity and stage output before treating a slow run as stalled; elapsed time alone is insufficient. Stop only the owned process on cancellation, and never report an interrupted run as verified. Do not start another copy while waiting.
|
|
17
21
|
|
|
18
22
|
## Change classification
|
|
@@ -41,11 +45,11 @@ The full local and CI behavioral gates run `test:client:visual`: one Client suit
|
|
|
41
45
|
|
|
42
46
|
`npm run verify` and the full branch of `npm run check` validate the ledger, compare generated bundles, check syntax, run Client tests, and run all ordinary backend tests through `npm run test:backend` without coverage instrumentation. The ordinary runner selects the sorted root `test/*.test.js` inventory. Coverage-only runner/report tests and collection probes live under `test/coverage/` and are excluded from daily execution. The release coverage runner includes both inventories and sets `DSH_PTC_TEST_COVERAGE_SUITE=1` for mixed instrumented cases; the ordinary runner clears that marker. Both runners share the concurrency calculation. Backend test files run in separate Node processes. Compiler boundary protocols are divided into disjoint storage, Promise and reflection files so the bounded file pool can execute them alongside the parameter/namespace/error cases without sharing their native or PTC realms. The default process count is half the available CPUs, rounded down and bounded between one and eight; each process can also own runtime Workers. Tests within each file retain their existing ordering. The ordinary runner enables module mocks without preloading coverage hooks and removes inherited coverage and temporary bytecode settings from its test-process environment.
|
|
43
47
|
|
|
44
|
-
After the shared ledger,
|
|
48
|
+
After the shared ledger, channel, semantic, build and syntax prerequisites pass, `scripts/deterministic-test-groups.mjs` selects its groups by gate: `--check` (the commit gate) runs only the semantic-evidence backend tier, the default `verify` runs the complete Client visual and backend groups concurrently on machines with at least 12 available CPUs, and `--ci` keeps the focused runtime group. Smaller machines retain serial phases. Every selected group must settle, and any nonzero exit, signal or startup failure rejects verification. Each browser pool retains its own CPU bound and process isolation.
|
|
45
49
|
|
|
46
|
-
|
|
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.
|
|
47
51
|
|
|
48
|
-
`host:
|
|
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.
|
|
49
53
|
|
|
50
54
|
## Release coverage
|
|
51
55
|
|
package/index.js
CHANGED
|
@@ -13,13 +13,15 @@ import { CapabilityStatisticsStore } from './internal/capability-statistics-stor
|
|
|
13
13
|
import { createCapabilityEnvironment } from './internal/capability-environment.js'
|
|
14
14
|
import { capabilityCommandReceipt } from './internal/capability-command-receipt.js'
|
|
15
15
|
import { toolSdkSchemas } from './internal/program-bindings.js'
|
|
16
|
+
import { hostTransportDefinition, hostTransportUnreadable } from './internal/host-tool-sdk.js'
|
|
16
17
|
import { createRuntimeMessageOwner, handBackRuntimeSessions } from './internal/runtime-contexts.js'
|
|
17
|
-
import { createSessionLogOwner } from './internal/session-log-view.js'
|
|
18
|
+
import { createSessionLogOwner, projectionRegistrationDisposer } from './internal/session-log-view.js'
|
|
18
19
|
import { PTC_DELIVERY_CONTEXT } from './internal/runtime-messages.js'
|
|
19
20
|
import { createCordisToolsOwner } from './internal/cordis-tools-owner.js'
|
|
20
21
|
import { createEditTransportOwner, EDIT_RUN_CODE } from './internal/edit-transport-owner.js'
|
|
21
22
|
import { createRuntimeBridgeOwner, RUN_CODE } from './internal/runtime-bridge-owner.js'
|
|
22
23
|
import { resolveConfig, watchVolatileConfig } from './internal/runtime-config.js'
|
|
24
|
+
import { configureRetention } from './internal/projection-retention.js'
|
|
23
25
|
import {
|
|
24
26
|
CONFIG_FIELDS,
|
|
25
27
|
SETTINGS_NAMESPACE,
|
|
@@ -31,10 +33,12 @@ import { valueLimitsFromConfig } from './internal/value-wire-schema.js'
|
|
|
31
33
|
import { createReplMemoryProjection } from './internal/repl-memory-projection.js'
|
|
32
34
|
import { createUserBindingDraftProjection } from './internal/user-binding-draft-projection.js'
|
|
33
35
|
import { createUserBindingsOwner } from './internal/user-bindings-owner.js'
|
|
36
|
+
import { latestBindingAdoption } from './internal/binding-adoption.js'
|
|
34
37
|
import { createHostRpc } from './internal/host-rpc.js'
|
|
35
38
|
import { RPC_CONTRACTS } from './internal/rpc-contract.js'
|
|
36
39
|
import { createReplObservationInterest } from './internal/repl-observation-interest.js'
|
|
37
40
|
import { guidancePolicies } from './internal/binding-update-policy.js'
|
|
41
|
+
import { PROMPT_SLOT_IDS, resolvePromptTextConfig } from './internal/prompt-text.js'
|
|
38
42
|
import * as dshSettings from '@deepseek-ai/dsh-settings'
|
|
39
43
|
|
|
40
44
|
const INSTALL_CLEANUP = Symbol('ptc-plus install cleanup')
|
|
@@ -59,7 +63,7 @@ export const Config = Schema.object(Object.fromEntries(
|
|
|
59
63
|
*/
|
|
60
64
|
export const inject = ['tools', 'systemPrompt', 'agents']
|
|
61
65
|
|
|
62
|
-
function replGuidance({ bindingPolicy, rewritesEnabled, languageSemantics, durableReplay, cordisToolsEnabled }) {
|
|
66
|
+
function replGuidance({ bindingPolicy, rewritesEnabled, languageSemantics, durableReplay, cordisToolsEnabled }, promptText) {
|
|
63
67
|
const looseTopLevelRedeclarations = bindingPolicy.variableRedeclarations
|
|
64
68
|
const looseTopLevelFunctionClassRedeclarations = bindingPolicy.functionClassRedeclarations
|
|
65
69
|
const autoRewriteImports = rewritesEnabled.autoRewriteImports
|
|
@@ -92,22 +96,35 @@ function replGuidance({ bindingPolicy, rewritesEnabled, languageSemantics, durab
|
|
|
92
96
|
const cordisRecovery = cordisToolsEnabled
|
|
93
97
|
? 'Keep large Cordis plugin source in a binding before the tool call so it can be reused after an error. A Cordis parse or validation failure can follow external effects; repeat the call only when its retry rules and current results establish that it is safe.'
|
|
94
98
|
: ''
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
99
|
+
// Each paragraph is an editable asset; a derived policy sentence is appended to
|
|
100
|
+
// the paragraph that owns it, so rewriting prose can never contradict the
|
|
101
|
+
// configuration that produced it. Omitting a paragraph keeps its derived clause.
|
|
102
|
+
const prose = id => (promptText.omitted(id) ? '' : promptText.text(id))
|
|
103
|
+
const withDerived = (id, derived) => {
|
|
104
|
+
const base = prose(id)
|
|
105
|
+
return base === '' ? derived : `${base} ${derived}`
|
|
106
|
+
}
|
|
107
|
+
const paragraphs = [
|
|
108
|
+
prose(PROMPT_SLOT_IDS.guidanceOpening),
|
|
109
|
+
prose(PROMPT_SLOT_IDS.guidanceErrorRecovery),
|
|
110
|
+
withDerived(PROMPT_SLOT_IDS.guidanceCellConventions, cellConventions),
|
|
111
|
+
prose(PROMPT_SLOT_IDS.guidanceCapabilities),
|
|
112
|
+
withDerived(PROMPT_SLOT_IDS.guidanceExecutionWorld, `${recovery}${cordisToolsEnabled ? ` ${cordisRecovery}` : ''}`),
|
|
113
|
+
]
|
|
114
|
+
return paragraphs.filter(paragraph => paragraph.trim().length > 0).join('\n\n')
|
|
115
|
+
}
|
|
105
116
|
|
|
106
|
-
|
|
117
|
+
/**
|
|
118
|
+
* The reserved `run_code` transport definition, from whichever surface this
|
|
119
|
+
* host publishes it on. Session execution is available exactly when one of
|
|
120
|
+
* those surfaces answers.
|
|
121
|
+
*/
|
|
122
|
+
function runCodeDefinitionFor(tools, scope) {
|
|
123
|
+
return hostTransportDefinition(tools, scope, RUN_CODE)
|
|
107
124
|
}
|
|
108
125
|
|
|
109
126
|
/** Register the session-bound REPL runtime. */
|
|
110
|
-
function installPtCRuntime(ctx, resolvedConfig, seam, toolSchemasForAgent, toolSdkSchemasForAgent, sessionId, sessionLog, settingsForCommand) {
|
|
127
|
+
function installPtCRuntime(ctx, resolvedConfig, seam, toolSchemasForAgent, toolSdkSchemasForAgent, sessionId, sessionLog, settingsForCommand, readPromptText) {
|
|
111
128
|
const presentationGeneration = randomUUID()
|
|
112
129
|
const replMemoryProjection = createReplMemoryProjection(presentationGeneration)
|
|
113
130
|
const userBindingDraftProjection = createUserBindingDraftProjection(presentationGeneration)
|
|
@@ -241,11 +258,17 @@ function installPtCRuntime(ctx, resolvedConfig, seam, toolSchemasForAgent, toolS
|
|
|
241
258
|
const cancelCordisActivation = () => pendingCordisActivation?.cancel()
|
|
242
259
|
const registerProjection = (projection, label) => {
|
|
243
260
|
try {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
261
|
+
// A newer host returns a ProjectionHandle (`stateOf`/`readerFor`/`dispose`,
|
|
262
|
+
// possibly async); earlier hosts returned a disposer. Accept either shape so
|
|
263
|
+
// the projection is not silently dropped on the newer contract.
|
|
264
|
+
const dispose = projectionRegistrationDisposer(projectionService?.register?.(projection))
|
|
265
|
+
if (dispose === undefined) {
|
|
266
|
+
throw new Error('ptc-plus: sessionProjections.register returned neither a disposer nor a handle')
|
|
247
267
|
}
|
|
248
|
-
|
|
268
|
+
// Unregistration failures belong to the caller that is suspending or
|
|
269
|
+
// reloading the projection: it retries and reports them. Swallowing one
|
|
270
|
+
// here would turn a failed disposal into a silent duplicate projection.
|
|
271
|
+
return () => dispose()
|
|
249
272
|
} catch (error) {
|
|
250
273
|
ctx.logger?.warn?.(`ptc-plus: ${label} projection unavailable`, error)
|
|
251
274
|
return undefined
|
|
@@ -342,9 +365,13 @@ function installPtCRuntime(ctx, resolvedConfig, seam, toolSchemasForAgent, toolS
|
|
|
342
365
|
readSessionLog: sessionLog.read,
|
|
343
366
|
enabled: activeConfig.userBindingsEnabled,
|
|
344
367
|
draftProjectionAvailable,
|
|
368
|
+
// A session's adoption is its own recorded decision, so it is read from
|
|
369
|
+
// that session's log instead of a process-local map.
|
|
370
|
+
bindingAdoptionForAgent: agent => latestBindingAdoption(sessionLog.project(agent)?.ptcMessages ?? []),
|
|
345
371
|
maxWallMs: activeConfig.maxWallMs,
|
|
346
372
|
maxOutputBytes: activeConfig.maxOutputBytes,
|
|
347
373
|
maxOldGenerationSizeMb: activeConfig.maxOldGenerationSizeMb,
|
|
374
|
+
workerReclaimMs: activeConfig.workerReclaimMs,
|
|
348
375
|
valueLimits: valueLimitsFromConfig(activeConfig),
|
|
349
376
|
})
|
|
350
377
|
capabilityEnvironment = createCapabilityEnvironment({ ctx, config: activeConfig,
|
|
@@ -353,6 +380,7 @@ function installPtCRuntime(ctx, resolvedConfig, seam, toolSchemasForAgent, toolS
|
|
|
353
380
|
runtimeBridge = createRuntimeBridgeOwner({
|
|
354
381
|
ctx,
|
|
355
382
|
seam,
|
|
383
|
+
readPromptText,
|
|
356
384
|
observeSession: id => observationInterest.has(id),
|
|
357
385
|
readSessionLog: sessionLog.read,
|
|
358
386
|
sessionConfig: activeConfig,
|
|
@@ -375,6 +403,7 @@ function installPtCRuntime(ctx, resolvedConfig, seam, toolSchemasForAgent, toolS
|
|
|
375
403
|
presentationGeneration,
|
|
376
404
|
sessionId,
|
|
377
405
|
toolSchemasForAgent,
|
|
406
|
+
promptText: resolvePromptTextConfig(activeConfig),
|
|
378
407
|
})
|
|
379
408
|
directSurface = createDirectSurfaceOwner({
|
|
380
409
|
sessionLogView: sessionLog.project,
|
|
@@ -384,7 +413,8 @@ function installPtCRuntime(ctx, resolvedConfig, seam, toolSchemasForAgent, toolS
|
|
|
384
413
|
sessionId,
|
|
385
414
|
toolSchemasForAgent,
|
|
386
415
|
capabilityEnvironment,
|
|
387
|
-
|
|
416
|
+
declaresRunCode: agent => runCodeDefinitionFor(ctx.tools, agent) !== undefined,
|
|
417
|
+
userBindingsForAgent: agent => userBindings.snapshotForAgent(agent),
|
|
388
418
|
setAgentPresentation: (agent, presentation) => (
|
|
389
419
|
userBindings.setAgentPresentation(agent, presentation)
|
|
390
420
|
),
|
|
@@ -393,25 +423,33 @@ function installPtCRuntime(ctx, resolvedConfig, seam, toolSchemasForAgent, toolS
|
|
|
393
423
|
name: 'tools:ptc-plus-repl',
|
|
394
424
|
order: 98,
|
|
395
425
|
text: context => {
|
|
396
|
-
if (ctx.tools
|
|
397
|
-
return replGuidance(guidancePolicies(activeConfig))
|
|
426
|
+
if (runCodeDefinitionFor(ctx.tools, context?.scope) === undefined) return ''
|
|
427
|
+
return replGuidance(guidancePolicies(activeConfig), resolvePromptTextConfig(activeConfig))
|
|
398
428
|
},
|
|
399
429
|
}))
|
|
430
|
+
let unreadableTransportReported = false
|
|
400
431
|
disposers.push(ctx.on('system-prompt/assemble', async (assembly, context, next) => {
|
|
401
432
|
const projected = await directSurface.assemble(assembly, context, next)
|
|
402
|
-
if (ctx.tools
|
|
433
|
+
if (runCodeDefinitionFor(ctx.tools, context?.agent) !== undefined) runtimeBridge.prepareAgent(context?.agent)
|
|
434
|
+
else if (!unreadableTransportReported && hostTransportUnreadable(ctx.tools, context?.agent)) {
|
|
435
|
+
// The host declares the reserved transport and this plugin cannot read it.
|
|
436
|
+
// Every gate that needs execution answers "unavailable", so the operator is
|
|
437
|
+
// told which of the two facts holds instead of finding the features gone.
|
|
438
|
+
unreadableTransportReported = true
|
|
439
|
+
ctx.logger?.warn?.('ptc-plus: the host tool executor declares run_code but publishes no transport definition this plugin can read; session execution stays unavailable')
|
|
440
|
+
}
|
|
403
441
|
return projected
|
|
404
442
|
}))
|
|
405
443
|
disposers.push(() => capabilityEnvironment.dispose())
|
|
406
444
|
disposers.push(ctx.on('agent/pre-step', (payload, next) => capabilityEnvironment.preStep(payload, next), { prepend: true }))
|
|
407
445
|
disposers.push(ctx.on('agent/created', ({ agent }) => {
|
|
408
|
-
if (ctx.tools
|
|
446
|
+
if (runCodeDefinitionFor(ctx.tools, agent) !== undefined) {
|
|
409
447
|
runtimeBridge.prepareAgent(agent)
|
|
410
448
|
editTransport.ensureInstalled(agent)
|
|
411
449
|
}
|
|
412
450
|
}))
|
|
413
451
|
for (const agent of ctx.agents?.list?.() ?? []) {
|
|
414
|
-
if (ctx.tools
|
|
452
|
+
if (runCodeDefinitionFor(ctx.tools, agent) !== undefined) {
|
|
415
453
|
runtimeBridge.prepareAgent(agent)
|
|
416
454
|
editTransport.ensureInstalled(agent)
|
|
417
455
|
}
|
|
@@ -440,17 +478,22 @@ function installPtCRuntime(ctx, resolvedConfig, seam, toolSchemasForAgent, toolS
|
|
|
440
478
|
input: { hint: 'refresh | list [all|skills|tools|mcp|mcp_tools] [query] | set session|global <id> [availability|disclosure] <choice>' },
|
|
441
479
|
handler: async ({ commandId, agent, rawInput, signal }) => {
|
|
442
480
|
const input = rawInput.trim()
|
|
443
|
-
const receipt = (state, error) => capabilityCommandReceipt(state, String(agent.session.id), error,
|
|
481
|
+
const receipt = (state, error, proof) => capabilityCommandReceipt(state, String(agent.session.id), error,
|
|
444
482
|
state === 'pending' && capabilityEnvironment.pendingChoiceMessage(agent)
|
|
445
|
-
? { commandId, rawInput, message: capabilityEnvironment.pendingChoiceMessage(agent) } : undefined
|
|
483
|
+
? { commandId, rawInput, message: capabilityEnvironment.pendingChoiceMessage(agent) } : undefined,
|
|
484
|
+
proof)
|
|
446
485
|
const selection = /^set (session|global) (\S+) (?:(availability|disclosure) )?(full|on-demand|closed|automatic|inherit|enabled|host|adaptive)$/.exec(input)
|
|
447
486
|
if (selection) {
|
|
448
487
|
if (!capabilityEnvironment.canRefresh(agent)) return { kind: 'error', text: receipt('unavailable') }
|
|
449
488
|
const changed = await capabilityEnvironment.change(agent, {
|
|
450
489
|
scope: selection[1], id: decodeURIComponent(selection[2]), dimension: selection[3], selection: selection[4], commandId,
|
|
451
490
|
}, 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]))
|
|
452
495
|
return { kind: changed.sessionApplied ? 'success' : 'error',
|
|
453
|
-
text: receipt(changed.sessionApplied ? changed.publication === 'pending' ? 'pending' : 'recorded' : 'saved-only', changed.error) }
|
|
496
|
+
text: receipt(changed.sessionApplied ? changed.publication === 'pending' ? 'pending' : 'recorded' : 'saved-only', changed.error, proof) }
|
|
454
497
|
}
|
|
455
498
|
const listing = /^list(?: (all|skills|tools|mcp|mcp_tools))?(?: (.*))?$/.exec(input)
|
|
456
499
|
if (listing) {
|
|
@@ -624,8 +667,11 @@ export const apply = (ctx, config = {}) => {
|
|
|
624
667
|
// Configuration is validated before the plugin waits for the host's seam, so
|
|
625
668
|
// an unusable configuration fails at the load call rather than later.
|
|
626
669
|
const resolvedConfig = resolveConfig(config)
|
|
670
|
+
// The retention window is user configuration; install it before any fold can run.
|
|
671
|
+
configureRetention(resolvedConfig)
|
|
627
672
|
return installExecutionSeam(ctx, {
|
|
628
673
|
services: inject,
|
|
674
|
+
attachmentTimeoutMs: resolvedConfig.executionSeamTimeoutMs,
|
|
629
675
|
attach: (scope, seam) => applyWithExecutionSeam(scope, resolvedConfig, seam, ctx, config),
|
|
630
676
|
reportFailure: (error, seamName) => ctx.logger?.warn?.(
|
|
631
677
|
`ptc-plus: execution seam ${seamName} failed to attach`,
|
|
@@ -719,7 +765,9 @@ function applyWithExecutionSeam(ctx, resolvedConfig, seam, volatileHost, volatil
|
|
|
719
765
|
// Execution ownership is a per-attachment property: `owned` installs the
|
|
720
766
|
// session runtime, `deferred` leaves run_code to the official provider.
|
|
721
767
|
const executionShape = nextConfig => (
|
|
722
|
-
defersToOfficialRepl(nextConfig,
|
|
768
|
+
defersToOfficialRepl(nextConfig, {
|
|
769
|
+
get retainsState() { return seam.originalRetainsState },
|
|
770
|
+
}) ? 'deferred' : 'owned'
|
|
723
771
|
)
|
|
724
772
|
const startActivation = record => {
|
|
725
773
|
if (record.cancelled || disposed || activating !== record) {
|
|
@@ -733,7 +781,7 @@ function applyWithExecutionSeam(ctx, resolvedConfig, seam, volatileHost, volatil
|
|
|
733
781
|
try {
|
|
734
782
|
candidate = record.shape === 'deferred'
|
|
735
783
|
? createDeferredRuntime()
|
|
736
|
-
: installPtCRuntime(ctx, record.config, seam, toolSchemasForAgent, toolSdkSchemasForAgent, sessionId, sessionLog, () => settingsWriter)
|
|
784
|
+
: installPtCRuntime(ctx, record.config, seam, toolSchemasForAgent, toolSdkSchemasForAgent, sessionId, sessionLog, () => settingsWriter, currentPromptText)
|
|
737
785
|
record.runtime = candidate
|
|
738
786
|
} catch (error) {
|
|
739
787
|
const failedRuntime = error?.[INSTALL_CLEANUP]
|
|
@@ -886,9 +934,17 @@ function applyWithExecutionSeam(ctx, resolvedConfig, seam, volatileHost, volatil
|
|
|
886
934
|
ctx.effect(() => async () => controller.dispose(), 'ptc-plus runtime lifecycle')
|
|
887
935
|
|
|
888
936
|
const handbackOwners = Object.fromEntries(['sessions', 'sessionPersistence', 'sessionQuery'].map(name => [name, ctx.get(name)]))
|
|
937
|
+
// One spelling of "the current document": the same resolved configuration the
|
|
938
|
+
// reconciliation loop applies, so a delivered message and the assembled prompt can
|
|
939
|
+
// never read two different prompt-text documents. One holder per attachment, handed
|
|
940
|
+
// to the runtime it installs, so two attachments in one process keep their own
|
|
941
|
+
// document. Read lazily: the settings section replaces `configSource` after
|
|
942
|
+
// activation.
|
|
943
|
+
const currentPromptText = () => resolvePromptTextConfig(configSource())
|
|
889
944
|
const messageOwner = createRuntimeMessageOwner(context => (
|
|
890
945
|
committed?.retiring ? [] : committed?.runtime.contextsForRequest(context) ?? []
|
|
891
|
-
), sessionLog.project, () => committed !== undefined && committed.retiring !== true, handbackOwners.sessionQuery
|
|
946
|
+
), sessionLog.project, () => committed !== undefined && committed.retiring !== true, handbackOwners.sessionQuery,
|
|
947
|
+
currentPromptText)
|
|
892
948
|
ctx.effect(() => ctx.systemPrompt.context({
|
|
893
949
|
name: PTC_DELIVERY_CONTEXT, order: 98, text: '',
|
|
894
950
|
}), 'ptc-plus dynamic message delivery witness')
|
|
@@ -901,10 +957,21 @@ function applyWithExecutionSeam(ctx, resolvedConfig, seam, volatileHost, volatil
|
|
|
901
957
|
ctx.effect(() => ctx.on('agent/disposed', ({ agent }) => messageOwner.disposeAgent(agent)), 'ptc-plus message agent disposal')
|
|
902
958
|
ctx.effect(() => () => messageOwner.dispose(), 'ptc-plus dynamic message lifecycle')
|
|
903
959
|
ctx.effect(() => async () => {
|
|
904
|
-
|
|
960
|
+
// The model-visible handback states a fact about execution, so it must not
|
|
961
|
+
// ride on whether every projection unregistered: a failed disposal is
|
|
962
|
+
// reported by the caller that retries it, and the runtime is gone either way.
|
|
963
|
+
let disposalFailure
|
|
964
|
+
try {
|
|
965
|
+
await controller.dispose()
|
|
966
|
+
} catch (error) {
|
|
967
|
+
disposalFailure = error
|
|
968
|
+
}
|
|
905
969
|
await handBackRuntimeSessions(ctx, handbackOwners)
|
|
970
|
+
if (disposalFailure !== undefined) throw disposalFailure
|
|
906
971
|
}, 'ptc-plus provider handback')
|
|
907
972
|
|
|
973
|
+
// The settings section replaces this source after activation; the prompt-text
|
|
974
|
+
// accessor above reads it lazily for exactly that reason.
|
|
908
975
|
let configSource = () => resolveConfig(volatileConfig ?? resolvedConfig)
|
|
909
976
|
let configurationGeneration = 0
|
|
910
977
|
let configurationRollback = false
|
|
@@ -935,6 +1002,7 @@ function applyWithExecutionSeam(ctx, resolvedConfig, seam, volatileHost, volatil
|
|
|
935
1002
|
}
|
|
936
1003
|
const reconcile = (propagateFailure = false) => {
|
|
937
1004
|
const current = resolveConfig(configSource())
|
|
1005
|
+
configureRetention(current)
|
|
938
1006
|
const generation = ++configurationGeneration
|
|
939
1007
|
if (!current.enabled) {
|
|
940
1008
|
const operation = controller.uninstall()
|
|
@@ -982,6 +1050,7 @@ function applyWithExecutionSeam(ctx, resolvedConfig, seam, volatileHost, volatil
|
|
|
982
1050
|
}
|
|
983
1051
|
ctx.effect(() => seam.onSourceChange(() => {
|
|
984
1052
|
const current = resolveConfig(configSource())
|
|
1053
|
+
configureRetention(current)
|
|
985
1054
|
if (!current.enabled) return undefined
|
|
986
1055
|
if (seam.language !== 'typescript') {
|
|
987
1056
|
const language = seam.language
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
|
2
|
+
import { isPtcMessageSource, PTC_MESSAGE_SOURCE_KIND } from './message-sources.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Session binding adoption: which Global User Binding catalog entries this
|
|
6
|
+
* session uses.
|
|
7
|
+
*
|
|
8
|
+
* The question is answered per session and per entry, and the answer is a
|
|
9
|
+
* decision of exactly two states: this session uses the entry, or it does not.
|
|
10
|
+
* An entry the session never named is used, because the global catalog already
|
|
11
|
+
* answered the supply question. That default is the absence of a decision, not
|
|
12
|
+
* a third state: "use" and "never mentioned" lead to the same environment, so
|
|
13
|
+
* they are one state rather than two.
|
|
14
|
+
*
|
|
15
|
+
* Adoption can only take an entry away from this session. It cannot supply one,
|
|
16
|
+
* enable one, or name a source: an entry the global configuration disabled is
|
|
17
|
+
* absent from the catalog, so "adoption revives a deleted entry" has no
|
|
18
|
+
* encoding here and needs no rule forbidding it. Naming an entry the catalog
|
|
19
|
+
* does not offer is rejected at the surface that states the decision, because
|
|
20
|
+
* the record would otherwise claim authority over supply.
|
|
21
|
+
*
|
|
22
|
+
* The neighbour this must not be confused with is the capability pane's
|
|
23
|
+
* `automatic`, which means "the host derives this value". A binding has no host
|
|
24
|
+
* derivation to defer to, so borrowing that word would misdescribe both.
|
|
25
|
+
*
|
|
26
|
+
* The decision is recorded as one PTC message in the session log, so it is
|
|
27
|
+
* reconstructed by replaying that log after a restart instead of being kept in
|
|
28
|
+
* process memory.
|
|
29
|
+
*/
|
|
30
|
+
export const BINDING_ADOPTION_FORM = 'binding-adoption'
|
|
31
|
+
export const BINDING_ADOPTION_SCHEMA = 'ptc-binding-adoption/v1'
|
|
32
|
+
|
|
33
|
+
const MAX_DECLINED = 64
|
|
34
|
+
const ENTRY_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/
|
|
35
|
+
const PREFIX = 'PTC Plus session binding adoption. This session uses every entry the global catalog supplies except the declined ones. Adoption can only withdraw an entry from this session: it cannot supply, enable or revive one, and the global configuration decides which entries exist at all. Declined entry IDs:'
|
|
36
|
+
|
|
37
|
+
function isRecord(value) {
|
|
38
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function sortIds(ids) {
|
|
42
|
+
return [...ids].sort((left, right) => left < right ? -1 : left > right ? 1 : 0)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function normalizeBindingAdoption(value) {
|
|
46
|
+
if (!isRecord(value) || Reflect.ownKeys(value).length !== 2
|
|
47
|
+
|| !Object.hasOwn(value, 'schema') || !Object.hasOwn(value, 'declined')) {
|
|
48
|
+
throw new TypeError('invalid binding adoption')
|
|
49
|
+
}
|
|
50
|
+
if (value.schema !== BINDING_ADOPTION_SCHEMA) throw new TypeError('invalid binding adoption schema')
|
|
51
|
+
if (!Array.isArray(value.declined) || value.declined.length > MAX_DECLINED) {
|
|
52
|
+
throw new TypeError('invalid binding adoption declined entries')
|
|
53
|
+
}
|
|
54
|
+
const declined = []
|
|
55
|
+
for (const id of value.declined) {
|
|
56
|
+
if (typeof id !== 'string' || !ENTRY_ID.test(id)) throw new TypeError('invalid binding adoption entry id')
|
|
57
|
+
if (declined.includes(id)) throw new TypeError('duplicate binding adoption entry id')
|
|
58
|
+
declined.push(id)
|
|
59
|
+
}
|
|
60
|
+
return Object.freeze({ schema: BINDING_ADOPTION_SCHEMA, declined: Object.freeze(sortIds(declined)) })
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The exact model-visible wording one adoption record carries. */
|
|
64
|
+
export function bindingAdoptionText(adoption) {
|
|
65
|
+
return `${PREFIX}\n${JSON.stringify(normalizeBindingAdoption(adoption).declined)}`
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function bindingAdoptionMessage(adoption) {
|
|
69
|
+
const text = bindingAdoptionText(adoption)
|
|
70
|
+
const message = createUserMessage({
|
|
71
|
+
source: { kind: PTC_MESSAGE_SOURCE_KIND, form: BINDING_ADOPTION_FORM },
|
|
72
|
+
content: [{ type: 'text', text }],
|
|
73
|
+
})
|
|
74
|
+
if (readBindingAdoption(message) === undefined) throw new TypeError('invalid binding adoption message')
|
|
75
|
+
return message
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Read one adoption record back from a message, message projection or message fact. */
|
|
79
|
+
export function readBindingAdoption(value) {
|
|
80
|
+
const source = value?.source
|
|
81
|
+
if (!isPtcMessageSource(source) || source.form !== BINDING_ADOPTION_FORM) return undefined
|
|
82
|
+
if (!Array.isArray(value.content) || value.content.length !== 1 || value.content[0]?.type !== 'text') return undefined
|
|
83
|
+
const text = value.content[0].text
|
|
84
|
+
if (typeof text !== 'string' || !text.startsWith(`${PREFIX}\n`)) return undefined
|
|
85
|
+
try {
|
|
86
|
+
const declined = JSON.parse(text.slice(PREFIX.length + 1))
|
|
87
|
+
const adoption = normalizeBindingAdoption({ schema: BINDING_ADOPTION_SCHEMA, declined })
|
|
88
|
+
return bindingAdoptionText(adoption) === text ? adoption : undefined
|
|
89
|
+
} catch {
|
|
90
|
+
return undefined
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The newest adoption this session recorded, or `undefined` while it recorded
|
|
96
|
+
* none. The newest record replaces earlier ones, because stating a decision
|
|
97
|
+
* again is how a session changes its mind.
|
|
98
|
+
*/
|
|
99
|
+
export function latestBindingAdoption(records) {
|
|
100
|
+
if (!Array.isArray(records)) return undefined
|
|
101
|
+
for (let index = records.length - 1; index >= 0; index -= 1) {
|
|
102
|
+
const record = records[index]
|
|
103
|
+
if (record?.form !== BINDING_ADOPTION_FORM) continue
|
|
104
|
+
try {
|
|
105
|
+
return normalizeBindingAdoption({ schema: BINDING_ADOPTION_SCHEMA, declined: record.declined })
|
|
106
|
+
} catch {
|
|
107
|
+
// A record that cannot be read is not evidence for an earlier record
|
|
108
|
+
// either: the newest statement is the current one, and it is unreadable.
|
|
109
|
+
return undefined
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return undefined
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The declined entry IDs of one adoption, as a set the environment uses.
|
|
117
|
+
* An adoption that was never stated declines nothing.
|
|
118
|
+
*/
|
|
119
|
+
export function declinedBindingEntries(adoption) {
|
|
120
|
+
return new Set(adoption === undefined ? [] : normalizeBindingAdoption(adoption).declined)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** State one entry as used (`adopt` true) or not used, leaving the rest alone. */
|
|
124
|
+
export function withBindingAdoption(adoption, entryId, adopt) {
|
|
125
|
+
if (typeof entryId !== 'string' || !ENTRY_ID.test(entryId)) throw new TypeError('invalid binding adoption entry id')
|
|
126
|
+
if (typeof adopt !== 'boolean') throw new TypeError('binding adoption decision must be a boolean')
|
|
127
|
+
const declined = declinedBindingEntries(adoption)
|
|
128
|
+
if (adopt) declined.delete(entryId)
|
|
129
|
+
else declined.add(entryId)
|
|
130
|
+
return normalizeBindingAdoption({ schema: BINDING_ADOPTION_SCHEMA, declined: [...declined] })
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Drop every declined entry: this session again uses the whole catalog. */
|
|
134
|
+
export function clearedBindingAdoption() {
|
|
135
|
+
return normalizeBindingAdoption({ schema: BINDING_ADOPTION_SCHEMA, declined: [] })
|
|
136
|
+
}
|