dsh-ptc-plus 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,9 @@
1
1
  # dsh-ptc-plus
2
2
 
3
+ English | [简体中文](README.zh.md)
4
+
3
5
  [![Node.js ^22.19.0 || >=24.0.0](https://img.shields.io/badge/Node.js-%5E22.19.0%20%7C%7C%20%3E%3D24.0.0-5fa04e?logo=nodedotjs&logoColor=white)](package.json)
6
+ [![npm version](https://img.shields.io/npm/v/dsh-ptc-plus?logo=npm)](https://www.npmjs.com/package/dsh-ptc-plus)
4
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
5
8
  [![DeepSeek Harness](https://img.shields.io/badge/DeepSeek%20Harness-PTC%20mode-4b6bfb)](https://github.com/deepseek-ai/deepseek-harness)
6
9
  [![Status: community plugin](https://img.shields.io/badge/Status-community%20plugin-lightgrey)](docs/publishing.md)
@@ -70,6 +73,8 @@ edit_run_code({ edits: [{ old_string: 'deps.length', new_string: 'deps' }] })
70
73
 
71
74
  Only the diff goes in. The full source stays out of the conversation. Exact replacements and regular expressions both have limits, so a bad pattern cannot hang.
72
75
 
76
+ When a rejected cell has exactly one validated missing closing token at the end, its diagnostic includes the complete `edit_run_code(...)` call needed to apply that correction and rerun the cell. That generated call carries an `expected_target_call_seq` precondition, so it is rejected without execution if another cell becomes the edit target first. PTC Plus never applies the suggestion automatically; ambiguous repairs or repairs without a persistent target identity still require corrected source.
77
+
73
78
  ### Module syntax
74
79
 
75
80
  DSH PTC mode executes each `run_code` as an async function body, where static `import` and `export` declarations are invalid. PTC Plus adapts those forms before execution with AST analysis.
@@ -107,10 +112,12 @@ Open **Settings → Plugin configuration** to use the card shown above. The `ena
107
112
 
108
113
  Every setting applies live and keeps existing bindings. A submitted cell uses one configuration for its complete execution; changes made while it runs apply to cells submitted afterward. A failed change rolls back. Node fixes a worker's V8 old-generation limit when the worker starts, so that one setting is rejected while a session worker is active and can be changed after the session is disposed. A failed enable is rolled back and persisted as disabled.
109
114
 
110
- `cordisToolsEnabled` is off by default. Turning it on atomically adds DSH's official Cordis tools, owner guidance, and `cordis-plugin-development` companion Skill to PTC agents; turning it off removes all three. It neither switches presets nor changes the direct `run_code`/`edit_run_code` surface. Cordis runs model-written plugins against the live DSH runtime, so enabling it requires shell-level trust.
115
+ `cordisToolsEnabled` is off by default. Turning it on atomically adds DSH's official Cordis tools, owner guidance, and exactly the `cordis-plugin-development` companion Skill to PTC agents; sibling Skills in the shipped preset are not exposed. Turning it off removes all three. It neither switches presets nor changes the direct `run_code`/`edit_run_code` surface. Cordis runs model-written plugins against the live DSH runtime, so enabling it requires shell-level trust.
111
116
 
112
117
  When a Cordis call rejects after a cell has assigned a large host or client source string, that top-level binding remains live. Retry only the Cordis call from a short later cell and reuse the binding instead of resending the source.
113
118
 
119
+ After cold recovery or re-enabling Cordis, recorded Cordis values remain historical data but do not prove that process-local Plugins, Runs, approvals, or earlier Inspect observations are still live. PTC Plus adds a bounded recovery context until a new successful Cordis Inspect call validates the current process.
120
+
114
121
  See [Client UI](docs/client-ui.md), [ADR 0019](docs/adr/0019-plugin-settings-and-kill-switch.md), and [ADR 0020](docs/adr/0020-optional-cordis-tools-in-ptc-mode.md).
115
122
 
116
123
  ## Scope
@@ -119,14 +126,14 @@ PTC Plus provides the session-bound persistent `run_code` layer. DSH and the ope
119
126
 
120
127
  ## Install
121
128
 
122
- Requires Node.js `^22.19.0 || >=24.0.0` and DSH with TypeScript PTC mode:
129
+ Requires Node.js `^22.19.0 || >=24.0.0` and DSH with TypeScript PTC mode. Install the published package from npm into the profile you use:
123
130
 
124
131
  ```sh
125
- dsh plugin --profile <profile> add github:muyuanjin/dsh-ptc-plus#main
132
+ dsh plugin --profile <profile> add dsh-ptc-plus
126
133
  dsh --profile <profile> --dump-config
127
134
  ```
128
135
 
129
- Other install methods and compatibility details are in the [installation guide](docs/installation.md).
136
+ Restart that DSH profile after installation. Version-pinned npm, GitHub, local-checkout, and tarball installs are covered in the [installation guide](docs/installation.md).
130
137
 
131
138
  `danger-full-access` is the primary supported experience. The worker isolates lifecycle, not malicious code.
132
139
 
package/README.zh.md CHANGED
@@ -1,6 +1,9 @@
1
1
  # dsh-ptc-plus
2
2
 
3
+ [English](README.md) | 简体中文
4
+
3
5
  [![Node.js ^22.19.0 || >=24.0.0](https://img.shields.io/badge/Node.js-%5E22.19.0%20%7C%7C%20%3E%3D24.0.0-5fa04e?logo=nodedotjs&logoColor=white)](package.json)
6
+ [![npm version](https://img.shields.io/npm/v/dsh-ptc-plus?logo=npm)](https://www.npmjs.com/package/dsh-ptc-plus)
4
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
5
8
  [![DeepSeek Harness](https://img.shields.io/badge/DeepSeek%20Harness-PTC%20mode-4b6bfb)](https://github.com/deepseek-ai/deepseek-harness)
6
9
  [![Status: community plugin](https://img.shields.io/badge/Status-community%20plugin-lightgrey)](docs/publishing.md)
@@ -70,6 +73,8 @@ edit_run_code({ edits: [{ old_string: 'deps.length', new_string: 'deps' }] })
70
73
 
71
74
  模型只发改动,完整源码留在对话之外。精确文本替换和正则替换都有限制,坏的正则不会卡住。
72
75
 
76
+ 当被拒 cell 的末尾可唯一验证为缺少一个闭合符时,诊断会直接给出应用该修正并重运行所需的完整 `edit_run_code(...)` 调用。生成的调用带有 `expected_target_call_seq` 前置条件;如果另一个 cell 已先成为 edit 目标,它会拒绝且不执行。PTC Plus 不会自动应用这项建议;有歧义或没有持久目标身份的修复仍需提交修正后的源码。
77
+
73
78
  ### 模块语法
74
79
 
75
80
  DSH 的 PTC 模式把每个 `run_code` 当作 async function body 执行,静态 `import` 和 `export` 声明在这个函数体里无效。PTC Plus 会在执行前用 AST 分析适配这些形式。
@@ -107,10 +112,12 @@ import { readFile } from 'node:fs/promises'
107
112
 
108
113
  所有设置都会实时应用,并保留已有 binding。已提交的 cell 在完整执行期间使用同一份配置;执行期间发生的更新用于随后提交的 cell。更新失败会回滚。Node 在 worker 创建时固定 V8 old-generation 上限,因此活动 session worker 存在时修改这一项会被拒绝,释放 session 后才能修改。启用失败时,运行时会回滚并把设置保持为停用。
109
114
 
110
- `cordisToolsEnabled` 默认关闭。打开后,DSH 官方 Cordis 工具、owner guidance 与 `cordis-plugin-development` companion Skill 会作为一个整体加入 PTC agent,关闭时也会一起移除;它不切换 preset,也不改变 `run_code`/`edit_run_code` 的直接调用面。Cordis 能在实时 DSH runtime 中运行模型编写的插件,开启它需要接受 shell 级信任。
115
+ `cordisToolsEnabled` 默认关闭。打开后,DSH 官方 Cordis 工具、owner guidance 与精确的 `cordis-plugin-development` companion Skill 会作为一个整体加入 PTC agent;同一 shipped preset 目录中的其他 Skill 不会随之暴露。关闭时这三项也会一起移除;它不切换 preset,也不改变 `run_code`/`edit_run_code` 的直接调用面。Cordis 能在实时 DSH runtime 中运行模型编写的插件,开启它需要接受 shell 级信任。
111
116
 
112
117
  如果 Cordis 调用在 cell 已经赋值大段 host 或 client 源码之后才失败,这些顶层 binding 仍会保留。后续只需在短 cell 中重试 Cordis 调用并复用该 binding,无需再次传输源码。
113
118
 
119
+ cold recovery 或重新启用 Cordis 后,已记录的 Cordis value 仍是历史数据,但不能证明进程内 Plugin、Run、approval 或先前 Inspect observation 仍然存活。PTC Plus 会提供有界恢复 context,直到一次新的成功 Cordis Inspect 调用验证当前进程。
120
+
114
121
  详见 [客户端 UI](docs/client-ui.md)、[ADR 0019](docs/adr/0019-plugin-settings-and-kill-switch.md) 与 [ADR 0020](docs/adr/0020-optional-cordis-tools-in-ptc-mode.md)。
115
122
 
116
123
  ## 范围
@@ -119,14 +126,14 @@ PTC Plus 提供会话绑定的持久 `run_code` 层。原生工具的权限、
119
126
 
120
127
  ## 安装
121
128
 
122
- 要求 Node.js `^22.19.0 || >=24.0.0`,并已安装带 TypeScript PTC 模式的 DSH:
129
+ 要求 Node.js `^22.19.0 || >=24.0.0`,并已安装带 TypeScript PTC 模式的 DSH。把 npm 正式包安装到你实际使用的 profile:
123
130
 
124
131
  ```sh
125
- dsh plugin --profile <profile> add github:muyuanjin/dsh-ptc-plus#main
132
+ dsh plugin --profile <profile> add dsh-ptc-plus
126
133
  dsh --profile <profile> --dump-config
127
134
  ```
128
135
 
129
- 其他安装方法和兼容性说明见[安装指南](docs/installation.md)。
136
+ 安装后重启对应的 DSH profile。固定 npm 版本、GitHub、本地 checkout 和 tarball 安装方式见[安装指南](docs/installation.md)。
130
137
 
131
138
  `danger-full-access` 是首要支持方式。worker 只隔离生命周期,不隔离恶意代码。
132
139
 
@@ -6,7 +6,8 @@ not achieve the intended result. PTC Plus therefore registers `edit_run_code` as
6
6
 
7
7
  ```ts
8
8
  edit_run_code({
9
- edits: Array<{ old_string: string; new_string: string }>
9
+ edits: Array<{ old_string: string; new_string: string }>,
10
+ expected_target_call_seq?: number
10
11
  })
11
12
 
12
13
  edit_run_code({
@@ -15,7 +16,8 @@ edit_run_code({
15
16
  flags: string
16
17
  replacement: string
17
18
  expected_matches: number
18
- }>
19
+ }>,
20
+ expected_target_call_seq?: number
19
21
  })
20
22
  ```
21
23
 
@@ -65,7 +67,11 @@ session-log projection captures the eligible target at the edit call event and a
65
67
  source only when its target sequence matches that snapshot and its journal is valid and non-noop,
66
68
  so live and recovered eligibility derive from the same persisted relation.
67
69
 
68
- The caller sends exactly one of `edits` or `regex_edits`. Exact edits contain at most 16 items; each
70
+ The caller sends exactly one of `edits` or `regex_edits`, plus an optional non-negative
71
+ `expected_target_call_seq` precondition. The precondition must equal the call sequence of the target
72
+ captured for the persisted edit event; a mismatch returns an unedited result before derived
73
+ dispatch. Ordinary model-authored edits omit it and retain latest-target selection. Exact edits
74
+ contain at most 16 items; each
69
75
  non-empty `old_string` must differ from `new_string` and occur exactly once in the target. Regular
70
76
  expression edits also contain at most 16 items, use unique `gimsu` flags, reject zero-length matches,
71
77
  and require an exact `expected_matches` count. All ranges resolve against the original target and
@@ -98,6 +104,22 @@ operation and outcome; changing an aggregate runtime-context snapshot merely to
98
104
  would repeat unrelated policy context. Rewrite feedback and unrelated recovery tips retain their
99
105
  own independently justified lifecycles.
100
106
 
107
+ A live `PTC-C001` at the exact source EOF may carry a validated one-call repair. The analyzer tries
108
+ only the three single-token suffixes `}`, `)`, and `]` against the same binding catalog, reserved
109
+ bindings, redeclaration mode, rewrite policy, and import state as the rejected cell. Exactly one
110
+ candidate must complete preparation without a binding collision, and the existing exact editor
111
+ must be able to express it from a bounded unique source suffix. The rejected call must also have a
112
+ persistent event sequence. The diagnostic then renders those already-validated
113
+ `edit_run_code({ edits: [...], expected_target_call_seq })` arguments on one help line. At dispatch,
114
+ the precondition binds the proof to that rejected cell even if another matching source has become
115
+ editable. This evidence takes precedence over the generic source-length recommendation, but remains
116
+ conditional on
117
+ `edit_run_code` being declared for that request. Zero or multiple candidates, non-EOF errors,
118
+ editor budget failures, missing target identity, and candidates rejected later in preparation
119
+ retain the generic guidance.
120
+ The plugin never applies this repair itself: syntax/preflight acceptance does not prove model
121
+ intent, and explicit edit dispatch preserves DSH authority and truthful history.
122
+
101
123
  ## Alternatives considered
102
124
 
103
125
  **Rewrite `edit_run_code` to `run_code` before persistence.** This reuses DSH's default `run_code`-only
@@ -6,12 +6,14 @@ The persistent REPL contract must stay short enough for every request, while som
6
6
 
7
7
  ## Decision
8
8
 
9
- PTC Plus keeps the stable REPL guidance limited to invariants, discovery entry points, and environment-neutral boundaries. `internal/session-log-view.js` scans the event sequence once, pairs settled calls within each open turn, validates execution journals and rewrite records once, and returns immutable facts for edit targeting, successful-run reset, context distance, and canonical DSH system-prompt snapshots. Each snapshot preserves its named sections. `internal/runtime-contexts.js` renders rewrite feedback only when the rewritten cell failed or lacks a valid journal, plus at most one context from the reserved `tools:ptc-plus-tip/<trigger>/<ordinal>` name family. Successful transparent rewrites remain result metadata and do not create aggregate snapshot churn. It never emits edit provenance or target-lifecycle context: those facts belong to the registered tool call/result and its private derived-execution metadata. Tips never change the tool list, tool schemas, or system sections.
9
+ PTC Plus keeps the stable REPL guidance limited to invariants, discovery entry points, and environment-neutral boundaries. `internal/session-log-view.js` scans the event sequence once, pairs settled calls within each open turn, validates execution journals and rewrite records once, and returns immutable facts for edit targeting, successful-run reset, context distance, normalized Cordis call/inspection counts, and canonical DSH system-prompt snapshots. Each snapshot preserves its named sections. `internal/runtime-contexts.js` renders rewrite feedback only when the rewritten cell failed or lacks a valid journal, the fixed `tools:ptc-plus-cordis-recovery` context when a new agent or Cordis enable generation contains historical Cordis transcript without a newer successful live Inspect, plus at most one context from the reserved `tools:ptc-plus-tip/<trigger>/<ordinal>` name family. Successful transparent rewrites remain result metadata and do not create aggregate snapshot churn. It never emits edit provenance or target-lifecycle context: those facts belong to the registered tool call/result and its private derived-execution metadata. These contexts never change the tool list, tool schemas, or system sections.
10
10
 
11
11
  The default triggers are a repeated binding failure and a failure whose diagnostic or structured cause identifies an executable, shell, or path problem. The first tip is concise. A matching trigger may produce another tip only after `tipCooldownMessages` model-context steps; unresolved matching tips reach the detailed form after `tipEscalationFailures` occurrences. A successful cell resets the unresolved escalation count. Every emitted tip name carries its stable trigger identity and next per-trigger ordinal. Reconstruction accepts only canonical snapshots from DSH's system-prompt owner, treats a named section as one effective-state transition even when another owner's aggregate update repeats it, and ignores legacy or malformed plugin prose. Visible wording does not determine cooldown or escalation identity.
12
12
 
13
13
  Platform wording names execution-world differences without assuming Windows, WSL, POSIX, a shell, or package-runner availability. `edit_run_code` remains independently available as a fixed real tool and does not depend on a recovery-tip trigger.
14
14
 
15
+ The Cordis recovery context is not a failure tip and is not controlled by fatigue settings. It narrows the meaning of recorded values after a lifecycle boundary: they remain historical program data but cannot prove current process-local state. It clears only after a newly settled successful `cordis_inspect*` transcript, does not infer whether the original process survived, and never redispatches a historical effect.
16
+
15
17
  ## Alternatives considered
16
18
 
17
19
  - **Keep all recovery instructions in the stable system prompt.** This makes every request pay for rare failures and makes platform-specific text look universally applicable.
@@ -20,6 +22,7 @@ Platform wording names execution-world differences without assuming Windows, WSL
20
22
  - **Infer tip identity from rendered prose or one fixed section name.** Wording changes would reset history, unrelated plugin text could create false matches, and an unchanged aggregate context would not persist repeated occurrences.
21
23
  - **Change the available tools when a tip is needed.** This would break the fixed code-only direct-tool prefix and make failure state observable through schema churn.
22
24
  - **Let each runtime-context renderer scan the event log.** Independent scans duplicate parsing and can disagree about call/result pairing, turn boundaries, repair consumption, or successful-run resets. A shared projection keeps event interpretation separate from presentation policy.
25
+ - **Mark all Cordis calls volatile.** This would discard reconstructable REPL bindings and special values merely because their external referents need revalidation. Recorded-value replay and live-state proof are separate contracts.
23
26
 
24
27
  ## Consequences
25
28
 
@@ -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 `run_code`; disabling it disposes both fibers immediately. The provider resolves the shipped `cordis` preset through the public `agentPresets` service and publishes only its sibling `skills` directory under a PTC Plus-specific provider name. It neither switches the current preset nor includes default roots. If an agent is created before `run_code` becomes visible, the owner retains it as pending and retries on the DSH tools-change signal.
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 `run_code`; 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, watch, invalidation, 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. If an agent is created before `run_code` becomes visible, the owner retains it as pending and retries on the DSH tools-change signal.
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 missing or broken preset, Skill service/tool, declared service, publication, or child fiber is a configuration error and rolls back every contribution.
20
20
 
@@ -22,6 +22,8 @@ The official plugin also registers Host inspect providers in the process-global
22
22
 
23
23
  Cordis tools remain native program bindings under `tools.*`. The PTC code-only direct-tool projection remains exactly `[run_code, edit_run_code]`; native agents and unrelated scopes do not inherit this option.
24
24
 
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 historical 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
+
25
27
  ## Alternatives Considered
26
28
 
27
29
  1. Add the Cordis row to the PTC preset. Rejected: PTC Plus is installed as a host plugin and does not own or fork DSH's shipped preset files; the operator asked for a plugin setting.
@@ -30,7 +32,8 @@ Cordis tools remain native program bindings under `tools.*`. The PTC code-only d
30
32
  4. Enable the tools by default. Rejected: the tools can execute model-written plugins against the live runtime and carry a fixed schema cost. That trust and context expansion must be explicit.
31
33
  5. Mount one global Cordis tool fiber and hide it from native agents. Rejected: its prompt hook and `@pluginId` context listener would remain global even if tool schemas were restricted, so unrelated agents would inherit behavior they did not enable.
32
34
  6. Copy `SKILL.md` into PTC Plus or add the Cordis preset's directory to global Skill roots. Rejected: copied instructions drift, while a global root leaks the optional workflow into agents that did not enable it.
35
+ 7. Publish the complete preset-local Skill directory. Rejected: directory membership is not the PTC feature contract; unrelated sibling additions would silently change the model surface and may target a different authoring workflow or tool generation.
33
36
 
34
37
  ## Consequences
35
38
 
36
- PTC Plus declares the maintained DSH Cordis tool, Skill filesystem, settings, and tool-runtime packages as required peers through the current release channel and mirrors them in development dependencies. They resolve from DSH's installation-maintained profile fallback; installing PTC Plus must not add a profile-local DSH core package instance. When enabled, the official Cordis schemas, guidance, and companion Skill become available together in PTC agent scopes; disabled configurations remain unchanged. Changing this option updates live agent scopes without a DSH restart, including repeated disable/re-enable cycles. A DSH release may rename or reshape its Cordis surface without requiring a mirrored-name change in PTC Plus, provided the public plugin, preset, Skill, and inspect-provider contracts remain compatible.
39
+ PTC Plus declares the maintained DSH Cordis tool, Skill filesystem, settings, and tool-runtime packages as required peers through the current release channel and mirrors them in development dependencies. They resolve from DSH's installation-maintained profile fallback; installing PTC Plus must not add a profile-local DSH core package instance. When enabled, the official Cordis schemas, guidance, and exact companion Skill become available together in PTC agent scopes; disabled configurations remain unchanged. Changing this option updates live agent scopes without a DSH restart, including repeated disable/re-enable cycles. Resume and re-enable may append one bounded recovery snapshot but do not change stable tool schemas or system sections. A DSH release may rename or reshape its Cordis tool surface without requiring a mirrored-name change in PTC Plus, while the named companion Skill remains an explicit compatibility contract and fails activation if unavailable or inconsistent.
@@ -20,7 +20,7 @@ DSH 为一个 agent composition 固定选择 `native`、`code` 或 `both`。首
20
20
 
21
21
  `edit_run_code` 对其持久化 `tool/call` 事件出现时当前 open turn 最近的可编辑 cell 生效,无论该 cell 被拒绝、运行失败,还是成功执行但结果需要微调。执行通过 call event sequence 读取该快照,后续 settlement 或 handler 调度不改变目标;派生 cell 则按外层 `tool/result` event sequence 进入恢复历史,使 cold replay 保持 live kernel 的实际结算顺序。
22
22
  调查 tool 不擦除目标;edit 发起后的其他 settlement 不追溯改变其目标;一次成功 edit 生成的新 cell 成为下一次 edit 的目标。调用方必须且只能提交 `edits` 或
23
- `regex_edits` 之一。`internal/rejected-cell-editor.js` 统一拥有 schema、匹配、replacement 语义、预算和原子源码组装;
23
+ `regex_edits` 之一,并可携带 `expected_target_call_seq` 作为目标前置条件;它与 call event 已捕获目标不一致时,在任何派生 dispatch 前返回未编辑结果。`internal/rejected-cell-editor.js` 统一拥有 schema、匹配、replacement 语义、预算和原子源码组装;
24
24
  所有位置都针对原始目标源码解析,全部范围不得重叠,预算通过后才以单次线性扫描物化结果。
25
25
 
26
26
  注册工具通过 DSH 公共 `tools.execute()` 将完整修改后源码派生执行为 `run_code`。外层历史仍是模型真实发出的
@@ -29,7 +29,7 @@ DSH 为一个 agent composition 固定选择 `native`、`code` 或 `both`。首
29
29
  `run_code`。无目标或参数不合法时不执行,返回 `{ edited: false, reason }` 并保留原目标。
30
30
  进程内 claim 区分 executing 与 settled:抛错、取消或缺少有效 PTC journal 的派生结果立即释放;已由 journal 证明进入 runtime 的派生执行继续占用旧 target,直到外层 result 的私有 metadata 通过最终 policy 并由 session log 投影为新 target。最终 result 丢失 metadata 时由 `tools/result` 释放。session log 投影在 edit call event 处记录 eligible target,只接受 target call sequence 匹配该快照且 journal 有效、非 noop 的派生源码,因此 live 与 cold recovery 从同一持久关系决定可编辑目标。
31
31
 
32
- `internal/session-log-view.js` 单次前向扫描 session events,分别投影最新执行、可编辑目标、rewrite metadata 和恢复 tip
32
+ `internal/session-log-view.js` 单次前向扫描 session events,分别投影最新执行、可编辑目标、rewrite metadata、规范 journal 中的 Cordis transcript 计数和恢复 tip
33
33
  所需事实。`edit_run_code` 不产生专用 runtime context:真实 call/result 已完整表达操作身份和结果,额外 contribution
34
34
  只会触发 DSH 聚合 runtime-context 全量快照并重复无关 policy 文本。只有失败或完成状态不可信的 rewrite feedback 才保留独立生命周期;成功的透明改写不产生 runtime context。
35
35
 
@@ -40,11 +40,13 @@ return 或打印需要展示的值。失败恢复先按状态分类:解析或
40
40
  `run_code` 并复用已有 binding,完整重写只保留给结构性改动或超出编辑预算的修正。能力和命令执行依赖当前 request
41
41
  与 execution world,模型必须先探查 live binding、实际 executable 和路径语义,不假设某个平台、shell 或 package runner。
42
42
 
43
+ live `PTC-C001` 若精确落在源码 EOF,独立的 bounded analyzer 只把追加单个 `}`、`)`、`]` 的三个源码分别送入该 cell 提交时的同一 preparation context。仅当唯一候选通过、无 binding collision,现有 exact editor 能从不超过固定预算的唯一尾部物化同一源码,并且 runtime 已取得该 rejected `run_code` 的持久 call sequence 时,诊断才输出一行可直接调用的 `edit_run_code({ edits: [...], expected_target_call_seq })`。edit transport 把该 sequence 与其 call event 捕获的目标比较,目标已变化时不编辑也不派生执行。这项 validated repair 优先于长度阈值,但只证明语法/preflight 接受;插件不自动 dispatch,也不改写原始 `run_code`。非 EOF、多候选、多 token、超预算、缺少目标身份或后续 preparation 拒绝均回到原有 length-adaptive help。
44
+
43
45
  稳定指引保留这些跨任务不变量和失败恢复的优先动作。长 cell 失败的短提示直接作为当前 `PTC-X001` 的结构化 `help` 输出,避免一次性建议触发完整 runtime-context 快照;重复绑定失败和当前 execution world 中由诊断确认的 executable、shell 或 path 错误仍由当前 session log 派生为 `tools:ptc-plus-tip/<trigger>/<ordinal>` runtime context,分别提示能力探查或重新确认环境。投影只接受 DSH system-prompt owner 的规范快照,并按命名 section 的有效状态变化重建提示;聚合快照重复同一 section 不增加次数,正文不参与身份判断。
44
46
  相同提示受 `tipCooldownMessages` 间隔约束,连续未解决时才升级为详细版本,成功 cell 会重置未解决计数;提示不会改变
45
47
  system sections、tool schema 或 tool order,也不假设 Windows、WSL、POSIX、shell 或 package runner。
46
48
 
47
- 当前策略位于独立的 `internal/recovery-tips.js` local provider,核心只消费其有界的 named context。未来外部决策插件的接入必须等待稳定的 facts/decision contract;在此之前不猜测跨插件 API,local provider 继续作为 fallback。
49
+ 当前策略位于独立的 `internal/recovery-tips.js` local provider,核心只消费其有界的 named context。另一个固定名称 `tools:ptc-plus-cordis-recovery` 只在新 agent 或 Cordis 重新启用代际观察到历史 Cordis transcript 时出现,将 replay value 限定为历史数据,并保持到 session log 出现新的成功 `cordis_inspect*` settlement;它不受疲劳阈值控制、不重放调用,也不推断 live process 是否实际丢失。未来外部决策插件的接入必须等待稳定的 facts/decision contract;在此之前不猜测跨插件 API,local provider 继续作为 fallback。
48
50
 
49
51
  ## 设置与启用开关
50
52
 
@@ -66,7 +68,7 @@ Client half 通过 `settings.plugin.item` 卡片呈现全部配置。`enabled`
66
68
  | Replacement | 只有 DSH 拥有的 compaction 或其他显式 surface replacement 可以替换已保留历史;PTC Plus 不用 replacement 表达瞬时状态。 |
67
69
  | Independent request | 新的辅助模型调用必须单独说明 route、prefix 和 token 影响,不能用它的缓存表现证明主会话前缀稳定。当前插件不发起辅助模型请求。 |
68
70
 
69
- 一次性或会变化的 session 状态必须作为 `PromptAssembly.contexts` 的命名贡献交给 DSH。DSH 将完整快照记录为带来源的 `user/message` 并追加到历史尾部;值未变时不重复,值变化或全部消失时追加更新或 clearance。完整快照携带当时所有 owner 的命名 context,因此任一贡献变化都会使未变化贡献在新的尾部消息中再次出现;这会增加 append-only history token,但不会改写已有前缀。PTC Plus 不通过绕过 prompt assembly 的私有消息通道规避这项宿主聚合成本。此类状态不得进入 `PromptAssembly.sections`,也不得通过增删 tool、改变 schema 字段或调整 tool order 传递。
71
+ 一次性或会变化的 session 状态必须作为 `PromptAssembly.contexts` 的命名贡献交给 DSH。DSH 将完整快照记录为带来源的 `user/message` 并追加到历史尾部;值未变时不重复,值变化或全部消失时追加更新或 clearance。完整快照携带当时所有 owner 的命名 context,因此任一贡献变化都会使未变化贡献在新的尾部消息中再次出现;这会增加 append-only history token,但不会改写已有前缀。PTC Plus 不通过绕过 prompt assembly 的私有消息通道规避这项宿主聚合成本。Cordis 恢复 context 使用规范 journal 的历史 transcript 与当前 agent/enable generation 作为事实源,不能从进程内 Plugin registry 直接生成未记录输入。此类状态不得进入 `PromptAssembly.sections`,也不得通过增删 tool、改变 schema 字段或调整 tool order 传递。
70
72
 
71
73
  所有模型可见输入都必须能从 session log 重建。静态 system/schema 由 `request/header` 保存,动态 context 由带来源的 `user/message` 保存;进程内临时状态不能直接成为未记录的模型输入。在同一插件版本与配置下,普通执行结果、可编辑目标、诊断和其他插件拥有的运行期变化都不得改变 header;`edit_run_code` 的身份与结果由真实 call/result 表达,不生成 edit feedback context。插件升级、显式配置变化、provider/model route 变化、真实 native capability schema 变化,以及 DSH 拥有的 history replacement 可以使缓存从首个变化 token 起失效。
72
74
 
@@ -111,7 +113,7 @@ CodeRuntime request 已携带的 owner-provided program namespace 会被原样
111
113
  }
112
114
  ```
113
115
 
114
- `calls` 只保存 global、member、PTC Value Graph 编码的 args/result 或 error,以及 settlement 序号。cold replay 校验调用名称、参数、数量和提交顺序,并按 recorded settlement order 释放 recorded result;不会重新 dispatch program binding 或重做外部 effect。该规则同样适用于 native tools、owner-provided namespace 和 `code.run`,不按名称分支。若基础设施终止时仍有未结算 binding,heap 回滚到 durable frontier,discarded journal 以最先观察到的 `global.member` 保留 possible-effect boundary。effect、completeness 和 source metadata 属于 capability explorer,不伪装成 journal 字段。
116
+ `calls` 只保存 global、member、PTC Value Graph 编码的 args/result 或 error,以及 settlement 序号。cold replay 校验调用名称、参数、数量和提交顺序,并按 recorded settlement order 释放 recorded result;不会重新 dispatch program binding 或重做外部 effect。该规则同样适用于 native tools、owner-provided namespace 和 `code.run`,不按名称分支。Cordis 的进程内对象不会因此被宣称已恢复;presentation 可以从已验证 transcript 派生重新检查要求,但不能改变 replay。若基础设施终止时仍有未结算 binding,heap 回滚到 durable frontier,discarded journal 以最先观察到的 `global.member` 保留 possible-effect boundary。effect、completeness 和 source metadata 属于 capability explorer,不伪装成 journal 字段。
115
117
 
116
118
  journal 通过 `run_code.output.presentationMeta` 附着到最终 result,再由 `tools/result` 做两阶段确认。缺失、损坏或被替换的 journal 形成 unknown/volatile 边界;未进入 runtime 的 call 由后续 `confirms` 以对应 `tool/call.seq` 证明为 no-op。volatile 源码保留在原 session log,但不参与 cold replay。
117
119
 
package/docs/client-ui.md CHANGED
@@ -12,7 +12,7 @@ Host half 通过 DSH 公共 `settings` 服务注册命名空间 `ptc-plus`。字
12
12
  | 分组 | 字段 | 说明 |
13
13
  | --- | --- | --- |
14
14
  | 开关 | `enabled` | 关闭后不注册 `run_code`/`edit_run_code`、不修改 system prompt、不创建 session runtime;只保留设置 UI。 |
15
- | Cordis | `cordisToolsEnabled` | 默认关闭;开启后为 PTC agent 加入创造模式使用的官方 Cordis 工具、指引与 companion Skill。 |
15
+ | Cordis | `cordisToolsEnabled` | 默认关闭;开启后为 PTC agent 加入官方 Cordis 工具、指引与精确的 `cordis-plugin-development` companion Skill,不发布同目录 sibling。 |
16
16
  | 计算 | `computeMs` / `maxWallMs` | 单 cell CPU 与墙钟预算。 |
17
17
  | 输出 | `maxOutputBytes` / `maxValueNodes` / `maxValueEdges` / `maxValueArrayLength` / `maxValueBigIntDigits` | Value Graph 与输出字节预算。 |
18
18
  | Worker | `maxOldGenerationSizeMb` / `maxNestedRunCodeDepth` | kernel worker 内存与嵌套执行深度。 |
@@ -87,7 +87,7 @@ journal、diagnostic、source、cause、call、operation、completion 和 comple
87
87
  }
88
88
  ```
89
89
 
90
- `settle` 必须是从 0 开始的连续序列。重放按源码产生调用,但不重新 dispatch;它校验 global、member、args 和提交数量,再按 recorded settlement order 释放 recorded value/error。这个 `recorded-value` 承诺重建的是 REPL 计算状态,不表示外部 effect 被重做、撤销或验证。completeness、effect、authority 和 source evidence 属于 capability metadata,不伪装成 journal 字段。
90
+ `settle` 必须是从 0 开始的连续序列。重放按源码产生调用,但不重新 dispatch;它校验 global、member、args 和提交数量,再按 recorded settlement order 释放 recorded value/error。这个 `recorded-value` 承诺重建的是 REPL 计算状态,不表示外部 effect 被重做、撤销或验证。对进程内 Cordis 状态,恢复值明确只是历史数据:Cordis 可见时,固定 runtime context 要求在把旧 ID、Run、approval 或 capability observation 当作当前事实前完成一次新的成功 live Inspect。completeness、effect、authority 和 source evidence 属于 capability metadata,不伪装成 journal 字段。
91
91
 
92
92
  journal 的 capability-call `args`/`value` 与 completion value 保存 `ptc-value-graph/v1` canonical envelope,不保存 decoded rich JS value,也不依赖递归 `JSON.stringify` 或 `structuredClone`。因此深层数组/对象、own `__proto__`、`undefined`、special number、BigInt、hole、shared identity 和 cycle 不会被外层 session JSON 改写。完整支持域和预算见 [PTC Value Graph V1](value-wire.md)。
93
93
 
@@ -23,7 +23,7 @@ The optional `cordisToolsEnabled` integration requires the current DSH installat
23
23
  Use this form after the selected version is available from the npm registry:
24
24
 
25
25
  ```sh
26
- dsh plugin --profile <profile> add dsh-ptc-plus@0.2.1
26
+ dsh plugin --profile <profile> add dsh-ptc-plus@0.2.3
27
27
  dsh --profile <profile> --dump-config
28
28
  ```
29
29
 
@@ -60,7 +60,7 @@ pnpm dsh --profile <profile> --dump-config
60
60
 
61
61
  ```sh
62
62
  npm pack
63
- dsh plugin --profile <profile> add /absolute/path/to/dsh-ptc-plus-0.2.1.tgz
63
+ dsh plugin --profile <profile> add /absolute/path/to/dsh-ptc-plus-0.2.3.tgz
64
64
  dsh --profile <profile> --dump-config
65
65
  ```
66
66
 
@@ -79,4 +79,4 @@ dsh plugin add github:muyuanjin/dsh-ptc-plus#main
79
79
  dsh --dump-config
80
80
  ```
81
81
 
82
- After an npm release, the package spec may instead be `dsh-ptc-plus@0.2.1`. 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.
82
+ After an npm release, the package spec may instead be `dsh-ptc-plus@0.2.3`. 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.
@@ -18,7 +18,7 @@ DSH authority/policy -> native dispatch -> canonical result
18
18
  `-> args/result + settlement transcript
19
19
  ```
20
20
 
21
- cold replay 校验相同的调用序列并返回 recorded value,不重新执行工具。这保证 REPL 计算状态可重建,但不声称外部 effect 可逆或仍与历史相同。
21
+ cold replay 校验相同的调用序列并返回 recorded value,不重新执行工具。这保证 REPL 计算状态可重建,但不声称外部 effect 可逆或仍与历史相同。Cordis Plugin、Run 和 Inspect observation 属于进程内实时状态;恢复出的 Cordis value 只作为历史数据保留,后续状态性决策必须先按恢复 context 执行新的 live Inspect。
22
22
 
23
23
  同一 settlement 规则适用于当前 request 中的全部 program binding,包括 owner-provided namespace 与 `code.run`。已结算的 value/error 可进入 durable transcript;若取消、超时或 worker failure 发生时调用仍未结算,cell heap 回滚,discarded journal 以实际 `global.member` 保留 possible-effect boundary。分类来自可观察生命周期,不来自 capability 名称表。
24
24
 
@@ -8,10 +8,12 @@ The default loose binding mode lets a complete top-level `const` or `let` declar
8
8
 
9
9
  Every capability namespace is leased to one cell. Captured `tools`, `capabilities`, `repl`, `code`, or member functions expire when that cell ends.
10
10
 
11
- For PTC mode requests using the code-only direct-tool projection, PTC Plus presents two tools in stable order: `run_code` and `edit_run_code`. The latter edits the most recent eligible cell visible when the edit call is dispatched, including a cell that completed successfully but needs a small adjustment. That target remains fixed while the call is in flight; later tool settlements cannot retarget it. The edit accepts exactly one atomic `edits` or `regex_edits` array; invalid or unavailable edits return `{ edited: false, reason }` without executing source or consuming the target.
11
+ For PTC mode requests using the code-only direct-tool projection, PTC Plus presents two tools in stable order: `run_code` and `edit_run_code`. The latter edits the most recent eligible cell visible when the edit call is dispatched, including a cell that completed successfully but needs a small adjustment. That target remains fixed while the call is in flight; later tool settlements cannot retarget it. The edit accepts exactly one atomic `edits` or `regex_edits` array and an optional `expected_target_call_seq` precondition. Invalid, unavailable, or mismatched-target edits return `{ edited: false, reason }` without executing source or consuming the target.
12
12
 
13
13
  `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.
14
14
 
15
+ When a live parse error occurs exactly at the cell EOF, PTC Plus checks the three single-closing-token corrections `}`, `)`, 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.
16
+
15
17
  Only values explicitly returned or printed appear in a cell result:
16
18
 
17
19
  ```ts
