dsh-ptc-plus 0.5.0 → 0.5.2

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.
Files changed (74) hide show
  1. package/README.en.md +15 -3
  2. package/README.md +15 -3
  3. package/client.cjs +1983 -234
  4. package/compiler-core.cjs +132 -132
  5. package/compiler-core.cjs.map +3 -3
  6. package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +2 -0
  7. package/docs/adr/0017-track-the-latest-dsh-public-surface.md +2 -2
  8. package/docs/adr/0023-global-user-bindings.md +1 -1
  9. package/docs/adr/0027-run-the-session-kernel-in-a-killable-helper-process.md +11 -1
  10. package/docs/adr/0032-own-session-log-facts-in-a-host-only-projection.md +31 -1
  11. package/docs/adr/0038-freeze-session-capability-plans.md +1 -1
  12. package/docs/adr/0044-coexist-with-the-official-persistent-repl.md +35 -0
  13. package/docs/adr/0045-customize-model-visible-prompt-text.md +38 -0
  14. package/docs/adr/0046-gate-the-commit-with-the-semantic-evidence-tier.md +35 -0
  15. package/docs/adr/0047-store-journal-argument-digests-in-the-projection.md +88 -0
  16. package/docs/adr/README.md +2 -0
  17. package/docs/architecture.md +2 -2
  18. package/docs/capability-planning-components.md +2 -2
  19. package/docs/client-ui.md +13 -10
  20. package/docs/client-visual-contract.md +1 -1
  21. package/docs/durability-design.md +7 -1
  22. package/docs/installation.md +5 -3
  23. package/docs/publishing.md +2 -2
  24. package/docs/releases/v0.5.2.md +79 -0
  25. package/docs/runtime-reference.md +42 -3
  26. package/docs/unmeasured-cell-recovery-repair-proposal.md +387 -0
  27. package/docs/verification.md +6 -2
  28. package/index.js +63 -19
  29. package/internal/bounded-literal.js +85 -0
  30. package/internal/capability-command-receipt.js +3 -1
  31. package/internal/capability-environment.js +171 -13
  32. package/internal/capability-plan.js +11 -8
  33. package/internal/capability-statistics.js +6 -2
  34. package/internal/cell-error.js +13 -0
  35. package/internal/config-spec.js +122 -2
  36. package/internal/direct-surface-owner.js +14 -10
  37. package/internal/edit-transport-owner.js +96 -17
  38. package/internal/execution-provider-owner.js +25 -0
  39. package/internal/execution-seam-compat.js +18 -0
  40. package/internal/isolated-worker.js +134 -17
  41. package/internal/kernel-worker.js +167 -3
  42. package/internal/legacy-cell-analysis.js +14 -2
  43. package/internal/legacy-repl-convenience.js +16 -3
  44. package/internal/official-repl-priority.js +51 -0
  45. package/internal/projection-retention.js +238 -0
  46. package/internal/prompt-text.js +858 -0
  47. package/internal/recovery-tips.js +50 -11
  48. package/internal/rejected-cell-editor.js +3 -2
  49. package/internal/runtime-bridge-owner.js +11 -8
  50. package/internal/runtime-config.js +2 -0
  51. package/internal/runtime-contexts.js +14 -5
  52. package/internal/runtime-messages.js +43 -15
  53. package/internal/sdk-projection.js +2 -2
  54. package/internal/session-cell-executor.js +132 -11
  55. package/internal/session-journal-recovery.js +1009 -99
  56. package/internal/session-journal-schema.js +7 -5
  57. package/internal/session-journal.js +177 -14
  58. package/internal/session-log-view.js +111 -24
  59. package/internal/session-runtime.js +674 -33
  60. package/internal/session-state.js +27 -0
  61. package/internal/stateful-root-compiler.js +11 -0
  62. package/internal/stateful-root-runtime.js +17 -0
  63. package/internal/user-binding-console.js +7 -0
  64. package/internal/user-bindings-owner.js +8 -27
  65. package/internal/user-bindings.js +2 -2
  66. package/internal/validated-parse-repair.js +6 -1
  67. package/internal/worker-client.js +27 -4
  68. package/package.json +5 -2
  69. package/src/client-activity.js +27 -6
  70. package/src/client-capabilities-view.js +774 -125
  71. package/src/client-copy.js +231 -18
  72. package/src/client-repl-view.js +6 -1
  73. package/src/client-styles.js +68 -2
  74. package/src/client-tool-view.js +9 -2
@@ -5,3 +5,16 @@ export class ModuleRewriteError extends SyntaxError {
5
5
  if (cellPosition !== undefined) this.cellPosition = cellPosition
6
6
  }
7
7
  }
8
+
9
+ /**
10
+ * The kernel refuses to admit a cell because a previous helper was not reclaimed.
11
+ *
12
+ * This is a fact about the kernel's own lifecycle, not about the recorded history
13
+ * or the submitted source, so consumers must not contract a frontier or blame a
14
+ * cell for it.
15
+ */
16
+ export class CellAdmissionBlocked extends AggregateError {
17
+ constructor(failures) {
18
+ super(failures, 'previous computation helper reclamation failed; dispose the session before continuing')
19
+ }
20
+ }
@@ -1,4 +1,5 @@
1
1
  import { CAPABILITY_POLICY_DEFAULT } from './capability-policy.js'
