dsh-ptc-plus 0.5.1 → 0.5.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.
Files changed (85) hide show
  1. package/README.en.md +10 -4
  2. package/README.md +10 -4
  3. package/client.cjs +1623 -104
  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 +4 -2
  8. package/docs/adr/0019-plugin-settings-and-kill-switch.md +2 -2
  9. package/docs/adr/0023-global-user-bindings.md +12 -0
  10. package/docs/adr/0027-run-the-session-kernel-in-a-killable-helper-process.md +11 -1
  11. package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +1 -1
  12. package/docs/adr/0030-serve-both-typert-codec-generations-from-one-codec.md +2 -2
  13. package/docs/adr/0032-own-session-log-facts-in-a-host-only-projection.md +31 -1
  14. package/docs/adr/0044-coexist-with-the-official-persistent-repl.md +4 -4
  15. package/docs/adr/0045-customize-model-visible-prompt-text.md +38 -0
  16. package/docs/adr/0046-gate-the-commit-with-the-semantic-evidence-tier.md +35 -0
  17. package/docs/adr/0047-store-journal-argument-digests-in-the-projection.md +88 -0
  18. package/docs/adr/0048-supersede-a-rewritten-cell.md +48 -0
  19. package/docs/adr/0049-prove-a-settlement-against-its-published-record.md +53 -0
  20. package/docs/adr/0050-read-host-surfaces-through-their-published-contract.md +48 -0
  21. package/docs/adr/README.md +7 -1
  22. package/docs/architecture.md +3 -3
  23. package/docs/client-ui.md +5 -2
  24. package/docs/durability-design.md +15 -2
  25. package/docs/installation.md +9 -9
  26. package/docs/releases/v0.5.2.md +91 -0
  27. package/docs/releases/v0.5.3.md +77 -0
  28. package/docs/runtime-reference.md +43 -3
  29. package/docs/unmeasured-cell-recovery-repair-proposal.md +387 -0
  30. package/docs/user-bindings.en.md +15 -0
  31. package/docs/user-bindings.md +15 -0
  32. package/docs/v0.5.3-goals.md +117 -0
  33. package/docs/verification.md +7 -3
  34. package/index.js +100 -31
  35. package/internal/binding-adoption.js +136 -0
  36. package/internal/bounded-literal.js +85 -0
  37. package/internal/capability-command-receipt.js +18 -4
  38. package/internal/capability-delivery-proof.js +70 -0
  39. package/internal/capability-environment.js +200 -11
  40. package/internal/capability-plan.js +11 -8
  41. package/internal/cell-error.js +13 -0
  42. package/internal/config-spec.js +126 -6
  43. package/internal/cordis-tools-owner.js +2 -1
  44. package/internal/direct-surface-owner.js +39 -21
  45. package/internal/edit-transport-owner.js +81 -15
  46. package/internal/execution-provider-owner.js +6 -0
  47. package/internal/execution-seam-compat.js +25 -0
  48. package/internal/host-tool-sdk.js +308 -0
  49. package/internal/isolated-worker.js +134 -17
  50. package/internal/kernel-worker.js +167 -3
  51. package/internal/legacy-cell-analysis.js +14 -2
  52. package/internal/legacy-repl-convenience.js +16 -3
  53. package/internal/official-repl-priority.js +31 -10
  54. package/internal/program-bindings.js +12 -1
  55. package/internal/projection-retention.js +238 -0
  56. package/internal/prompt-text.js +858 -0
  57. package/internal/recovery-tips.js +50 -11
  58. package/internal/rejected-cell-editor.js +3 -2
  59. package/internal/repl-memory-projection.js +21 -2
  60. package/internal/runtime-bridge-owner.js +114 -9
  61. package/internal/runtime-config.js +2 -0
  62. package/internal/runtime-contexts.js +14 -5
  63. package/internal/runtime-messages.js +48 -15
  64. package/internal/session-cell-executor.js +132 -11
  65. package/internal/session-journal-recovery.js +1204 -116
  66. package/internal/session-journal-schema.js +10 -5
  67. package/internal/session-journal.js +214 -14
  68. package/internal/session-log-view.js +133 -26
  69. package/internal/session-runtime.js +746 -38
  70. package/internal/session-state.js +27 -0
  71. package/internal/stateful-root-compiler.js +11 -0
  72. package/internal/stateful-root-runtime.js +17 -0
  73. package/internal/user-binding-console.js +7 -0
  74. package/internal/user-bindings-owner.js +112 -6
  75. package/internal/user-bindings.js +2 -2
  76. package/internal/validated-parse-repair.js +6 -1
  77. package/internal/worker-client.js +27 -4
  78. package/package.json +16 -6
  79. package/src/client-activity.js +24 -4
  80. package/src/client-capabilities-view.js +504 -27
  81. package/src/client-copy.js +187 -2
  82. package/src/client-indicator-view.js +2 -1
  83. package/src/client-repl-view.js +35 -11
  84. package/src/client-styles.js +64 -2
  85. package/src/client.js +3 -1