@@ -71,10 +73,12 @@ The configuration uses `ptc-plus` as the DSH plugin ID and `dsh-ptc-plus` as the
71
73
 
72
74
  `enabled: false` is the settings-based kill switch: the Host keeps only the settings namespace and client card, and removes every runtime hook, prompt section, and tool surface. 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 is available under Settings → Plugin configuration, shows “已启用” or “已停用”, and disables every control except `enabled` while the plugin is off. 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.
73
75
 
74
- `cordisToolsEnabled: true` atomically mounts the official `@deepseek-ai/dsh-tool-cordis` plugin and the shipped `cordis-plugin-development` companion Skill in PTC agent scopes. PTC Plus resolves the official `cordis` preset through DSH's public preset service and publishes only that preset's Skill directory through the maintained filesystem provider; it does not copy the Skill or switch the agent's preset. The first request waits until the Cordis tool fiber, owner guidance, Skill provider, and scoped Skill load are all ready. A missing or broken preset, Skill/tool service, declared Cordis service, inconsistent inspect manifest, or rejected activation fails the setting change and removes every provisional contribution. 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]`. Agents whose `run_code` surface appears after creation are retried on the DSH tools-change signal. Cordis can evaluate model-written plugins against the live runtime, so enabling it grants shell-equivalent trust.
76
+ `cordisToolsEnabled: true` atomically mounts the official `@deepseek-ai/dsh-tool-cordis` plugin and exactly the shipped `cordis-plugin-development` companion Skill in PTC agent scopes. PTC Plus resolves the official `cordis` preset through DSH's public preset service, lets the maintained filesystem provider own its Skill root, 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. A missing or broken preset, Skill/tool service, declared Cordis service, inconsistent inspect manifest, or rejected activation fails the setting change and removes every provisional contribution. 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]`. Agents whose `run_code` surface appears after creation are retried on the DSH tools-change signal. Cordis can evaluate model-written plugins against the live runtime, so enabling it grants shell-equivalent trust.
75
77
 
