dsh-ptc-plus 0.2.2 → 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 +2 -0
- package/README.zh.md +2 -0
- package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +25 -3
- package/docs/architecture.md +3 -1
- package/docs/installation.md +3 -3
- package/docs/runtime-reference.md +4 -2
- package/internal/edit-transport-owner.js +12 -1
- package/internal/failure-reporting.js +1 -0
- package/internal/recovery-tips.js +0 -1
- package/internal/rejected-cell-editor.js +21 -7
- package/internal/session-cell-executor.js +52 -26
- package/internal/session-runtime.js +12 -4
- package/internal/validated-parse-repair.js +82 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -73,6 +73,8 @@ edit_run_code({ edits: [{ old_string: 'deps.length', new_string: 'deps' }] })
|
|
|
73
73
|
|
|
74
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.
|
|
75
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
|
+
|
|
76
78
|
### Module syntax
|
|
77
79
|
|
|
78
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.
|
package/README.zh.md
CHANGED
|
@@ -73,6 +73,8 @@ edit_run_code({ edits: [{ old_string: 'deps.length', new_string: 'deps' }] })
|
|
|
73
73
|
|
|
74
74
|
模型只发改动,完整源码留在对话之外。精确文本替换和正则替换都有限制,坏的正则不会卡住。
|
|
75
75
|
|
|
76
|
+
当被拒 cell 的末尾可唯一验证为缺少一个闭合符时,诊断会直接给出应用该修正并重运行所需的完整 `edit_run_code(...)` 调用。生成的调用带有 `expected_target_call_seq` 前置条件;如果另一个 cell 已先成为 edit 目标,它会拒绝且不执行。PTC Plus 不会自动应用这项建议;有歧义或没有持久目标身份的修复仍需提交修正后的源码。
|
|
77
|
+
|
|
76
78
|
### 模块语法
|
|
77
79
|
|
|
78
80
|
DSH 的 PTC 模式把每个 `run_code` 当作 async function body 执行,静态 `import` 和 `export` 声明在这个函数体里无效。PTC Plus 会在执行前用 AST 分析适配这些形式。
|
|
@@ -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
|
|
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
|
package/docs/architecture.md
CHANGED
|
@@ -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`
|
|
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`。外层历史仍是模型真实发出的
|
|
@@ -40,6 +40,8 @@ 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。
|
package/docs/installation.md
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
@@ -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
|
|
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
|
|
@@ -95,7 +97,7 @@ Rewrites are recorded as `meta.dshPtcPlusRewrites` on the tool result (parallel
|
|
|
95
97
|
|
|
96
98
|
| Code | Meaning | State effect |
|
|
97
99
|
| --- | --- | --- |
|
|
98
|
-
| `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 |
|
|
99
101
|
| `PTC-C002` | Preflight rejected a kernel-control import | Not executed; REPL unchanged |
|
|
100
102
|
| `PTC-N001` | Top-level binding conflict | Not executed; REPL unchanged |
|
|
101
103
|
| `PTC-O001` | Unsupported or over-budget output | Cell executed; earlier mutations may exist |
|
|
@@ -12,7 +12,11 @@ import {
|
|
|
12
12
|
normalizeRewrites,
|
|
13
13
|
validatedRewrites,
|
|
14
14
|
} from './session-journal.js'
|
|
15
|
-
import {
|
|
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
|
-
|
|
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
|
|
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(`${
|
|
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(
|
|
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(
|
|
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:
|
|
338
|
+
description: operation === 'edits'
|
|
325
339
|
? EDIT_RUN_CODE_EXECUTION_DESCRIPTION
|
|
326
340
|
: REGEX_EDIT_RUN_CODE_EXECUTION_DESCRIPTION,
|
|
327
341
|
})
|
|
@@ -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 {
|
|
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
|
|
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
|
-
...(
|
|
67
|
-
source: { cell: 'current', start:
|
|
68
|
-
}
|
|
69
|
-
help:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 >=
|
|
495
|
+
longCellFailure: active.request.program.length >= LONG_CELL_CODE_UNITS,
|
|
470
496
|
})
|
|
471
497
|
active.appliedBindingCatalog = message.moduleLoadFailed === true
|
|
472
498
|
? active.priorBindingCatalog
|
|
@@ -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
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
+
}
|