@@ -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
@@ -129,10 +130,10 @@ export const CONFIG_FIELDS = Object.freeze([
129
130
  type: 'enum',
130
131
  options: Object.freeze(['stateful', 'protected']),
131
132
  default: 'stateful',
132
- label: '允许重声明和覆盖',
133
- labelEn: 'Allow redeclarations and overrides',
134
- description: '同一作用域的声明和赋值可更新变量、函数、类及导入。关闭后检测跨 cell 重声明,并保护新建的 const 和 import;静态 import/export 始终可用。',
135
- descriptionEn: 'Declarations and assignments update variables, functions, classes and imports in the same scope. Disabling detects cross-cell redeclarations and protects newly created const and import bindings; static import/export remains available.',
133
+ label: '跨 cell 声明策略',
134
+ labelEn: 'Cross-cell declaration policy',
135
+ description: '同一作用域的声明和赋值可更新变量、函数、类及导入。选“启用名称保护”时改为检测跨 cell 重声明,并保护新建的 const 和 import;静态 import/export 始终可用。',
136
+ descriptionEn: 'Declarations and assignments update variables, functions, classes and imports in the same scope. With “Protect names” selected, cross-cell redeclarations are detected instead and newly created const and import bindings are protected; static import/export remains available.',
136
137
  },
137
138
  {
138
139
  key: 'canonicalizeToolCalls',
@@ -215,6 +216,68 @@ 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
+ },
218
281
  {
219
282
  key: 'officialReplPriority',
220
283
  type: 'boolean',
@@ -224,6 +287,15 @@ export const CONFIG_FIELDS = Object.freeze([
224
287
  description: '检测到官方持久 REPL 模式时由官方运行时接管 PTC 会话执行;PTC Plus 让出 run_code 及其进程内会话状态语义,关闭时始终由 PTC Plus 接管。',
225
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.',
226
289
  },
290
+ {
291
+ key: 'editRewritesHistory',
292
+ type: 'boolean',
293
+ default: false,
294
+ label: '编辑时替换原单元格',
295
+ labelEn: 'Replace the edited cell in place',
296
+ description: '默认关闭,一般也不需要打开:编辑只会追加一次执行,原单元格和它创建的绑定都还在。开启后,编辑会替换原单元格,之后的重建按新版本进行,新版本里不再声明的名字会被点名"已被替换",而不是悄悄消失。只有当你希望环境表现得"这一格一直就是新版本"时才需要打开。',
297
+ descriptionEn: 'Off by default, and usually unnecessary: an edit appends one more execution, leaving the original cell and the bindings it created in place. When on, the edit replaces the original cell, later rebuilds follow the new version, and a name the new version no longer declares is reported as replaced rather than silently disappearing. Turn it on only when the environment should behave as if that cell had always been the new version.',
298
+ },
227
299
  {
228
300
  key: 'tipsEnabled',
229
301
  type: 'boolean',
@@ -288,6 +360,50 @@ export const CONFIG_FIELDS = Object.freeze([
288
360
  description: '限制单个 cell 的日志和返回结果的合计大小。',
289
361
  descriptionEn: 'Limits the combined size of one cell\'s logs and returned result.',
290
362
  },
363
+ {
364
+ key: 'sampleBoundMs',
365
+ type: 'integer',
366
+ default: 1_000,
367
+ min: 1,
368
+ max: MAX_TIMER_DELAY_MS,
369
+ label: '计算基线采样等待上限 (ms)',
370
+ labelEn: 'Compute-baseline sample wait limit (ms)',
371
+ description: '等待 worker 回答计算基线采样的上限;宿主忙碌时先读空 helper 通道再判断缺失,未观察到的采样按未测量 no-op 结算,是否保留 worker 由 unmeasuredCellLimit 决定;transport 报告退出、断连或无法发送时仍走 worker-exit 重置。',
372
+ 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.',
373
+ },
374
+ {
375
+ key: 'workerReclaimMs',
376
+ type: 'integer',
377
+ default: 5_000,
378
+ min: 1_000,
379
+ max: MAX_TIMER_DELAY_MS,
380
+ label: 'worker 回收确认上限 (ms)',
381
+ labelEn: 'Worker reclamation confirmation limit (ms)',
382
+ description: '停止 helper 的关闭阶段上限(至少 1000ms);exit 确认与其后的输出关闭各自以此为上界,release grace 会被压缩以给 kill 预留至少 1000ms 的确认窗口。worker 创建与关闭时读取当前配置。cell 触发的 session kernel exit 超时会保留 helper 并在会话释放前阻止后续 cell;startup、idle 或输出关闭路径的失败,以及 binding console 与 candidate helper,只保留实例并在释放时报告。',
383
+ 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.',
384
+ },
385
+ {
386
+ key: 'startupProbeMs',
387
+ type: 'integer',
388
+ default: 5_000,
389
+ min: 1,
390
+ max: MAX_TIMER_DELAY_MS,
391
+ label: 'REPL 启动探针上限 (ms)',
392
+ labelEn: 'REPL startup probe limit (ms)',
393
+ description: 'worker 启动时校验 REPL 契约的等待上限;超时按启动失败结算,不执行任何 cell。worker 创建时读取当前配置。',
394
+ 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.',
395
+ },
396
+ {
397
+ key: 'executionSeamTimeoutMs',
398
+ type: 'integer',
399
+ default: 30_000,
400
+ min: 1,
401
+ max: MAX_TIMER_DELAY_MS,
402
+ label: '等待宿主执行入口上限 (ms)',
403
+ labelEn: 'Host execution-seam wait limit (ms)',
404
+ description: '激活时等待宿主注册执行入口的上限;超时把当前部署报告为不支持。只在插件激活或重载时读取,运行中的修改在下次激活生效。',
405
+ 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.',
406
+ },
291
407
  {
292
408
  key: 'maxValueNodes',
293
409
  type: 'integer',
@@ -376,7 +492,7 @@ export const CONFIG_GROUPS = Object.freeze([
376
492
  key: 'recovery',
377
493
  label: '状态与恢复',
378
494
  labelEn: 'State and recovery',
379
- fields: Object.freeze(['durableReplay', 'officialReplPriority', 'tipsEnabled', 'tipCooldownMessages', 'tipEscalationFailures']),
495
+ fields: Object.freeze(['durableReplay', 'editRewritesHistory', 'unmeasuredCellLimit', 'officialReplPriority', 'tipsEnabled', 'tipCooldownMessages', 'tipEscalationFailures']),
380
496
  },
381
497
  {
382
498
  key: 'extensions',
@@ -406,6 +522,10 @@ export const CONFIG_GROUPS = Object.freeze([
406
522
  'maxOldGenerationSizeMb',
407
523
  'maxNestedRunCodeDepth',
408
524
  'maxOutputBytes',
525
+ 'sampleBoundMs',
526
+ 'workerReclaimMs',
527
+ 'startupProbeMs',
528
+ 'executionSeamTimeoutMs',
409
529
  'maxValueNodes',
410
530
  'maxValueEdges',
411
531
  'maxValueArrayLength',
@@ -414,7 +534,7 @@ export const CONFIG_GROUPS = Object.freeze([
414
534
  },
415
535
  { key: 'capabilities', label: '能力与上下文', labelEn: 'Capabilities and context',
416
536
  fields: Object.freeze(['capabilityContextEnabled', 'capabilityManagementEnabled', 'capabilityBaseline', 'capabilityOverrides',
417
- 'capabilityStatistics', 'capabilityPolicy', 'capabilityRecoveryEnabled', 'modelPreviewEnabled', 'capabilityProfiles', 'capabilityBudgetBytes', 'capabilityDirectoryBytes', 'capabilityRecoveryBytes', 'modelPreviewTokens']) },
537
+ 'capabilityStatistics', 'capabilityPolicy', 'capabilityRecoveryEnabled', 'modelPreviewEnabled', 'capabilityProfiles', 'capabilityBudgetBytes', 'capabilityDirectoryBytes', 'capabilityRecoveryBytes', 'modelPreviewTokens', 'retentionPayloadRuns', 'retentionPayloadBytes', 'retentionClusters', 'capabilityPromptTextEnabled', 'promptText']) },
418
538
  ])
419
539
 
420
540
  /** Functional modules share the same section order across every settings entry. */
@@ -5,6 +5,7 @@ import { createRequire } from 'node:module'
5
5
  import { dirname, isAbsolute, join } from 'node:path'
6
6
  import { isDeepStrictEqual } from 'node:util'
7
7
  import { RUN_CODE } from './runtime-bridge-owner.js'
8
+ import { sessionExecutionAvailable } from './host-tool-sdk.js'
8
9
 
9
10
  const CORDIS_PRESET_ID = 'cordis'
10
11
  const CORDIS_SKILL_NAME = 'cordis-plugin-development'
@@ -111,7 +112,7 @@ function requireFiberServices(agent, fiber, owner) {
111
112
  function absentCompanionSurfaces(agent) {
112
113
  if (typeof agent?.ctx?.get !== 'function') return undefined
113
114
  const missing = []
114
- if (agent.ctx.tools?.get?.(RUN_CODE, agent) === undefined) missing.push(`the ${RUN_CODE} tool`)
115
+ if (!sessionExecutionAvailable(agent.ctx.tools, agent, RUN_CODE)) missing.push(`the ${RUN_CODE} tool`)
115
116
  if (agent.ctx.tools?.get?.(COMPANION_SKILL_TOOL, agent) === undefined) {
116
117
  missing.push(`the ${COMPANION_SKILL_TOOL} tool`)
117
118
  }
@@ -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
+ export 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
  }
@@ -111,9 +109,14 @@ export function createCordisRecoveryPolicy(initiallyEnabled) {
111
109
  })
112
110
  }
113
111
 
114
- function presentationState(assembly) {
115
- const tools = Array.isArray(assembly?.tools) ? assembly.tools : []
116
- if (!tools.some(tool => tool?.name === RUN_CODE)) {
112
+ function presentationState(assembly, declaresRunCode, agent) {
113
+ const tools = Array.isArray(assembly?.tools) ? assembly.tools : undefined
114
+ // A host that declares tools after the presentation waterfall carries no
115
+ // array here; the transport it publishes is then the same fact.
116
+ const present = tools === undefined
117
+ ? declaresRunCode?.(agent) === true
118
+ : tools.some(tool => tool?.name === RUN_CODE)
119
+ if (!present) {
117
120
  return { presentation: 'native', ownerProven: false, collapseSectionName: undefined }
118
121
  }
119
122
  const collapse = PTC_COLLAPSE_SECTION_NAMES
@@ -455,6 +458,7 @@ export function createDirectSurfaceOwner({
455
458
  canonicalizeToolCalls,
456
459
  sessionId,
457
460
  toolSchemasForAgent,
461
+ declaresRunCode,
458
462
  userBindingsForAgent = async () => undefined,
459
463
  setAgentPresentation = async () => {},
460
464
  sessionLogView,
@@ -472,11 +476,13 @@ export function createDirectSurfaceOwner({
472
476
  let disposed = false
473
477
  let currentCanonicalizeToolCalls = canonicalizeToolCalls
474
478
  let currentAutoDescribeRunCode = runtimeConfig.autoDescribeRunCode
479
+ let currentPromptText = resolvePromptTextConfig(runtimeConfig)
475
480
  let currentLanguageSemantics = executionPolicies(runtimeConfig).languageSemantics
476
481
  const tipConfig = {
477
482
  enabled: runtimeConfig.tipsEnabled,
478
483
  cooldownMessages: runtimeConfig.tipCooldownMessages,
479
484
  escalationFailures: runtimeConfig.tipEscalationFailures,
485
+ get prompt() { return currentPromptText },
480
486
  }
481
487
 
482
488
  const sessionOwner = (id) => {
@@ -561,6 +567,7 @@ export function createDirectSurfaceOwner({
561
567
  currentLanguageSemantics = executionPolicies(nextConfig).languageSemantics
562
568
  currentCanonicalizeToolCalls = nextConfig.canonicalizeToolCalls
563
569
  currentAutoDescribeRunCode = nextConfig.autoDescribeRunCode
570
+ currentPromptText = resolvePromptTextConfig(nextConfig)
564
571
  cordisRecovery.reconfigure(nextConfig.cordisToolsEnabled)
565
572
  tipConfig.enabled = nextConfig.tipsEnabled
566
573
  tipConfig.cooldownMessages = nextConfig.tipCooldownMessages
@@ -577,7 +584,7 @@ export function createDirectSurfaceOwner({
577
584
  const requestOwner = id === undefined ? undefined : sessionOwner(id)
578
585
  const isCurrent = () => !disposed && (requestOwner === undefined
579
586
  || (requestOwner.active && sessions.get(id) === requestOwner))
580
- const initialState = presentationState(initialAssembly)
587
+ const initialState = presentationState(initialAssembly, declaresRunCode, agent)
581
588
  let composition = compositions.get(agent)
582
589
  if (composition === undefined && id !== undefined
583
590
  && (initialState.ownerProven || initialState.presentation === 'ptc')) {
@@ -589,11 +596,17 @@ export function createDirectSurfaceOwner({
589
596
 
590
597
  const assembly = await next()
591
598
  if (!isCurrent()) return assembly
599
+ // A host may declare the model-facing tool array after this waterfall, in
600
+ // which case the assembly carries none. The declaration surface is then
601
+ // host-owned: the plugin still owns presentation, sections, contexts and
602
+ // request policy, but it must not claim to have replaced declarations the
603
+ // host has not asked it for. A present but non-array declaration is still
604
+ // a malformed assembly.
592
605
  const tools = assembly.tools
593
- if (!Array.isArray(tools)) {
606
+ if (tools !== undefined && !Array.isArray(tools)) {
594
607
  throw new Error('ptc-plus: incompatible prompt assembly; expected a tools array')
595
608
  }
596
- const completedState = presentationState(assembly)
609
+ const completedState = presentationState(assembly, declaresRunCode, agent)
597
610
  if (composition === undefined && id !== undefined) {
598
611
  const state = initialState.ownerProven ? initialState : completedState
599
612
  composition = captureComposition(agent, id, state.presentation)
@@ -606,7 +619,7 @@ export function createDirectSurfaceOwner({
606
619
  const requestSignal = context?.signal !== null && typeof context?.signal === 'object'
607
620
  ? context.signal
608
621
  : undefined
609
- if (!tools.some(tool => tool?.name === RUN_CODE)) {
622
+ if (completedState.presentation === 'native') {
610
623
  if (presentation !== 'native') {
611
624
  throw new Error(`ptc-plus: ${presentation} agent composition assembled without run_code`)
612
625
  }
@@ -627,11 +640,14 @@ export function createDirectSurfaceOwner({
627
640
  if (sessionPtc) await capabilityEnvironment?.prepare(agent, assembly.sections)
628
641
  const userBindings = sessionPtc ? await userBindingsForAgent(agent) : undefined
629
642
  if (!isCurrent()) return assembly
630
- const runCode = tools.find(tool => tool?.name === RUN_CODE)
631
- let directTools = sessionPtc ? capabilityEnvironment?.visibleSchemas(agent, tools) ?? tools : tools
643
+ const runCode = tools?.find(tool => tool?.name === RUN_CODE)
644
+ const promptText = currentPromptText
645
+ const hostDeclaresTools = tools !== undefined
646
+ let directTools = tools === undefined ? undefined
647
+ : sessionPtc ? capabilityEnvironment?.visibleSchemas(agent, tools) ?? tools : tools
632
648
  if (sessionPtcProjection) {
633
649
  editTransport.ensureInstalled(agent)
634
- directTools = [adaptRunCodeSchema(runCode)]
650
+ if (hostDeclaresTools) directTools = [adaptRunCodeSchema(runCode, promptText)]
635
651
  }
636
652
  const runtimeContexts = sessionPtc
637
653
  ? sessionRuntimeContexts(agent, tipConfig, {
@@ -641,7 +657,9 @@ export function createDirectSurfaceOwner({
641
657
  },
642
658
  })
643
659
  : { contexts: [] }
644
- const bindingDefaults = userBindings === undefined ? undefined : userBindingsConfiguredContext(userBindings)
660
+ const bindingDefaults = userBindings === undefined ? undefined : userBindingsConfiguredContext(userBindings, {
661
+ entryLabel: promptAssetText(promptText, PROMPT_SLOT_IDS.userBindingsEntryLabel),
662
+ })
645
663
  const contexts = [
646
664
  ...runtimeContexts.contexts,
647
665
  ...(bindingDefaults === undefined ? [] : [bindingDefaults]),
@@ -681,11 +699,11 @@ export function createDirectSurfaceOwner({
681
699
 
682
700
  return {
683
701
  ...assembly,
684
- tools: sessionPtcProjection
702
+ ...(directTools === undefined ? {} : { tools: sessionPtcProjection
685
703
  ? directTools
686
704
  : directTools.map(tool => tool?.name === RUN_CODE
687
- ? adaptRunCodeSchema(tool)
688
- : tool),
705
+ ? adaptRunCodeSchema(tool, promptText)
706
+ : tool) }),
689
707
  sections,
690
708
  }
691
709
  },
@@ -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
+ withSettlementProof,
14
16
  } from './session-journal.js'
15
17
  import {
16
18
  EXPECTED_TARGET_CALL_SEQ,
@@ -18,10 +20,16 @@ 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'
24
31
  import { RUN_CODE } from './runtime-bridge-owner.js'
32
+ import { dispatchToolCall } from './host-tool-sdk.js'
25
33
  import { CAPABILITY_EVIDENCE_KEY } from './capability-environment.js'
26
34
  import { isRecord } from './record-utils.js'
27
35
  import {
@@ -72,8 +80,35 @@ function nestedEditStart(sessionLog, exec) {
72
80
  || event.data.rootCallId !== exec.rootCallId
73
81
  || event.data.parentCallId !== exec.rootCallId
74
82
  || !deepEqualJson(event.data.arguments, exec.arguments)) return undefined
75
- const target = projectSessionLog({ session: { events: events.slice(0, index + 1) } }).editableRun
76
- return { startSeq: event.seq, rootCallSeq, target }
83
+ const slice = { session: { events: events.slice(0, index + 1) } }
84
+ const view = projectSessionLog(slice)
85
+ let target = view.editableRun
86
+ // A dropped cluster is not gone for good: the log still holds the events, so the
87
+ // edit path refolds that slice with pruning disabled and reads the cell from
88
+ // there. Only when the log itself cannot supply it does the edit report that the
89
+ // retention window cut it.
90
+ if (target === undefined && view.retentionSummary !== undefined) {
91
+ const refolded = refoldEditableSettlement(events.slice(0, index + 1), view.retentionSummary)
92
+ target = refolded.run
93
+ // The summary is the checkpoint's only record of a dropped cluster; the log is
94
+ // the authority. When the two disagree the edit must not proceed on either.
95
+ //
96
+ // This call site cannot produce that disagreement: the summary and the refold
97
+ // come from the same fold of the same event slice, and the run the refold reads
98
+ // is the one the pruned fold pins, so `refoldEditableSettlement` never finds a
99
+ // summary record for it. The comparison stays for the exported contract, which
100
+ // covers both outcomes in test/projection-retention.test.js.
101
+ /* c8 ignore start -- same-fold determinism: the call site cannot disagree with itself (see above) */
102
+ if (refolded.mismatch) {
103
+ return { startSeq: event.seq, rootCallSeq, target: undefined, retentionSummary: view.retentionSummary,
104
+ summaryMismatch: true }
105
+ }
106
+ /* c8 ignore stop */
107
+ }
108
+ // The summary travels with the target so an unavailable edit can say why it is
109
+ // unavailable: the cell fell outside the retention window instead of never
110
+ // existing.
111
+ return { startSeq: event.seq, rootCallSeq, target, retentionSummary: view.retentionSummary }
77
112
  }
78
113
 
79
114
  function unavailableDerivedResult(inner) {
@@ -118,6 +153,7 @@ export function createEditTransportOwner(ctx, {
118
153
  sessionId,
119
154
  toolSchemasForAgent,
120
155
  readSessionLog,
156
+ promptText,
121
157
  }) {
122
158
  const editClaims = new Map()
123
159
  const pendingAcceptances = new Map()
@@ -137,7 +173,7 @@ export function createEditTransportOwner(ctx, {
137
173
  }
138
174
  }
139
175
 
140
- const definition = editRunCodeSchema()
176
+ const definition = editRunCodeSchema(promptAssetText(promptText, PROMPT_SLOT_IDS.editRunCodeDescription))
141
177
  const derivedMetadata = (derived) => {
142
178
  const meta = {
143
179
  [JOURNAL_KEY]: normalizeJournal(derived.journal),
@@ -164,9 +200,12 @@ export function createEditTransportOwner(ctx, {
164
200
  derived.draftCapability,
165
201
  presentationGeneration,
166
202
  )
203
+ // The derived settlement proves its own facts, so a replay reads the same
204
+ // settlement the root path publishes.
205
+ const withWording = withSettlementProof(withDraft, withDraft[JOURNAL_KEY])
167
206
  return derived.replMemory === undefined
168
- ? withDraft
169
- : withReplMemorySnapshot(withDraft, derived.replMemory, presentationGeneration)
207
+ ? withWording
208
+ : withReplMemorySnapshot(withWording, derived.replMemory, presentationGeneration)
170
209
  }
171
210
  definition.output = {
172
211
  schema: {
@@ -193,8 +232,17 @@ export function createEditTransportOwner(ctx, {
193
232
  throw new Error('ptc-plus: edit_run_code requires a unique persisted PTC sub-call start')
194
233
  }
195
234
  const target = start.target
235
+ /* c8 ignore start -- reachable only through `nestedEditStart`'s mismatch, which cannot disagree with itself (see there) */
236
+ if (start.summaryMismatch === true) {
237
+ return { accepted: false, scheduled: false,
238
+ reason: `${PRUNED_SUMMARY_MISMATCH} / ${PRUNED_SUMMARY_MISMATCH_EN}` }
239
+ }
240
+ /* c8 ignore stop */
196
241
  if (target?.source === undefined || target.callSeq === undefined) {
197
- return { accepted: false, scheduled: false, reason: 'no run_code cell is currently eligible for safe editing' }
242
+ return { accepted: false, scheduled: false,
243
+ reason: start.retentionSummary === undefined
244
+ ? 'no run_code cell is currently eligible for safe editing'
245
+ : `${PRUNED_EDIT} / ${PRUNED_EDIT_EN}` }
198
246
  }
199
247
  const edited = editRejectedCell(args, target.source)
200
248
  if (!edited.edited) {
@@ -263,7 +311,7 @@ export function createEditTransportOwner(ctx, {
263
311
  const executionStart = agent.session.append('tool/ptc-dispatch-start', data)
264
312
  const derivedExec = { callId: data.subCallId, rootCallId: data.rootCallId,
265
313
  parent: rootExec.token, name: RUN_CODE, arguments: argumentsValue, agent, signal: rootSignal }
266
- const dispatch = () => ctx.tools.execute(derivedExec)
314
+ const dispatch = () => dispatchToolCall(ctx.tools, derivedExec)
267
315
  const tentative = await executeTentative(executionStart.seq, start.rootCallSeq,
268
316
  dispatch, executionLog, derivedExec)
269
317
  const inner = tentative.result
@@ -292,7 +340,7 @@ export function createEditTransportOwner(ctx, {
292
340
  const journalValue = isRecord(metadata) && Object.hasOwn(metadata, JOURNAL_KEY)
293
341
  ? metadata[JOURNAL_KEY] : undefined
294
342
  const journal = journalValue === undefined ? undefined : normalizeJournal(journalValue)
295
- if (journal === undefined || journal.status === 'noop'
343
+ if (journal === undefined
296
344
  || inner.isError !== (journal.completion?.kind === 'throw' || journal.status === 'discarded')) {
297
345
  this.finish(false)
298
346
  return { outcome: unavailableDerivedResult(inner), presentation: derivedPresentation(inner) }
@@ -310,16 +358,34 @@ export function createEditTransportOwner(ctx, {
310
358
  code: edited.code,
311
359
  description: edited.description,
312
360
  }
361
+ const metaValue = derivedMetadata(derived)
362
+ let settlement
363
+ try {
364
+ settlement = normalizeDeferredEditSettlement(metaValue, target.callSeq)
365
+ } catch {
366
+ // Metadata this relation cannot prove must not reach the root result.
367
+ this.finish(false)
368
+ return { outcome: unavailableDerivedResult(inner), presentation: derivedPresentation(inner) }
369
+ }
370
+ const provenance = {
371
+ startSeq: start.startSeq,
372
+ executionStartSeq: executionStart.seq,
373
+ rootCallSeq: start.rootCallSeq,
374
+ subCallId: exec.callId,
375
+ meta: metaValue,
376
+ }
377
+ if (settlement.unexecuted === true) {
378
+ // The accepted edit is proved by a settlement that never ran. Publishing
379
+ // the relation keeps the root cell provable, while the outcome and the
380
+ // unexecuted journal keep the revision from executing, from publishing a
381
+ // binding and from becoming the next edit target.
382
+ this.finish(false)
383
+ return { outcome: unavailableDerivedResult(inner), presentation: derivedPresentation(inner), provenance }
384
+ }
313
385
  return {
314
386
  outcome: derivedEditResult(inner),
315
387
  presentation: derivedPresentation(inner),
316
- provenance: {
317
- startSeq: start.startSeq,
318
- executionStartSeq: executionStart.seq,
319
- rootCallSeq: start.rootCallSeq,
320
- subCallId: exec.callId,
321
- meta: derivedMetadata(derived),
322
- },
388
+ provenance,
323
389
  }
324
390
  },
325
391
  }
@@ -1,4 +1,5 @@
1
1
  import { createExecutionSeam, EXECUTION_SEAM_SERVICE, LEGACY_EXECUTION_SEAM_SERVICE } from './execution-seam-compat.js'
2
+ import { providerRetainsState } from './official-repl-priority.js'
2
3
 
3
4
  export const name = 'ptc-plus-execution-provider'
4
5
  export const inject = ['loader']
@@ -70,6 +71,11 @@ export function apply(ctx, config = {}) {
70
71
  get language() { return readOriginal().language },
71
72
  get isolation() { return readOriginal().isolation },
72
73
  get programFormat() { return readOriginal().programFormat ?? 'function-body' },
74
+ // This composition isolates state owners and forwards every run to the
75
+ // original provider, so the original's retention declaration is the
76
+ // truthful answer. A host plugin that reads it must not see a stateful
77
+ // environment reported as stateless.
78
+ get retainsState() { return providerRetainsState(readOriginal()) },
73
79
  get executionInstructions() { return readOriginal().executionInstructions },
74
80
  get sandboxMode() { return readOriginal().sandboxMode },
75
81
  get timeout() { return readOriginal().timeout },
@@ -1,4 +1,6 @@
1
1
  /** Resolved-spec Cordis execution contract. */
2
+ import { providerRetainsState } from './official-repl-priority.js'
3
+
2
4
  export const EXECUTION_SEAM_SERVICE = 'ptcRuntime'
3
5
 
4
6
  /** Request-only Cordis execution contract. */
@@ -63,7 +65,24 @@ export function createExecutionSeam(service, serviceName) {
63
65
  // Source evaluation form belongs to the provider that actually receives
64
66
  // the program. Program-format consumers delegate to the original provider,
65
67
  // so this descriptor always follows the original generation.
68
+ //
69
+ // Retention is different: while an installed adapter runs the program this
70
+ // provider keeps a per-session environment, so the answer describes what
71
+ // this provider does, not what the original would do. The ownership decision
72
+ // asks the original through `seam.originalRetainsState` instead.
66
73
  get programFormat() { requireCompatible(); return service.programFormat ?? 'function-body' },
74
+ // Retention belongs to the provider that actually receives the program, so
75
+ // this descriptor follows the original generation exactly like the source
76
+ // form. A host plugin that reads it must not see a stateful environment
77
+ // reported as stateless.
78
+ get retainsState() {
79
+ requireCompatible()
80
+ // While the installed adapter runs the program, this provider retains a
81
+ // per-session environment, whatever the original would do. Reporting the
82
+ // original's answer here would describe a stateful environment as
83
+ // stateless to every host plugin that asks.
84
+ return installed === undefined ? providerRetainsState(service) : true
85
+ },
67
86
  get executionInstructions() { requireCompatible(); return installed === undefined ? service.executionInstructions ?? '' : '' },
68
87
  get sandboxMode() { requireCompatible(); return installed === undefined ? service.sandboxMode : undefined },
69
88
  get timeout() { requireCompatible(); return installed === undefined ? service.timeout : undefined },
@@ -93,6 +112,12 @@ export function createExecutionSeam(service, serviceName) {
93
112
  })
94
113
  const seam = Object.freeze({
95
114
  serviceName,
115
+ /**
116
+ * Whether the original provider retains state per owner. Execution ownership
117
+ * is decided against the original — the provider that would receive the
118
+ * program if this attachment stood down — not against the composition.
119
+ */
120
+ get originalRetainsState() { requireAvailable(); return providerRetainsState(service) },
96
121
  get language() { requireAvailable(); return service.language },
97
122
  provider,
98
123
  get active() { return installed !== undefined && service.language === installed.language },