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 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`. 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
@@ -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`。外层历史仍是模型真实发出的
@@ -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。
@@ -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.
@@ -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
@@ -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 { 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
  })
@@ -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
@@ -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.2",
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",