dsh-ptc-plus 0.3.1 → 0.4.1

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 (200) hide show
  1. package/README.en.md +66 -73
  2. package/README.md +66 -73
  3. package/assets/ptc-plus-bindings-en.png +0 -0
  4. package/assets/ptc-plus-bindings-zh.png +0 -0
  5. package/assets/ptc-plus-repair-en.png +0 -0
  6. package/assets/ptc-plus-repair-zh.png +0 -0
  7. package/assets/ptc-plus-repl-workspace-en.png +0 -0
  8. package/assets/ptc-plus-repl-workspace-zh.png +0 -0
  9. package/assets/ptc-plus-settings-en.png +0 -0
  10. package/assets/ptc-plus-settings-zh.png +0 -0
  11. package/client.js +30610 -722
  12. package/compiler-amaro.cjs +5 -0
  13. package/compiler-amaro.cjs.map +6 -0
  14. package/compiler-asset-124f80c1bba92ff5fe79cfec6e8ad518f476de4a42514cae3919538887f510e8.txt +1 -0
  15. package/compiler-assets.json +3 -0
  16. package/compiler-core.cjs +554 -0
  17. package/compiler-core.cjs.map +6 -0
  18. package/compiler-platform.cjs +2 -0
  19. package/compiler-platform.cjs.map +6 -0
  20. package/compiler-typescript.cjs +388 -0
  21. package/compiler-typescript.cjs.map +6 -0
  22. package/docs/README.md +6 -0
  23. package/docs/adr/0004-declare-program-bindings-at-the-host.md +1 -1
  24. package/docs/adr/0006-rewrite-module-syntax-with-an-ast.md +6 -4
  25. package/docs/adr/0007-separate-worker-transport-from-session-semantics.md +15 -3
  26. package/docs/adr/0009-bound-expensive-acceptance-default.md +2 -0
  27. package/docs/adr/0010-session-log-derived-recovery-tips.md +59 -5
  28. package/docs/adr/0011-isolate-the-local-recovery-tip-provider.md +1 -1
  29. package/docs/adr/0012-preserve-mixed-destructuring-initialization.md +3 -1
  30. package/docs/adr/0013-isolate-repl-redeclaration-convenience.md +9 -3
  31. package/docs/adr/0014-persist-cell-rewrite-policy.md +11 -5
  32. package/docs/adr/0015-preserve-source-positions-through-rewrites.md +1 -1
  33. package/docs/adr/0016-archive-resolved-review-findings.md +1 -1
  34. package/docs/adr/0017-track-the-latest-dsh-public-surface.md +49 -0
  35. package/docs/adr/0018-use-versioned-fixture-for-ab-benchmark.md +3 -3
  36. package/docs/adr/0019-plugin-settings-and-kill-switch.md +8 -6
  37. package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +2 -2
  38. package/docs/adr/0021-separate-host-bootstrap-from-session-runtime.md +17 -3
  39. package/docs/adr/0023-global-user-bindings.md +104 -0
  40. package/docs/adr/0024-repl-console-observation.md +33 -0
  41. package/docs/adr/0025-use-versioned-logical-binding-identities.md +119 -0
  42. package/docs/adr/0026-persist-the-delivery-verdict-against-the-verified-candidate.md +21 -0
  43. package/docs/adr/0027-run-the-session-kernel-in-a-killable-helper-process.md +37 -0
  44. package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +103 -0
  45. package/docs/adr/0029-keep-plugin-unavailability-out-of-the-host-agent-lifecycle.md +27 -0
  46. package/docs/adr/0030-serve-both-typert-codec-generations-from-one-codec.md +27 -0
  47. package/docs/architecture.md +82 -22
  48. package/docs/binding-review-placement-design.md +200 -0
  49. package/docs/capability-projection.md +1 -1
  50. package/docs/client-ui.md +96 -20
  51. package/docs/durability-design.md +61 -24
  52. package/docs/evaluation.md +78 -6
  53. package/docs/installation.md +41 -6
  54. package/docs/program-data-plane.md +6 -0
  55. package/docs/publishing.md +5 -3
  56. package/docs/runtime-reference.md +79 -25
  57. package/docs/semantic-validation.md +29 -0
  58. package/docs/stateful-binding-updates-proposal.md +755 -0
  59. package/docs/user-bindings.en.md +64 -0
  60. package/docs/user-bindings.md +64 -0
  61. package/docs/value-wire.md +3 -3
  62. package/docs/verification.md +57 -0
  63. package/index.js +374 -92
  64. package/internal/ast-traversal.js +5 -2
  65. package/internal/binding-descriptors.js +1 -0
  66. package/internal/binding-pattern-traversal.js +25 -0
  67. package/internal/binding-pattern.js +26 -29
  68. package/internal/binding-source-analysis.js +391 -0
  69. package/internal/binding-update-policy.js +66 -0
  70. package/internal/bounded-worker-round.js +56 -0
  71. package/internal/callable-definition-environment.js +42 -0
  72. package/internal/callable-source-catalog.js +129 -0
  73. package/internal/callable-source-encoding.js +47 -0
  74. package/internal/callable-source-facts.js +459 -0
  75. package/internal/callable-source-format.js +2 -0
  76. package/internal/callable-source-lowering.js +123 -0
  77. package/internal/cell-analysis-contract.js +25 -0
  78. package/internal/cell-analysis.js +252 -242
  79. package/internal/cell-completion.js +15 -0
  80. package/internal/cell-error.js +7 -0
  81. package/internal/cell-parser.js +66 -0
  82. package/internal/cell-rewriter.js +3 -553
  83. package/internal/commonjs-export-evidence.js +63 -0
  84. package/internal/commonjs-source-evidence.js +72 -0
  85. package/internal/compiler-amaro.cjs +2 -0
  86. package/internal/compiler-compression.js +12 -0
  87. package/internal/compiler-data.js +58 -0
  88. package/internal/compiler-descriptors.js +54 -0
  89. package/internal/compiler-dynamic-cache.js +48 -0
  90. package/internal/compiler-entry.js +35 -0
  91. package/internal/compiler-generated-names.js +17 -0
  92. package/internal/compiler-intrinsics.js +84 -0
  93. package/internal/compiler-module-links.js +34 -0
  94. package/internal/compiler-operations.js +61 -0
  95. package/internal/compiler-platform-factory.js +84 -0
  96. package/internal/compiler-platform.js +136 -0
  97. package/internal/compiler-record-roles.js +53 -0
  98. package/internal/compiler-region-output.js +259 -0
  99. package/internal/compiler-resource-lowering.js +103 -0
  100. package/internal/compiler-scope-facts.js +115 -0
  101. package/internal/compiler-service.js +109 -0
  102. package/internal/compiler-source-cache.js +32 -0
  103. package/internal/compiler-source-pieces.js +93 -0
  104. package/internal/compiler-source-regions.js +132 -0
  105. package/internal/compiler-storage-source.js +54 -0
  106. package/internal/compiler-text.js +15 -0
  107. package/internal/compiler-typescript.cjs +2 -0
  108. package/internal/config-spec.js +91 -25
  109. package/internal/cordis-tools-owner.js +130 -21
  110. package/internal/diagnostic.js +27 -1
  111. package/internal/direct-surface-owner.js +76 -23
  112. package/internal/dynamic-binding-evidence.js +16 -0
  113. package/internal/dynamic-environment-compiler.js +779 -0
  114. package/internal/dynamic-environment-integration.js +87 -0
  115. package/internal/dynamic-environment-runtime.js +362 -0
  116. package/internal/dynamic-native-calls.js +150 -0
  117. package/internal/dynamic-native-runtime.js +248 -0
  118. package/internal/dynamic-scope-analysis.js +90 -0
  119. package/internal/edit-transport-owner.js +28 -3
  120. package/internal/execution-seam-compat.js +193 -0
  121. package/internal/failure-reporting.js +118 -18
  122. package/internal/host-rpc.js +66 -0
  123. package/internal/isolated-worker.js +548 -0
  124. package/internal/kernel-child.js +139 -0
  125. package/internal/kernel-worker.js +911 -116
  126. package/internal/language-semantics.js +14 -0
  127. package/internal/legacy-binding-catalog.js +48 -0
  128. package/internal/legacy-cell-analysis.js +600 -0
  129. package/internal/legacy-cell-parser.js +50 -0
  130. package/internal/legacy-cell-rewriter.js +631 -0
  131. package/internal/legacy-repl-convenience.js +291 -0
  132. package/internal/managed-module-operations.js +170 -0
  133. package/internal/module-compilation.js +321 -0
  134. package/internal/module-runtime-platform.js +19 -0
  135. package/internal/module-transform-contract.js +12 -0
  136. package/internal/native-await-declarations.js +68 -0
  137. package/internal/native-root-compilation.js +54 -0
  138. package/internal/native-root-dynamic.js +67 -0
  139. package/internal/recovery-tips.js +27 -15
  140. package/internal/rejected-cell-editor.js +13 -8
  141. package/internal/repl-convenience.js +2 -201
  142. package/internal/repl-memory-projection.js +81 -12
  143. package/internal/repl-observation-interest.js +70 -0
  144. package/internal/repl-rewrite-contract.js +21 -0
  145. package/internal/repl-scope-normalizer.js +2091 -0
  146. package/internal/repl-value-observer.js +114 -0
  147. package/internal/rpc-contract.js +52 -0
  148. package/internal/runtime-bridge-owner.js +172 -28
  149. package/internal/runtime-config.js +48 -51
  150. package/internal/runtime-contexts.js +82 -10
  151. package/internal/runtime-messages.js +111 -0
  152. package/internal/session-cell-executor.js +455 -91
  153. package/internal/session-journal-recovery.js +684 -0
  154. package/internal/session-journal-schema.js +106 -0
  155. package/internal/session-journal.js +237 -440
  156. package/internal/session-log-view.js +42 -9
  157. package/internal/session-runtime.js +325 -80
  158. package/internal/session-state.js +249 -40
  159. package/internal/source-position-map.js +211 -71
  160. package/internal/stateful-module-compiler.js +154 -0
  161. package/internal/stateful-module-runtime.js +303 -0
  162. package/internal/stateful-root-compiler.js +598 -0
  163. package/internal/stateful-root-runtime.js +340 -0
  164. package/internal/typert-codec-compat.js +28 -0
  165. package/internal/typescript-transform.js +29 -0
  166. package/internal/user-binding-authoring.js +70 -0
  167. package/internal/user-binding-console-worker.js +184 -0
  168. package/internal/user-binding-console.js +148 -0
  169. package/internal/user-binding-draft-projection.js +335 -0
  170. package/internal/user-binding-model-context.js +31 -0
  171. package/internal/user-binding-runner.js +56 -0
  172. package/internal/user-bindings-owner.js +1065 -0
  173. package/internal/user-bindings-store.js +293 -0
  174. package/internal/user-bindings.js +353 -0
  175. package/internal/value-wire-schema.js +28 -0
  176. package/internal/value-wire.js +29 -20
  177. package/internal/worker-client.js +57 -67
  178. package/internal/worker-environment.js +45 -0
  179. package/internal/worker-output-relay.js +29 -0
  180. package/internal/worker-shutdown.js +14 -0
  181. package/package.json +81 -11
  182. package/src/client-activity.js +109 -45
  183. package/src/client-authoring-view.js +580 -0
  184. package/src/client-binding-review.js +230 -0
  185. package/src/client-bindings-data.js +29 -0
  186. package/src/client-catalog.js +127 -0
  187. package/src/client-code-editor.js +84 -0
  188. package/src/client-console.js +112 -0
  189. package/src/client-copy.js +370 -0
  190. package/src/client-feature-gates.js +62 -0
  191. package/src/client-host-compat.js +150 -0
  192. package/src/client-indicator-view.js +267 -0
  193. package/src/client-repl-view.js +292 -0
  194. package/src/client-rpc.js +20 -0
  195. package/src/client-settings-view.js +221 -0
  196. package/src/client-styles.js +106 -0
  197. package/src/client-tool-view.js +108 -0
  198. package/src/client-transport.js +159 -0
  199. package/src/client-workbench.js +521 -0
  200. package/src/client.js +233 -615