2
+ import { PROMPT_TEXT_DEFAULT } from './prompt-text.js'
2
3
 
3
4
  /** Configuration fields shared by the Host schema and settings card. */
4
5
  export const MAX_TIMER_DELAY_MS = 2_147_483_647
@@ -215,6 +216,77 @@ export const CONFIG_FIELDS = Object.freeze([
215
216
  description: 'worker 重启后从 session journal 重建可恢复的 REPL 状态。',
216
217
  descriptionEn: 'Rebuilds recoverable REPL state from the session journal after a worker restart.',
217
218
  },
219
+ {
220
+ key: 'retentionPayloadRuns',
221
+ type: 'integer',
222
+ default: 8,
223
+ min: 8,
224
+ max: 512,
225
+ label: '保留最近单元格数',
226
+ labelEn: 'Retained recent cells',
227
+ description: '检查点保留最近多少个单元格的参数原文。调大能直接编辑更久以前的计算,但检查点更大;更早的单元格需要从会话日志重新折叠才能编辑或重新发布。',
228
+ descriptionEn: 'How many recent cells keep their argument text in the checkpoint. A larger window keeps more history editable without a log read but grows the checkpoint; older cells must be refolded from the session log to be edited or republished.',
229
+ },
230
+ {
231
+ key: 'retentionPayloadBytes',
232
+ type: 'integer',
233
+ default: 8388608,
234
+ min: 1048576,
235
+ max: 16777216,
236
+ label: '保留载荷预算 (UTF-8 bytes)',
237
+ labelEn: 'Retained payload budget (UTF-8 bytes)',
238
+ description: '保留单元格参数原文的累计上限。达到上限后最旧的原文被移除,检查点体积因此与会话写入总量无关;单个超过预算的单元格也会被移除并从日志重折。检查点体积约为本预算的两倍,上限 16 MiB 对应约 32 MB 的最坏情况。',
239
+ descriptionEn: 'Cumulative budget for the retained argument text. Past it the oldest payload is removed, which keeps the checkpoint independent of how much the session wrote; a single cell larger than the budget is removed as well and refolded from the log. A checkpoint is roughly twice this budget, so the 16 MiB maximum bounds the worst case near 32 MB.',
240
+ },
241
+ {
242
+ key: 'retentionClusters',
243
+ type: 'integer',
244
+ default: 8,
245
+ min: 0,
246
+ max: 50000,
247
+ label: '保留单元格簇上限',
248
+ labelEn: 'Retained cluster limit',
249
+ description: '检查点保留多少个单元格簇的身份与观测,超过后最旧的簇折进区间摘要(其后续操作需要从日志重折)。默认与"保留最近单元格数"一致,所以检查点不会随会话长度线性增长;调大能保留更多无需重折的恢复证据,但检查点更大。填 0 表示不限制:不做按数量的折叠,只由载荷预算约束体积。',
250
+ descriptionEn: 'How many cell clusters keep their identity and observations in the checkpoint; older ones fold into an interval summary and need a log refold afterwards. It defaults to the retained-cells window, which is what keeps a long session from growing linearly; a larger value keeps more recovery evidence without a refold at the cost of a bigger checkpoint. 0 means unlimited: no count-based folding, with the payload budget as the only bound.',
251
+ },
252
+ {
253
+ key: 'capabilityPromptTextEnabled',
254
+ type: 'boolean',
255
+ default: true,
256
+ label: '提示文本',
257
+ labelEn: 'Prompt text',
258
+ description: '把「提示文本」页签里改写的固定句子应用到模型的请求、工具说明、能力方案与提示;关闭后这些渠道一律回到内置文本,已保存的改写保留但暂停生效。',
259
+ descriptionEn: 'Applies the sentences edited in the Prompt text tab to model requests, tool descriptions, the capability plan and tips. When off, those channels use the built-in text; saved edits are kept but paused.',
260
+ },
261
+ {
262
+ key: 'promptText',
263
+ type: 'string',
264
+ default: PROMPT_TEXT_DEFAULT,
265
+ label: '提示文本覆盖',
266
+ labelEn: 'Prompt text overrides',
267
+ description: '按槽位 id 覆盖插件写给模型的固定提示文本;空对象表示使用内置文本。诊断、信封与由配置派生的策略句始终只读。',
268
+ descriptionEn: 'Override the fixed prompt text the plugin writes for the model, keyed by slot id; an empty document keeps the built-in text. Diagnostics, message envelopes and configuration-derived policy sentences stay read-only.',
269
+ },
270
+ {
271
+ key: 'unmeasuredCellLimit',
272
+ type: 'integer',
273
+ default: 2,
274
+ min: 1,
275
+ max: Number.MAX_SAFE_INTEGER,
276
+ label: '连续未测量 cell 丢弃阈值',
277
+ labelEn: 'Consecutive unmeasured cells before the worker is dropped',
278
+ description: '连续第几个未测量 cell 触发丢弃 live worker(默认 2:第一个保留,第二个丢弃);已证明的 frontier 保留给下一 cell 重放。',
279
+ descriptionEn: 'The consecutive unmeasured cell that drops the live worker (default 2: the first is kept, the second drops it). The proved frontier stays replayable for the next cell.',
280
+ },
281
+ {
282
+ key: 'officialReplPriority',
283
+ type: 'boolean',
284
+ default: false,
285
+ label: '官方 REPL 模式优先',
286
+ labelEn: 'Prioritize the official REPL mode',
287
+ description: '检测到官方持久 REPL 模式时由官方运行时接管 PTC 会话执行;PTC Plus 让出 run_code 及其进程内会话状态语义,关闭时始终由 PTC Plus 接管。',
288
+ descriptionEn: 'Lets the official runtime own PTC session execution when the official persistent REPL mode is active; PTC Plus then leaves run_code and its in-process session-state semantics to DSH. Disabled keeps PTC Plus in charge.',
289
+ },
218
290
  {
219
291
  key: 'tipsEnabled',
220
292
  type: 'boolean',
@@ -279,6 +351,50 @@ export const CONFIG_FIELDS = Object.freeze([
279
351
  description: '限制单个 cell 的日志和返回结果的合计大小。',
280
352
  descriptionEn: 'Limits the combined size of one cell\'s logs and returned result.',
281
353
  },
354
+ {
355
+ key: 'sampleBoundMs',
356
+ type: 'integer',
357
+ default: 1_000,
358
+ min: 1,
359
+ max: MAX_TIMER_DELAY_MS,
360
+ label: '计算基线采样等待上限 (ms)',
361
+ labelEn: 'Compute-baseline sample wait limit (ms)',
362
+ description: '等待 worker 回答计算基线采样的上限;宿主忙碌时先读空 helper 通道再判断缺失,未观察到的采样按未测量 no-op 结算,是否保留 worker 由 unmeasuredCellLimit 决定;transport 报告退出、断连或无法发送时仍走 worker-exit 重置。',
363
+ descriptionEn: 'Bounds the wait for the worker compute-baseline answer. A busy host reads the helper channel out before concluding an answer is missing; an unobserved sample settles as an unmeasured no-op and unmeasuredCellLimit decides whether the worker stays; a transport-reported exit, disconnect or unsendable request keeps the worker-exit reset route.',
364
+ },
365
+ {
366
+ key: 'workerReclaimMs',
367
+ type: 'integer',
368
+ default: 5_000,
369
+ min: 1_000,
370
+ max: MAX_TIMER_DELAY_MS,
371
+ label: 'worker 回收确认上限 (ms)',
372
+ labelEn: 'Worker reclamation confirmation limit (ms)',
373
+ description: '停止 helper 的关闭阶段上限(至少 1000ms);exit 确认与其后的输出关闭各自以此为上界,release grace 会被压缩以给 kill 预留至少 1000ms 的确认窗口。worker 创建与关闭时读取当前配置。cell 触发的 session kernel exit 超时会保留 helper 并在会话释放前阻止后续 cell;startup、idle 或输出关闭路径的失败,以及 binding console 与 candidate helper,只保留实例并在释放时报告。',
374
+ descriptionEn: 'Bounds one close stage of a stopped helper (at least 1000 ms): exit confirmation and the following output close are each bounded by it, with the release grace capped so the kill keeps at least a 1000 ms confirmation window. Read from the current configuration when a worker is created or closed. A cell-driven session-kernel exit miss retains the helper and blocks later cells until the session is released; a startup, idle or output-close miss, and the binding console and candidate helpers, only retain the instance and report it at release.',
375
+ },
376
+ {
377
+ key: 'startupProbeMs',
378
+ type: 'integer',
379
+ default: 5_000,
380
+ min: 1,
381
+ max: MAX_TIMER_DELAY_MS,
382
+ label: 'REPL 启动探针上限 (ms)',
383
+ labelEn: 'REPL startup probe limit (ms)',
384
+ description: 'worker 启动时校验 REPL 契约的等待上限;超时按启动失败结算,不执行任何 cell。worker 创建时读取当前配置。',
385
+ descriptionEn: 'Bounds the worker startup check of the REPL contract. A timeout settles as a startup failure and no cell executes. Read from the current configuration when a worker is created.',
386
+ },
387
+ {
388
+ key: 'executionSeamTimeoutMs',
389
+ type: 'integer',
390
+ default: 30_000,
391
+ min: 1,
392
+ max: MAX_TIMER_DELAY_MS,
393
+ label: '等待宿主执行入口上限 (ms)',
394
+ labelEn: 'Host execution-seam wait limit (ms)',
395
+ description: '激活时等待宿主注册执行入口的上限;超时把当前部署报告为不支持。只在插件激活或重载时读取,运行中的修改在下次激活生效。',
396
+ descriptionEn: 'Bounds the activation wait for the host to register its execution seam. A timeout reports the deployment as unsupported. Read only when the plugin activates or reloads; a live change applies at the next activation.',
397
+ },
282
398
  {
283
399
  key: 'maxValueNodes',
284
400
  type: 'integer',
@@ -367,7 +483,7 @@ export const CONFIG_GROUPS = Object.freeze([
367
483
  key: 'recovery',
368
484
  label: '状态与恢复',
369
485
  labelEn: 'State and recovery',
370
- fields: Object.freeze(['durableReplay', 'tipsEnabled', 'tipCooldownMessages', 'tipEscalationFailures']),
486
+ fields: Object.freeze(['durableReplay', 'unmeasuredCellLimit', 'officialReplPriority', 'tipsEnabled', 'tipCooldownMessages', 'tipEscalationFailures']),
371
487
  },
372
488
  {
373
489
  key: 'extensions',
@@ -397,6 +513,10 @@ export const CONFIG_GROUPS = Object.freeze([
397
513
  'maxOldGenerationSizeMb',
398
514
  'maxNestedRunCodeDepth',
399
515
  'maxOutputBytes',
516
+ 'sampleBoundMs',
517
+ 'workerReclaimMs',
518
+ 'startupProbeMs',
519
+ 'executionSeamTimeoutMs',
400
520
  'maxValueNodes',
401
521
  'maxValueEdges',
402
522
  'maxValueArrayLength',
@@ -405,7 +525,7 @@ export const CONFIG_GROUPS = Object.freeze([
405
525
  },
406
526
  { key: 'capabilities', label: '能力与上下文', labelEn: 'Capabilities and context',
407
527
  fields: Object.freeze(['capabilityContextEnabled', 'capabilityManagementEnabled', 'capabilityBaseline', 'capabilityOverrides',
408
- 'capabilityStatistics', 'capabilityPolicy', 'capabilityRecoveryEnabled', 'modelPreviewEnabled', 'capabilityProfiles', 'capabilityBudgetBytes', 'capabilityDirectoryBytes', 'capabilityRecoveryBytes', 'modelPreviewTokens']) },
528
+ 'capabilityStatistics', 'capabilityPolicy', 'capabilityRecoveryEnabled', 'modelPreviewEnabled', 'capabilityProfiles', 'capabilityBudgetBytes', 'capabilityDirectoryBytes', 'capabilityRecoveryBytes', 'modelPreviewTokens', 'retentionPayloadRuns', 'retentionPayloadBytes', 'retentionClusters', 'capabilityPromptTextEnabled', 'promptText']) },
409
529
  ])
410
530
 
411
531
  /** Functional modules share the same section order across every settings entry. */
@@ -16,10 +16,8 @@ import {
16
16
  import { userBindingsConfiguredContext } from './user-bindings.js'
17
17
  import { BINDING_AUTHORING_SDK } from './user-binding-authoring.js'
18
18
  import { executionPolicies } from './binding-update-policy.js'
19
+ import { PROMPT_SLOT_IDS, promptAssetText, resolvePromptTextConfig } from './prompt-text.js'
19
20
 
20
- const RUN_CODE_TOOL_DESCRIPTION = 'Evaluate the next TypeScript cell in this session-bound persistent REPL, reusing earlier top-level bindings. Use `code` for the cell and `description` for its short UI summary. Successful image-bearing subtool results are attached after the cell.'
21
- const RUN_CODE_CODE_DESCRIPTION = 'Code for the next PTC TypeScript cell, following the selected language conventions.'
22
- const RUN_CODE_DESCRIPTION_DESCRIPTION = 'Short active-voice summary of what this cell does, 5-10 words (shown in the UI).'
23
21
  const CODE_TRANSPORT_INSTRUCTION = '`run_code` is the only tool callable directly. Call `tools.edit_run_code` and other SDK tools from inside a program. An accepted edit runs after the current cell finishes.'
24
22
  const PTC_COLLAPSE_SECTION_NAMES = Object.freeze(['tools:ptc-only', 'tools:code-only'])
25
23
  const INVALID_TOOL_JSON_FAILURE = Object.freeze({
@@ -29,7 +27,7 @@ const INVALID_TOOL_JSON_FAILURE = Object.freeze({
29
27
  const MALFORMED_TOOL_REJECTION_ID = 'dsh-ptc-plus-malformed-tool-json'
30
28
  const MALFORMED_TOOL_REJECTION_ARGUMENTS = '{"dsh_ptc_plus_malformed_tool_json":'
31
29
 
32
- function adaptRunCodeSchema(tool) {
30
+ function adaptRunCodeSchema(tool, promptText) {
33
31
  const parameters = tool.parameters
34
32
  const properties = isRecord(parameters) ? parameters.properties : undefined
35
33
  const code = isRecord(properties) ? properties.code : undefined
@@ -41,13 +39,13 @@ function adaptRunCodeSchema(tool) {
41
39
  }
42
40
  return {
43
41
  ...tool,
44
- description: RUN_CODE_TOOL_DESCRIPTION,
42
+ description: promptAssetText(promptText, PROMPT_SLOT_IDS.runCodeDescription),
45
43
  parameters: {
46
44
  ...parameters,
47
45
  properties: {
48
46
  ...properties,
49
- code: { ...code, description: RUN_CODE_CODE_DESCRIPTION },
50
- description: { ...description, description: RUN_CODE_DESCRIPTION_DESCRIPTION },
47
+ code: { ...code, description: promptAssetText(promptText, PROMPT_SLOT_IDS.runCodeCodeArg) },
48
+ description: { ...description, description: promptAssetText(promptText, PROMPT_SLOT_IDS.runCodeDescriptionArg) },
51
49
  },
52
50
  },
53
51
  }
@@ -472,11 +470,13 @@ export function createDirectSurfaceOwner({
472
470
  let disposed = false
473
471
  let currentCanonicalizeToolCalls = canonicalizeToolCalls
474
472
  let currentAutoDescribeRunCode = runtimeConfig.autoDescribeRunCode
473
+ let currentPromptText = resolvePromptTextConfig(runtimeConfig)
475
474
  let currentLanguageSemantics = executionPolicies(runtimeConfig).languageSemantics
476
475
  const tipConfig = {
477
476
  enabled: runtimeConfig.tipsEnabled,
478
477
  cooldownMessages: runtimeConfig.tipCooldownMessages,
479
478
  escalationFailures: runtimeConfig.tipEscalationFailures,
479
+ get prompt() { return currentPromptText },
480
480
  }
481
481
 
482
482
  const sessionOwner = (id) => {
@@ -561,6 +561,7 @@ export function createDirectSurfaceOwner({
561
561
  currentLanguageSemantics = executionPolicies(nextConfig).languageSemantics
562
562
  currentCanonicalizeToolCalls = nextConfig.canonicalizeToolCalls
563
563
  currentAutoDescribeRunCode = nextConfig.autoDescribeRunCode
564
+ currentPromptText = resolvePromptTextConfig(nextConfig)
564
565
  cordisRecovery.reconfigure(nextConfig.cordisToolsEnabled)
565
566
  tipConfig.enabled = nextConfig.tipsEnabled
566
567
  tipConfig.cooldownMessages = nextConfig.tipCooldownMessages
@@ -628,10 +629,11 @@ export function createDirectSurfaceOwner({
628
629
  const userBindings = sessionPtc ? await userBindingsForAgent(agent) : undefined
629
630
  if (!isCurrent()) return assembly
630
631
  const runCode = tools.find(tool => tool?.name === RUN_CODE)
632
+ const promptText = currentPromptText
631
633
  let directTools = sessionPtc ? capabilityEnvironment?.visibleSchemas(agent, tools) ?? tools : tools
632
634
  if (sessionPtcProjection) {
633
635
  editTransport.ensureInstalled(agent)
634
- directTools = [adaptRunCodeSchema(runCode)]
636
+ directTools = [adaptRunCodeSchema(runCode, promptText)]
635
637
  }
636
638
  const runtimeContexts = sessionPtc
637
639
  ? sessionRuntimeContexts(agent, tipConfig, {
@@ -641,7 +643,9 @@ export function createDirectSurfaceOwner({
641
643
  },
642
644
  })
643
645
  : { contexts: [] }
644
- const bindingDefaults = userBindings === undefined ? undefined : userBindingsConfiguredContext(userBindings)
646
+ const bindingDefaults = userBindings === undefined ? undefined : userBindingsConfiguredContext(userBindings, {
647
+ entryLabel: promptAssetText(promptText, PROMPT_SLOT_IDS.userBindingsEntryLabel),
648
+ })
645
649
  const contexts = [
646
650
  ...runtimeContexts.contexts,
647
651
  ...(bindingDefaults === undefined ? [] : [bindingDefaults]),
@@ -684,7 +688,7 @@ export function createDirectSurfaceOwner({
684
688
  tools: sessionPtcProjection
685
689
  ? directTools
686
690
  : directTools.map(tool => tool?.name === RUN_CODE
687
- ? adaptRunCodeSchema(tool)
691
+ ? adaptRunCodeSchema(tool, promptText)
688
692
  : tool),
689
693
  sections,
690
694
  }
@@ -7,10 +7,12 @@ import {
7
7
  REWRITES_KEY,
8
8
  isCanonicalSequence,
9
9
  liveToolCallSeq,
10
+ normalizeDeferredEditSettlement,
10
11
  normalizeJournal,
11
12
  normalizeRecoveryBoundaries,
12
13
  normalizeRewrites,
13
14
  validatedRewrites,
15
+ withDiagnosticWording,
14
16
  } from './session-journal.js'
15
17
  import {
16
18
  EXPECTED_TARGET_CALL_SEQ,
@@ -18,6 +20,11 @@ import {
18
20
  editRunCodeSchema,
19
21
  } from './rejected-cell-editor.js'
20
22
  import { projectSessionLog } from './session-log-view.js'
23
+ import { refoldEditableSettlement } from './session-journal-recovery.js'
24
+ import {
25
+ PRUNED_EDIT, PRUNED_EDIT_EN, PRUNED_SUMMARY_MISMATCH, PRUNED_SUMMARY_MISMATCH_EN,
26
+ } from './projection-retention.js'
27
+ import { PROMPT_SLOT_IDS, promptAssetText } from './prompt-text.js'
21
28
  import { sessionEvents } from './session-events.js'
22
29
  import { deepEqualJson, snapshotJsonValue } from '@deepseek-ai/dsh-util-values'
23
30
  import { scopeTarget } from '@deepseek-ai/dsh-scope'
@@ -39,6 +46,15 @@ import {
39
46
 
40
47
  export const EDIT_RUN_CODE = 'edit_run_code'
41
48
 
49
+ const TARGET_FIRST_LINE_LIMIT = 120
50
+
51
+ /** Bounded identity of the cell an edit actually captured, for model-facing reasons. */
52
+ function targetSummary(target) {
53
+ const line = typeof target?.source === 'string' ? target.source.split(/\r?\n/, 1)[0].trim() : ''
54
+ const firstLine = line.length > TARGET_FIRST_LINE_LIMIT ? `${line.slice(0, TARGET_FIRST_LINE_LIMIT)}…` : line
55
+ return `run_code call ${target?.callSeq}${firstLine === '' ? '' : `, first line: ${firstLine}`}`
56
+ }
57
+
42
58
  function nestedEditStart(sessionLog, exec) {
43
59
  const events = sessionEvents(sessionLog)
44
60
  if (!Array.isArray(events) || typeof exec?.callId !== 'string'
@@ -63,8 +79,35 @@ function nestedEditStart(sessionLog, exec) {
63
79
  || event.data.rootCallId !== exec.rootCallId
64
80
  || event.data.parentCallId !== exec.rootCallId
65
81
  || !deepEqualJson(event.data.arguments, exec.arguments)) return undefined
66
- const target = projectSessionLog({ session: { events: events.slice(0, index + 1) } }).editableRun
67
- return { startSeq: event.seq, rootCallSeq, target }
82
+ const slice = { session: { events: events.slice(0, index + 1) } }
83
+ const view = projectSessionLog(slice)
84
+ let target = view.editableRun
85
+ // A dropped cluster is not gone for good: the log still holds the events, so the
86
+ // edit path refolds that slice with pruning disabled and reads the cell from
87
+ // there. Only when the log itself cannot supply it does the edit report that the
88
+ // retention window cut it.
89
+ if (target === undefined && view.retentionSummary !== undefined) {
90
+ const refolded = refoldEditableSettlement(events.slice(0, index + 1), view.retentionSummary)
91
+ target = refolded.run
92
+ // The summary is the checkpoint's only record of a dropped cluster; the log is
93
+ // the authority. When the two disagree the edit must not proceed on either.
94
+ //
95
+ // This call site cannot produce that disagreement: the summary and the refold
96
+ // come from the same fold of the same event slice, and the run the refold reads
97
+ // is the one the pruned fold pins, so `refoldEditableSettlement` never finds a
98
+ // summary record for it. The comparison stays for the exported contract, which
99
+ // covers both outcomes in test/projection-retention.test.js.
100
+ /* c8 ignore start -- same-fold determinism: the call site cannot disagree with itself (see above) */
101
+ if (refolded.mismatch) {
102
+ return { startSeq: event.seq, rootCallSeq, target: undefined, retentionSummary: view.retentionSummary,
103
+ summaryMismatch: true }
104
+ }
105
+ /* c8 ignore stop */
106
+ }
107
+ // The summary travels with the target so an unavailable edit can say why it is
108
+ // unavailable: the cell fell outside the retention window instead of never
109
+ // existing.
110
+ return { startSeq: event.seq, rootCallSeq, target, retentionSummary: view.retentionSummary }
68
111
  }
69
112
 
70
113
  function unavailableDerivedResult(inner) {
@@ -109,6 +152,7 @@ export function createEditTransportOwner(ctx, {
109
152
  sessionId,
110
153
  toolSchemasForAgent,
111
154
  readSessionLog,
155
+ promptText,
112
156
  }) {
113
157
  const editClaims = new Map()
114
158
  const pendingAcceptances = new Map()
@@ -128,7 +172,7 @@ export function createEditTransportOwner(ctx, {
128
172
  }
129
173
  }
130
174
 
131
- const definition = editRunCodeSchema()
175
+ const definition = editRunCodeSchema(promptAssetText(promptText, PROMPT_SLOT_IDS.editRunCodeDescription))
132
176
  const derivedMetadata = (derived) => {
133
177
  const meta = {
134
178
  [JOURNAL_KEY]: normalizeJournal(derived.journal),
@@ -155,9 +199,12 @@ export function createEditTransportOwner(ctx, {
155
199
  derived.draftCapability,
156
200
  presentationGeneration,
157
201
  )
202
+ // The derived settlement records its own diagnostic wording, so a replay reads
203
+ // the same pairing the root path publishes.
204
+ const withWording = withDiagnosticWording(withDraft, withDraft[JOURNAL_KEY])
158
205
  return derived.replMemory === undefined
159
- ? withDraft
160
- : withReplMemorySnapshot(withDraft, derived.replMemory, presentationGeneration)
206
+ ? withWording
207
+ : withReplMemorySnapshot(withWording, derived.replMemory, presentationGeneration)
161
208
  }
162
209
  definition.output = {
163
210
  schema: {
@@ -184,17 +231,29 @@ export function createEditTransportOwner(ctx, {
184
231
  throw new Error('ptc-plus: edit_run_code requires a unique persisted PTC sub-call start')
185
232
  }
186
233
  const target = start.target
234
+ /* c8 ignore start -- reachable only through `nestedEditStart`'s mismatch, which cannot disagree with itself (see there) */
235
+ if (start.summaryMismatch === true) {
236
+ return { accepted: false, scheduled: false,
237
+ reason: `${PRUNED_SUMMARY_MISMATCH} / ${PRUNED_SUMMARY_MISMATCH_EN}` }
238
+ }
239
+ /* c8 ignore stop */
187
240
  if (target?.source === undefined || target.callSeq === undefined) {
188
- return { accepted: false, scheduled: false, reason: 'no run_code cell is currently eligible for safe editing' }
241
+ return { accepted: false, scheduled: false,
242
+ reason: start.retentionSummary === undefined
243
+ ? 'no run_code cell is currently eligible for safe editing'
244
+ : `${PRUNED_EDIT} / ${PRUNED_EDIT_EN}` }
189
245
  }
190
246
  const edited = editRejectedCell(args, target.source)
191
- if (!edited.edited) return { accepted: false, scheduled: false, reason: edited.reason }
247
+ if (!edited.edited) {
248
+ return { accepted: false, scheduled: false, reason: `${edited.reason} (edit target: ${targetSummary(target)})` }
249
+ }
192
250
  const expectedTargetCallSeq = args[EXPECTED_TARGET_CALL_SEQ]
193
251
  if (expectedTargetCallSeq !== undefined && expectedTargetCallSeq !== target.callSeq) {
194
252
  return {
195
253
  accepted: false,
196
254
  scheduled: false,
197
- reason: `validated repair targets run_code call ${expectedTargetCallSeq}, but this edit captured call ${target.callSeq}`,
255
+ reason: `validated repair targets run_code call ${expectedTargetCallSeq}, but this edit captured ${targetSummary(target)}.`
256
+ + ' Only the most recent settled cell stays editable; if the intended cell is no longer editable, re-submit its corrected source with run_code instead.',
198
257
  }
199
258
  }
200
259
  if (typeof ctx.tools.execute !== 'function') {
@@ -207,7 +266,8 @@ export function createEditTransportOwner(ctx, {
207
266
  editClaims.set(id, claims)
208
267
  }
209
268
  if (claims.has(target.callSeq)) {
210
- return { accepted: false, scheduled: false, reason: 'the current edit target already has a derived execution' }
269
+ return { accepted: false, scheduled: false,
270
+ reason: `the current edit target already has a derived execution (edit target: ${targetSummary(target)})` }
211
271
  }
212
272
  claims.set(target.callSeq, 'scheduled')
213
273
  const receipt = { accepted: true, scheduled: true, target_call_seq: target.callSeq }
@@ -219,6 +279,7 @@ export function createEditTransportOwner(ctx, {
219
279
  rootCallSeq: start.rootCallSeq,
220
280
  subCallId: exec.callId,
221
281
  targetCallSeq: target.callSeq,
282
+ targetDescription: targetSummary(target),
222
283
  finalized: false,
223
284
  finalizeTentative: undefined,
224
285
  finish(confirmed) {
@@ -278,7 +339,7 @@ export function createEditTransportOwner(ctx, {
278
339
  const journalValue = isRecord(metadata) && Object.hasOwn(metadata, JOURNAL_KEY)
279
340
  ? metadata[JOURNAL_KEY] : undefined
280
341
  const journal = journalValue === undefined ? undefined : normalizeJournal(journalValue)
281
- if (journal === undefined || journal.status === 'noop'
342
+ if (journal === undefined
282
343
  || inner.isError !== (journal.completion?.kind === 'throw' || journal.status === 'discarded')) {
283
344
  this.finish(false)
284
345
  return { outcome: unavailableDerivedResult(inner), presentation: derivedPresentation(inner) }
@@ -296,16 +357,34 @@ export function createEditTransportOwner(ctx, {
296
357
  code: edited.code,
297
358
  description: edited.description,
298
359
  }
360
+ const metaValue = derivedMetadata(derived)
361
+ let settlement
362
+ try {
363
+ settlement = normalizeDeferredEditSettlement(metaValue, target.callSeq)
364
+ } catch {
365
+ // Metadata this relation cannot prove must not reach the root result.
366
+ this.finish(false)
367
+ return { outcome: unavailableDerivedResult(inner), presentation: derivedPresentation(inner) }
368
+ }
369
+ const provenance = {
370
+ startSeq: start.startSeq,
371
+ executionStartSeq: executionStart.seq,
372
+ rootCallSeq: start.rootCallSeq,
373
+ subCallId: exec.callId,
374
+ meta: metaValue,
375
+ }
376
+ if (settlement.unexecuted === true) {
377
+ // The accepted edit is proved by a settlement that never ran. Publishing
378
+ // the relation keeps the root cell provable, while the outcome and the
379
+ // unexecuted journal keep the revision from executing, from publishing a
380
+ // binding and from becoming the next edit target.
381
+ this.finish(false)
382
+ return { outcome: unavailableDerivedResult(inner), presentation: derivedPresentation(inner), provenance }
383
+ }
299
384
  return {
300
385
  outcome: derivedEditResult(inner),
301
386
  presentation: derivedPresentation(inner),
302
- provenance: {
303
- startSeq: start.startSeq,
304
- executionStartSeq: executionStart.seq,
305
- rootCallSeq: start.rootCallSeq,
306
- subCallId: exec.callId,
307
- meta: derivedMetadata(derived),
308
- },
387
+ provenance,
309
388
  }
310
389
  },
311
390
  }
@@ -64,12 +64,32 @@ export function apply(ctx, config = {}) {
64
64
  }
65
65
  const publish = () => {
66
66
  const resolutions = new WeakMap()
67
+ const ownerOriginals = new WeakMap()
68
+ const isOwner = owner => owner !== null && (typeof owner === 'object' || typeof owner === 'function')
67
69
  const delegate = {
68
70
  get language() { return readOriginal().language },
69
71
  get isolation() { return readOriginal().isolation },
72
+ get programFormat() { return readOriginal().programFormat ?? 'function-body' },
70
73
  get executionInstructions() { return readOriginal().executionInstructions },
71
74
  get sandboxMode() { return readOriginal().sandboxMode },
72
75
  get timeout() { return readOriginal().timeout },
76
+ release(owner) {
77
+ const originals = isOwner(owner) ? ownerOriginals.get(owner) : undefined
78
+ if (originals !== undefined) {
79
+ return Promise.all([...originals].map(original => Promise.resolve()
80
+ .then(() => original.release(owner))
81
+ .then(() => originals.delete(original)))).then(() => undefined)
82
+ }
83
+ let original
84
+ try {
85
+ original = readOriginal()
86
+ } catch {
87
+ // No retained dispatch belongs to this owner and execution is unavailable.
88
+ return Promise.resolve()
89
+ }
90
+ if (typeof original.release !== 'function') return Promise.resolve()
91
+ return Promise.resolve(original.release.call(original, owner))
92
+ },
73
93
  resolve(request) {
74
94
  const original = readOriginal()
75
95
  const spec = serviceName === EXECUTION_SEAM_SERVICE ? original.resolve(request) : request
@@ -82,6 +102,11 @@ export function apply(ctx, config = {}) {
82
102
  const resolved = resolutions.get(spec)
83
103
  if (resolved?.generation?.active === false) throw new Error('ptc-plus: resolved spec belongs to an unavailable original generation')
84
104
  const original = resolved?.original ?? readOriginal()
105
+ if (isOwner(spec?.stateOwner) && typeof original.release === 'function') {
106
+ let originals = ownerOriginals.get(spec.stateOwner)
107
+ if (originals === undefined) ownerOriginals.set(spec.stateOwner, originals = new Set())
108
+ originals.add(original)
109
+ }
85
110
  return original.run(spec)
86
111
  },
87
112
  }
@@ -60,6 +60,10 @@ export function createExecutionSeam(service, serviceName) {
60
60
  const provider = Object.freeze({
61
61
  get language() { requireCompatible(); return service.language },
62
62
  get isolation() { requireCompatible(); return installed === undefined ? service.isolation : 'worker-thread' },
63
+ // Source evaluation form belongs to the provider that actually receives
64
+ // the program. Program-format consumers delegate to the original provider,
65
+ // so this descriptor always follows the original generation.
66
+ get programFormat() { requireCompatible(); return service.programFormat ?? 'function-body' },
63
67
  get executionInstructions() { requireCompatible(); return installed === undefined ? service.executionInstructions ?? '' : '' },
64
68
  get sandboxMode() { requireCompatible(); return installed === undefined ? service.sandboxMode : undefined },
65
69
  get timeout() { requireCompatible(); return installed === undefined ? service.timeout : undefined },
@@ -71,6 +75,20 @@ export function createExecutionSeam(service, serviceName) {
71
75
  requireCompatible()
72
76
  return installed === undefined ? upstreamRun.call(service, input) : installed.execute(input)
73
77
  },
78
+ /**
79
+ * Release one owner's retained environment. The resources live in the
80
+ * original generation (the session kernel releases its state through its
81
+ * own lifecycle). Adapter retirement only closes execution; the original
82
+ * still owns cleanup for references retained by its consumers.
83
+ */
84
+ release(owner) {
85
+ if (typeof service.release !== 'function') return Promise.resolve()
86
+ try {
87
+ return Promise.resolve(service.release.call(service, owner))
88
+ } catch (error) {
89
+ return Promise.reject(error)
90
+ }
91
+ },
74
92
  [PLUGIN_EXECUTION_SEAM]() { return seam },
75
93
  })
76
94
  const seam = Object.freeze({