dsh-ptc-plus 0.4.3 → 0.4.8
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 +4 -0
- package/README.md +4 -0
- package/client.js +6 -4
- package/docs/README.md +1 -0
- package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +41 -4
- package/docs/adr/0017-track-the-latest-dsh-public-surface.md +5 -1
- package/docs/adr/0019-plugin-settings-and-kill-switch.md +1 -1
- package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +2 -2
- package/docs/adr/0030-serve-both-typert-codec-generations-from-one-codec.md +2 -2
- package/docs/architecture.md +3 -1
- package/docs/installation.md +26 -8
- package/docs/publishing.md +4 -4
- package/docs/release-notes.md +109 -0
- package/docs/runtime-reference.md +7 -1
- package/docs/verification.md +5 -1
- package/internal/cordis-tools-owner.js +72 -16
- package/internal/direct-surface-owner.js +331 -7
- package/internal/rejected-cell-editor.js +11 -21
- package/package.json +8 -2
- package/src/client-workbench.js +6 -4
package/README.en.md
CHANGED
|
@@ -41,6 +41,8 @@ With the default interface settings, the session has a **REPL** tab; wide screen
|
|
|
41
41
|
|
|
42
42
|
Once installed, describe your task to the model as usual. The `run_code` and `edit_run_code` examples below show how the model uses these features. See the [installation guide](docs/installation.md) for other installation methods, Desktop, the local development launcher, upgrades, and troubleshooting.
|
|
43
43
|
|
|
44
|
+
For Windows source testing, double-click `scripts\run-upstream-dsh.cmd` to fetch the official DSH default branch, reuse an isolated build cache, and load this plugin checkout. The first run installs dependencies and builds DSH; see [upstream source testing](docs/installation.md#upstream-source-development-launcher-windows).
|
|
45
|
+
|
|
44
46
|
## Feature overview
|
|
45
47
|
|
|
46
48
|
Default PTC mode starts every execution in a fresh environment: variables from the previous call are gone, and a change that depends on earlier results means resending the setup code. PTC Plus turns `run_code` into a session-bound TypeScript REPL and closes the gaps below.
|
|
@@ -52,6 +54,7 @@ Default PTC mode starts every execution in a fresh environment: variables from t
|
|
|
52
54
|
| Change a small part of the code | A one-line change means resending the complete source | Submit a delta through `edit_run_code`, then execute the complete revised cell |
|
|
53
55
|
| Use module syntax | Static `import` and `export` are invalid inside the function body | Write them directly in a cell; the plugin adapts the module syntax |
|
|
54
56
|
| Omit a call summary | Missing `run_code.description` fails validation | Supply a display summary automatically so otherwise valid code proceeds |
|
|
57
|
+
| Provider rejects malformed tool JSON | The whole turn ends with a provider error | Repair or preserve a buffered tool-call tail containing only tool framing and at most one usage event; route every other buffered tail through schema rejection so the model continues |
|
|
55
58
|
| Recover a misplaced top-level tool call | Undeclared top-level calls are rejected in PTC mode | Convert the call to `run_code` when the live tool definitions uniquely identify the target and validate its arguments |
|
|
56
59
|
| Locate a code error | Return the native error and stack | Map the error to the cell source; suggest an edit when a missing trailing delimiter has one verified correction |
|
|
57
60
|
| Work with the current project | Relative Node paths depend on the host process directory | Resolve file paths, session module imports, and default child-process directories from the session project directory |
|
|
@@ -131,6 +134,7 @@ An edit **reruns the entire cell**; this example returns `12`. Earlier state cha
|
|
|
131
134
|
| --- | --- |
|
|
132
135
|
| `run_code` omits its outer `description` | Supply a display summary so code with otherwise valid arguments can execute |
|
|
133
136
|
| The model calls a native tool outside the PTC direct-tool list | Convert it to the corresponding `run_code` when current tool definitions uniquely identify the target and validate the arguments |
|
|
137
|
+
| Argument validation reports a missing `description` | Distinguish outer and nested tool arguments and provide correction guidance, saved as a complete message so session history remains loadable |
|
|
134
138
|
| Code fails to parse | Identify the source position; at EOF, suggest a target-bound edit if appending one or two closing delimiters has exactly one validated correction |
|
|
135
139
|
|
|
136
140
|
The first two behaviors default to on and can be disabled in settings. Syntax suggestions do not execute automatically or establish that the code matches the task's intent.
|
package/README.md
CHANGED
|
@@ -41,6 +41,8 @@ dsh plugin --profile <profile> add dsh-ptc-plus
|
|
|
41
41
|
|
|
42
42
|
安装完成后正常向模型描述任务即可,下面的 `run_code` 和 `edit_run_code` 示例说明模型如何使用这些能力。其他安装方式、Desktop、本地开发启动器、升级和故障排查见[安装指南](docs/installation.md)。
|
|
43
43
|
|
|
44
|
+
Windows 源码测试:双击 `scripts\run-upstream-dsh.cmd`,拉取官方 DSH 默认分支并复用隔离的构建缓存,加载当前插件工作区。首次运行需要安装依赖和构建;详见[上游源码测试](docs/installation.md#upstream-source-development-launcher-windows)。
|
|
45
|
+
|
|
44
46
|
## 功能概览
|
|
45
47
|
|
|
46
48
|
默认 PTC 模式的每次调用都从新环境开始:上次的变量不再存在,改动依赖前面的结果时只能重发准备代码。PTC Plus 把 `run_code` 变成会话级的 TypeScript REPL,补上下面的落差。
|
|
@@ -52,6 +54,7 @@ dsh plugin --profile <profile> add dsh-ptc-plus
|
|
|
52
54
|
| 只改一小段代码 | 改动一行也要重新发送整段源码 | `edit_run_code` 只提交差异,再执行修改后的完整代码 |
|
|
53
55
|
| 使用模块语法 | 函数体里不能直接写静态 `import` / `export` | 在 cell 中直接写,插件自动适配模块语法 |
|
|
54
56
|
| 调用漏填摘要 | 缺少 `run_code.description` 时校验失败 | 自动补充显示摘要,其余参数合法的代码继续执行 |
|
|
57
|
+
| 提供方拒绝畸形工具 JSON | 整个 turn 以 provider 错误终止 | 缓冲的工具调用尾部仅含工具块和至多一个用量事件时可修复或保留参数;其他缓冲尾部转为 schema 拒绝结果,模型继续下一步 |
|
|
55
58
|
| 误发顶层工具调用 | PTC 模式下未声明的顶层调用被拒绝 | 当前工具定义能唯一确认目标且参数合法时,转为对应的 `run_code` |
|
|
56
59
|
| 定位代码错误 | 只返回原生错误与堆栈 | 对应到 cell 源码位置;末尾缺少闭合符且修正可唯一验证时给出编辑建议 |
|
|
57
60
|
| 访问当前项目 | 相对路径按宿主进程目录解析 | 按会话项目目录解析文件路径、会话模块导入和默认子进程目录 |
|
|
@@ -131,6 +134,7 @@ edit_run_code({
|
|
|
131
134
|
| --- | --- |
|
|
132
135
|
| `run_code` 漏填外层 `description` | 补充显示摘要,让其余参数合法的代码继续执行 |
|
|
133
136
|
| 模型误发 PTC 直接工具列表之外的原生工具调用 | 当前工具定义能唯一确认目标且参数合法时,转为对应的 `run_code` |
|
|
137
|
+
| 参数校验报告缺少 `description` | 区分外层与嵌套工具参数并给出修正提示;提示以完整消息保存,保持会话历史可加载 |
|
|
134
138
|
| 代码解析失败 | 标出源码位置;若错误在末尾,且追加一到两个闭合符只有一种通过验证的修正,给出绑定原调用的编辑建议 |
|
|
135
139
|
|
|
136
140
|
前两项默认开启,可在设置中关闭。语法建议不会自动执行,也不代表插件能判断代码是否符合任务意图。
|
package/client.js
CHANGED
|
@@ -31321,10 +31321,12 @@
|
|
|
31321
31321
|
const content2 = React.useRef(null);
|
|
31322
31322
|
const localReturnFocus = React.useRef(null);
|
|
31323
31323
|
const returnFocus = returnFocusRef ?? localReturnFocus;
|
|
31324
|
-
React.
|
|
31324
|
+
const attachContent = React.useCallback((element) => {
|
|
31325
|
+
content2.current = element;
|
|
31326
|
+
if (element === null) return;
|
|
31325
31327
|
const previous = document.activeElement;
|
|
31326
|
-
if (!
|
|
31327
|
-
|
|
31328
|
+
if (!element.contains(previous) && canReturnFocus(previous)) returnFocus.current = previous;
|
|
31329
|
+
element.querySelector("button")?.focus();
|
|
31328
31330
|
}, [returnFocus]);
|
|
31329
31331
|
const close = () => {
|
|
31330
31332
|
const target = canReturnFocus(returnFocus.current) ? returnFocus.current : getReturnFocus?.();
|
|
@@ -31347,7 +31349,7 @@
|
|
|
31347
31349
|
{ open: true, onClose: close, title: t2("bindings.title"), headless: true, className: "ptcPlusBindingsModal" },
|
|
31348
31350
|
h(
|
|
31349
31351
|
"div",
|
|
31350
|
-
{ className: "ptcPlusBindingsDialog", ref:
|
|
31352
|
+
{ className: "ptcPlusBindingsDialog", ref: attachContent, onKeyDown: trapFocus },
|
|
31351
31353
|
h(
|
|
31352
31354
|
"div",
|
|
31353
31355
|
{ className: "ptcPlusBindingsDialogHead" },
|
package/docs/README.md
CHANGED
|
@@ -27,4 +27,5 @@
|
|
|
27
27
|
- [Focused Verification](verification-optimization-work.md): bounded coverage diagnostics and their relationship to the final verification gate.
|
|
28
28
|
- [Semantic Validation](semantic-validation.md): pinned Test262 cases, dialect contracts, native differential matrices and their limits.
|
|
29
29
|
- [Publishing](publishing.md): release checks, package contents, permissions, and platform validation.
|
|
30
|
+
- [GitHub Release Notes](release-notes.md): user-perspective priority order, required sections, bilingual style, and a worked example for release notes.
|
|
30
31
|
- [Client UI](client-ui.md): current UI boundary and reevaluation criteria.
|
|
@@ -68,8 +68,10 @@ session-log projection captures the eligible target at the edit call event and a
|
|
|
68
68
|
source only when its target sequence matches that snapshot and its journal is valid and non-noop,
|
|
69
69
|
so live and recovered eligibility derive from the same persisted relation.
|
|
70
70
|
|
|
71
|
-
The
|
|
72
|
-
`expected_target_call_seq`
|
|
71
|
+
The public schema is one closed object with optional `edits`, `regex_edits`, and
|
|
72
|
+
`expected_target_call_seq` properties, avoiding duplicated object alternatives in the provider
|
|
73
|
+
prompt. Runtime validation still requires exactly one of `edits` or `regex_edits`, plus an optional
|
|
74
|
+
non-negative `expected_target_call_seq` precondition. The precondition must equal the call sequence of the target
|
|
73
75
|
captured for the persisted edit event; a mismatch returns an unedited result before derived
|
|
74
76
|
dispatch. Ordinary model-authored edits omit it and retain latest-target selection. Exact edits
|
|
75
77
|
contain at most 16 items; each
|
|
@@ -95,11 +97,43 @@ tool/result: { edited: true, value?, error?, logs }
|
|
|
95
97
|
The complete derived source, its journal, and its target call sequence are stored only in private tool-result metadata, including
|
|
96
98
|
when the derived cell fails after entering the runtime. Cold
|
|
97
99
|
recovery folds that explicitly marked derived run into the durable REPL history. The source is not
|
|
98
|
-
copied into the model-visible edit result
|
|
100
|
+
copied into the model-visible edit result. Valid assistant stream chunks for the two declared
|
|
99
101
|
transports pass through unchanged.
|
|
100
102
|
Consequently the UI and later model requests do not attribute a generated `run_code` call to the
|
|
101
103
|
model.
|
|
102
104
|
|
|
105
|
+
The stream owner handles one bounded provider protocol failure before DSH assembles the assistant
|
|
106
|
+
message. It buffers tool-call material until the terminal event. If the response reports the exact
|
|
107
|
+
invalid-tool-JSON `MALFORMED_RESPONSE` as a terminal chunk or stream exception, it changes that
|
|
108
|
+
terminal reason to `tool-calls`.
|
|
109
|
+
DSH's existing Agent Loop
|
|
110
|
+
then preserves an unparseable argument as a string and routes it through ordinary tool validation,
|
|
111
|
+
execution, and result recording, after which the model receives another step. A malformed tool argument therefore
|
|
112
|
+
cannot become a turn-ending provider failure merely because this adapter parses it earlier than the
|
|
113
|
+
Agent Loop.
|
|
114
|
+
|
|
115
|
+
The stream owner preserves calls only when the buffered body consists entirely of
|
|
116
|
+
tool-call-specific framing plus at most one `usage` event. When that trusted body contains exactly
|
|
117
|
+
one complete, internally consistent declared `run_code` or `edit_run_code` block, the stream owner
|
|
118
|
+
first considers one correction: delete exactly one final `}`. The block index, call ID, name,
|
|
119
|
+
concatenated deltas, and final arguments must agree, and the candidate must parse as an object
|
|
120
|
+
accepted by that request's live direct-tool schema. A successful correction changes the final delta
|
|
121
|
+
and final block arguments before the same `tool-calls` settlement. Without that proof, trusted
|
|
122
|
+
internally consistent complete blocks retain their original identity and argument bytes for the
|
|
123
|
+
ordinary tool-error path. The owner re-emits those calls with canonical
|
|
124
|
+
`block-start`, full-argument delta, and `block-end` framing so provider variants tolerated by the
|
|
125
|
+
assembler also satisfy DSH's stream invariant. A final `block-end` is canonical even when the
|
|
126
|
+
provider emitted no deltas; when deltas exist, their supplied identity fields and accumulated
|
|
127
|
+
arguments must agree with it. An already visible open text or reasoning block is closed from its
|
|
128
|
+
accumulated content before the synthetic call. If any tool block is incomplete or inconsistent, or no tool block was
|
|
129
|
+
emitted, more than one usage event appears, or any other event occurs in the buffered body, the owner
|
|
130
|
+
removes unsafe block and unknown events, retains at most the first protocol-valid `usage`, and emits
|
|
131
|
+
one deterministic `run_code` call with an argument string that cannot parse as a JSON object. That
|
|
132
|
+
call can only fail DSH's ordinary schema
|
|
133
|
+
validation, so no untrusted partial call executes and the model still receives another step. Valid streams and every
|
|
134
|
+
other provider failure pass through unchanged. The rule does not insert missing delimiters, search
|
|
135
|
+
arbitrary edits, reinterpret native calls, or retry effects.
|
|
136
|
+
|
|
103
137
|
No edit-specific runtime context is emitted. The truthful call/result pair already carries the
|
|
104
138
|
operation and outcome; changing an aggregate runtime-context snapshot merely to restate that fact
|
|
105
139
|
would repeat unrelated policy context. Rewrite feedback and unrelated recovery tips retain their
|
|
@@ -140,7 +174,10 @@ claim that the earlier execution was undone or idempotent.
|
|
|
140
174
|
out-of-surface call are different protocol facts. Rewriting a real `edit_run_code` destroys a valid
|
|
141
175
|
identity; normalizing a live-schema-proven native miscall to the declared `run_code` transport gives
|
|
142
176
|
an otherwise invalid call its executable representation. Native-call canonicalization therefore
|
|
143
|
-
remains available
|
|
177
|
+
remains available. The bounded malformed-input correction likewise retains a declared call's identity.
|
|
178
|
+
Fallback settlement preserves arguments for trusted complete internally consistent blocks. Unsafe,
|
|
179
|
+
partial, or mixed buffered material is replaced by a validation-rejected `run_code` call only after
|
|
180
|
+
the provider itself reports the exact terminal protocol failure.
|
|
144
181
|
|
|
145
182
|
## Consequences
|
|
146
183
|
|
|
@@ -10,6 +10,10 @@ PTC Plus must remain independently installable on unmodified official DSH distri
|
|
|
10
10
|
|
|
11
11
|
The compatibility target is the latest available DSH release. Production behavior, configuration, tests, installation guidance, and current compatibility claims depend on public extension contracts and observed live schemas, never on a DSH version allowlist or comparison. An upstream default projection is not an extension ceiling: when public scoped registration and presentation support a plugin-owned capability, PTC Plus preserves that capability and its model-visible identity instead of deleting or renaming it to match the default. Deterministic normalization remains valid for a call that is outside the declared direct surface when the live schema proves one native target and the lowering preserves arguments, call identity, authority, and result semantics. Deterministic tests exercise the consumed contracts; model-backed and packaging acceptance runners use the installed DSH and record `dsh --version` in generated reports so failures remain attributable without turning the observed value into policy. Concrete DSH release numbers may appear only in source comments that identify historical integration or evaluation evidence and cannot influence execution or acceptance.
|
|
12
12
|
|
|
13
|
+
Release discovery is separated from candidate verification. The root manifest declares the official `next` channel for the DSH packages used by development and freezes the resolved cohort in the root lockfile. `compat/latest` declares only the official DSH meta-package on `latest` and freezes that complete graph in its own lockfile. Resolved versions in these generated lock snapshots are reproducibility evidence selected by the channel declarations; they are not compatibility allowlists or runtime branches. Ordinary `verify` and `check` runs make no registry request and fail quickly when the installed root graph differs from the root lock.
|
|
14
|
+
|
|
15
|
+
The scheduled baseline refresh queries the official registry. When either channel moves, it updates both lock snapshots on an automation-owned branch, opens or updates one pull request, and dispatches the normal CI workflow once for that frozen candidate. GitHub suppresses workflow runs from branch and pull-request events produced by the workflow's `GITHUB_TOKEN`, so the explicit dispatch is the candidate's only CI run. If neither tag moved, it does not resolve or install the dependency graph. The Host contract suite tests the packed plugin against the root `next` graph and creates a repository-external consumer from the tracked `latest` lock. That consumer exposes the public packages from the selected Host graph to the plugin and runs the execution-seam, RPC-codec, and compressed Session persistence contracts. No compatibility run independently selects tags for DSH service packages, because their dist-tags need not move as one cohort.
|
|
16
|
+
|
|
13
17
|
Preceding public contracts remain supported where capability detection is
|
|
14
18
|
unambiguous. Tools presentation is selected by the installed public schema;
|
|
15
19
|
Client contributions depend on their slots and supplied hooks rather than unused
|
|
@@ -69,4 +73,4 @@ capability, storage and observation rules independently of transport.
|
|
|
69
73
|
|
|
70
74
|
Feature completion requires evidence from the actual packaged plugin running on the selected unmodified Host, including the affected user flow. Component fixtures and public-interface unit tests validate narrower properties; they do not prove that a required renderer exists or is loaded in that distribution. A clean code review and passing deterministic checks are separate from product acceptance. Both must cover the final change before a feature is described as delivered; commit and publish authorization remain separate user decisions.
|
|
71
75
|
|
|
72
|
-
Each upstream DSH release becomes the immediate compatibility target and must pass the public-surface, packaging, and model-backed checks appropriate to the change. Users are not blocked by stale version metadata. Historical reports remain attributable through generated runtime metadata or source comments, while a passing older run never substitutes for validation against the latest release.
|
|
76
|
+
Each upstream DSH release becomes the immediate compatibility target and must pass the public-surface, packaging, and model-backed checks appropriate to the change. The automated pull request makes an upstream tag movement an explicit source candidate; the lockfile and CI result keep its proof reproducible after the registry moves again. Users are not blocked by stale version metadata. Historical reports remain attributable through generated runtime metadata or source comments, while a passing older run never substitutes for validation against the latest release.
|
|
@@ -8,7 +8,7 @@ Accepted
|
|
|
8
8
|
|
|
9
9
|
## Context
|
|
10
10
|
|
|
11
|
-
PTC Plus 0.1.0 deliberately had no Client UI. The runtime surface was the DSH PTC mode `run_code`/`edit_run_code` transport, and adding a settings bundle was judged not to improve the core REPL path. The plugin also lacked a user-facing way to disable its runtime without removing the Cordis entry, and enabling/disabling state was invisible outside the session log. PTC Plus requires a plugin settings UI containing every Host Config field, a kill switch, and an explicit enabled marker. DSH's public settings service registers a namespace; the browser half contributes a card through the generation's settings seat, and the client reads the installed
|
|
11
|
+
PTC Plus 0.1.0 deliberately had no Client UI. The runtime surface was the DSH PTC mode `run_code`/`edit_run_code` transport, and adding a settings bundle was judged not to improve the core REPL path. The plugin also lacked a user-facing way to disable its runtime without removing the Cordis entry, and enabling/disabling state was invisible outside the session log. PTC Plus requires a plugin settings UI containing every Host Config field, a kill switch, and an explicit enabled marker. DSH's public settings service registers a namespace; the browser half contributes a card through the generation's settings seat, and the client reads whichever public settings transport the installed Host exposes (`configForms` or `settingsScope`).
|
|
12
12
|
|
|
13
13
|
## Decision
|
|
14
14
|
|
|
@@ -14,7 +14,7 @@ Tool registration must finish before the prompt assembly returned to the agent;
|
|
|
14
14
|
|
|
15
15
|
## Decision
|
|
16
16
|
|
|
17
|
-
Add `cordisToolsEnabled: boolean`, default `false`, as a live PTC Plus setting. When enabled, PTC Plus mounts the official `@deepseek-ai/dsh-tool-cordis` plugin and a maintained `@deepseek-ai/dsh-skill-filesystem` provider in each agent scope that exposes every surface the mount publishes through: `run_code`, the `skill` tool, and the public `agentPresets` and `skills` services; disabling it disposes both fibers immediately. The provider resolves the shipped `cordis` preset through the public `agentPresets` service and reads its sibling `skills` root without including default roots. At the public `skills.registerProvider` seam, PTC Plus transparently filters discovery and loading to exactly `cordis-plugin-development`; the maintained filesystem provider still owns parsing, resource bases,
|
|
17
|
+
Add `cordisToolsEnabled: boolean`, default `false`, as a live PTC Plus setting. When enabled, PTC Plus mounts the official `@deepseek-ai/dsh-tool-cordis` plugin and a maintained `@deepseek-ai/dsh-skill-filesystem` provider in each agent scope that exposes every surface the mount publishes through: `run_code`, the `skill` tool, and the public `agentPresets` and `skills` services; disabling it disposes both fibers immediately. The provider resolves the shipped `cordis` preset through the public `agentPresets` service and reads its sibling `skills` root as an immutable bundled Host resource without including default roots or watching the packaged directory. At the public `skills.registerProvider` seam, PTC Plus transparently filters discovery and loading to exactly `cordis-plugin-development`; the maintained filesystem provider still owns parsing, resource bases, incomplete observations, and body loading. This prevents unrelated or obsolete sibling Skills from silently enlarging the PTC surface without copying the companion content or switching the current preset. A scope that omits one of those surfaces is not an error: the owner keeps the agent pending, publishes nothing there, and retries on the DSH tools-change signal or the next prompt assembly, because a later registration or provided service can supply the missing member ([ADR 0029](0029-keep-plugin-unavailability-out-of-the-host-agent-lifecycle.md)).
|
|
18
18
|
|
|
19
19
|
The Skill provider and official Cordis child fiber form one reversible mount. Both must settle, and the scoped `skills` service must prove that `cordis-plugin-development` resolves from the added provider as model-invocable and loadable, before the first prompt assembly is returned. A prepended public `system-prompt/assemble` waterfall listener is the final race barrier: if collection began before the mount settled, it waits for readiness, discards that stale assembly, and invokes public assembly again so DSH recollects the official providers. It does not synthesize or mutate Cordis schemas or guidance. The Cordis fiber's resolved `inject` declaration is the authoritative required-service list, and its live registrations are the authoritative tool names, schemas, and guidance. A scope whose exposed preset, Skill service/tool, declared service, publication, or child fiber contradicts the DSH contract is a configuration error and rolls back every contribution; an absent surface only defers the mount. An unusable scope reached through the host's own calls — `agent/created` and the prepended waterfall — withdraws and reports one warning instead of failing the host operation that created the scope ([ADR 0029](0029-keep-plugin-unavailability-out-of-the-host-agent-lifecycle.md)).
|
|
20
20
|
|
|
@@ -24,7 +24,7 @@ Cordis tools remain native program bindings under `tools.*`. The PTC code-only d
|
|
|
24
24
|
|
|
25
25
|
Cordis definitions and Runs are process-local, while PTC journals replay every settled program binding as a recorded value without redispatch. On the first prompt for an agent or a new Cordis enable generation, PTC Plus derives whether normalized journals contain a Cordis transcript. If they do, it emits the fixed `tools:ptc-plus-cordis-recovery` runtime context: recorded values remain journal data, but the model must perform a live read-only Cordis Inspect before relying on prior IDs, state, approvals, or capability observations. The context remains until a newly settled successful `cordis_inspect*` call appears in the session log. It neither asserts that live state was lost nor reruns a mutating call.
|
|
26
26
|
|
|
27
|
-
Companion Skill resolution is capability-based.
|
|
27
|
+
Companion Skill resolution is capability-based. A generation that publishes an absolute composition path for the shipped `cordis` preset uses its sibling `skills` root. For a pathless generation, PTC Plus asks the Host's public `pluginPackages.packageOf` service for the active installation's `@deepseek-ai/dsh-agent-preset` or `@deepseek-ai/dsh-agent-presets` resource, then selects that generation's known Skill layout. Older generations that do not publish `pluginPackages` may resolve only the historical plural package from the Host's `ctx.baseUrl`; this compatibility path is disabled whenever the service exists, including when it returns no package. It never uses plugin-anchored ambient Node lookup, because a profile or Desktop installation may contain a stale package from another Host generation. PTC Plus accepts a root only when it contains the exact `cordis-plugin-development/SKILL.md`; it does not infer support from a package name or directory alone. An unusable preset (`broken`) is reported as unavailable before path or package resolution, while an invalid published path remains a host contradiction rather than falling through to a different generation.
|
|
28
28
|
|
|
29
29
|
## Alternatives Considered
|
|
30
30
|
|
|
@@ -12,7 +12,7 @@ PTC Plus publishes its Remote services through DSH's TYPERT registry, and that r
|
|
|
12
12
|
|
|
13
13
|
Each generation validates only the member it owns and ignores the other, so one published contract serves whichever shape the installed host implements. Compatibility follows the live acceptance behavior of the registry rather than a release version, and the decoder is published once, so a host that reads both members cannot obtain two different decoders for one wire member.
|
|
14
14
|
|
|
15
|
-
CI registers the packed contract with the installed host's own registry
|
|
15
|
+
CI registers the packed contract with the installed host's own registry from the frozen official `latest` and `next` graphs (`scripts/dsh-rpc-contract-smoke.mjs`), so a generation that changes this validation fails the packaging check instead of reaching users.
|
|
16
16
|
|
|
17
17
|
## Alternatives Considered
|
|
18
18
|
|
|
@@ -24,4 +24,4 @@ CI registers the packed contract with the installed host's own registry on both
|
|
|
24
24
|
|
|
25
25
|
## Consequences
|
|
26
26
|
|
|
27
|
-
One plugin build serves the
|
|
27
|
+
One plugin build serves the preceding and current public codec generations for this surface, and the compatibility rule lives in a single module with the decoder published once. A future generation that requires another member, or that rejects a codec carrying a member it does not know, needs this owner updated; the regression test is CI's registration against the installed registry, which fails on the frozen Host graph that changed instead of on the user's machine. The decoder keeps the project's maintained schema runtime, so a host that validates payloads itself still evaluates the same semantics the plugin's own tests exercise through both members.
|
package/docs/architecture.md
CHANGED
|
@@ -52,7 +52,7 @@ system sections、tool schema 或 tool order,也不假设 Windows、WSL、POSI
|
|
|
52
52
|
|
|
53
53
|
## 设置与启用开关
|
|
54
54
|
|
|
55
|
-
Host half 通过 DSH 公共 settings 服务接入 `ptc-plus` 命名空间:提供 provider-owned `installSection()` 的代际直接注册,提供根模块 `installSettingsSection()` 的代际经兼容适配注册;两条路径共享同一 fallback、watch、rollback 与 disposal 语义。既不提供 section installer、又由宿主从插件导出的 `Config`
|
|
55
|
+
Host half 通过 DSH 公共 settings 服务接入 `ptc-plus` 命名空间:提供 provider-owned `installSection()` 的代际直接注册,提供根模块 `installSettingsSection()` 的代际经兼容适配注册;两条路径共享同一 fallback、watch、rollback 与 disposal 语义。既不提供 section installer、又由宿主从插件导出的 `Config` 管理设置文档的代际被视为 host-owned surface:导出 schema 的全部字段带 volatile 标记,宿主据此提供表单并通过自身 config-editor 把 volatile-only 变更写入 retained handles;插件订阅宿主的 `loader/volatile-update` 事件,从同一 schema 重新解析 live 值并复用既有 reconcile、rollback 与 per-cell 配置代际语义。字段清单、默认值与校验来自 `internal/config-spec.js`。
|
|
56
56
|
Client half 在侧栏 **插件 → 已安装 → dsh-ptc-plus** 的插件行配置页呈现全部设置,通过 `plugins.row.config`(keyed,键 `<包名>#<行 id>`)注册;旧宿主的插件设置使用 `settings.plugin.item`(keyed,按 namespace)。`src/client-host-compat.js` 等待对应页面声明槽位,并按 `summary`/`page` 分别渲染一句描述与完整表单,不向通用设置注册。星光菜单在应用内弹窗复用完整表单与写入 owner;包元数据的 `icon` 指向随包发布的 SVG,由宿主显示在插件列表与详情页([ADR 0019](adr/0019-plugin-settings-and-kill-switch.md))。`enabled` 是 kill switch:关闭时保留 settings 注册、设置卡片与撤销旧 PTC 声明所需的被动 presentation cleanup;`enhancedToolView` 是默认开启的正文展示开关,关闭时只交还 DSH 原生 `run_code`/`edit_run_code` generic row,
|
|
57
57
|
总开关关闭时不注册 runtime、执行 hook、tool surface 或 system prompt section,也不读取 binding 存储;`ptc` 与兼容 `code` preset 会话在头部单独显示 `PTC Plus` 指示器。设置卡片显示“已启用/已停用”,稳定指引不包含 UI 品牌名。
|
|
58
58
|
`Config` 的字段形态由 `internal/settings-compat.js` 按已安装代际的公共能力决定:仍提供 section installer 的代际保留普通描述字段,由自身文档路径提交校验后的具体默认值;只有自管设置文档的代际才加 volatile 标记,字段构建器不具备该能力的代际保持可加载。版本号不参与该判断。
|
|
@@ -63,6 +63,8 @@ Client half 在侧栏 **插件 → 已安装 → dsh-ptc-plus** 的插件行配
|
|
|
63
63
|
|
|
64
64
|
`cordisToolsEnabled` 默认关闭且即时生效。Host 只在同时暴露 `run_code`、`skill` 工具与 `agentPresets`/`skills` service 的 agent scope 中挂载官方 `@deepseek-ai/dsh-tool-cordis`,并通过公共 `agentPresets` service 定位 shipped `cordis` preset,再用维护中的 Skill filesystem plugin 把其 companion Skill 目录发布到同一 scope。两个 child fiber、tool guidance 与 `cordis-plugin-development` Skill 是一个 mount;首轮 request 等待完整发布和 scoped Skill load 验证,关闭、agent/runtime 释放或任一激活失败时逆序卸载。缺少其中任一 surface 的 scope 不是错误:owner 保持 pending、不在该 scope 发布内容,并在工具 surface 变化或下一次 prompt assembly 时重试;由宿主自身调用触达的不可用 scope 只从该 scope 撤出并记一条 warning,不使 `agents.create()` 或 prompt assembly 失败([ADR 0029](adr/0029-keep-plugin-unavailability-out-of-the-host-agent-lifecycle.md))。工具名、数量、schema 和 guidance 直接来自 official tool fiber,Skill 内容直接来自 shipped preset,Host 均不复制。官方插件同时向 process-global `cordisInspect` 注册 Host provider;owner 将 manifest 相同的 per-agent 注册合并为引用计数 lease,查询委托给当前仍存活的官方 registration,最后一份 lease 释放后才注销 provider。manifest 不一致时启用失败。该开关不切换 preset,也不改变 code-only direct-tool projection。
|
|
65
65
|
|
|
66
|
+
Skill 根在当前 generation 发布绝对 composition path 时取其 sibling;pathless generation 改由 active Host 的公共 `pluginPackages.packageOf` service 定位该安装的官方单数或复数 preset package resource。完全没有该 service 的旧 generation 仅可从 Host `ctx.baseUrl` 解析历史复数包;service 已存在但返回空或无效结果时不进入兼容路径,因此不会读取插件 profile 周围可能属于另一宿主的 ambient package。只有准确存在 `cordis-plugin-development/SKILL.md` 才进入 mount;已发布但无效的 path 不回退到另一 generation 的 package layout。
|
|
67
|
+
|
|
66
68
|
## Global User Bindings
|
|
67
69
|
|
|
68
70
|
`internal/user-bindings.js` 是条目、文档、请求快照和有界声明的严格规范化 owner。它从命名 value export 的 TypeScript 源码派生 symbols、binding kind、body-free declaration、durability 与 fingerprint,并封闭校验所有衍生字段;调用方不能提交一份与源码不一致的声明或 snapshot。源码声明保留未被遮蔽的环境类型,并收集公开签名引用的本地 interface、type alias 与 class 闭包;各条目使用独立声明 namespace,公开 class shape 包含实例字段与 constructor parameter property。无法形成自足声明的 imported type 在持久化前拒绝,不能静默替换为 `unknown`。`namespace` scope 产生一个条目名对象,`top-level` scope 产生选定导出;保留名称和启用集合中的调用标识符冲突在持久化或请求激活前拒绝。
|
package/docs/installation.md
CHANGED
|
@@ -18,20 +18,20 @@ Install the plugin into the profile that actually runs the target DSH surface. D
|
|
|
18
18
|
|
|
19
19
|
`danger-full-access` is the primary supported experience. The worker isolates the REPL lifecycle; it is not a malicious-code sandbox. DSH continues to own native-tool scope, policy, approval, cancellation, sandboxing, and scheduling. Narrower profiles expose only their available capabilities; PTC Plus does not simulate missing authority or add another permission system.
|
|
20
20
|
|
|
21
|
-
The optional `cordisToolsEnabled` integration requires the current DSH installation to provide its shipped `cordis` preset plus the public preset, Skill, Cordis, settings, and tool-runtime
|
|
21
|
+
The optional `cordisToolsEnabled` integration requires the current DSH installation to provide its shipped `cordis` preset plus the public preset, Skill, Cordis, settings, and tool-runtime services. PTC Plus declares the shared service packages as unrestricted required peers instead of installing private runtime copies. For a pathless preset it asks the Host's `pluginPackages.packageOf` service for the active generation's official preset package and requires the exact `cordis-plugin-development/SKILL.md` before mounting it; an older generation with no package service may resolve only its historical plural preset package relative to the Host's `ctx.baseUrl`. This compatibility path is disabled whenever `pluginPackages` exists, so packages merely reachable from the plugin profile cannot override an active Host result. Runtime capability validation owns compatibility, and CI loads the packed plugin against frozen official `latest` and `next` DSH graphs. That load step mounts the pathless Cordis companion from a separated active-host resource while a resolvable stale package is present beside the packed plugin, runs `scripts/dsh-execution-seam-smoke.mjs` to read and take over the execution service the installed tool runtime uses, and runs `scripts/dsh-rpc-contract-smoke.mjs` to register the packed Remote descriptors with the installed TYPERT registry and evaluate the decoder that generation reads ([ADR 0030](adr/0030-serve-both-typert-codec-generations-from-one-codec.md)); an import alone cannot distinguish a loaded plugin from one whose injection is unsatisfied, because Cordis leaves the latter pending without an error, and it cannot show that the Host accepts a wire contract it validates only at registration. The same suite round-trips argument diagnostics through the selected Host's compressed JSONL Session persistence. Service packages retain the dependency graph selected by their Host distribution rather than independently selecting each service's release tag. DSH's profile module fallback must resolve the peers, while `pluginPackages` identifies the official preset resource from an active installation that publishes the service. Do not copy `SKILL.md` or add the Cordis preset's Skill directory to global roots. If the host surface is incomplete, plugin activation or enabling the setting fails instead of loading a second DSH core.
|
|
22
22
|
|
|
23
23
|
The plugin attaches to whichever program-execution service the installed DSH registers, and it presents the capabilities of the execution it performs ([ADR 0028](adr/0028-attach-to-the-host-ptc-execution-seam.md)). Cells run in the session kernel under the session's configured budgets, so `run_code` advertises neither a per-call deadline nor a file sandbox, and DSH rejects `timeoutMs` and `sandbox_permissions` with its own message instead of the plugin accepting an input it would then ignore.
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
Supported installations use an unmodified official DSH release. Local plugin development packages this checkout. The release development launcher uses the published Host; the opt-in [upstream source launcher](#upstream-source-development-launcher-windows) builds the unmodified official repository for testing unreleased changes. Neither path patches the Host. Missing public layout interfaces remain a feature limitation, not an instruction to customize DSH. See [ADR 0017](adr/0017-track-the-latest-dsh-public-surface.md) for distribution and acceptance requirements.
|
|
26
26
|
|
|
27
|
-
The Client root requires `slots`, `locale`, `connection`, and `remote`; it resolves the installed
|
|
27
|
+
The Client root requires `slots`, `locale`, `connection`, and `remote`; it resolves whichever public settings transport the installed Host exposes (`configForms` or `settingsScope`) through an optional injection. Contributions wait for their public slots and consume the supplied session hooks; the composer action additionally requires `remote.commands`. Neither an unused `ui-session` module nor a renamed conversation registry gates activation. Preset selection prefers `agentPreset` projection evidence, falling back only to the preceding public session summary field when that projection is absent. Binding values and draft capabilities always require their own projections. No private store or raw-log fallback is used. Slot and provider disposal withdraw the contributions they own.
|
|
28
28
|
|
|
29
29
|
## npm Release
|
|
30
30
|
|
|
31
31
|
Use this form after the selected version is available from the npm registry:
|
|
32
32
|
|
|
33
33
|
```sh
|
|
34
|
-
dsh plugin --profile <profile> add dsh-ptc-plus@0.4.
|
|
34
|
+
dsh plugin --profile <profile> add dsh-ptc-plus@0.4.8
|
|
35
35
|
dsh --profile <profile> --dump-config
|
|
36
36
|
```
|
|
37
37
|
|
|
@@ -68,7 +68,7 @@ pnpm dsh --profile <profile> --dump-config
|
|
|
68
68
|
|
|
69
69
|
```sh
|
|
70
70
|
npm pack
|
|
71
|
-
dsh plugin --profile <profile> add /absolute/path/to/dsh-ptc-plus-0.4.
|
|
71
|
+
dsh plugin --profile <profile> add /absolute/path/to/dsh-ptc-plus-0.4.8.tgz
|
|
72
72
|
dsh --profile <profile> --dump-config
|
|
73
73
|
```
|
|
74
74
|
|
|
@@ -89,14 +89,32 @@ On Windows, `--browser-channel msedge` can use the existing Edge installation. T
|
|
|
89
89
|
|
|
90
90
|
## Isolated latest-DSH development launcher
|
|
91
91
|
|
|
92
|
-
To test this checkout in a separate DSH installation, double-click `scripts\run-dev-dsh.cmd`. It
|
|
92
|
+
To test this checkout in a separate DSH installation, double-click `scripts\run-dev-dsh.cmd`. It orders all published DSH releases by semantic version, prereleases included, and selects the newest release whose complete dependency cohort npm can install, rather than following a dist-tag that a release can leave behind. If a newly published root package still has missing exact-version dependencies, the launcher warns and tries the preceding release; an explicit `DSH_DEV_VERSION` remains exact and fails instead of falling back. It installs only when the selected cached version changes, creates the shipped `web` profile inside an isolated `DSH_HOME`, installs this checkout, and starts DSH. When no port is supplied, the default Web profile reuses its cached loopback port while that port is available; an invalid cache entry or occupied port is replaced with a free port. This keeps browser authentication stable across ordinary restarts without blocking another DSH on port 3080. The launched Host receives a 64 KiB HTTP request-header limit while retaining existing Node options to accommodate development authentication cookies and the combined Client plugin request during Web boot. The launcher does not modify the repository or your normal DSH home.
|
|
93
93
|
|
|
94
94
|
The launcher defaults to `https://registry.npmjs.org/` for version queries, npm installs and DSH's pnpm subprocesses, including the `@deepseek-ai` scope. This avoids mirror synchronization gaps where a new DSH package is available before its dependencies. It prints the selected registry and refreshes version metadata online; an unavailable version query can still reuse the previously cached version. Set `DSH_DEV_REGISTRY` to an absolute HTTP(S) registry URL to select another source explicitly. The choice applies only to the launcher process and its children; npm configuration files and persistent environment settings are unchanged. Other explicitly configured package scopes retain their own registries.
|
|
95
95
|
|
|
96
|
-
The default cache is `%LOCALAPPDATA%\dsh-ptc-plus-dev`. It contains the isolated `DSH_HOME`, DSH installations, immutable plugin snapshots, and a pnpm store. Cleanup aims to retain three DSH versions and plugin snapshots, always including the selected installation and snapshot even when another directory has a newer timestamp. Locked directories warn and remain for a later cleanup attempt; their installation marker is removed before deletion so partially deleted installations cannot be reused. Failed pnpm store pruning also warns and continues startup.
|
|
96
|
+
The default cache is `%LOCALAPPDATA%\dsh-ptc-plus-dev`. It contains the isolated `DSH_HOME`, DSH installations, immutable plugin snapshots, and a pnpm store. Cleanup aims to retain three DSH versions and plugin snapshots, always including the selected installation and snapshot even when another directory has a newer timestamp. Locked directories warn and remain for a later cleanup attempt; their installation marker is removed before deletion so partially deleted or failed installations cannot be reused. Failed pnpm store pruning also warns and continues startup. Failure of every published candidate, or of an explicitly selected version, stops the launcher. Set `DSH_DEV_CACHE` to move the cache, `DSH_DEV_PROFILE` to change the profile name, `DSH_DEV_VERSION` to pin an npm dist-tag or version instead of the newest installable published release, `DSH_DEV_PORT` to choose a fixed Web port, or `DSH_DEV_MAX_VERSIONS` to change the retention count (1-10).
|
|
97
97
|
|
|
98
98
|
The Windows launchers normalize the process, machine, and user `PATH` values in memory, remove duplicate entries, and add the Node/cache directories they need. They do not create drive mappings, junction trees, or persistent environment changes. If a genuinely unique PATH is still too long for `cmd.exe`, the launcher stops before npm or DSH runs and asks you to shorten the relevant PATH value.
|
|
99
99
|
|
|
100
|
+
### Upstream source development launcher (Windows)
|
|
101
|
+
|
|
102
|
+
Double-click `scripts\run-upstream-dsh.cmd` to test unreleased official DSH code with this plugin checkout. Git, Node.js and npm must be on the Windows PATH. The launcher fetches the official repository's default-branch HEAD, prints its full commit, installs the exact pnpm declared by that commit, and follows the official `pnpm install --frozen-lockfile`, `pnpm run build`, and `pnpm run dsh` source workflow. It does not patch upstream source. First startup can take several minutes and substantial disk space; upstream build prerequisites and failures remain visible.
|
|
103
|
+
|
|
104
|
+
The source launcher shares plugin packaging, profile selection and port handling with `run-dev-dsh.cmd`, but uses an `upstream` subdirectory of the development cache. Its DSH home, profiles, authentication, pnpm store and build snapshots are separate from the release launcher and your normal DSH installation. `DSH_DEV_CACHE`, `DSH_DEV_REGISTRY`, `DSH_DEV_PROFILE`, `DSH_DEV_PORT`, and `DSH_DEV_MAX_VERSIONS` apply; `DSH_DEV_VERSION` does not select source commits. For example:
|
|
105
|
+
|
|
106
|
+
```bat
|
|
107
|
+
scripts\run-upstream-dsh.cmd web --no-open
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Each build cache key includes the full commit, Node version/platform/architecture/ABI, upstream pnpm declaration, registry and inherited `DSH_CLIENT_*` / `DSH_BUILD_*` environment. A successful build is marked only after its CLI version check succeeds. Unchanged inputs reuse that build; failed builds have no completion marker and are retried. Tracked changes or a different HEAD in a completed checkout stop startup instead of overwriting edits. An interrupted initial checkout can be recreated on the next run.
|
|
111
|
+
|
|
112
|
+
A failed fetch warns and uses the last fetched commit if available; it never labels that fallback as current upstream HEAD. A failed install or build stops startup instead of silently launching an older successful revision. The launcher holds an exclusive OS file lock through Host shutdown, so another launch using the same cache fails clearly. The lock releases when the process exits; the remaining `source.lock` file is harmless and should not be removed to bypass a running process.
|
|
113
|
+
|
|
114
|
+
Cleanup retains the configured number of build snapshots and plugin tarballs (three by default), including the selected ones. Locked cleanup targets warn and are retried later. Git objects, exact pnpm runtimes and the shared pnpm store are retained to speed later revisions; their size is not bounded by the snapshot count. With this source launcher stopped, delete `upstream\builds` to force fresh builds while retaining downloads; remove `upstream\pnpm-store` as well to reclaim the shared download cache. The generated CMD shims use launcher-process environment variables to preserve Unicode paths and are not standalone entry points. Do not delete `upstream\dsh-home` unless you intend to remove this test environment's settings and sessions.
|
|
115
|
+
|
|
116
|
+
This is an opt-in upstream test environment. Published-release compatibility remains the plugin's supported delivery target.
|
|
117
|
+
|
|
100
118
|
## DSH Desktop
|
|
101
119
|
|
|
102
120
|
On Windows or macOS, choose **Open DSH Terminal** from the Desktop tray. Bare commands in that terminal target the active profile:
|
|
@@ -106,7 +124,7 @@ dsh plugin add github:muyuanjin/dsh-ptc-plus#main
|
|
|
106
124
|
dsh --dump-config
|
|
107
125
|
```
|
|
108
126
|
|
|
109
|
-
After an npm release, the package spec may instead be `dsh-ptc-plus@0.4.
|
|
127
|
+
After an npm release, the package spec may instead be `dsh-ptc-plus@0.4.8`. For a local package, use its absolute tarball path. Restart DSH Desktop after installation. Linux Desktop is not a current DSH Desktop release target; use DSH CLI/Web on Linux.
|
|
110
128
|
|
|
111
129
|
## Upgrades
|
|
112
130
|
|
package/docs/publishing.md
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
npm 发布遵循 [ADR 0022](adr/0022-stage-npm-releases-with-oidc.md),使用 `.github/workflows/release.yml` 声明的 Trusted Publisher,不向 GitHub 保存 npm write token。npm package settings 必须把 publisher 精确绑定到 `muyuanjin/dsh-ptc-plus`、workflow filename `release.yml` 和 environment `npm-release`,且只允许 `npm stage publish`。Publishing access 选择页面中最严格且标记为 recommended 的 2FA 选项;npm 文档称其为 `Require two-factor authentication and disallow tokens`,当前 npm 页面也可能显示 `Require two-factor authentication and disallow bypass 2fa tokens (recommended)`。不要从滚动变化的标签推断账户中已不存在传统 token。OIDC 只把经过验证的 tarball 放入 npm staging,维护者仍须在 npm CLI 或 npmjs.com 检查 staged package 并以 2FA approve,包才会进入公开 registry。
|
|
22
22
|
|
|
23
|
-
普通 `CI`
|
|
23
|
+
普通 `CI` 只在 `main` push 或 pull request 包含非纯文档变更时运行:只改动 `**.md`、`assets/**` 或 `LICENSE` 的提交不触发矩阵。发布 commit 因包含 `package.json` 与 `package-lock.json` 变更必然产生一次 `main` push run,Release workflow 仍要求该 commit 有成功的 run 才继续。发布前创建 annotated `v<package.version>` tag,原子推送 release commit 与 tag,等待该 commit 的 `CI` 成功,然后以该 tag 作为 ref 显式运行 `Release` workflow。workflow 不接受另一个 tag input,并会重新证明 dispatch 的 `GITHUB_REF`、`GITHUB_SHA`、annotated tag target、checkout HEAD、manifest 版本和成功 CI 的 SHA 完全一致;release job 使用 GitHub-hosted Node `24.15.0`、npm `12.0.2`、无 package-manager cache 和最小 `id-token: write` 权限,构建一个 tarball、执行 clean-consumer smoke,再对同一文件执行 `npm stage publish`。`package.json#allowScripts` 显式拒绝当前不需要的 `esbuild` install script;`replace-registry-host=always` 让 release install 将 lockfile 中的 registry tarball 路由到显式 npm 官方 registry,而不放宽 npm 12 的 Git/remote dependency 默认拒绝。
|
|
24
24
|
|
|
25
25
|
```powershell
|
|
26
26
|
git tag -a "v$((Get-Content package.json | ConvertFrom-Json).version)" -m "v$((Get-Content package.json | ConvertFrom-Json).version)"
|
|
@@ -42,13 +42,13 @@ npm.cmd stage approve '<stage-id>' --registry=https://registry.npmjs.org/
|
|
|
42
42
|
|
|
43
43
|
## Release checklist
|
|
44
44
|
|
|
45
|
-
1. 在 Linux Node `22.19.0`、24.x
|
|
46
|
-
2. 运行 `npm audit`、`npx publint`、`git diff --check`、Markdown local-link check 和 `npm pack --silent --dry-run --json
|
|
45
|
+
1. 提交前在稳定工作树上运行完整 `npm run check`,生成 coverage 与 review proof。仓库 CI 在 Linux Node `22.19.0`、24.x、26.x,以及 Windows 与 macOS Node 24.x 的干净安装上运行 `npm run verify:ci`,检查构建、Client 与跨平台关键运行契约;Ubuntu Node 24.x 另运行打包后的 `latest` / `next` Host contracts。CI 不重复收集 coverage 或执行穷尽式后端套件。
|
|
46
|
+
2. 运行 `npm audit --omit=dev`、`npx publint`、`git diff --check`、Markdown local-link check 和 `npm pack --silent --dry-run --json`。audit 检查随包安装的 production 依赖图;宿主 DSH 及其开发依赖不进入插件 tarball,由冻结宿主契约和 CI 单独验证。确认 tarball 只包含发布白名单内的运行时代码、文档和展示资产,不含凭据、本地路径、开发脚本或测试夹具,并从 tarball 安装到空目录执行 ESM import smoke。机器读取 `npm pack --json` 时保持 `--silent`,避免 lifecycle 输出混入 JSON stream。
|
|
47
47
|
3. 更新到最新可用 DSH release 并记录 `dsh --version`。分别从 npm 包、固定 Git commit、源码 checkout 和 tarball 安装到临时 profile,再执行 `dsh --profile <profile> --dump-config`,确认只新增 `ptc-plus` row。每个上游 release 都重新验证公共扩展面、CLI/Web 集成和 profile 装配,不设置版本白名单。
|
|
48
48
|
4. 在 Windows 与 Linux 运行无模型调用的 `npm run test:client:web -- --dsh-entry <latest-installed-dsh/lib/bin.js>`,验证 tarball 安装后的真实 Web Client、设置与对话 surface;浏览器前置条件见 [安装指南](installation.md)。`build:check`、Host ESM import 和 factory smoke 均不能替代浏览器激活验收。macOS 由原生 runner 验证 CLI/Web;Windows 与 macOS Desktop 从托盘打开当前 profile 的 DSH Terminal 安装,重启 Desktop 后做一次 PTC 模式 smoke。Desktop 当前发布平台不包括 Linux。
|
|
49
49
|
5. 只有在明确授权模型消耗后才运行 `npm run test:expensive` 与 `npm run test:ab`。发布结论以结构化测试结果为准,不把一次模型样本推广为普遍保证。
|
|
50
50
|
6. 确认 npm 包名可用、待发布版本高于 registry 版本、README 截图来自真实 DSH 会话,并核对 repository、homepage、bugs、license 与 `dsh-plugin` topic。
|
|
51
|
-
7. 最后检查 `npm pack --silent --json` 的名称、版本、大小与文件列表,创建 annotated release tag,并确认 tag commit 已通过 `CI`。运行 `Release` workflow 将同一源码产生的 tarball送入 npm staging;人工检查并以 2FA approve 后,等待 registry 扫描完成,再创建 GitHub Release。
|
|
51
|
+
7. 最后检查 `npm pack --silent --json` 的名称、版本、大小与文件列表,创建 annotated release tag,并确认 tag commit 已通过 `CI`。运行 `Release` workflow 将同一源码产生的 tarball送入 npm staging;人工检查并以 2FA approve 后,等待 registry 扫描完成,再创建 GitHub Release。Release notes 的内容取舍与风格由 [release-notes.md](release-notes.md) 拥有。
|
|
52
52
|
|
|
53
53
|
远程仓库、registry、签名凭据、目标平台运行状态和模型额度都属于发布时事实,不能由本地 manifest 推断。
|
|
54
54
|
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# GitHub Release Notes
|
|
2
|
+
|
|
3
|
+
本文件拥有每个 GitHub Release 的**内容取舍与写作风格**。发布流程(tag、CI 身份校验、npm staging、2FA approve)由 [publishing.md](publishing.md) 拥有;包内容由 manifest 与发布白名单拥有;面向用户的长期说明由 [README.md](../README.md) 与 [README.en.md](../README.en.md) 拥有。写 notes 时按本文件执行,不需要回看历史 Release。
|
|
4
|
+
|
|
5
|
+
## 读者与边界
|
|
6
|
+
|
|
7
|
+
读者是使用插件的用户。notes 只写他们需要知道的产品事实:能不能装、能不能跑、行为变了什么、坏掉的东西修好了什么、升级要做什么。
|
|
8
|
+
|
|
9
|
+
不写产物的生成过程:
|
|
10
|
+
|
|
11
|
+
- 不写写作时间或发行状态(“写作时”“目前”“本文完成于”);需要指明目标发行时直接写版本号
|
|
12
|
+
- 不写验证与取证过程(谁在哪个客户端上核对过、跑过哪些命令、门禁结果)
|
|
13
|
+
- 不写内部实现与流程术语(内部模块名、实现步骤、评审与 CI 与发布自动化)
|
|
14
|
+
- 不写协商过程与修正记录(“按反馈”“不再使用某说法”)
|
|
15
|
+
|
|
16
|
+
验证证据留在交付记录里(CI run、发布日志、维护文档),不进入 notes。
|
|
17
|
+
|
|
18
|
+
## 先按用户视角计算重要性
|
|
19
|
+
|
|
20
|
+
从 `git log --format='%h %s%n%b' <上一个 tag>..<本次 tag>` 收集全部变更,按用户的遭遇排序后再提炼;提交顺序、提交类型(feat/fix/docs/chore)和改动行数都不参与排序。
|
|
21
|
+
|
|
22
|
+
1. **还能不能用**:对当前官方 DSH 发布与上一代宿主契约的适配、安装与升级路径、激活与启动。用户最先问的是“装上还能不能跑”,排第一。
|
|
23
|
+
2. **有什么变化**:新增能力、默认值变化、既有行为改变,以及需要用户动手的迁移。
|
|
24
|
+
3. **哪些毛病没了**:用户可观察到的错误、闪烁、失效入口、丢失的数据关系。
|
|
25
|
+
4. **升级要求**:Node 版本、是否需要重启、是否有配置迁移。
|
|
26
|
+
5. **内部交付**默认不出现;只有当它改变用户可见结果时,才用用户能理解的方式提及。
|
|
27
|
+
|
|
28
|
+
兼容性声明落在公共扩展面上——设置座位、slot、公共 `Menu` 区域、会话投影、能力探测。版本号只用于标明本次适配的目标发行,不构成版本白名单;能力与座位仍以宿主声明的公共扩展面为准([ADR 0017](adr/0017-track-the-latest-dsh-public-surface.md))。
|
|
29
|
+
|
|
30
|
+
## 风格
|
|
31
|
+
|
|
32
|
+
- 双语且等价:中文在前,`---` 之后是 English;两段不互相省略,小节顺序一致。小节标题用 `<h3>`,中文 anchor 用 `cn-v0XY`,英文用 `en-v0XY`。
|
|
33
|
+
- 一条 bullet 只写一个用户可观察的结果;写清触发条件与边界(何时生效、何时不做、是否自动执行)。
|
|
34
|
+
- 用用户能观察到的对象描述变化:绑定、星光菜单、设置卡片、状态提示、cell 执行。宿主声明的座位键名(如 `plugins.row.config`)保留,内部模块名和实现步骤不写。
|
|
35
|
+
- 不写营销形容词,也不承诺恢复能力、永久记忆等无法保证的结果。
|
|
36
|
+
- 结构稳定,便于读者按标题 diff 两个版本:宿主适配 → 新增 → 修复 → 升级(含安装命令)→ `Full Changelog` compare 链接。
|
|
37
|
+
|
|
38
|
+
## 模板
|
|
39
|
+
|
|
40
|
+
````markdown
|
|
41
|
+
[中文](#cn-v0XY) | [English](#en-v0XY)
|
|
42
|
+
|
|
43
|
+
<h3 id="cn-v0XY">宿主适配</h3>
|
|
44
|
+
|
|
45
|
+
* 本次适配的目标 DSH 发行与它使用的公共扩展面
|
|
46
|
+
* 不满足该契约的宿主如何继续工作
|
|
47
|
+
|
|
48
|
+
<h3>新增</h3>
|
|
49
|
+
|
|
50
|
+
* ……
|
|
51
|
+
|
|
52
|
+
<h3>修复</h3>
|
|
53
|
+
|
|
54
|
+
* ……
|
|
55
|
+
|
|
56
|
+
<h3>升级</h3>
|
|
57
|
+
|
|
58
|
+
* 需要 Node.js `^22.19.0 || >=24.0.0`;升级插件后请重启 DSH
|
|
59
|
+
* 是否有配置迁移,以及旧配置的表现
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
dsh plugin --profile <profile> add dsh-ptc-plus@<version>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
<h3 id="en-v0XY">Host Compatibility</h3>
|
|
68
|
+
|
|
69
|
+
* ……
|
|
70
|
+
|
|
71
|
+
<h3>New Features</h3>
|
|
72
|
+
|
|
73
|
+
* ……
|
|
74
|
+
|
|
75
|
+
<h3>Fixes</h3>
|
|
76
|
+
|
|
77
|
+
* ……
|
|
78
|
+
|
|
79
|
+
<h3>Upgrade</h3>
|
|
80
|
+
|
|
81
|
+
* Requires Node.js `^22.19.0 || >=24.0.0`; restart DSH after upgrading the plugin
|
|
82
|
+
|
|
83
|
+
```sh
|
|
84
|
+
dsh plugin --profile <profile> add dsh-ptc-plus@<version>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
Full Changelog: https://github.com/muyuanjin/dsh-ptc-plus/compare/<上一个 tag>...<本次 tag>
|
|
90
|
+
````
|
|
91
|
+
|
|
92
|
+
## 示例(v0.4.3 的宿主适配小节)
|
|
93
|
+
|
|
94
|
+
````markdown
|
|
95
|
+
<h3 id="cn-v043">宿主适配</h3>
|
|
96
|
+
|
|
97
|
+
* 适配 DSH 0.1.7 的公共扩展面:设置卡片注册到宿主的插件配置座位(侧栏 **插件 → 已安装** 的 `plugins.row.config`),星光入口的四个动作进入公共 `Menu` 的 `children` 区域
|
|
98
|
+
* 不渲染 `children` 的宿主同样可用:同一套插件自有动作区由 pinned 区域承载,上一代设置座位 `settings.plugin.item` 继续生效
|
|
99
|
+
* 兼容性由公共能力探测决定:探测只记录一次已挂载观测,支持 `children` 的宿主不会重复渲染
|
|
100
|
+
````
|
|
101
|
+
|
|
102
|
+
同一版的 `feat(review)`、`chore(release)` 等内部提交不进入 notes;测试夹具修复属于交付记录,不作为用户条目。
|
|
103
|
+
|
|
104
|
+
## 发布时的用法
|
|
105
|
+
|
|
106
|
+
1. 取全量变更:`git log --format='%h %s%n%b' <上一个 tag>..<本次 tag>`。
|
|
107
|
+
2. 按用户视角排序,决定每条要不要写、写在哪个小节。
|
|
108
|
+
3. 逐条确认陈述在产品里成立;无法确认的条目删除。
|
|
109
|
+
4. 按模板写完;npm 版本可安装后创建 GitHub Release(流程见 [publishing.md](publishing.md))。
|
|
@@ -20,6 +20,8 @@ For PTC mode requests using the code-only direct-tool projection, PTC Plus prese
|
|
|
20
20
|
|
|
21
21
|
`edit_run_code` is a real registered tool call. Session history and later model requests retain its original name and delta arguments. The plugin executes the materialized source as a host-derived `run_code`, returns only the edit status, value, and logs to the model, and keeps the complete source plus journal in private replay metadata. It does not rewrite assistant history or emit edit-specific runtime context.
|
|
22
22
|
|
|
23
|
+
One provider failure has a bounded transport recovery. When the provider reports the exact invalid-tool-JSON `MALFORMED_RESPONSE` as a terminal chunk or stream exception, PTC Plus changes that terminal reason to `tool-calls`; this error cannot terminate the whole turn. DSH already preserves an unparseable argument string and routes it through ordinary validation, tool execution/result recording, and the next model step. A response is trusted for call preservation only when its buffered body consists entirely of tool-call-specific framing plus at most one `usage` event. If such a response contains exactly one internally consistent declared `run_code` or `edit_run_code` block, PTC Plus also tests deletion of one final `}`. It applies that correction only when the resulting value is an object accepted by the live direct-tool schema. The call keeps its index, identity, and name and proceeds through ordinary DSH validation, recording, scheduling, authority, and execution. Without a unique correction, trusted internally consistent complete blocks retain their original identity and argument bytes for the ordinary rejected-tool-result path; PTC Plus re-emits canonical start/delta/end framing so every tolerated provider variant passes DSH's stream invariant. If a tool block is incomplete or inconsistent, none was emitted, multiple usage events appear, or any other event occurs in the buffered body, PTC Plus closes any already visible text or reasoning block from its accumulated content, removes buffered unsafe block material, and emits one deterministic `run_code` call whose argument string cannot parse as a JSON object. DSH rejects it during ordinary schema validation, so no partial model call executes and the model still receives another step. Valid streams and other provider failures retain their original result. This is not a general JSON repair mechanism.
|
|
24
|
+
|
|
23
25
|
When a live parse error occurs exactly at the cell EOF, PTC Plus checks all 12 suffixes of one or two closing tokens drawn from `}`, `)`, and `]` with the same preparation context. If exactly one correction succeeds, the existing editor can express it from a bounded unique suffix, and the rejected call has a persistent event identity, `PTC-C001` includes a directly callable `edit_run_code({ edits: [...], expected_target_call_seq })` invocation. The guard must match the target captured when the edit call is persisted, so a later editable cell suppresses execution even when the same literal replacement would match it. The plugin does not apply or execute the correction automatically; ambiguous, broader, and unbound repairs keep the ordinary length-adaptive guidance.
|
|
24
26
|
|
|
25
27
|
Use `return` for an explicit result and the native Node `console` surface for logs. Its formatting methods, including `dir` options, tables, counters, groups, timers, assertions, and traces, retain Node semantics. `process.stdout/stderr.write`, direct fd 1/2 writes and inherited child stdio are also captured. Console entries retain call order and each descriptor pipe retains byte order; the merged log does not claim a total chronology across stdout, stderr and the private control channel. All forms share the cell's aggregate output budget. Native expression completion can also supply a result value:
|
|
@@ -61,6 +63,8 @@ The configuration uses `ptc-plus` as the DSH plugin ID and `dsh-ptc-plus` as the
|
|
|
61
63
|
|
|
62
64
|
`autoDescribeRunCode` defaults to `true` and appears as “Allow run_code execution without a summary.” When enabled, a call that omits the outer `run_code.description` uses derived arguments for local DSH validation, and the fallback summary enters presentation metadata only. When disabled, DSH validates the original arguments. Both states expose byte-identical model requests with the required `description` declaration; original call arguments, existing summaries, cell source, and nested native-tool JSON remain unchanged.
|
|
63
65
|
|
|
66
|
+
Missing-description diagnostics append a complete DSH user message to `additionalContexts`, with an identity, text content and a `plugin:ptc-plus` notice source. The diagnostic distinguishes outer transport arguments from nested native-tool arguments and preserves existing additional messages. These notices are ordinary inbox messages, separate from recovery-tip escalation and state snapshots. This prevents new malformed diagnostic records; it does not repair session files already written by older plugin versions.
|
|
67
|
+
|
|
64
68
|
```yaml
|
|
65
69
|
- id: ptc-plus
|
|
66
70
|
name: dsh-ptc-plus
|
|
@@ -91,7 +95,9 @@ The five legacy binding and module switches remain migration inputs. Complete en
|
|
|
91
95
|
|
|
92
96
|
`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. Every setting is applied live when its owner can reconcile it. A submitted cell retains one configuration snapshot through preflight, execution, binding calls, completion validation, and diagnostics; an update accepted while it runs applies to cells submitted afterward. Node fixes a worker's V8 old-generation limit at creation, so changing `maxOldGenerationSizeMb` while a session worker is active 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)) — shows “已启用” or “已停用”, and disables every control except `enabled` 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.
|
|
93
97
|
|
|
94
|
-
`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
|
|
98
|
+
`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, edit_run_code]`. Cordis can evaluate model-written plugins against the live runtime, so enabling it grants shell-equivalent trust.
|
|
99
|
+
|
|
100
|
+
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.
|
|
95
101
|
|
|
96
102
|
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.
|
|
97
103
|
|
package/docs/verification.md
CHANGED
|
@@ -41,6 +41,10 @@ The final verdict uses `dsh-review-verdict/v2`. Pre-commit verifies its plan has
|
|
|
41
41
|
|
|
42
42
|
`npm run verify` and `npm run check` validate the ledger, compare generated bundles, check syntax, run Client tests, and run all backend tests with coverage. Backend test files run in separate Node processes. The default process count is half the available CPUs, rounded down and bounded between one and four; each process can also own runtime Workers. Tests within each file retain their existing ordering. Coverage still requires 100% lines and functions and at least 95% branches, including compiler source remapping. The graded merge additionally asserts that the plugin-source closure it claims — `index.js` and every `internal/*.js` owner — appears in the merged report, so a module no instrumented process loads fails the gate instead of silently leaving it. Generated artifacts stay outside that closure: the root `compiler-*.cjs` bundles and the `internal/*.cjs` shims are remapped onto their original `internal/` sources or verified by the bundle checks, and `scripts/migrate-session-log.mjs` is verified by its own migration suite.
|
|
43
43
|
|
|
44
|
+
Coverage, the exhaustive backend suite, and the review-bound proof belong to the local pre-commit `npm run check`. CI starts from a clean lockfile install and runs `verify:ci` on every supported Node and operating-system entry: frozen-baseline 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. Ubuntu Node 24 additionally exercises the packed plugin against both frozen official `latest` and `next` DSH graphs. CI therefore proves portable installation and runtime integration without repeating coverage or the exhaustive semantic suite on hosted runners.
|
|
45
|
+
|
|
46
|
+
`host:baseline:verify`, which is part of deterministic verification, is offline. It requires the installed root DSH `next` cohort to match `package-lock.json` and validates the tracked `compat/latest` declaration and lock without installing it. CI separately runs `test:host-contract`: the root graph exercises `next`, while a temporary consumer created from `compat/latest/package-lock.json` exercises `latest` against the packed plugin. Registry tag lookup and lock regeneration belong only to `host:baseline:update` and the scheduled baseline-refresh workflow. That workflow publishes a changed frozen candidate 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.
|
|
47
|
+
|
|
44
48
|
Use focused coverage while developing or after a full gate identifies a narrow coverage failure. Select every source whose threshold matters and the smallest test set that can distinguish the correction:
|
|
45
49
|
|
|
46
50
|
```bash
|
|
@@ -73,7 +77,7 @@ Coverage runs additionally set `DSH_PTC_TEST_WORKER_COVERAGE=1` and load `script
|
|
|
73
77
|
|
|
74
78
|
`npm run coverage` and `npm run coverage:focus` print their current stage and total elapsed time. Each invocation collects worker coverage in its own temporary directory under `coverage/`, removed after c8 finishes reporting, so simultaneous runs cannot erase or combine each other's evidence. The default report is printed to the terminal. A separate `coverage/run-*.tap` log retains Node's test diagnostics, including process exit codes and signals that the concise terminal reporter can omit. An explicit Node test reporter option replaces these default reporters. Source changes during verification still invalidate a `check` proof; separate coverage directories do not make concurrent source edits safe.
|
|
75
79
|
|
|
76
|
-
Windows Node 24 and 26 reproductions and stacks identify a cppgc finalizer (`node::contextify::ContextifyScript`, or `ContextifyContext` when coverage was enabled) running after the environment that owned its private realm was gone, which wrote through a dangling `node::Realm` during isolate teardown. The reproduction ended the whole DSH process when an in-host session worker was terminated; macOS runners also observed whole-file aborts under coverage, but the available evidence does not establish that those aborts came from the same Windows stack or that the same cause applies across platforms. The session kernel now runs in a plugin-owned helper process (`internal/kernel-child.js`); `IsolatedWorker` sends a cooperative shutdown and, if the helper does not leave within its bound, kills only that helper. DSH is no longer the worker-teardown target on any supported host, so a wedged kernel is contained as a helper failure rather than a host-native crash.
|
|
80
|
+
Windows Node 24 and 26 reproductions and stacks identify a cppgc finalizer (`node::contextify::ContextifyScript`, or `ContextifyContext` when coverage was enabled) running after the environment that owned its private realm was gone, which wrote through a dangling `node::Realm` during isolate teardown. The reproduction ended the whole DSH process when an in-host session worker was terminated; macOS runners also observed whole-file aborts under coverage, but the available evidence does not establish that those aborts came from the same Windows stack or that the same cause applies across platforms. The session kernel now runs in a plugin-owned helper process (`internal/kernel-child.js`); `IsolatedWorker` sends a cooperative shutdown and, if the helper does not leave within its bound, kills only that helper. DSH is no longer the worker-teardown target on any supported host, so a wedged kernel is contained as a helper failure rather than a host-native crash. The former file-level retry wrapper is gone, and any failing CI smoke or local exhaustive test fails its run. This boundary removes the plugin's in-host kill exposure; it neither repairs the upstream Node path nor proves the macOS abort shared the Windows cause.
|
|
77
81
|
|
|
78
82
|
Native oracle workers and external Windows command fixtures explicitly disable inherited `NODE_V8_COVERAGE`; they execute no code included by the plugin coverage gate. Subprocesses testing plugin implementations still collect coverage. PowerShell semantic-version observations share one invocation, and Windows registry fixtures read the three effective registry values with one real npm configuration query. Each retains its original inputs and assertions.
|
|
79
83
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import * as CordisTools from '@deepseek-ai/dsh-tool-cordis'
|
|
2
2
|
import * as CordisSkillFilesystem from '@deepseek-ai/dsh-skill-filesystem'
|
|
3
|
+
import { existsSync } from 'node:fs'
|
|
4
|
+
import { createRequire } from 'node:module'
|
|
3
5
|
import { dirname, isAbsolute, join } from 'node:path'
|
|
4
6
|
import { isDeepStrictEqual } from 'node:util'
|
|
5
7
|
import { RUN_CODE } from './runtime-bridge-owner.js'
|
|
@@ -8,6 +10,16 @@ const CORDIS_PRESET_ID = 'cordis'
|
|
|
8
10
|
const CORDIS_SKILL_NAME = 'cordis-plugin-development'
|
|
9
11
|
const CORDIS_SKILL_PROVIDER = 'ptc-plus-cordis'
|
|
10
12
|
const COMPANION_SKILL_TOOL = 'skill'
|
|
13
|
+
const COMPANION_PACKAGE_LAYOUTS = Object.freeze([
|
|
14
|
+
Object.freeze({
|
|
15
|
+
name: '@deepseek-ai/dsh-agent-preset',
|
|
16
|
+
skillRoot: directory => join(directory, 'skills'),
|
|
17
|
+
}),
|
|
18
|
+
Object.freeze({
|
|
19
|
+
name: '@deepseek-ai/dsh-agent-presets',
|
|
20
|
+
skillRoot: directory => join(directory, 'presets', CORDIS_PRESET_ID, 'skills'),
|
|
21
|
+
}),
|
|
22
|
+
])
|
|
11
23
|
|
|
12
24
|
function exactCompanionCandidates(observation) {
|
|
13
25
|
if (Array.isArray(observation)) {
|
|
@@ -131,25 +143,51 @@ function requireCompanionServices(agent) {
|
|
|
131
143
|
return { agentPresets, skills }
|
|
132
144
|
}
|
|
133
145
|
|
|
134
|
-
|
|
146
|
+
function containsCompanionSkill(directory, fileExists) {
|
|
147
|
+
return fileExists(join(directory, CORDIS_SKILL_NAME, 'SKILL.md'))
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Resolve the official Cordis companion Skill across DSH preset generations. */
|
|
151
|
+
export async function resolveCompanionSkillDirectory(agentPresets, options = {}) {
|
|
152
|
+
const fileExists = options.fileExists ?? existsSync
|
|
135
153
|
const preset = await agentPresets.resolve(CORDIS_PRESET_ID)
|
|
136
|
-
if (typeof preset
|
|
154
|
+
if (typeof preset?.broken === 'string' && preset.broken.length > 0) {
|
|
137
155
|
throw new Error(`ptc-plus: DSH cordis preset is unavailable: ${preset.broken}`)
|
|
138
156
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
157
|
+
if (preset?.path !== undefined) {
|
|
158
|
+
if (typeof preset.path !== 'string' || !isAbsolute(preset.path)) {
|
|
159
|
+
throw new Error('ptc-plus: DSH cordis preset did not expose an absolute composition path')
|
|
160
|
+
}
|
|
161
|
+
const directory = join(dirname(preset.path), 'skills')
|
|
162
|
+
if (!containsCompanionSkill(directory, fileExists)) {
|
|
163
|
+
throw new Error(`ptc-plus: DSH cordis preset does not contain ${CORDIS_SKILL_NAME}/SKILL.md beside its composition`)
|
|
164
|
+
}
|
|
165
|
+
return directory
|
|
148
166
|
}
|
|
149
|
-
|
|
150
|
-
|
|
167
|
+
|
|
168
|
+
for (const layout of COMPANION_PACKAGE_LAYOUTS) {
|
|
169
|
+
const pkg = options.packageOf?.(layout.name, options.packageBaseUrl)
|
|
170
|
+
if (pkg === undefined) continue
|
|
171
|
+
if (pkg?.name !== layout.name || typeof pkg.dir !== 'string' || !isAbsolute(pkg.dir)) {
|
|
172
|
+
throw new Error(`ptc-plus: DSH pluginPackages returned an invalid ${layout.name} package resource`)
|
|
173
|
+
}
|
|
174
|
+
const directory = layout.skillRoot(pkg.dir)
|
|
175
|
+
if (containsCompanionSkill(directory, fileExists)) return directory
|
|
151
176
|
}
|
|
152
|
-
|
|
177
|
+
|
|
178
|
+
if (options.packageOf === undefined && options.resolveLegacyPackageManifest !== undefined) {
|
|
179
|
+
let manifest
|
|
180
|
+
try {
|
|
181
|
+
manifest = options.resolveLegacyPackageManifest('@deepseek-ai/dsh-agent-presets/package.json')
|
|
182
|
+
} catch (error) {
|
|
183
|
+
if (error?.code !== 'MODULE_NOT_FOUND' && error?.code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED') throw error
|
|
184
|
+
}
|
|
185
|
+
if (manifest !== undefined) {
|
|
186
|
+
const directory = join(dirname(manifest), 'presets', CORDIS_PRESET_ID, 'skills')
|
|
187
|
+
if (containsCompanionSkill(directory, fileExists)) return directory
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
throw new Error(`ptc-plus: the active DSH installation does not publish a loadable ${CORDIS_SKILL_NAME} companion Skill through its package-resolution surface`)
|
|
153
191
|
}
|
|
154
192
|
|
|
155
193
|
async function requireCompanionSkill(agent, skills) {
|
|
@@ -270,6 +308,7 @@ export function createCordisToolsOwner(
|
|
|
270
308
|
ctx,
|
|
271
309
|
cordisPlugin = CordisTools,
|
|
272
310
|
skillFilesystemPlugin = CordisSkillFilesystem,
|
|
311
|
+
companionResolution = undefined,
|
|
273
312
|
) {
|
|
274
313
|
if (typeof ctx?.agents?.list !== 'function') {
|
|
275
314
|
throw new Error('ptc-plus: cordisToolsEnabled requires the DSH agents.list API')
|
|
@@ -281,6 +320,22 @@ export function createCordisToolsOwner(
|
|
|
281
320
|
const cordisInspectLeases = createCordisInspectLeases(ctx)
|
|
282
321
|
const scopedCordisPlugin = cordisInspectLeases.plugin(cordisPlugin)
|
|
283
322
|
const scopedSkillPlugin = exactCompanionSkillPlugin(skillFilesystemPlugin)
|
|
323
|
+
const pluginPackages = ctx.get?.('pluginPackages')
|
|
324
|
+
const packageBaseUrl = ctx.baseUrl
|
|
325
|
+
const skillResolution = companionResolution ?? {
|
|
326
|
+
packageOf: pluginPackages === undefined
|
|
327
|
+
? undefined
|
|
328
|
+
: (name, parentURL) => {
|
|
329
|
+
if (typeof pluginPackages?.packageOf !== 'function') {
|
|
330
|
+
throw new Error('ptc-plus: cordisToolsEnabled requires the DSH pluginPackages.packageOf API')
|
|
331
|
+
}
|
|
332
|
+
return pluginPackages.packageOf(name, parentURL)
|
|
333
|
+
},
|
|
334
|
+
packageBaseUrl,
|
|
335
|
+
resolveLegacyPackageManifest: pluginPackages === undefined && typeof packageBaseUrl === 'string'
|
|
336
|
+
? specifier => createRequire(packageBaseUrl).resolve(specifier)
|
|
337
|
+
: undefined,
|
|
338
|
+
}
|
|
284
339
|
let disposed = false
|
|
285
340
|
|
|
286
341
|
const leafErrors = error => error instanceof AggregateError
|
|
@@ -364,13 +419,14 @@ export function createCordisToolsOwner(
|
|
|
364
419
|
mounts.set(agent, mount)
|
|
365
420
|
mount.activation = Promise.resolve().then(async () => {
|
|
366
421
|
const { agentPresets, skills } = requireCompanionServices(agent)
|
|
367
|
-
const skillDirectory = await
|
|
422
|
+
const skillDirectory = await resolveCompanionSkillDirectory(agentPresets, skillResolution)
|
|
368
423
|
if (mount.disposed) return
|
|
369
424
|
|
|
370
425
|
const skillFiber = agent.ctx.plugin(scopedSkillPlugin, {
|
|
371
426
|
providerName: CORDIS_SKILL_PROVIDER,
|
|
372
427
|
includeDefaultRoots: false,
|
|
373
|
-
|
|
428
|
+
bundledSkillDir: skillDirectory,
|
|
429
|
+
watch: false,
|
|
374
430
|
})
|
|
375
431
|
if (typeof skillFiber?.dispose !== 'function') {
|
|
376
432
|
throw new Error('ptc-plus: DSH Context.plugin did not return a disposable Cordis Skill fiber')
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
/** Own PTC direct presentation, prompt projection, and stream normalization. */
|
|
2
|
+
import { boundContextSummary, createUserMessage } from '@deepseek-ai/dsh-llm'
|
|
3
|
+
import { validateJsonSchemaValue } from '@deepseek-ai/dsh-tools'
|
|
4
|
+
import { PTC_MESSAGE_SOURCE_KIND } from './message-sources.js'
|
|
2
5
|
import { canonicalizeToolCallStream } from './tool-call-canonicalizer.js'
|
|
3
6
|
import { editRunCodeSchema } from './rejected-cell-editor.js'
|
|
4
7
|
import { sessionRuntimeContexts } from './runtime-contexts.js'
|
|
@@ -19,6 +22,12 @@ const RUN_CODE_CODE_DESCRIPTION = 'Code for the next REPL cell, parsed as the bo
|
|
|
19
22
|
const RUN_CODE_DESCRIPTION_DESCRIPTION = 'Short active-voice summary of what this cell does, 5-10 words (shown in the UI).'
|
|
20
23
|
const CODE_TRANSPORT_INSTRUCTION = '`run_code` and `edit_run_code` are the only tools callable directly. Call every native tool declared by the SDK from inside a program.'
|
|
21
24
|
const PTC_COLLAPSE_SECTION_NAMES = Object.freeze(['tools:ptc-only', 'tools:code-only'])
|
|
25
|
+
const INVALID_TOOL_JSON_FAILURE = Object.freeze({
|
|
26
|
+
code: 'MALFORMED_RESPONSE',
|
|
27
|
+
message: 'DeepSeek Messages stream: tool input is invalid JSON',
|
|
28
|
+
})
|
|
29
|
+
const MALFORMED_TOOL_REJECTION_ID = 'dsh-ptc-plus-malformed-tool-json'
|
|
30
|
+
const MALFORMED_TOOL_REJECTION_ARGUMENTS = '{"dsh_ptc_plus_malformed_tool_json":'
|
|
22
31
|
|
|
23
32
|
function adaptRunCodeSchema(tool) {
|
|
24
33
|
const parameters = tool.parameters
|
|
@@ -139,6 +148,314 @@ async function* bindCallPolicies(source, policy, calls) {
|
|
|
139
148
|
}
|
|
140
149
|
}
|
|
141
150
|
|
|
151
|
+
function invalidToolJsonFinish(chunk) {
|
|
152
|
+
const failure = chunk?.reason?.failure
|
|
153
|
+
return chunk?.type === 'finish'
|
|
154
|
+
&& chunk.reason.kind === 'error'
|
|
155
|
+
&& failure?.code === INVALID_TOOL_JSON_FAILURE.code
|
|
156
|
+
&& failure?.message === INVALID_TOOL_JSON_FAILURE.message
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function invalidToolJsonError(error) {
|
|
160
|
+
const failure = error?.failure ?? error
|
|
161
|
+
return failure?.code === INVALID_TOOL_JSON_FAILURE.code
|
|
162
|
+
&& failure?.message === INVALID_TOOL_JSON_FAILURE.message
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function directToolSchemas(tools) {
|
|
166
|
+
const schemas = new Map()
|
|
167
|
+
if (!Array.isArray(tools)) return schemas
|
|
168
|
+
for (const tool of tools) {
|
|
169
|
+
if (tool?.name !== RUN_CODE && tool?.name !== EDIT_RUN_CODE) continue
|
|
170
|
+
if (schemas.has(tool.name) || !isRecord(tool.parameters)) return new Map()
|
|
171
|
+
schemas.set(tool.name, tool.parameters)
|
|
172
|
+
}
|
|
173
|
+
return schemas
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function repairableDirectCall(chunks, tools) {
|
|
177
|
+
const finish = chunks.at(-1)
|
|
178
|
+
if (!invalidToolJsonFinish(finish)) return undefined
|
|
179
|
+
const completed = completedToolBlocks(chunks)
|
|
180
|
+
if (completed?.length !== 1) return undefined
|
|
181
|
+
const body = chunks.slice(0, -1)
|
|
182
|
+
const starts = body.filter(chunk => chunk?.type === 'block-start' && chunk.blockType === 'tool-call')
|
|
183
|
+
const deltas = body.filter(chunk => chunk?.type === 'tool-call-delta')
|
|
184
|
+
const ends = body.filter(chunk => chunk?.type === 'block-end' && chunk.block?.type === 'tool-call')
|
|
185
|
+
const usages = body.filter(chunk => chunk?.type === 'usage')
|
|
186
|
+
if (starts.length !== 1 || deltas.length === 0 || ends.length !== 1
|
|
187
|
+
|| usages.length > 1
|
|
188
|
+
|| body.some(chunk => chunk?.type !== 'usage' && !toolStreamChunk(chunk))) {
|
|
189
|
+
return undefined
|
|
190
|
+
}
|
|
191
|
+
const index = starts[0].index
|
|
192
|
+
const end = ends[0]
|
|
193
|
+
const id = end.block.id
|
|
194
|
+
const name = end.block.name
|
|
195
|
+
if (!Number.isSafeInteger(index) || index < 0 || typeof id !== 'string' || id.length === 0
|
|
196
|
+
|| (name !== RUN_CODE && name !== EDIT_RUN_CODE)
|
|
197
|
+
|| end.index !== index
|
|
198
|
+
|| typeof end.block.arguments !== 'string'
|
|
199
|
+
|| deltas.some(chunk => chunk.index !== index || chunk.id !== id
|
|
200
|
+
|| (chunk.name !== undefined && chunk.name !== name)
|
|
201
|
+
|| typeof chunk.argumentsDelta !== 'string')) {
|
|
202
|
+
return undefined
|
|
203
|
+
}
|
|
204
|
+
const raw = deltas.map(chunk => chunk.argumentsDelta).join('')
|
|
205
|
+
if (raw !== end.block.arguments || !raw.endsWith('}')) return undefined
|
|
206
|
+
const candidate = raw.slice(0, -1)
|
|
207
|
+
let value
|
|
208
|
+
try {
|
|
209
|
+
value = JSON.parse(candidate)
|
|
210
|
+
} catch {
|
|
211
|
+
return undefined
|
|
212
|
+
}
|
|
213
|
+
const schema = directToolSchemas(tools).get(name)
|
|
214
|
+
if (!isRecord(value) || schema === undefined
|
|
215
|
+
|| validateJsonSchemaValue(schema, value).length !== 0) return undefined
|
|
216
|
+
return { index, candidate }
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function canonicalToolBlock(index, block) {
|
|
220
|
+
return [
|
|
221
|
+
{ type: 'block-start', index, blockType: 'tool-call' },
|
|
222
|
+
{
|
|
223
|
+
type: 'tool-call-delta', index, id: block.id, name: block.name,
|
|
224
|
+
argumentsDelta: block.arguments,
|
|
225
|
+
},
|
|
226
|
+
{ type: 'block-end', index, block },
|
|
227
|
+
]
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function repairedDirectChunks(chunks, repair, usageAlreadyEmitted) {
|
|
231
|
+
const end = chunks.find(chunk => chunk?.type === 'block-end'
|
|
232
|
+
&& chunk.index === repair.index && chunk.block?.type === 'tool-call')
|
|
233
|
+
const block = { ...end.block, arguments: repair.candidate }
|
|
234
|
+
const usage = usageAlreadyEmitted ? undefined : firstUsageChunk(chunks)
|
|
235
|
+
return [
|
|
236
|
+
...canonicalToolBlock(repair.index, block),
|
|
237
|
+
...usage === undefined ? [] : [usage],
|
|
238
|
+
{ type: 'finish', reason: { kind: 'tool-calls' } },
|
|
239
|
+
]
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function completedToolBlocks(chunks) {
|
|
243
|
+
const body = chunks.slice(0, -1)
|
|
244
|
+
if (body.filter(chunk => chunk?.type === 'usage').length > 1
|
|
245
|
+
|| body.some(chunk => chunk?.type !== 'usage' && !toolStreamChunk(chunk))) {
|
|
246
|
+
return undefined
|
|
247
|
+
}
|
|
248
|
+
const calls = new Map()
|
|
249
|
+
for (const chunk of chunks) {
|
|
250
|
+
if (chunk?.type === 'block-start' && chunk.blockType === 'tool-call') {
|
|
251
|
+
if (!Number.isSafeInteger(chunk.index) || chunk.index < 0) return undefined
|
|
252
|
+
const call = calls.get(chunk.index)
|
|
253
|
+
if (call?.started === true || call?.block !== undefined) return undefined
|
|
254
|
+
if (call === undefined) calls.set(chunk.index, {
|
|
255
|
+
arguments: '', hasDelta: false, started: true,
|
|
256
|
+
})
|
|
257
|
+
else call.started = true
|
|
258
|
+
} else if (chunk?.type === 'tool-call-delta') {
|
|
259
|
+
if (!Number.isSafeInteger(chunk.index) || chunk.index < 0) return undefined
|
|
260
|
+
let call = calls.get(chunk.index)
|
|
261
|
+
if (call === undefined) {
|
|
262
|
+
call = { arguments: '', hasDelta: false, started: false }
|
|
263
|
+
calls.set(chunk.index, call)
|
|
264
|
+
}
|
|
265
|
+
if (call.block !== undefined
|
|
266
|
+
|| typeof chunk.id !== 'string' || chunk.id.length === 0
|
|
267
|
+
|| (call.id !== undefined && call.id !== chunk.id)
|
|
268
|
+
|| (chunk.name !== undefined && (typeof chunk.name !== 'string' || chunk.name.length === 0
|
|
269
|
+
|| (call.name !== undefined && call.name !== chunk.name)))
|
|
270
|
+
|| typeof chunk.argumentsDelta !== 'string') return undefined
|
|
271
|
+
call.id ??= chunk.id
|
|
272
|
+
if (chunk.name !== undefined) call.name ??= chunk.name
|
|
273
|
+
call.hasDelta = true
|
|
274
|
+
call.arguments += chunk.argumentsDelta
|
|
275
|
+
} else if (chunk?.type === 'block-end' && chunk.block?.type === 'tool-call') {
|
|
276
|
+
if (!Number.isSafeInteger(chunk.index) || chunk.index < 0) return undefined
|
|
277
|
+
let call = calls.get(chunk.index)
|
|
278
|
+
if (call === undefined) {
|
|
279
|
+
call = { arguments: '', hasDelta: false, started: false }
|
|
280
|
+
calls.set(chunk.index, call)
|
|
281
|
+
}
|
|
282
|
+
if (call.block !== undefined
|
|
283
|
+
|| typeof chunk.block.id !== 'string' || chunk.block.id.length === 0
|
|
284
|
+
|| typeof chunk.block.name !== 'string' || chunk.block.name.length === 0
|
|
285
|
+
|| typeof chunk.block.arguments !== 'string'
|
|
286
|
+
|| (call.id !== undefined && call.id !== chunk.block.id)
|
|
287
|
+
|| (call.name !== undefined && call.name !== chunk.block.name)
|
|
288
|
+
|| (call.hasDelta && call.arguments !== chunk.block.arguments)) return undefined
|
|
289
|
+
call.block = chunk.block
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (calls.size === 0 || [...calls.values()].some(call => call.block === undefined)) {
|
|
293
|
+
return undefined
|
|
294
|
+
}
|
|
295
|
+
const completed = [...calls].map(([index, call]) => ({ index, block: call.block }))
|
|
296
|
+
if (new Set(completed.map(call => call.block.id)).size !== completed.length) return undefined
|
|
297
|
+
return completed
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function firstUsageChunk(chunks) {
|
|
301
|
+
return chunks.find(chunk => chunk?.type === 'usage')
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function settleMalformedToolCalls(chunks, priorIndexes, usageAlreadyEmitted) {
|
|
305
|
+
if (!invalidToolJsonFinish(chunks.at(-1))) return undefined
|
|
306
|
+
const calls = completedToolBlocks(chunks)
|
|
307
|
+
if (calls === undefined || calls.some(call => priorIndexes.has(call.index))) return undefined
|
|
308
|
+
const usage = usageAlreadyEmitted ? undefined : firstUsageChunk(chunks)
|
|
309
|
+
return [
|
|
310
|
+
...calls.flatMap(call => canonicalToolBlock(call.index, call.block)),
|
|
311
|
+
...usage === undefined ? [] : [usage],
|
|
312
|
+
{ type: 'finish', reason: { kind: 'tool-calls' } },
|
|
313
|
+
]
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function toolStreamChunk(chunk) {
|
|
317
|
+
return chunk?.type === 'tool-call-delta'
|
|
318
|
+
|| (chunk?.type === 'block-start' && chunk.blockType === 'tool-call')
|
|
319
|
+
|| (chunk?.type === 'block-end' && chunk.block?.type === 'tool-call')
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function unusedString(base, used) {
|
|
323
|
+
let value = base
|
|
324
|
+
for (let suffix = 2; used.has(value); suffix++) value = `${base}-${suffix}`
|
|
325
|
+
return value
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function visibleBlockClosures(openBlocks, chunks) {
|
|
329
|
+
const closing = new Map([...openBlocks].map(([index, block]) => [index, { ...block }]))
|
|
330
|
+
for (const chunk of chunks) {
|
|
331
|
+
const block = closing.get(chunk?.index)
|
|
332
|
+
if (block === undefined) continue
|
|
333
|
+
if (chunk.type === `${block.type}-delta` && typeof chunk.text === 'string') {
|
|
334
|
+
block.text += chunk.text
|
|
335
|
+
} else if (chunk.type === 'block-end' && chunk.block?.type === block.type) {
|
|
336
|
+
block.final = chunk.block
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return [...closing].map(([index, block]) => ({
|
|
340
|
+
type: 'block-end',
|
|
341
|
+
index,
|
|
342
|
+
block: block.final ?? { type: block.type, text: block.text },
|
|
343
|
+
}))
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function rejectedMalformedToolStream(
|
|
347
|
+
chunks,
|
|
348
|
+
priorIndexes = new Set(),
|
|
349
|
+
openBlocks = new Map(),
|
|
350
|
+
usageAlreadyEmitted = false,
|
|
351
|
+
) {
|
|
352
|
+
const usedIndexes = new Set(priorIndexes)
|
|
353
|
+
const usedIds = new Set()
|
|
354
|
+
for (const chunk of chunks) {
|
|
355
|
+
if (Number.isSafeInteger(chunk?.index) && chunk.index >= 0) usedIndexes.add(chunk.index)
|
|
356
|
+
if (typeof chunk?.id === 'string') usedIds.add(chunk.id)
|
|
357
|
+
if (typeof chunk?.block?.id === 'string') usedIds.add(chunk.block.id)
|
|
358
|
+
}
|
|
359
|
+
let index = 0
|
|
360
|
+
while (usedIndexes.has(index)) index++
|
|
361
|
+
const id = unusedString(MALFORMED_TOOL_REJECTION_ID, usedIds)
|
|
362
|
+
const usage = usageAlreadyEmitted ? undefined : firstUsageChunk(chunks)
|
|
363
|
+
return [
|
|
364
|
+
...visibleBlockClosures(openBlocks, chunks),
|
|
365
|
+
{ type: 'block-start', index, blockType: 'tool-call' },
|
|
366
|
+
{
|
|
367
|
+
type: 'tool-call-delta', index, id, name: RUN_CODE,
|
|
368
|
+
argumentsDelta: MALFORMED_TOOL_REJECTION_ARGUMENTS,
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
type: 'block-end', index,
|
|
372
|
+
block: {
|
|
373
|
+
type: 'tool-call', id, name: RUN_CODE,
|
|
374
|
+
arguments: MALFORMED_TOOL_REJECTION_ARGUMENTS,
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
...usage === undefined ? [] : [usage],
|
|
378
|
+
{ type: 'finish', reason: { kind: 'tool-calls' } },
|
|
379
|
+
]
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function recoveredMalformedChunks(chunks, tools, priorIndexes, openBlocks, usageAlreadyEmitted) {
|
|
383
|
+
if (usageAlreadyEmitted && firstUsageChunk(chunks) !== undefined) {
|
|
384
|
+
return rejectedMalformedToolStream(chunks, priorIndexes, openBlocks, true)
|
|
385
|
+
}
|
|
386
|
+
const repair = repairableDirectCall(chunks, tools)
|
|
387
|
+
const settled = repair !== undefined && !priorIndexes.has(repair.index)
|
|
388
|
+
? repairedDirectChunks(chunks, repair, usageAlreadyEmitted)
|
|
389
|
+
: settleMalformedToolCalls(chunks, priorIndexes, usageAlreadyEmitted)
|
|
390
|
+
return settled === undefined
|
|
391
|
+
? rejectedMalformedToolStream(chunks, priorIndexes, openBlocks, usageAlreadyEmitted)
|
|
392
|
+
: [...visibleBlockClosures(openBlocks, chunks), ...settled]
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function observeVisibleBlock(openBlocks, chunk) {
|
|
396
|
+
if (chunk?.type === 'block-start'
|
|
397
|
+
&& (chunk.blockType === 'text' || chunk.blockType === 'reasoning')) {
|
|
398
|
+
openBlocks.set(chunk.index, { type: chunk.blockType, text: '' })
|
|
399
|
+
} else if (chunk?.type === 'text-delta' || chunk?.type === 'reasoning-delta') {
|
|
400
|
+
const block = openBlocks.get(chunk.index)
|
|
401
|
+
const type = chunk.type === 'text-delta' ? 'text' : 'reasoning'
|
|
402
|
+
if (block?.type === type) block.text += chunk.text
|
|
403
|
+
} else if (chunk?.type === 'block-end') {
|
|
404
|
+
openBlocks.delete(chunk.index)
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async function* recoverMalformedToolCallStream(source, tools) {
|
|
409
|
+
let pending = []
|
|
410
|
+
const priorIndexes = new Set()
|
|
411
|
+
const openBlocks = new Map()
|
|
412
|
+
let usageSeen = false
|
|
413
|
+
try {
|
|
414
|
+
for await (const chunk of source) {
|
|
415
|
+
if (pending.length === 0) {
|
|
416
|
+
if (toolStreamChunk(chunk)) pending.push(chunk)
|
|
417
|
+
else if (chunk?.type === 'usage' && usageSeen) pending.push(chunk)
|
|
418
|
+
else if (invalidToolJsonFinish(chunk)) {
|
|
419
|
+
yield* rejectedMalformedToolStream([chunk], priorIndexes, openBlocks, usageSeen)
|
|
420
|
+
openBlocks.clear()
|
|
421
|
+
} else {
|
|
422
|
+
if (Number.isSafeInteger(chunk?.index) && chunk.index >= 0) priorIndexes.add(chunk.index)
|
|
423
|
+
observeVisibleBlock(openBlocks, chunk)
|
|
424
|
+
if (chunk?.type === 'usage') usageSeen = true
|
|
425
|
+
yield chunk
|
|
426
|
+
}
|
|
427
|
+
continue
|
|
428
|
+
}
|
|
429
|
+
pending.push(chunk)
|
|
430
|
+
if (chunk?.type !== 'finish') continue
|
|
431
|
+
const output = invalidToolJsonFinish(chunk)
|
|
432
|
+
? recoveredMalformedChunks(pending, tools, priorIndexes, openBlocks, usageSeen)
|
|
433
|
+
: pending
|
|
434
|
+
for (const outputChunk of output) {
|
|
435
|
+
if (Number.isSafeInteger(outputChunk?.index) && outputChunk.index >= 0) {
|
|
436
|
+
priorIndexes.add(outputChunk.index)
|
|
437
|
+
}
|
|
438
|
+
if (outputChunk?.type === 'usage') usageSeen = true
|
|
439
|
+
yield outputChunk
|
|
440
|
+
}
|
|
441
|
+
if (invalidToolJsonFinish(chunk)) openBlocks.clear()
|
|
442
|
+
pending = []
|
|
443
|
+
}
|
|
444
|
+
} catch (error) {
|
|
445
|
+
if (invalidToolJsonError(error)) {
|
|
446
|
+
yield* recoveredMalformedChunks([
|
|
447
|
+
...pending,
|
|
448
|
+
{ type: 'finish', reason: { kind: 'error', failure: INVALID_TOOL_JSON_FAILURE } },
|
|
449
|
+
], tools, priorIndexes, openBlocks, usageSeen)
|
|
450
|
+
return
|
|
451
|
+
} else {
|
|
452
|
+
yield* pending
|
|
453
|
+
throw error
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
yield* pending
|
|
457
|
+
}
|
|
458
|
+
|
|
142
459
|
export function createDirectSurfaceOwner({
|
|
143
460
|
editTransport,
|
|
144
461
|
runtimeConfig,
|
|
@@ -383,7 +700,8 @@ export function createDirectSurfaceOwner({
|
|
|
383
700
|
editToolName: EDIT_RUN_CODE,
|
|
384
701
|
})
|
|
385
702
|
: next()
|
|
386
|
-
|
|
703
|
+
const recovered = recoverMalformedToolCallStream(source, options.tools)
|
|
704
|
+
return bindCallPolicies(recovered, policy, policy.owner.calls)
|
|
387
705
|
},
|
|
388
706
|
executionRejection(exec) {
|
|
389
707
|
if (exec.name === EDIT_RUN_CODE && exec.parent !== undefined) {
|
|
@@ -430,12 +748,18 @@ export function createDirectSurfaceOwner({
|
|
|
430
748
|
const outer = missingPath === 'description'
|
|
431
749
|
&& policy?.autoDescribeRunCode !== true
|
|
432
750
|
&& !(isRecord(exec.arguments) && typeof exec.arguments.description === 'string')
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
751
|
+
const text = outer
|
|
752
|
+
? 'The run_code outer transport arguments are invalid at JSON path $.description. Add a sibling string `description` field to run_code; a description nested inside a native-tool argument does not satisfy this outer requirement.'
|
|
753
|
+
: `A nested native-tool argument is missing required property "${missingPath}". Add a string description at that path in the native-tool arguments; the outer run_code description does not satisfy this nested requirement.`
|
|
754
|
+
// additionalContexts enter the durable inbox as messages, not prompt sections.
|
|
755
|
+
const context = createUserMessage({
|
|
756
|
+
source: {
|
|
757
|
+
kind: PTC_MESSAGE_SOURCE_KIND,
|
|
758
|
+
form: 'notice',
|
|
759
|
+
summary: boundContextSummary('tools:ptc-plus-run-code-arguments'),
|
|
760
|
+
},
|
|
761
|
+
content: [{ type: 'text', text }],
|
|
762
|
+
})
|
|
439
763
|
return {
|
|
440
764
|
...diagnosed,
|
|
441
765
|
additionalContexts: [...diagnosed.additionalContexts ?? [], context],
|
|
@@ -252,29 +252,15 @@ function assemble(source, replacements) {
|
|
|
252
252
|
return changed ? { code: chunks.join('') } : { error: 'edits must change at least one matched fragment' }
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
-
function editRunCodeParameterBranch(operation, operationSchema) {
|
|
256
|
-
return {
|
|
257
|
-
type: 'object',
|
|
258
|
-
additionalProperties: false,
|
|
259
|
-
properties: {
|
|
260
|
-
[operation]: operationSchema,
|
|
261
|
-
[EXPECTED_TARGET_CALL_SEQ]: {
|
|
262
|
-
type: 'integer', minimum: 0,
|
|
263
|
-
description: 'Optional target precondition copied from a validated diagnostic. The edit is rejected if the captured cell has another call sequence.',
|
|
264
|
-
},
|
|
265
|
-
},
|
|
266
|
-
required: [operation],
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
255
|
export function editRunCodeSchema() {
|
|
271
256
|
return {
|
|
272
257
|
name: 'edit_run_code',
|
|
273
258
|
description: 'Edit the most recent eligible cell captured at dispatch and run the complete corrected cell. A successful edit becomes the next target. This reruns the entire cell, including prior effects; use it after pre-execution rejection or when repeating those effects is safe under the called API\'s retry rules. Otherwise continue in a new run_code cell using retained values. A diagnostic may supply a localized correction; use it when it matches your intent and preserve its expected_target_call_seq. Send exactly one edits or regex_edits array: edits replaces unique literal fragments; regex_edits replaces a counted set of pattern matches. All ranges must be non-overlapping in the original cell. Regex replacements use JavaScript $1/$<name> captures, not \\1.',
|
|
274
259
|
parameters: {
|
|
275
260
|
type: 'object',
|
|
276
|
-
|
|
277
|
-
|
|
261
|
+
additionalProperties: false,
|
|
262
|
+
properties: {
|
|
263
|
+
edits: {
|
|
278
264
|
type: 'array', minItems: 1, maxItems: EDIT_LIMITS.exactEdits,
|
|
279
265
|
description: 'Atomic exact replacements, all resolved against the original target cell.',
|
|
280
266
|
items: {
|
|
@@ -285,8 +271,8 @@ export function editRunCodeSchema() {
|
|
|
285
271
|
},
|
|
286
272
|
required: ['old_string', 'new_string'],
|
|
287
273
|
},
|
|
288
|
-
}
|
|
289
|
-
|
|
274
|
+
},
|
|
275
|
+
regex_edits: {
|
|
290
276
|
type: 'array', minItems: 1, maxItems: EDIT_LIMITS.regexEdits,
|
|
291
277
|
description: 'Atomic regular-expression replacements, all matched against the original target cell with JavaScript pattern and replacement semantics; each replacement is a JavaScript template (see replacement).',
|
|
292
278
|
items: {
|
|
@@ -299,8 +285,12 @@ export function editRunCodeSchema() {
|
|
|
299
285
|
},
|
|
300
286
|
required: ['pattern', 'flags', 'replacement', 'expected_matches'],
|
|
301
287
|
},
|
|
302
|
-
}
|
|
303
|
-
|
|
288
|
+
},
|
|
289
|
+
[EXPECTED_TARGET_CALL_SEQ]: {
|
|
290
|
+
type: 'integer', minimum: 0,
|
|
291
|
+
description: 'Optional target precondition copied from a validated diagnostic. The edit is rejected if the captured cell has another call sequence.',
|
|
292
|
+
},
|
|
293
|
+
},
|
|
304
294
|
},
|
|
305
295
|
}
|
|
306
296
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-ptc-plus",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.8",
|
|
4
4
|
"icon": "assets/ptc-plus-icon.svg",
|
|
5
5
|
"description": "A session-bound agent-native REPL for DeepSeek Harness PTC mode.",
|
|
6
6
|
"type": "module",
|
|
@@ -92,6 +92,9 @@
|
|
|
92
92
|
"semantic:check": "node scripts/semantic-obligations.mjs",
|
|
93
93
|
"test:client:web": "node scripts/client-web-smoke.mjs",
|
|
94
94
|
"test:client:layout": "node scripts/client-binding-layout.mjs",
|
|
95
|
+
"host:baseline:update": "node scripts/dsh-dependency-baseline.mjs update",
|
|
96
|
+
"host:baseline:verify": "node scripts/dsh-dependency-baseline.mjs verify",
|
|
97
|
+
"test:host-contract": "node scripts/dsh-host-contract-suite.mjs",
|
|
95
98
|
"prepack": "npm run build:check",
|
|
96
99
|
"hooks:install": "node scripts/review-findings.mjs install-hook",
|
|
97
100
|
"review:new": "node scripts/review-findings.mjs create",
|
|
@@ -106,7 +109,9 @@
|
|
|
106
109
|
"session:migrate": "node scripts/migrate-session-log.mjs",
|
|
107
110
|
"coverage": "node scripts/coverage.mjs",
|
|
108
111
|
"coverage:focus": "node scripts/coverage.mjs --focus",
|
|
109
|
-
"
|
|
112
|
+
"test:ci:runtime": "node --test --test-concurrency=1 test/package-contract.test.js test/compiler-platform.test.js test/direct-surface-policy.test.js test/isolated-entrance.test.js test/plugin-native-call-transport.test.js",
|
|
113
|
+
"verify": "npm run review:validate && npm run host:baseline:verify && npm run semantic:check && npm run build:check && node --check index.js && node --check internal/kernel-worker.js && node --check scripts/migrate-session-log.mjs && node --check scripts/expensive-headless-acceptance.mjs && node --check scripts/ab-headless-trajectories.mjs && npm run test:client && npm run coverage",
|
|
114
|
+
"verify:ci": "npm run review:validate && npm run host:baseline:verify && npm run semantic:check && npm run build:check && node --check index.js && node --check internal/kernel-worker.js && node --check scripts/migrate-session-log.mjs && npm run test:client && npm run test:ci:runtime",
|
|
110
115
|
"check": "node scripts/review-findings.mjs check",
|
|
111
116
|
"test:expensive": "node scripts/expensive-headless-acceptance.mjs",
|
|
112
117
|
"test:ab": "node scripts/ab-headless-trajectories.mjs"
|
|
@@ -151,6 +156,7 @@
|
|
|
151
156
|
}
|
|
152
157
|
},
|
|
153
158
|
"devDependencies": {
|
|
159
|
+
"@deepseek-ai/dsh": "next",
|
|
154
160
|
"@deepseek-ai/dsh-agent-loop": "next",
|
|
155
161
|
"@deepseek-ai/dsh-api-gateway": "next",
|
|
156
162
|
"@deepseek-ai/dsh-api-remotes": "next",
|
package/src/client-workbench.js
CHANGED
|
@@ -484,11 +484,13 @@ export function createUserBindingsWorkbench(React, deps) {
|
|
|
484
484
|
const content = React.useRef(null)
|
|
485
485
|
const localReturnFocus = React.useRef(null)
|
|
486
486
|
const returnFocus = returnFocusRef ?? localReturnFocus
|
|
487
|
-
React.
|
|
487
|
+
const attachContent = React.useCallback(element => {
|
|
488
|
+
content.current = element
|
|
489
|
+
if (element === null) return
|
|
488
490
|
const previous = document.activeElement
|
|
489
491
|
// A resumed mount may have no focused control; keep the last valid entry.
|
|
490
|
-
if (!
|
|
491
|
-
|
|
492
|
+
if (!element.contains(previous) && canReturnFocus(previous)) returnFocus.current = previous
|
|
493
|
+
element.querySelector('button')?.focus()
|
|
492
494
|
}, [returnFocus])
|
|
493
495
|
const close = () => {
|
|
494
496
|
const target = canReturnFocus(returnFocus.current) ? returnFocus.current : getReturnFocus?.()
|
|
@@ -509,7 +511,7 @@ export function createUserBindingsWorkbench(React, deps) {
|
|
|
509
511
|
}
|
|
510
512
|
}
|
|
511
513
|
return h(Modal, { open: true, onClose: close, title: t('bindings.title'), headless: true, className: 'ptcPlusBindingsModal' },
|
|
512
|
-
h('div', { className: 'ptcPlusBindingsDialog', ref:
|
|
514
|
+
h('div', { className: 'ptcPlusBindingsDialog', ref: attachContent, onKeyDown: trapFocus },
|
|
513
515
|
h('div', { className: 'ptcPlusBindingsDialogHead' },
|
|
514
516
|
h('h2', null, t('bindings.title')),
|
|
515
517
|
h('button', { type: 'button', className: 'ptcPlusDialogClose', onClick: close,
|