package/index.js CHANGED
@@ -1,14 +1,16 @@
1
1
  /**
2
2
  * Session-bound REPL for DeepSeek Harness PTC mode.
3
3
  *
4
- * DSH's run_code bridge does not pass session identity to CodeRuntime.run().
5
- * The tools/execute around-hook carries that identity into the runtime bridge,
6
- * which redirects only those runs to a persistent per-session kernel.
4
+ * DSH's run_code bridge does not pass session identity to the execution seam it
5
+ * calls. The tools/execute around-hook carries that identity into the runtime
6
+ * bridge, which redirects only those runs to a persistent per-session kernel.
7
7
  */
8
8
 
9
9
  import Schema from '@deepseek-ai/schemastery'
10
10
  import { randomUUID } from 'node:crypto'
11
11
  import { createDirectSurfaceOwner } from './internal/direct-surface-owner.js'
12
+ import { createRuntimeMessageOwner } from './internal/runtime-contexts.js'
13
+ import { PTC_DELIVERY_CONTEXT } from './internal/runtime-messages.js'
12
14
  import { createCordisToolsOwner } from './internal/cordis-tools-owner.js'
13
15
  import { createEditTransportOwner, EDIT_RUN_CODE } from './internal/edit-transport-owner.js'
14
16
  import { createRuntimeBridgeOwner, RUN_CODE } from './internal/runtime-bridge-owner.js'