76
78
  Keep large Cordis host or client source in a top-level binding before calling a Cordis tool. A parse or validation rejection is a runtime failure, so bindings assigned before it remain live for a short retry cell; reuse the binding instead of resending the source.
77
79
 
80
+ Cordis Plugins and Runs are process-local. On a resumed agent or after Cordis is re-enabled, normalized journal calls can restore their recorded return values but do not prove that prior IDs, approvals, Runs, or Inspect observations are live. The fixed `tools:ptc-plus-cordis-recovery` context remains present until a new successful `cordis_inspect*` call is settled. Use that live read-only inspection before a stateful Cordis decision; do not rerun a mutating historical call merely to rebuild state.
81
+
78
82
  `durableReplay: false` starts new kernels without historical REPL state while preserving live bindings in the current process. It does not delete session logs.
79
83
 
80
84
  Recovery tips are disabled with `tipsEnabled: false`. When enabled, a bounded runtime context in the reserved `tools:ptc-plus-tip/<trigger>/<ordinal>` name family may appear after a repeated binding failure or a diagnostic that identifies an executable, shell, or path problem in the current execution world. The trigger and per-trigger ordinal are reconstructed from canonical named system-prompt snapshots; repeated aggregate copies and visible wording do not advance the history. A tip is subject to `tipCooldownMessages` and becomes detailed only after `tipEscalationFailures` unresolved matches; it never changes the code-only direct-tool list or schema. `edit_run_code` does not emit a runtime context because its real call and result already carry the relevant fact.