@@ -18,7 +20,14 @@ import {
18
20
  SETTINGS_NAMESPACE,
19
21
  } from './internal/config-spec.js'
20
22
  import { installSettingsSectionCompat } from './internal/settings-compat.js'
23
+ import { installExecutionSeam } from './internal/execution-seam-compat.js'
24
+ import { valueLimitsFromConfig } from './internal/value-wire-schema.js'
21
25
  import { createReplMemoryProjection } from './internal/repl-memory-projection.js'
26
+ import { createUserBindingDraftProjection } from './internal/user-binding-draft-projection.js'
27
+ import { createUserBindingsOwner } from './internal/user-bindings-owner.js'
28
+ import { createHostRpc } from './internal/host-rpc.js'
29
+ import { createReplObservationInterest } from './internal/repl-observation-interest.js'
30
+ import { guidancePolicies } from './internal/binding-update-policy.js'
22
31
  import * as dshSettings from '@deepseek-ai/dsh-settings'
23
32
 
24
33
  const INSTALL_CLEANUP = Symbol('ptc-plus install cleanup')
@@ -30,7 +39,10 @@ export const name = 'ptc-plus'
30
39
  function configSchemaField(field) {
31
40
  const base = field.type === 'boolean'
32
41
  ? Schema.boolean().default(field.default)
33
- : Schema.number().step(1).min(field.min).max(field.max).default(field.default)
42
+ : field.type === 'enum'
43
+ // Keep omission visible until resolveConfig migrates legacy policies.
44
+ ? Schema.union(field.options.map(option => Schema.const(option)))
45
+ : Schema.number().step(1).min(field.min).max(field.max).default(field.default)
34
46
  return base.description(field.description)
35
47
  }
36
48
 
@@ -38,20 +50,26 @@ export const Config = Schema.object(Object.fromEntries(
38
50
  CONFIG_FIELDS.map(field => [field.key, configSchemaField(field)]),
39
51
  ))
40
52
 
41
- /** Services required by the plugin. */
42
- export const inject = ['tools', 'codeRuntime', 'systemPrompt', 'agents', 'llm']
53
+ /**
54
+ * Core services required by the plugin. Optional authoring services are
55
+ * injected on demand. The execution seam is not listed here: the host registers
56
+ * it under the service name of one generation, so `installExecutionSeam`
57
+ * selects the live one.
58
+ */
59
+ export const inject = ['tools', 'systemPrompt', 'agents', 'llm']
43
60
 
44
- function replGuidance(
45
- looseTopLevelRedeclarations,
46
- durableReplay,
47
- autoRewriteImports,
48
- autoStripExports,
49
- autoSplitRedeclarations,
50
- cordisToolsEnabled,
51
- ) {
52
- const redeclaration = looseTopLevelRedeclarations
61
+ function replGuidance({ bindingPolicy, rewritesEnabled, languageSemantics, durableReplay, cordisToolsEnabled }) {
62
+ const looseTopLevelRedeclarations = bindingPolicy.variableRedeclarations
63
+ const looseTopLevelFunctionClassRedeclarations = bindingPolicy.functionClassRedeclarations
64
+ const autoRewriteImports = rewritesEnabled.autoRewriteImports
65
+ const autoStripExports = rewritesEnabled.autoStripExports
66
+ const autoSplitRedeclarations = rewritesEnabled.autoSplitRedeclarations
67
+ const variableRedeclaration = looseTopLevelRedeclarations
53
68
  ? 'Repeated top-level `const`/`let` declarations replace existing bindings.'
54
- : 'Redeclaring an existing top-level name fails before execution, so reuse it or place one-off declarations inside a block.'
69
+ : 'Repeated top-level variable declarations fail before execution, so reuse existing bindings or place one-off declarations inside a block.'
70
+ const functionClassRedeclaration = looseTopLevelFunctionClassRedeclarations
71
+ ? 'Repeated top-level named `function`/`class` declarations replace existing writable bindings at their declaration position. Do not rely on function hoisting or class TDZ; define a replacement before using that name in the cell.'
72
+ : 'Repeated top-level `function`/`class` declarations remain unsupported; assign a function or class expression to an existing writable binding, or place one-off declarations inside a block.'
55
73
  const moduleSyntax = autoRewriteImports && autoStripExports
56
74
  ? 'static `import` declarations are adapted with live, read-only bindings and top-level `export` modifiers are stripped automatically.'
57
75
  : autoRewriteImports
@@ -62,72 +80,133 @@ function replGuidance(
62
80
  const splitSyntax = autoSplitRedeclarations
63
81
  ? 'Mixed new/existing top-level destructuring is split automatically while preserving assignment semantics.'
64
82
  : 'Mixed new/existing top-level destructuring remains unsupported; separate the declaration from the assignment.'
83
+ const cellConventions = languageSemantics === 'legacy-v1'
84
+ ? `Cells are async function bodies; ${moduleSyntax} Use dynamic import or require explicitly when static module syntax is unsupported. ${variableRedeclaration} ${functionClassRedeclaration} ${splitSyntax}`
85
+ : `Cells support TypeScript, top-level await, return, static import and top-level export. ${languageSemantics === 'stateful-v1'
86
+ ? 'Declarations and assignments update the same logical binding within its scope, including const, functions, classes and imports. A bare declaration preserves an existing value. A declaration publishes its bindings after its whole pattern initializes; explicit assignments preserve actual partial writes. Imports follow their module until locally overwritten; importing again changes that same binding source. Existing closures observe later updates, while saved values retain their identity.'
87
+ : 'Redeclaration protection is enabled: existing session names cannot be redeclared, and newly created const and import bindings reject assignment. Other declarations follow native scope rules. Previously writable bindings stay writable.'}`
65
88
  const recovery = durableReplay
66
89
  ? 'Direct Node/OS access remains live but is not replayed after a kernel restart.'
67
90
  : 'Durable replay is disabled for this profile. Bindings remain reusable only in the current process; a new kernel starts empty.'
68
91
  const cordisRecovery = cordisToolsEnabled
69
- ? 'When using Cordis tools, keep large host or client source in a top-level binding before the Cordis call. A Cordis parse or validation error is a runtime failure: bindings assigned before that failure remain live, so retry only the Cordis call with the existing binding instead of resending the source. Treat that binding as the read-only input for the retry.'
92
+ ? 'Keep large Cordis plugin source in a binding before the tool call so it can be reused after an error. A Cordis parse or validation failure can follow external effects; repeat the call only when its retry rules and current results establish that it is safe.'
70
93
  : ''
71
94
  return `\`run_code\` continues one persistent PTC REPL. Ordinary top-level bindings remain available to later cells, so reuse them instead of resending setup code. Choose the smallest cell that answers the request and return only the value the next step needs.
72
95
 
73
- The host may append a bounded recovery context after a qualifying failure. Treat that context as a session-log-derived diagnostic: use \`edit_run_code\` only when it explicitly proves a complete-cell rerun is safe; otherwise inspect live state in a new short \`run_code\` cell.
96
+ After an execution error, earlier statements may have taken effect. Inspect relevant values in a short cell and continue from them; repeat external operations only when their retry rules and current results establish that it is safe. \`edit_run_code\` edits and reruns a complete cell.
74
97
 
75
98
  ## Cell conventions
76
- Expressions that are neither returned nor printed produce no output. Keep large inspection results in bindings or reduce them to targeted excerpts: \`tools.read\` is bounded inspection, not a lossless whole-file reader. Cells are async function bodies; ${moduleSyntax} Use dynamic import or require explicitly when static module syntax is unsupported. ${redeclaration} ${splitSyntax}
99
+ Use \`return\` for an explicit result and \`console\` for logs. Keep large inspection results in bindings or reduce them to targeted excerpts: \`tools.read\` is bounded inspection, not a lossless whole-file reader. ${cellConventions}
77
100
 
78
101
  ## Available capabilities
79
- Use \`capabilities.tree()\`, \`capabilities.find()\`, and \`capabilities.inspect()\` to discover the current request's live \`tools.*\` members before calling an unfamiliar binding. Prefer direct current-cell work; reserve \`code.run\` for source already held as data.
102
+ Use \`capabilities.tree()\`, \`capabilities.find()\`, and \`capabilities.inspect()\` for unfamiliar program APIs. Prefer direct current-cell work; reserve \`code.run\` for isolated execution of source held as data.
103
+ Program API references expire when their cell ends. Reusable helpers must read the current namespace when called, for example \`async function readNow(args) { return tools.read(args) }\`, rather than retaining an earlier \`tools\` object or method. Background callbacks from completed cells cannot call program APIs.
80
104
 
81
105
  Native tool availability, executable names, shells, and path syntax depend on the current DSH profile and execution world; inspect them instead of assuming Windows, WSL, POSIX, or a particular shell. ${recovery}${cordisToolsEnabled ? ` ${cordisRecovery}` : ''}`
82
106
  }
83
107
 
84
108
  /** Register the session-bound REPL runtime. */
85
- function installPtCRuntime(ctx, resolvedConfig, toolSchemasForAgent, sessionId) {
109
+ function installPtCRuntime(ctx, resolvedConfig, seam, toolSchemasForAgent, sessionId) {
86
110
  const presentationGeneration = randomUUID()
87
111
  const replMemoryProjection = createReplMemoryProjection(presentationGeneration)
112
+ const userBindingDraftProjection = createUserBindingDraftProjection(presentationGeneration)
88
113
  let activeConfig = resolvedConfig
89
114
  let cordisTools
115
+ let cordisToolsReady = false
116
+ const cordisOwners = new Set()
90
117
  let runtimeBridge
91
118
  let editTransport
92
119
  let directSurface
120
+ let userBindings
121
+ let observationInterest
93
122
  let ready
94
123
  let pendingCordisActivation
124
+ let draftProjectionRegistration
125
+ let draftProjectionAvailable = false
126
+ let projectionService
95
127
  const disposers = []
128
+ const disposedOwners = new Set()
96
129
  let disposed = false
97
- async function dispose() {
98
- if (disposed) return
130
+ let disposal
131
+ function dispose() {
99
132
  disposed = true
100
133
  pendingCordisActivation?.cancel()
101
- const failures = []
102
- for (const dispose of [...disposers].reverse()) {
103
- if (typeof dispose !== 'function') continue
104
- try {
105
- await dispose()
106
- } catch (error) {
107
- failures.push(error)
108
- }
134
+ if (disposal !== undefined) return disposal
135
+ const tasks = []
136
+ for (let index = disposers.length - 1; index >= 0; index -= 1) {
137
+ const cleanup = disposers[index]
138
+ if (typeof cleanup !== 'function') continue
139
+ tasks.push({
140
+ run: cleanup,
141
+ complete: () => { if (disposers[index] === cleanup) disposers[index] = undefined },
142
+ })
143
+ }
144
+ for (const owner of [directSurface, editTransport, runtimeBridge, userBindings]) {
145
+ if (typeof owner?.dispose !== 'function' || disposedOwners.has(owner)) continue
146
+ tasks.push({ run: () => owner.dispose(), complete: () => disposedOwners.add(owner) })
109
147
  }
110
- for (const owner of [directSurface, editTransport, runtimeBridge, cordisTools]) {
148
+ for (const owner of cordisOwners) {
149
+ tasks.push({
150
+ run: () => owner.dispose(),
151
+ complete: () => {
152
+ cordisOwners.delete(owner)
153
+ if (cordisTools === owner) {
154
+ cordisTools = undefined
155
+ cordisToolsReady = false
156
+ }
157
+ },
158
+ })
159
+ }
160
+ const attempts = tasks.map(task => {
111
161
  try {
112
- await owner?.dispose()
162
+ return Promise.resolve(task.run())
113
163
  } catch (error) {
114
- failures.push(error)
164
+ return Promise.reject(error)
115
165
  }
166
+ })
167
+ const operation = Promise.allSettled(attempts)
168
+ .then(results => {
169
+ const failures = []
170
+ for (let index = 0; index < results.length; index += 1) {
171
+ const result = results[index]
172
+ if (result.status === 'fulfilled') tasks[index].complete()
173
+ else failures.push(result.reason)
174
+ }
175
+ if (failures.length > 0) throw new AggregateError(failures, 'ptc-plus runtime disposal failed')
176
+ })
177
+ disposal = operation
178
+ operation.then(
179
+ () => { if (disposal === operation) disposal = undefined },
180
+ () => { if (disposal === operation) disposal = undefined },
181
+ )
182
+ return operation
183
+ }
184
+ const disposeCordisToolsOwner = async (owner) => {
185
+ await owner.dispose()
186
+ cordisOwners.delete(owner)
187
+ if (cordisTools === owner) {
188
+ cordisTools = undefined
189
+ cordisToolsReady = false
116
190
  }
117
- if (failures.length > 0) throw new AggregateError(failures, 'ptc-plus runtime disposal failed')
191
+ }
192
+ const disposeRetainedCordisTools = async () => {
193
+ const owners = [...cordisOwners]
194
+ const results = await Promise.allSettled(owners.map(owner => disposeCordisToolsOwner(owner)))
195
+ const failures = results.filter(result => result.status === 'rejected').map(result => result.reason)
196
+ if (failures.length > 0) throw new AggregateError(failures, 'ptc-plus: retained Cordis disposal failed')
118
197
  }
119
198
  const awaitCordisToolsOwner = async (owner, cancelled) => {
120
199
  try {
121
200
  await Promise.race([owner.ready, cancelled])
201
+ if (cordisTools === owner) cordisToolsReady = true
122
202
  return owner
123
203
  } catch (error) {
124
204
  let rollbackError
125
205
  try {
126
- await owner.dispose()
206
+ await disposeCordisToolsOwner(owner)
127
207
  } catch (caught) {
128
208
  rollbackError = caught
129
209
  }
130
- if (cordisTools === owner) cordisTools = undefined
131
210
  if (rollbackError !== undefined) {
132
211
  throw new AggregateError(
133
212
  [error, rollbackError],
@@ -139,13 +218,17 @@ function installPtCRuntime(ctx, resolvedConfig, toolSchemasForAgent, sessionId)
139
218
  }
140
219
  }
141
220
  const activateCordisToolsOwner = async () => {
221
+ await disposeRetainedCordisTools()
222
+ if (disposed) throw new Error('ptc-plus: Cordis activation cancelled')
142
223
  const owner = createCordisToolsOwner(ctx)
224
+ cordisOwners.add(owner)
143
225
  let cancel
144
226
  const cancelled = new Promise((_resolve, reject) => {
145
227
  cancel = () => reject(new Error('ptc-plus: Cordis activation cancelled'))
146
228
  })
147
229
  const activation = { owner, cancel }
148
230
  cordisTools = owner
231
+ cordisToolsReady = false
149
232
  pendingCordisActivation = activation
150
233
  try {
151
234
  return await awaitCordisToolsOwner(owner, cancelled)
@@ -154,19 +237,92 @@ function installPtCRuntime(ctx, resolvedConfig, toolSchemasForAgent, sessionId)
154
237
  }
155
238
  }
156
239
  const cancelCordisActivation = () => pendingCordisActivation?.cancel()
240
+ const registerProjection = (projection, label) => {
241
+ try {
242
+ const unregister = projectionService?.register?.(projection)
243
+ if (typeof unregister !== 'function') {
244
+ throw new Error('ptc-plus: sessionProjections.register did not return a disposer')
245
+ }
246
+ return unregister
247
+ } catch (error) {
248
+ ctx.logger?.warn?.(`ptc-plus: ${label} projection unavailable`, error)
249
+ return undefined
250
+ }
251
+ }
252
+ const setDraftProjectionEnabled = async (enabled) => {
253
+ if (!enabled) {
254
+ const unregister = draftProjectionRegistration
255
+ draftProjectionAvailable = false
256
+ const results = await Promise.allSettled([
257
+ Promise.resolve().then(() => userBindings?.setDraftProjectionAvailable(false)),
258
+ Promise.resolve().then(() => unregister?.()),
259
+ ])
260
+ const failures = results.filter(result => result.status === 'rejected').map(result => result.reason)
261
+ if (failures.length > 0) {
262
+ throw new AggregateError(failures, 'ptc-plus: binding draft projection cleanup failed')
263
+ }
264
+ return
265
+ }
266
+ if (draftProjectionRegistration !== undefined) {
267
+ if (draftProjectionAvailable) return
268
+ await draftProjectionRegistration()
269
+ }
270
+ if (projectionService === undefined) return
271
+ const unregister = registerProjection(userBindingDraftProjection, 'binding draft')
272
+ if (unregister === undefined) return
273
+ let active = true
274
+ let releasing
275
+ const release = () => {
276
+ if (!active) return Promise.resolve()
277
+ if (releasing !== undefined) return releasing
278
+ const operation = Promise.resolve().then(unregister)
279
+ releasing = operation
280
+ operation.then(
281
+ () => {
282
+ active = false
283
+ if (draftProjectionRegistration === release) draftProjectionRegistration = undefined
284
+ if (releasing === operation) releasing = undefined
285
+ },
286
+ () => { if (releasing === operation) releasing = undefined },
287
+ )
288
+ return operation
289
+ }
290
+ draftProjectionRegistration = release
291
+ disposers.push(release)
292
+ draftProjectionAvailable = true
293
+ try {
294
+ await userBindings?.setDraftProjectionAvailable(true)
295
+ } catch (error) {
296
+ draftProjectionAvailable = false
297
+ await Promise.allSettled([
298
+ Promise.resolve().then(() => userBindings?.setDraftProjectionAvailable(false)),
299
+ release(),
300
+ ])
301
+ throw error
302
+ }
303
+ }
157
304
  try {
305
+ const hostRpc = createHostRpc(ctx)
306
+ disposers.push(() => hostRpc.dispose())
307
+ observationInterest = createReplObservationInterest(ctx, activeConfig.replViewEnabled,
308
+ (id, memory, signal) => runtimeBridge?.observeRepl(id, memory, signal))
309
+ disposers.push(() => observationInterest.dispose())
158
310
  if (typeof ctx.inject === 'function') {
159
311
  const projectionInjection = ctx.inject(['sessionProjections'], (scope) => {
160
312
  if (disposed) return
161
- try {
162
- const unregister = scope.sessionProjections?.register?.(replMemoryProjection)
163
- if (typeof unregister !== 'function') {
164
- throw new Error('ptc-plus: sessionProjections.register did not return a disposer')
165
- }
166
- disposers.push(unregister)
167
- } catch (error) {
168
- ctx.logger?.warn?.('ptc-plus: REPL memory projection unavailable', error)
169
- }
313
+ const service = scope.sessionProjections
314
+ projectionService = service
315
+ draftProjectionAvailable = false
316
+ const unregisterMemory = registerProjection(replMemoryProjection, 'REPL memory')
317
+ if (unregisterMemory !== undefined) disposers.push(unregisterMemory)
318
+ void setDraftProjectionEnabled(activeConfig.userBindingsEnabled).catch(error => {
319
+ ctx.logger?.warn?.('ptc-plus: binding draft projection unavailable', error)
320
+ })
321
+ scope.effect?.(() => async () => {
322
+ if (projectionService !== service) return
323
+ projectionService = undefined
324
+ await setDraftProjectionEnabled(false)
325
+ }, 'ptc-plus: binding draft projection availability')
170
326
  })
171
327
  if (typeof projectionInjection === 'function') {
172
328
  disposers.push(projectionInjection)
@@ -174,23 +330,32 @@ function installPtCRuntime(ctx, resolvedConfig, toolSchemasForAgent, sessionId)
174
330
  disposers.push(() => projectionInjection.dispose())
175
331
  }
176
332
  }
177
- cordisTools = activeConfig.cordisToolsEnabled
178
- ? createCordisToolsOwner(ctx)
179
- : undefined
333
+ cordisTools = activeConfig.cordisToolsEnabled ? createCordisToolsOwner(ctx) : undefined
180
334
  if (cordisTools !== undefined) {
181
335
  const initialCordisTools = cordisTools
182
- ready = awaitCordisToolsOwner(initialCordisTools, new Promise(() => {})).catch(error => {
183
- if (cordisTools === initialCordisTools) cordisTools = undefined
184
- throw error
185
- })
336
+ cordisOwners.add(initialCordisTools)
337
+ ready = awaitCordisToolsOwner(initialCordisTools, new Promise(() => {}))
186
338
  }
339
+ userBindings = createUserBindingsOwner(ctx, {
340
+ enabled: activeConfig.userBindingsEnabled,
341
+ draftProjectionAvailable,
342
+ maxWallMs: activeConfig.maxWallMs,
343
+ maxOutputBytes: activeConfig.maxOutputBytes,
344
+ maxOldGenerationSizeMb: activeConfig.maxOldGenerationSizeMb,
345
+ valueLimits: valueLimitsFromConfig(activeConfig),
346
+ })
187
347
  runtimeBridge = createRuntimeBridgeOwner({
188
348
  ctx,
349
+ seam,
350
+ observeSession: id => observationInterest.has(id),
189
351
  sessionConfig: activeConfig,
352
+ userBindingsCwd: userBindings.cwd,
190
353
  maxNestedRunCodeDepth: activeConfig.maxNestedRunCodeDepth,
191
354
  presentationGeneration,
192
355
  sessionId,
193
356
  toolSchemasForAgent,
357
+ userBindingDraftForAgent: agent => userBindings.draftCapabilityForAgent(agent),
358
+ bindingSubmissionForAgent: (agent, ensureLease, onAccepted) => userBindings.submissionForAgent(agent, ensureLease, onAccepted),
194
359
  })
195
360
  editTransport = createEditTransportOwner(ctx, {
196
361
  durableReplay: activeConfig.durableReplay,
@@ -205,20 +370,17 @@ function installPtCRuntime(ctx, resolvedConfig, toolSchemasForAgent, sessionId)
205
370
  canonicalizeToolCalls: activeConfig.canonicalizeToolCalls,
206
371
  sessionId,
207
372
  toolSchemasForAgent,
373
+ userBindingsForAgent: () => userBindings.snapshot(),
374
+ setAgentPresentation: (agent, presentation) => (
375
+ userBindings.setAgentPresentation(agent, presentation)
376
+ ),
208
377
  })
209
378
  disposers.push(ctx.systemPrompt.section({
210
379
  name: 'tools:ptc-plus-repl',
211
380
  order: 98,
212
381
  text: context => {
213
382
  if (ctx.tools.get(RUN_CODE, context?.scope) === undefined) return ''
214
- return replGuidance(
215
- activeConfig.looseTopLevelRedeclarations,
216
- activeConfig.durableReplay,
217
- activeConfig.autoRewriteImports,
218
- activeConfig.autoStripExports,
219
- activeConfig.autoSplitRedeclarations,
220
- activeConfig.cordisToolsEnabled,
221
- )
383
+ return replGuidance(guidancePolicies(activeConfig))
222
384
  },
223
385
  }))
224
386
  disposers.push(ctx.on('system-prompt/assemble', (assembly, context, next) => (
@@ -230,7 +392,13 @@ function installPtCRuntime(ctx, resolvedConfig, toolSchemasForAgent, sessionId)
230
392
  if (rejected !== undefined) return rejected
231
393
  if (exec.name === RUN_CODE) {
232
394
  const executionArguments = directSurface.executionArguments(exec)
233
- return Promise.resolve(runtimeBridge.handleExecute(exec, next, executionArguments))
395
+ const requestUserBindings = directSurface.executionUserBindings(exec)
396
+ return Promise.resolve(runtimeBridge.handleExecute(
397
+ exec,
398
+ next,
399
+ executionArguments,
400
+ requestUserBindings,
401
+ ))
234
402
  .then(result => directSurface.argumentDiagnostic(exec, result))
235
403
  }
236
404
  return next()
@@ -240,22 +408,48 @@ function installPtCRuntime(ctx, resolvedConfig, toolSchemasForAgent, sessionId)
240
408
  if (exec.name === EDIT_RUN_CODE) return editTransport.handleResult(exec, result)
241
409
  if (exec.name === RUN_CODE) return runtimeBridge.handleResult(exec, result)
242
410
  }))
243
- disposers.push(ctx.on('agent/disposed', ({ agent }) => {
411
+ disposers.push(ctx.on('agent/disposed', async ({ agent }) => {
244
412
  directSurface.disposeAgent(agent)
245
413
  editTransport.disposeAgent(agent)
246
- return runtimeBridge.disposeAgent(agent)
414
+ await Promise.all([
415
+ runtimeBridge.disposeAgent(agent),
416
+ userBindings.clearAgentPresentation(agent),
417
+ ])
247
418
  }))
248
- disposers.push(ctx.on('session/disposed', (session) => {
419
+ disposers.push(ctx.on('session/disposed', async (session) => {
249
420
  directSurface.disposeSession(session)
250
421
  editTransport.disposeSession(session)
251
- return runtimeBridge.disposeSession(session)
422
+ await Promise.all([
423
+ runtimeBridge.disposeSession(session),
424
+ userBindings.clearSessionPresentation(session?.id ?? session),
425
+ ])
426
+ }))
427
+ disposers.push(ctx.on('agent-preset/selected', (sessionId) => {
428
+ directSurface.resetSessionComposition(sessionId)
429
+ const agent = ctx.agents?.get?.(String(sessionId))
430
+ if (agent !== undefined) editTransport.disposeAgent(agent)
431
+ const cleanup = agent === undefined
432
+ ? userBindings.clearSessionPresentation(sessionId)
433
+ : userBindings.setAgentPresentation(agent)
434
+ const report = ctx.logger?.warn?.bind(
435
+ ctx.logger,
436
+ 'ptc-plus: failed to reconcile Global User Bindings authoring after preset selection',
437
+ ) ?? console.warn
438
+ void cleanup.catch(report)
252
439
  }))
253
440
  } catch (error) {
254
- const cleanup = dispose()
255
- if (error !== null && typeof error === 'object') {
256
- Object.defineProperty(error, INSTALL_CLEANUP, { value: cleanup })
441
+ const failedRuntime = Object.freeze({ dispose })
442
+ let failure = error
443
+ if (failure === null || (typeof failure !== 'object' && typeof failure !== 'function')) {
444
+ failure = new Error(String(error), { cause: error })
257
445
  }
258
- throw error
446
+ try {
447
+ Object.defineProperty(failure, INSTALL_CLEANUP, { value: failedRuntime })
448
+ } catch {
449
+ failure = new Error(failure.message ?? String(failure), { cause: failure })
450
+ Object.defineProperty(failure, INSTALL_CLEANUP, { value: failedRuntime })
451
+ }
452
+ throw failure
259
453
  }
260
454
  async function reconfigure(nextConfig) {
261
455
  if (disposed) return
@@ -268,26 +462,34 @@ function installPtCRuntime(ctx, resolvedConfig, toolSchemasForAgent, sessionId)
268
462
  rollbacks.push(() => editTransport.reconfigure(previousConfig))
269
463
  directSurface.reconfigure(nextConfig)
270
464
  rollbacks.push(() => directSurface.reconfigure(previousConfig))
465
+ await userBindings.reconfigure(nextConfig)
466
+ rollbacks.push(() => userBindings.reconfigure(previousConfig))
467
+ await setDraftProjectionEnabled(nextConfig.userBindingsEnabled)
468
+ rollbacks.push(() => setDraftProjectionEnabled(previousConfig.userBindingsEnabled))
271
469
 
470
+ if (nextConfig.cordisToolsEnabled && cordisTools !== undefined && !cordisToolsReady) {
471
+ await disposeRetainedCordisTools()
472
+ }
272
473
  if (nextConfig.cordisToolsEnabled && cordisTools === undefined) {
273
474
  await activateCordisToolsOwner()
274
475
  } else if (!nextConfig.cordisToolsEnabled && cordisTools !== undefined) {
275
476
  const currentCordis = cordisTools
477
+ cordisToolsReady = false
276
478
  try {
277
- await currentCordis.dispose()
278
- cordisTools = undefined
479
+ await disposeCordisToolsOwner(currentCordis)
279
480
  } catch (error) {
280
481
  try {
482
+ await disposeCordisToolsOwner(currentCordis)
281
483
  await activateCordisToolsOwner()
282
484
  /* c8 ignore next */
283
485
  } catch (rollbackError) {
284
- cordisTools = currentCordis
285
486
  throw new AggregateError([error, rollbackError], 'ptc-plus: Cordis reconfiguration and rollback failed', { cause: error })
286
487
  }
287
488
  throw error
288
489
  }
289
490
  }
290
491
  activeConfig = nextConfig
492
+ observationInterest.reconfigure(nextConfig.replViewEnabled)
291
493
  } catch (error) {
292
494
  const rollbackFailures = []
293
495
  /* c8 ignore next -- the rollback loop's rejection branch is host-specific. */
@@ -303,12 +505,25 @@ function installPtCRuntime(ctx, resolvedConfig, toolSchemasForAgent, sessionId)
303
505
  }
304
506
  }
305
507
 
306
- return Object.freeze({ cancelCordisActivation, dispose, reconfigure, ready })
508
+ return Object.freeze({
509
+ cancelCordisActivation, dispose, reconfigure, ready,
510
+ contextsForRequest: context => directSurface.contextsForRequest(context),
511
+ })
307
512
  }
308
513
 
309
514
  /** Register the session-bound REPL runtime. */
310
515
  export function apply(ctx, config = {}) {
516
+ // Configuration is validated before the plugin waits for the host's seam, so
517
+ // an unusable configuration fails at the load call rather than later.
311
518
  const resolvedConfig = resolveConfig(config)
519
+ return installExecutionSeam(ctx, {
520
+ services: inject,
521
+ attach: (scope, seam) => applyWithExecutionSeam(scope, resolvedConfig, seam),
522
+ })
523
+ }
524
+
525
+ /** Install the plugin into the scope of the seam the host registered. */
526
+ function applyWithExecutionSeam(ctx, resolvedConfig, seam) {
312
527
  const toolSchemasForAgent = agent => typeof ctx.tools.schemas === 'function'
313
528
  ? ctx.tools.schemas(agent)
314
529
  : []
@@ -318,7 +533,9 @@ export function apply(ctx, config = {}) {
318
533
  }
319
534
  let committed
320
535
  let activating
536
+ const retained = new Set()
321
537
  let disposed = false
538
+ let controllerDisposal
322
539
  let transitionTail = Promise.resolve()
323
540
  let pendingTransitions = 0
324
541
  const trackTransition = operation => {
@@ -343,9 +560,26 @@ export function apply(ctx, config = {}) {
343
560
  return { promise, resolve, reject }
344
561
  }
345
562
  const disposeRuntime = record => {
563
+ if (record?.disposed === true) return Promise.resolve()
346
564
  if (record?.disposal !== undefined) return record.disposal
347
- record.disposal = record.runtime.dispose()
348
- return record.disposal
565
+ retained.add(record)
566
+ const operation = Promise.resolve().then(() => record.runtime.dispose())
567
+ record.disposal = operation
568
+ operation.then(
569
+ () => {
570
+ record.disposed = true
571
+ retained.delete(record)
572
+ if (record.disposal === operation) record.disposal = undefined
573
+ },
574
+ () => { if (record.disposal === operation) record.disposal = undefined },
575
+ )
576
+ return operation
577
+ }
578
+ const disposeRetainedRuntimes = async () => {
579
+ const records = [...retained].filter(record => record.runtime !== undefined)
580
+ const results = await Promise.allSettled(records.map(record => disposeRuntime(record)))
581
+ const failures = results.filter(result => result.status === 'rejected').map(result => result.reason)
582
+ if (failures.length > 0) throw new AggregateError(failures, 'ptc-plus retained runtime disposal failed')
349
583
  }
350
584
  const activationError = (error, cleanupError) => new AggregateError(
351
585
  [error, cleanupError],
@@ -371,17 +605,18 @@ export function apply(ctx, config = {}) {
371
605
  record.completion.resolve({ status: 'superseded' })
372
606
  return
373
607
  }
374
- if (ctx.codeRuntime.language !== 'typescript') {
375
- throw new Error('ptc-plus: unsupported code runtime language ' + JSON.stringify(ctx.codeRuntime.language) + '; only "typescript" is supported')
608
+ if (seam.language !== 'typescript') {
609
+ throw new Error('ptc-plus: unsupported code runtime language ' + JSON.stringify(seam.language) + '; only "typescript" is supported')
376
610
  }
377
611
  let candidate
378
612
  try {
379
- candidate = installPtCRuntime(ctx, record.config, toolSchemasForAgent, sessionId)
613
+ candidate = installPtCRuntime(ctx, record.config, seam, toolSchemasForAgent, sessionId)
380
614
  record.runtime = candidate
381
615
  } catch (error) {
382
- const cleanup = error?.[INSTALL_CLEANUP]
383
- if (cleanup === undefined) throw error
384
- const trackedCleanup = trackTransition(cleanup)
616
+ const failedRuntime = error?.[INSTALL_CLEANUP]
617
+ if (failedRuntime === undefined) throw error
618
+ record.runtime = failedRuntime
619
+ const trackedCleanup = trackTransition(disposeRuntime(record))
385
620
  void trackedCleanup.then(
386
621
  () => record.completion.reject(error),
387
622
  cleanupError => record.completion.reject(activationError(error, cleanupError)),
@@ -422,6 +657,8 @@ export function apply(ctx, config = {}) {
422
657
  generation,
423
658
  runtime: undefined,
424
659
  disposal: undefined,
660
+ disposed: false,
661
+ retiring: false,
425
662
  trackedDisposal: undefined,
426
663
  cancelled: false,
427
664
  completion,
@@ -450,10 +687,22 @@ export function apply(ctx, config = {}) {
450
687
  apply(nextConfig, generation) {
451
688
  if (disposed) return Promise.resolve({ status: 'superseded' })
452
689
  if (activating !== undefined) {
453
- const cleanup = cancelActivation(activating)
690
+ cancelActivation(activating)
691
+ const cleanup = disposeRetainedRuntimes()
692
+ return beginActivation(nextConfig, generation, cleanup)
693
+ }
694
+ if (committed === undefined) {
695
+ const cleanup = retained.size === 0 ? undefined : disposeRetainedRuntimes()
696
+ return beginActivation(nextConfig, generation, cleanup)
697
+ }
698
+ if (committed.retiring) {
699
+ const current = committed
700
+ void disposeRuntime(current)
701
+ const cleanup = disposeRetainedRuntimes().then(() => {
702
+ if (committed === current) committed = undefined
703
+ })
454
704
  return beginActivation(nextConfig, generation, cleanup)
455
705
  }
456
- if (committed === undefined) return beginActivation(nextConfig, generation)
457
706
  const current = committed
458
707
  if (!nextConfig.cordisToolsEnabled) current.runtime.cancelCordisActivation()
459
708
  return enqueueTransition(async () => {
@@ -464,27 +713,60 @@ export function apply(ctx, config = {}) {
464
713
  return { status: 'applied' }
465
714
  })
466
715
  },
467
- uninstall() {
468
- const activationCleanup = activating === undefined
716
+ async uninstall() {
717
+ const active = activating
718
+ const activationCleanup = active === undefined
469
719
  ? Promise.resolve()
470
- : cancelActivation(activating)
720
+ : cancelActivation(active)
471
721
  const current = committed
472
- committed = undefined
722
+ if (current !== undefined) current.retiring = true
473
723
  const committedCleanup = current === undefined
474
724
  ? Promise.resolve()
475
725
  : trackTransition(disposeRuntime(current))
476
- return Promise.all([activationCleanup, committedCleanup]).then(() => undefined)
726
+ const records = [...retained].filter(record => (
727
+ record !== current && record !== active && record.runtime !== undefined
728
+ ))
729
+ const results = await Promise.allSettled([
730
+ activationCleanup,
731
+ committedCleanup,
732
+ ...records.map(record => disposeRuntime(record)),
733
+ ])
734
+ const failures = results.filter(result => result.status === 'rejected').map(result => result.reason)
735
+ if (current?.disposed === true && committed === current) committed = undefined
736
+ if (failures.length > 0) throw new AggregateError(failures, 'ptc-plus runtime uninstall failed')
477
737
  },
478
738
  committedConfig() {
479
- return committed?.config
739
+ return committed?.retiring ? undefined : committed?.config
480
740
  },
481
- async dispose() {
741
+ dispose() {
482
742
  disposed = true
483
- await controller.uninstall()
743
+ if (controllerDisposal !== undefined) return controllerDisposal
744
+ const operation = controller.uninstall()
745
+ controllerDisposal = operation
746
+ operation.then(
747
+ () => { if (controllerDisposal === operation) controllerDisposal = undefined },
748
+ () => { if (controllerDisposal === operation) controllerDisposal = undefined },
749
+ )
750
+ return operation
484
751
  },
485
752
  }
486
753
  ctx.effect(() => async () => controller.dispose(), 'ptc-plus runtime lifecycle')
487
754
 
755
+ const messageOwner = createRuntimeMessageOwner(context => (
756
+ committed?.retiring ? [] : committed?.runtime.contextsForRequest(context) ?? []
757
+ ))
758
+ ctx.effect(() => ctx.systemPrompt.context({
759
+ name: PTC_DELIVERY_CONTEXT, order: 98, text: '',
760
+ }), 'ptc-plus dynamic message delivery witness')
761
+ ctx.effect(() => ctx.on('system-prompt/assemble', (assembly, context, next) => (
762
+ messageOwner.assemble(assembly, context, next)
763
+ )), 'ptc-plus message assembly')
764
+ ctx.effect(() => ctx.on('agent/pre-step', (payload, next) => (
765
+ messageOwner.preStep(payload, next)
766
+ )), 'ptc-plus accepted messages')
767
+ ctx.effect(() => ctx.on('agent/disposed', ({ agent }) => messageOwner.disposeAgent(agent)), 'ptc-plus message agent disposal')
768
+ ctx.effect(() => () => messageOwner.dispose(), 'ptc-plus dynamic message lifecycle')
769
+
488
770
  let configSource = () => resolvedConfig
489
771
  let configurationGeneration = 0
490
772
  let settingsWriter