@@ -93,7 +97,7 @@ Rewrites are recorded as `meta.dshPtcPlusRewrites` on the tool result (parallel
93
97
 
94
98
  | Code | Meaning | State effect |
95
99
  | --- | --- | --- |
96
- | `PTC-C001` | The cell cannot be parsed | Not executed; REPL unchanged |
100
+ | `PTC-C001` | The cell cannot be parsed | Not executed; REPL unchanged; a uniquely validated and target-bound single-token EOF closure includes a directly callable declared `edit_run_code` repair; otherwise retry a short corrected cell with `run_code`, use edit for a localized long-cell correction, and resend with `run_code` for broad, ambiguous, or unbound repairs |
97
101
  | `PTC-C002` | Preflight rejected a kernel-control import | Not executed; REPL unchanged |
98
102
  | `PTC-N001` | Top-level binding conflict | Not executed; REPL unchanged |
99
103
  | `PTC-O001` | Unsupported or over-budget output | Cell executed; earlier mutations may exist |
@@ -8,6 +8,63 @@ const CORDIS_PRESET_ID = 'cordis'
8
8
  const CORDIS_SKILL_NAME = 'cordis-plugin-development'
9
9
  const CORDIS_SKILL_PROVIDER = 'ptc-plus-cordis'
10
10
 
11
+ function exactCompanionCandidates(observation) {
12
+ if (Array.isArray(observation)) {
13
+ return observation.filter(candidate => candidate?.name === CORDIS_SKILL_NAME)
14
+ }
15
+ if (observation === null || typeof observation !== 'object'
16
+ || !Array.isArray(observation.candidates)) return observation
17
+ return {
18
+ ...observation,
19
+ candidates: observation.candidates.filter(candidate => candidate?.name === CORDIS_SKILL_NAME),
20
+ }
21
+ }
22
+
23
+ function exactCompanionProvider(provider) {
24
+ if (provider === null || typeof provider !== 'object'
25
+ || typeof provider.name !== 'string'
26
+ || typeof provider.list !== 'function'
27
+ || typeof provider.get !== 'function') {
28
+ throw new Error('ptc-plus: DSH Cordis Skill provider is incompatible')
29
+ }
30
+ return {
31
+ ...provider,
32
+ async list(options) {
33
+ return exactCompanionCandidates(await Reflect.apply(provider.list, provider, [options]))
34
+ },
35
+ async get(candidate, options) {
36
+ if (candidate?.name !== CORDIS_SKILL_NAME) return undefined
37
+ const skill = await Reflect.apply(provider.get, provider, [candidate, options])
38
+ return skill?.name === CORDIS_SKILL_NAME ? skill : undefined
39
+ },
40
+ }
41
+ }
42
+
43
+ function exactCompanionSkillPlugin(skillFilesystemPlugin) {
44
+ return {
45
+ ...skillFilesystemPlugin,
46
+ apply(pluginCtx, config) {
47
+ if (typeof pluginCtx?.extend !== 'function') {
48
+ throw new Error('ptc-plus: cordisToolsEnabled requires the DSH Context.extend API')
49
+ }
50
+ const skills = pluginCtx.get?.('skills')
51
+ if (typeof skills?.registerProvider !== 'function') {
52
+ throw new Error('ptc-plus: cordisToolsEnabled requires the DSH skills.registerProvider API')
53
+ }
54
+ const facade = new Proxy(skills, {
55
+ get(target, property) {
56
+ if (property === 'registerProvider') {
57
+ return create => target.registerProvider(control => exactCompanionProvider(create(control)))
58
+ }
59
+ const value = Reflect.get(target, property, target)
60
+ return typeof value === 'function' ? value.bind(target) : value
61
+ },
62
+ })
63
+ return skillFilesystemPlugin.apply(pluginCtx.extend({ skills: facade }), config)
64
+ },
65
+ }
66
+ }
67
+
11
68
  function requireFiberServices(agent, fiber, owner) {
12
69
  if (typeof agent?.ctx?.get !== 'function') {
13
70
  throw new Error('ptc-plus: cordisToolsEnabled requires the DSH agent Context.get API')
@@ -32,8 +89,9 @@ function requireCompanionServices(agent) {
32
89
  throw new Error('ptc-plus: cordisToolsEnabled requires the DSH agentPresets.resolve API')
33
90
  }
34
91
  const skills = agent.ctx.get('skills')
35
- if (typeof skills?.list !== 'function' || typeof skills?.get !== 'function') {
36
- throw new Error('ptc-plus: cordisToolsEnabled requires the DSH skills list/get APIs')
92
+ if (typeof skills?.registerProvider !== 'function'
93
+ || typeof skills.list !== 'function' || typeof skills.get !== 'function') {
94
+ throw new Error('ptc-plus: cordisToolsEnabled requires the DSH skills registerProvider/list/get APIs')
37
95
  }
38
96
  if (agent.ctx.tools?.get?.('skill', agent) === undefined) {
39
97
  throw new Error('ptc-plus: cordisToolsEnabled requires the DSH skill tool in the PTC agent scope')
@@ -57,10 +115,11 @@ async function requireCompanionSkill(agent, skills) {
57
115
  cwd: agent.session?.header?.cwd,
58
116
  scope: agent,
59
117
  }
60
- const summary = (await skills.list(lookup)).find(skill => skill?.name === CORDIS_SKILL_NAME)
61
- if (summary?.provider !== CORDIS_SKILL_PROVIDER
118
+ const owned = (await skills.list(lookup)).filter(skill => skill?.provider === CORDIS_SKILL_PROVIDER)
119
+ const summary = owned[0]
120
+ if (owned.length !== 1 || summary?.name !== CORDIS_SKILL_NAME
62
121
  || summary.invocation?.modelInvocable !== true) {
63
- throw new Error(`ptc-plus: official Cordis companion Skill ${JSON.stringify(CORDIS_SKILL_NAME)} is not model-invocable`)
122
+ throw new Error(`ptc-plus: official Cordis Skill provider must publish exactly ${JSON.stringify(CORDIS_SKILL_NAME)} as model-invocable`)
64
123
  }
65
124
  const skill = await skills.get(CORDIS_SKILL_NAME, lookup)
66
125
  if (skill?.provider !== CORDIS_SKILL_PROVIDER
@@ -177,6 +236,7 @@ export function createCordisToolsOwner(
177
236
  const pending = new Set()
178
237
  const cordisInspectLeases = createCordisInspectLeases(ctx)
179
238
  const scopedCordisPlugin = cordisInspectLeases.plugin(cordisPlugin)
239
+ const scopedSkillPlugin = exactCompanionSkillPlugin(skillFilesystemPlugin)
180
240
  let disposed = false
181
241
 
182
242
  const leafErrors = error => error instanceof AggregateError
@@ -242,7 +302,7 @@ export function createCordisToolsOwner(
242
302
  const skillDirectory = await companionSkillDirectory(agentPresets)
243
303
  if (mount.disposed) return
244
304
 
245
- const skillFiber = agent.ctx.plugin(skillFilesystemPlugin, {
305
+ const skillFiber = agent.ctx.plugin(scopedSkillPlugin, {
246
306
  providerName: CORDIS_SKILL_PROVIDER,
247
307
  includeDefaultRoots: false,
248
308
  customSkillDirs: [skillDirectory],
@@ -59,6 +59,38 @@ function rejection(message) {
59
59
  }
60
60
  }
61
61
 
62
+ export function createCordisRecoveryPolicy(initiallyEnabled) {
63
+ const states = new WeakMap()
64
+ let enabled = initiallyEnabled
65
+ let generation = enabled ? 1 : 0
66
+ return Object.freeze({
67
+ reconfigure(nextEnabled) {
68
+ if (!enabled && nextEnabled) generation += 1
69
+ enabled = nextEnabled
70
+ },
71
+ required(agent, view) {
72
+ if (!enabled) return false
73
+ let state = states.get(agent)
74
+ if (state?.generation !== generation) {
75
+ state = {
76
+ generation,
77
+ baselineInspections: view.cordisTranscript.inspections,
78
+ required: view.cordisTranscript.calls > 0,
79
+ }
80
+ states.set(agent, state)
81
+ }
82
+ if (state.required
83
+ && view.cordisTranscript.inspections > state.baselineInspections) {
84
+ state.required = false
85
+ }
86
+ return state.required
87
+ },
88
+ disposeAgent(agent) {
89
+ states.delete(agent)
90
+ },
91
+ })
92
+ }
93
+
62
94
  function presentationState(assembly) {
63
95
  const tools = Array.isArray(assembly?.tools) ? assembly.tools : []
64
96
  if (!tools.some(tool => tool?.name === RUN_CODE)) {
@@ -110,6 +142,7 @@ export function createDirectSurfaceOwner({
110
142
  const compositions = new Map()
111
143
  const canonicalRequests = new WeakMap()
112
144
  const sessions = new Map()
145
+ const cordisRecovery = createCordisRecoveryPolicy(runtimeConfig.cordisToolsEnabled)
113
146
  let currentCanonicalizeToolCalls = canonicalizeToolCalls
114
147
  const tipConfig = {
115
148
  enabled: runtimeConfig.tipsEnabled,
@@ -175,6 +208,7 @@ export function createDirectSurfaceOwner({
175
208
  return Object.freeze({
176
209
  reconfigure(nextConfig) {
177
210
  currentCanonicalizeToolCalls = nextConfig.canonicalizeToolCalls
211
+ cordisRecovery.reconfigure(nextConfig.cordisToolsEnabled)
178
212
  tipConfig.enabled = nextConfig.tipsEnabled
179
213
  tipConfig.cooldownMessages = nextConfig.tipCooldownMessages
180
214
  tipConfig.escalationFailures = nextConfig.tipEscalationFailures
@@ -231,7 +265,11 @@ export function createDirectSurfaceOwner({
231
265
  directTools = [adaptRunCodeSchema(runCode), editRunCodeSchema()]
232
266
  }
233
267
  const runtimeContexts = sessionPtc
234
- ? sessionRuntimeContexts(agent, tipConfig)
268
+ ? sessionRuntimeContexts(agent, tipConfig, {
269
+ cordisRecoveryRequired(view) {
270
+ return cordisRecovery.required(agent, view)
271
+ },
272
+ })
235
273
  : { contexts: [] }
236
274
  const contexts = sessionPtc && Array.isArray(assembly.contexts)
237
275
  && runtimeContexts.contexts.length > 0
@@ -310,6 +348,7 @@ export function createDirectSurfaceOwner({
310
348
  disposeAgent(agent) {
311
349
  const id = sessionId(agent)
312
350
  compositions.delete(agent)
351
+ cordisRecovery.disposeAgent(agent)
313
352
  if (id !== undefined) clearSession(id)
314
353
  },
315
354
  disposeSession(session) {
@@ -12,7 +12,11 @@ import {
12
12
  normalizeRewrites,
13
13
  validatedRewrites,
14
14
  } from './session-journal.js'
15
- import { editRejectedCell, editRunCodeSchema } from './rejected-cell-editor.js'
15
+ import {
16
+ EXPECTED_TARGET_CALL_SEQ,
17
+ editRejectedCell,
18
+ editRunCodeSchema,
19
+ } from './rejected-cell-editor.js'
16
20
  import { editTargetForCall, projectSessionLog } from './session-log-view.js'
17
21
  import { RUN_CODE } from './runtime-bridge-owner.js'
18
22
  import { isRecord } from './record-utils.js'
@@ -126,6 +130,13 @@ export function createEditTransportOwner(ctx, {
126
130
  }
127
131
  const edited = editRejectedCell(args, target.source)
128
132
  if (!edited.edited) return edited
133
+ const expectedTargetCallSeq = args[EXPECTED_TARGET_CALL_SEQ]
134
+ if (expectedTargetCallSeq !== undefined && expectedTargetCallSeq !== target.callSeq) {
135
+ return {
136
+ edited: false,
137
+ reason: `validated repair targets run_code call ${expectedTargetCallSeq}, but this edit captured call ${target.callSeq}`,
138
+ }
139
+ }
129
140
  if (typeof ctx.tools.execute !== 'function') {
130
141
  throw new Error('ptc-plus: DSH tools.execute is required for derived edit execution')
131
142
  }
@@ -2,6 +2,7 @@ import { diagnostic } from './diagnostic.js'
2
2
 
3
3
  export const MAX_ERROR_LOG_BYTES = 4 * 1024
4
4
  export const FAILURE_HINT_THRESHOLD = 3
5
+ export const LONG_CELL_CODE_UNITS = 2_000
5
6
  const BINDING_FAILURE = Symbol('binding failure')
6
7
 
7
8
  export function markBindingFailure(error) {
@@ -1,6 +1,5 @@
1
1
  const PLATFORM_CAUSE_CODES = new Set(['EACCES', 'EINVAL', 'ENOTDIR', 'ENOENT', 'UNKNOWN'])
2
2
  const TIP_CONTEXT_PREFIX = 'tools:ptc-plus-tip/'
3
- export const LONG_FAILED_CELL_TIP_CODE_UNITS = 2_000
4
3
  const TIP_PREFIXES = Object.freeze({
5
4
  'repeated-binding-failure': 'The same binding failure has recurred.',
6
5
  'platform-command-failure': 'An executable, shell, or path failed in the current execution world.',
@@ -14,6 +14,7 @@ export const EDIT_LIMITS = Object.freeze({
14
14
 
15
15
  const EDIT_RUN_CODE_EXECUTION_DESCRIPTION = 'Edit and run TypeScript cell'
16
16
  const REGEX_EDIT_RUN_CODE_EXECUTION_DESCRIPTION = 'Regex-edit and run TypeScript cell'
17
+ export const EXPECTED_TARGET_CALL_SEQ = 'expected_target_call_seq'
17
18
 
18
19
  const REGEX_MATCH_SCRIPT = `(() => {
19
20
  const replacements = []
@@ -254,7 +255,7 @@ function assemble(source, replacements) {
254
255
  export function editRunCodeSchema() {
255
256
  return {
256
257
  name: 'edit_run_code',
257
- description: 'Make a small exact or regular-expression change to the most recent eligible cell captured when this edit call is dispatched, then run the complete corrected cell. A successful edit becomes the next eligible cell. Use this only when replaying the whole cell is safe. If earlier code may already have caused an external effect, use a new run_code cell and its existing variables instead; this tool does not resume at the error location. Send exactly one atomic edits or regex_edits array. Choose edits for a few unique literal fragments; choose regex_edits when one counted pattern covers repeated fragments. Every resolved range must be non-overlapping in the original cell.',
258
+ description: 'Make a small exact or regular-expression change to the most recent eligible cell captured when this edit call is dispatched, then run the complete corrected cell. A successful edit becomes the next eligible cell. Use this only when replaying the whole cell is safe. If earlier code may already have caused an external effect, use a new run_code cell and its existing variables instead; this tool does not resume at the error location. Send exactly one atomic edits or regex_edits array, plus expected_target_call_seq only when a diagnostic supplies it to bind a validated repair to its rejected cell. Choose edits for a few unique literal fragments; choose regex_edits when one counted pattern covers repeated fragments. Every resolved range must be non-overlapping in the original cell.',
258
259
  parameters: {
259
260
  type: 'object',
260
261
  additionalProperties: false,
@@ -285,6 +286,10 @@ export function editRunCodeSchema() {
285
286
  required: ['pattern', 'flags', 'replacement', 'expected_matches'],
286
287
  },
287
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
+ },
288
293
  },
289
294
  oneOf: [{ required: ['edits'] }, { required: ['regex_edits'] }],
290
295
  },
@@ -296,32 +301,41 @@ export function editRejectedCell(value, source, timeoutMs = DEFAULT_REGEX_TIMEOU
296
301
  return rejection('edit_run_code expects an object with exactly one edits or regex_edits array')
297
302
  }
298
303
  const keys = Object.keys(value)
299
- if (keys.length !== 1 || !['edits', 'regex_edits'].includes(keys[0]) || !Array.isArray(value[keys[0]])) {
304
+ const operationKeys = keys.filter(key => key === 'edits' || key === 'regex_edits')
305
+ if (operationKeys.length !== 1 || keys.length > 2
306
+ || keys.some(key => !['edits', 'regex_edits', EXPECTED_TARGET_CALL_SEQ].includes(key))
307
+ || !Array.isArray(value[operationKeys[0]])) {
300
308
  return rejection('edit_run_code expects exactly one edits or regex_edits array')
301
309
  }
310
+ if (Object.hasOwn(value, EXPECTED_TARGET_CALL_SEQ)
311
+ && (!Number.isSafeInteger(value[EXPECTED_TARGET_CALL_SEQ])
312
+ || value[EXPECTED_TARGET_CALL_SEQ] < 0)) {
313
+ return rejection('edit_run_code expected_target_call_seq must be a non-negative safe integer')
314
+ }
302
315
  if (source === undefined) return rejection('no run_code cell is currently eligible for safe editing')
303
- const resolved = keys[0] === 'edits'
316
+ const operation = operationKeys[0]
317
+ const resolved = operation === 'edits'
304
318
  ? exactReplacements(value.edits, source)
305
319
  : regexReplacements(value.regex_edits, source, timeoutMs)
306
320
  if (resolved.error !== undefined) return rejection(resolved.error)
307
321
  const replacements = resolved.replacements.sort((left, right) => left.start - right.start)
308
322
  for (let index = 1; index < replacements.length; index += 1) {
309
323
  if (replacements[index].start < replacements[index - 1].end) {
310
- return rejection(`${keys[0]}[${replacements[index].index}] overlaps ${keys[0]}[${replacements[index - 1].index}] in the target cell`)
324
+ return rejection(`${operation}[${replacements[index].index}] overlaps ${operation}[${replacements[index - 1].index}] in the target cell`)
311
325
  }
312
326
  }
313
327
  const preflight = preflightAssembly(source, replacements)
314
328
  if (preflight.error !== undefined) {
315
- return rejection(keys[0] === 'regex_edits' ? preflight.error.replace(/^edits/, 'regex_edits') : preflight.error)
329
+ return rejection(operation === 'regex_edits' ? preflight.error.replace(/^edits/, 'regex_edits') : preflight.error)
316
330
  }
317
331
  const assembled = assemble(source, preflight.replacements)
318
332
  if (assembled.error !== undefined) {
319
- return rejection(keys[0] === 'regex_edits' ? assembled.error.replace(/^edits/, 'regex_edits') : assembled.error)
333
+ return rejection(operation === 'regex_edits' ? assembled.error.replace(/^edits/, 'regex_edits') : assembled.error)
320
334
  }
321
335
  return Object.freeze({
322
336
  edited: true,
323
337
  code: assembled.code,
324
- description: keys[0] === 'edits'
338
+ description: operation === 'edits'
325
339
  ? EDIT_RUN_CODE_EXECUTION_DESCRIPTION
326
340
  : REGEX_EDIT_RUN_CODE_EXECUTION_DESCRIPTION,
327
341
  })
@@ -2,6 +2,8 @@ import { latestRecoveryTip } from './recovery-tips.js'
2
2
  import { projectSessionLog } from './session-log-view.js'
3
3
 
4
4
  const REWRITE_FEEDBACK = 'tools:ptc-plus-rewrite-info'
5
+ const CORDIS_RECOVERY = 'tools:ptc-plus-cordis-recovery'
6
+ const CORDIS_RECOVERY_TEXT = 'Recorded Cordis values in the recovered REPL are historical data, not proof that process-local Plugins, Runs, approvals, or Inspect observations still exist in the current DSH process. Before relying on prior Cordis IDs, state, or capability data, follow the current Cordis owner guidance and call the live read-only Cordis Inspect bindings through `tools.*`; do not rerun mutating Cordis calls merely to reconstruct history.'
5
7
 
6
8
  function continuationFeedback(view) {
7
9
  const run = view.latestRun
@@ -18,13 +20,17 @@ function continuationFeedback(view) {
18
20
  }
19
21
 
20
22
  /** Build all dynamic PTC contexts from one session-log projection. */
21
- export function sessionRuntimeContexts(agent, tipConfig) {
23
+ export function sessionRuntimeContexts(agent, tipConfig, options = {}) {
22
24
  const view = projectSessionLog(agent)
23
25
  const rewrite = continuationFeedback(view)
24
26
  const tip = latestRecoveryTip(view, tipConfig)
27
+ const cordisRecovery = options.cordisRecoveryRequired?.(view) === true
28
+ ? { name: CORDIS_RECOVERY, text: CORDIS_RECOVERY_TEXT }
29
+ : undefined
25
30
  return Object.freeze({
26
31
  contexts: Object.freeze([
27
32
  ...(rewrite === undefined ? [] : [{ name: REWRITE_FEEDBACK, text: rewrite }]),
33
+ ...(cordisRecovery === undefined ? [] : [cordisRecovery]),
28
34
  ...(tip === undefined ? [] : [tip]),
29
35
  ]),
30
36
  })
@@ -9,6 +9,7 @@ import { diagnostic, renderDiagnostic } from './diagnostic.js'
9
9
  import {
10
10
  firstLine,
11
11
  limitLogs,
12
+ LONG_CELL_CODE_UNITS,
12
13
  markBindingFailure,
13
14
  messageOf,
14
15
  oneLineMessage,
@@ -19,7 +20,7 @@ import { PreflightError, prepareProgram } from './cell-analysis.js'
19
20
  import { ModuleRewriteError } from './cell-rewriter.js'
20
21
  import { mapSourcePosition } from './source-position-map.js'
21
22
  import { durabilityState, transitionDurability } from './session-state.js'
22
- import { LONG_FAILED_CELL_TIP_CODE_UNITS } from './recovery-tips.js'
23
+ import { validatedEofClosureRepair } from './validated-parse-repair.js'
23
24
 
24
25
  const OUTPUT_LIMIT_MESSAGE = bytes => `output exceeded ${bytes} bytes; reduce the returned value or keep it in a REPL binding`
25
26
 
@@ -49,7 +50,7 @@ function isBindingReferenceError(error) {
49
50
  || /Cannot access ['"][^'"]+['"] before initialization/.test(error.message))
50
51
  }
51
52
 
52
- function parseDiagnostic(error, source) {
53
+ function parseCellPosition(error) {
53
54
  const cellPosition = error instanceof ModuleRewriteError ? error.cellPosition : undefined
54
55
  const line = Number.isSafeInteger(cellPosition?.line)
55
56
  ? cellPosition.line
@@ -57,16 +58,29 @@ function parseDiagnostic(error, source) {
57
58
  const column = Number.isSafeInteger(cellPosition?.column)
58
59
  ? cellPosition.column
59
60
  : Number.isSafeInteger(error?.loc?.column) ? error.loc.column + 1 : undefined
61
+ return line !== undefined && line >= 1 && column !== undefined
62
+ ? { line, column }
63
+ : undefined
64
+ }
65
+
66
+ function parseDiagnostic(error, source, position, repair) {
60
67
  return diagnostic({
61
68
  code: 'PTC-C001',
62
69
  severity: 'error',
63
70
  phase: 'parse',
64
71
  message: `cell could not be parsed: ${oneLineMessage(error)}`,
65
72
  stateEffect: 'unchanged',
66
- ...(line !== undefined && line >= 1 && column !== undefined ? {
67
- source: { cell: 'current', start: { line, column } },
68
- } : {}),
69
- help: ['repair the reported syntax and retry only this cell'],
73
+ ...(position === undefined ? {} : {
74
+ source: { cell: 'current', start: position },
75
+ }),
76
+ help: repair === undefined
77
+ ? source.length >= LONG_CELL_CODE_UNITS
78
+ ? ['this cell was not executed; when edit_run_code is declared for the current request and the correction is small and localized, use it to avoid resending this long source; otherwise retry only this cell with corrected source in run_code']
79
+ : ['this cell was not executed; correct the reported syntax and retry only this cell with run_code']
80
+ : [
81
+ `this cell was not executed; validated syntax repair: append ${JSON.stringify(repair.delimiter)} at the end of this cell`,
82
+ `when edit_run_code is declared for the current request, call ${repair.invocation} to apply this correction and rerun the cell; otherwise retry only this cell with the corrected source in run_code`,
83
+ ],
70
84
  })
71
85
  }
72
86
 
@@ -196,29 +210,41 @@ export class SessionCellExecutor {
196
210
  return result
197
211
  }
198
212
 
199
- let prepared
200
213
  const catalog = kernel.bindingCatalog.inputs()
201
- let looseTopLevelRedeclarations
214
+ const looseTopLevelRedeclarations = replayRecord === undefined
215
+ ? config.looseTopLevelRedeclarations
216
+ : replayRecord.bindingMode === 'loose'
217
+ const rewritesEnabled = replayRecord === undefined ? {
218
+ autoRewriteImports: config.autoRewriteImports,
219
+ autoStripExports: config.autoStripExports,
220
+ autoSplitRedeclarations: config.autoSplitRedeclarations,
221
+ } : replayRecord.rewritePolicy
222
+ const prepareCell = program => prepareProgram(
223
+ program,
224
+ catalog.knownBindings,
225
+ looseTopLevelRedeclarations,
226
+ request.bindingDescriptors.reservedNames,
227
+ rewritesEnabled,
228
+ catalog.importBindings,
229
+ catalog.importNamespaces,
230
+ )
231
+ let prepared
202
232
  try {
203
- looseTopLevelRedeclarations = replayRecord === undefined
204
- ? config.looseTopLevelRedeclarations
205
- : replayRecord.bindingMode === 'loose'
206
- prepared = prepareProgram(
207
- request.program,
208
- catalog.knownBindings,
209
- looseTopLevelRedeclarations,
210
- request.bindingDescriptors.reservedNames,
211
- replayRecord === undefined ? {
212
- autoRewriteImports: config.autoRewriteImports,
213
- autoStripExports: config.autoStripExports,
214
- autoSplitRedeclarations: config.autoSplitRedeclarations,
215
- } : replayRecord.rewritePolicy,
216
- catalog.importBindings,
217
- catalog.importNamespaces,
218
- )
233
+ prepared = prepareCell(request.program)
219
234
  } catch (error) {
220
235
  const result = earlyResult('exception', messageOf(error))
221
- const failure = error instanceof PreflightError ? preflightDiagnostic(error) : parseDiagnostic(error, request.program)
236
+ const position = parseCellPosition(error)
237
+ const repair = error instanceof PreflightError || replayRecord !== undefined
238
+ ? undefined
239
+ : validatedEofClosureRepair({
240
+ source: request.program,
241
+ position,
242
+ prepare: prepareCell,
243
+ targetCallSeq: request.sourceCallSeq,
244
+ })
245
+ const failure = error instanceof PreflightError
246
+ ? preflightDiagnostic(error)
247
+ : parseDiagnostic(error, request.program, position, repair)
222
248
  result.error.message = renderDiagnostic(failure, request.program)
223
249
  kernel.completeJournal(request.journal, 'noop', result, undefined, [failure])
224
250
  return result
@@ -466,7 +492,7 @@ export class SessionCellExecutor {
466
492
  active.prepared.sourceMap,
467
493
  ),
468
494
  declared: message.moduleLoadFailed === true ? new Set() : active.prepared.declared,
469
- longCellFailure: active.request.program.length >= LONG_FAILED_CELL_TIP_CODE_UNITS,
495
+ longCellFailure: active.request.program.length >= LONG_CELL_CODE_UNITS,
470
496
  })
471
497
  active.appliedBindingCatalog = message.moduleLoadFailed === true
472
498
  ? active.priorBindingCatalog
@@ -27,6 +27,20 @@ function isContextStep(event) {
27
27
  || (event?.type === 'user/message' && event.data?.source?.kind === 'user')
28
28
  }
29
29
 
30
+ function cordisTranscriptFacts(timeline) {
31
+ let calls = 0
32
+ let inspections = 0
33
+ for (const result of timeline.results.values()) {
34
+ for (const call of result?.journal?.calls ?? []) {
35
+ if (call.global !== 'tools' || typeof call.member !== 'string'
36
+ || !call.member.startsWith('cordis_')) continue
37
+ calls += 1
38
+ if (call.ok === true && call.member.startsWith('cordis_inspect')) inspections += 1
39
+ }
40
+ }
41
+ return Object.freeze({ calls, inspections })
42
+ }
43
+
30
44
  /** Project session events into immutable facts consumed by prompt presentation. */
31
45
  export function projectSessionLog(agent, requestedEdit) {
32
46
  const events = agent?.session?.events
@@ -39,6 +53,7 @@ export function projectSessionLog(agent, requestedEdit) {
39
53
  latestRun: undefined,
40
54
  editableRun: undefined,
41
55
  repairSource: undefined,
56
+ cordisTranscript: Object.freeze({ calls: 0, inspections: 0 }),
42
57
  })
43
58
  }
44
59
  let contextStep = 0
@@ -60,6 +75,7 @@ export function projectSessionLog(agent, requestedEdit) {
60
75
  : undefined
61
76
  const latestRun = timeline.openTurn ? timeline.latestRun : undefined
62
77
  const editableRun = timeline.openTurn ? timeline.editableRun : undefined
78
+ const cordisTranscript = cordisTranscriptFacts(timeline)
63
79
  return Object.freeze({
64
80
  openTurn: timeline.openTurn,
65
81
  contextStep,
@@ -68,6 +84,7 @@ export function projectSessionLog(agent, requestedEdit) {
68
84
  latestRun,
69
85
  editableRun,
70
86
  repairSource: editableRun?.source,
87
+ cordisTranscript,
71
88
  ...(requestedEdit === undefined ? {} : { requestedEditTarget }),
72
89
  })
73
90
  }
@@ -432,14 +432,22 @@ export class SessionRuntime {
432
432
  request = { ...request, bindings: bindingDescriptors.namespaces, bindingDescriptors }
433
433
  const { id: sessionId, session, callId, persistedCallSeq, cwd } = sessionOf(sessionContext)
434
434
  let callSeq
435
+ let sourceCallSeq
435
436
  try {
436
437
  if (persistedCallSeq !== undefined
437
438
  && (!Number.isSafeInteger(persistedCallSeq) || persistedCallSeq < 0)) {
438
439
  throw new Error('persisted tool call sequence must be a non-negative safe integer')
439
440
  }
440
- callSeq = cellConfig.durableReplay
441
- ? persistedCallSeq ?? liveToolCallSeq(session, callId, 'run_code')
442
- : undefined
441
+ if (cellConfig.durableReplay) {
442
+ sourceCallSeq = liveToolCallSeq(session, callId, 'run_code')
443
+ callSeq = persistedCallSeq ?? sourceCallSeq
444
+ } else {
445
+ try {
446
+ sourceCallSeq = liveToolCallSeq(session, callId, 'run_code')
447
+ } catch {
448
+ sourceCallSeq = undefined
449
+ }
450
+ }
443
451
  } catch (error) {
444
452
  return completed({ logs: [], error: { kind: 'recovery', message: `cannot identify current run_code call in session log: ${messageOf(error)}` } })
445
453
  }
@@ -471,7 +479,7 @@ export class SessionRuntime {
471
479
  const workerReservation = kernel.reserveWorkerConfiguration(cellConfig)
472
480
  let result
473
481
  try {
474
- result = await kernel.run({ ...request, journal, callSeq }, cellConfig)
482
+ result = await kernel.run({ ...request, journal, callSeq, sourceCallSeq }, cellConfig)
475
483
  } finally {
476
484
  kernel.releaseWorkerConfiguration(workerReservation)
477
485
  }
@@ -0,0 +1,82 @@
1
+ import { EXPECTED_TARGET_CALL_SEQ, editRejectedCell } from './rejected-cell-editor.js'
2
+
3
+ const CLOSING_DELIMITERS = Object.freeze(['}', ')', ']'])
4
+ const LINE_TERMINATORS = /\r\n|[\n\r\u2028\u2029]/gu
5
+ export const MAX_PARSE_REPAIR_ANCHOR_CODE_UNITS = 256
6
+
7
+ function sourceEndPosition(source) {
8
+ let line = 1
9
+ let lineStart = 0
10
+ for (const match of source.matchAll(LINE_TERMINATORS)) {
11
+ line += 1
12
+ lineStart = match.index + match[0].length
13
+ }
14
+ return { line, column: source.length - lineStart + 1 }
15
+ }
16
+
17
+ function isSourceEnd(source, position) {
18
+ if (!Number.isSafeInteger(position?.line) || !Number.isSafeInteger(position?.column)) return false
19
+ const end = sourceEndPosition(source)
20
+ return position.line === end.line && position.column === end.column
21
+ }
22
+
23
+ function startsInsideSurrogatePair(source, index) {
24
+ if (index <= 0) return false
25
+ const current = source.charCodeAt(index)
26
+ const previous = source.charCodeAt(index - 1)
27
+ return current >= 0xdc00 && current <= 0xdfff && previous >= 0xd800 && previous <= 0xdbff
28
+ }
29
+
30
+ function boundedUniqueSuffix(source) {
31
+ const first = Math.max(0, source.length - MAX_PARSE_REPAIR_ANCHOR_CODE_UNITS)
32
+ for (let start = source.length - 1; start >= first; start -= 1) {
33
+ if (startsInsideSurrogatePair(source, start)) continue
34
+ const suffix = source.slice(start)
35
+ if (source.indexOf(suffix) === source.lastIndexOf(suffix)) return suffix
36
+ }
37
+ return undefined
38
+ }
39
+
40
+ function repairArguments(anchor, delimiter, targetCallSeq) {
41
+ const edit = Object.freeze({ old_string: anchor, new_string: anchor + delimiter })
42
+ return Object.freeze({
43
+ edits: Object.freeze([edit]),
44
+ [EXPECTED_TARGET_CALL_SEQ]: targetCallSeq,
45
+ })
46
+ }
47
+
48
+ function renderInvocation(argumentsValue) {
49
+ const json = JSON.stringify(argumentsValue)
50
+ .replaceAll('\u2028', '\\u2028')
51
+ .replaceAll('\u2029', '\\u2029')
52
+ return `edit_run_code(${json})`
53
+ }
54
+
55
+ /** Returns a source-exact edit only when one bounded EOF-closing candidate is valid. */
56
+ export function validatedEofClosureRepair({ source, position, prepare, targetCallSeq }) {
57
+ if (!Number.isSafeInteger(targetCallSeq) || targetCallSeq < 0
58
+ || !isSourceEnd(source, position)) return undefined
59
+ const anchor = boundedUniqueSuffix(source)
60
+ if (anchor === undefined) return undefined
61
+ const candidates = []
62
+ for (const delimiter of CLOSING_DELIMITERS) {
63
+ const code = source + delimiter
64
+ let prepared
65
+ try {
66
+ prepared = prepare(code)
67
+ } catch {
68
+ continue
69
+ }
70
+ if (prepared.collisions.length > 0) continue
71
+ const argumentsValue = repairArguments(anchor, delimiter, targetCallSeq)
72
+ const edited = editRejectedCell(argumentsValue, source)
73
+ if (!edited.edited || edited.code !== code) continue
74
+ candidates.push({ delimiter, arguments: argumentsValue })
75
+ }
76
+ if (candidates.length !== 1) return undefined
77
+ const candidate = candidates[0]
78
+ return Object.freeze({
79
+ ...candidate,
80
+ invocation: renderInvocation(candidate.arguments),
81
+ })
82
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-ptc-plus",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "A session-bound agent-native REPL for DeepSeek Harness PTC mode.",
5
5
  "type": "module",
6
6
  "main": "./index.js",