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
@@ -1,20 +1,30 @@
1
+ import { isAbsolute } from 'node:path'
2
+ import { isDeepStrictEqual } from 'node:util'
1
3
  import { diagnostic, renderDiagnostic } from './diagnostic.js'
2
4
  import { createFailureTracker, messageOf } from './failure-reporting.js'
3
5
  import {
4
6
  createJournal,
5
7
  liveToolCallSeq,
6
8
  normalizeJournal,
7
- pathToHead,
8
9
  reduceStateOperations,
9
- recoverJournal,
10
10
  } from './session-journal.js'
11
+ import {
12
+ pathToHead,
13
+ recoveryBoundaryForHistory,
14
+ recoverJournal,
15
+ visibleExecutableCallSeqs,
16
+ } from './session-journal-recovery.js'
17
+ import { valueLimitsFromConfig } from './value-wire-schema.js'
11
18
  import { normalizeBindingDescriptors } from './binding-descriptors.js'
12
19
  import { resolveConfig } from './runtime-config.js'
13
20
  import { WorkerClient } from './worker-client.js'
21
+ import { compilerWorkerCache } from './compiler-service.js'
14
22
  import { BindingCatalog, durabilityState, transitionDurability } from './session-state.js'
15
23
  import { SessionCellExecutor } from './session-cell-executor.js'
24
+ import { executionPolicies } from './binding-update-policy.js'
16
25
  import {
17
26
  createReplMemorySnapshot,
27
+ normalizeReplMemorySnapshot,
18
28
  unavailableReplMemorySnapshot,
19
29
  } from './repl-memory-projection.js'
20
30
 
@@ -33,14 +43,6 @@ function recoveryDiagnostic(count) {
33
43
  })
34
44
  }
35
45
 
36
- function rewritePolicy(config) {
37
- return {
38
- autoRewriteImports: config.autoRewriteImports,
39
- autoStripExports: config.autoStripExports,
40
- autoSplitRedeclarations: config.autoSplitRedeclarations,
41
- }
42
- }
43
-
44
46
  function resolvedRuntimeConfig(config) {
45
47
  return Object.freeze(resolveConfig(config))
46
48
  }
@@ -64,15 +66,25 @@ class ReplayCancelled extends Error {
64
66
  }
65
67
 
66
68
  class SessionKernel {
67
- constructor({ config, history, cwd, session, withInitiator }) {
69
+ constructor({ config, history, cwd, session, userBindingsCwd, withInitiator, observeValues }) {
68
70
  this.config = config
69
71
  this.history = history
72
+ this.initialRecoveryBoundary = recoveryBoundaryForHistory(history)
73
+ this.surfaceGeneration = undefined
74
+ try {
75
+ this.surfaceGeneration = session?.surface?.replaceGeneration
76
+ } catch {
77
+ this.surfaceGeneration = undefined
78
+ }
70
79
  this.cwd = cwd
80
+ this.userBindingsCwd = userBindingsCwd
81
+ this.observeValues = observeValues
71
82
  this.session = session
72
83
  this.withInitiator = withInitiator
73
84
  this.durability = durabilityState()
74
85
  this.bindingCatalog = new BindingCatalog()
75
86
  this.replayed = false
87
+ this.liveCallSeqs = new Set()
76
88
  this.recoveryNotice = history.volatileSuffix.length === 0
77
89
  ? undefined
78
90
  : recoveryDiagnostic(history.volatileSuffix.length)
@@ -80,28 +92,56 @@ class SessionKernel {
80
92
  this.sequence = 0
81
93
  this.tail = Promise.resolve()
82
94
  this.tentatives = new WeakMap()
95
+ this.unsettledCells = 0
96
+ this.pendingInspection = undefined
97
+ this.pendingObservation = undefined
98
+ this.workerObservation = undefined
83
99
  this.workerReservations = new Set()
84
100
  this.cellExecutor = new SessionCellExecutor(this)
85
101
  this.client = new WorkerClient({
86
102
  workerUrl: WORKER_URL,
87
103
  cwd,
104
+ compilerCache: compilerWorkerCache,
88
105
  onMessage: message => this.cellExecutor.onMessage(message),
89
- onFailure: message => this.active?.resolve({
90
- logs: [], error: { kind: 'worker-exit', message },
91
- }, true),
106
+ onFailure: message => {
107
+ this.pendingInspection?.finish()
108
+ this.workerObservation = undefined
109
+ this.active?.resolve({ logs: [], error: { kind: 'worker-exit', message } }, true)
110
+ },
92
111
  })
93
112
  this.failures = createFailureTracker()
113
+ this.reclamationSequence = 0
114
+ this.reclamationRecords = new Map()
115
+ this.reclamationAttempts = new Set()
94
116
  this.disposed = false
117
+ this.disposal = undefined
95
118
  }
96
119
 
97
- valueLimits(config = this.config) {
98
- return {
99
- maxNodes: config.maxValueNodes,
100
- maxEdges: config.maxValueEdges,
101
- maxArrayLength: config.maxValueArrayLength,
102
- maxBigIntDigits: config.maxValueBigIntDigits,
103
- maxStringBytes: config.maxOutputBytes,
120
+ resetWorker(worker) {
121
+ const sequence = ++this.reclamationSequence
122
+ const record = { failed: false, reason: undefined }
123
+ this.reclamationRecords.set(sequence, record)
124
+ let reset
125
+ try {
126
+ reset = this.client.reset(worker)
127
+ } catch (error) {
128
+ record.failed = true
129
+ record.reason = error
130
+ return
104
131
  }
132
+ const attempt = Promise.resolve(reset).then(
133
+ () => { this.reclamationRecords.delete(sequence) },
134
+ error => {
135
+ record.failed = true
136
+ record.reason = error
137
+ },
138
+ )
139
+ this.reclamationAttempts.add(attempt)
140
+ attempt.then(() => { this.reclamationAttempts.delete(attempt) })
141
+ }
142
+
143
+ valueLimits(config = this.config) {
144
+ return valueLimitsFromConfig(config)
105
145
  }
106
146
 
107
147
  assertReconfigurationAllowed(config) {
@@ -135,11 +175,109 @@ class SessionKernel {
135
175
  return result
136
176
  }
137
177
 
178
+ /** Observe only the existing, settled live worker; never recover or execute a cell for the UI. */
179
+ observe(expected, signal) {
180
+ if (this.pendingInspection !== undefined) return Promise.resolve(undefined)
181
+ const current = () => {
182
+ if (this.disposed || !this.config.replViewEnabled || signal?.aborted
183
+ || this.client.worker === undefined || this.unsettledCells > 0) return false
184
+ try {
185
+ return this.session?.surface?.replaceGeneration === this.surfaceGeneration
186
+ && isDeepStrictEqual(expected, createReplMemorySnapshot(this.bindingCatalog.snapshot()))
187
+ } catch { return false }
188
+ }
189
+ return new Promise(resolve => {
190
+ let finished = false
191
+ let releaseQueue
192
+ const id = ++this.sequence
193
+ const finish = observation => {
194
+ if (finished) return
195
+ finished = true
196
+ clearTimeout(timer)
197
+ signal?.removeEventListener('abort', onAbort)
198
+ if (this.pendingInspection?.id === id) this.pendingInspection = undefined
199
+ resolve(current() && observation !== undefined
200
+ ? createReplMemorySnapshot(this.bindingCatalog.snapshot(), observation) : undefined)
201
+ releaseQueue?.()
202
+ }
203
+ const onAbort = () => finish()
204
+ const timer = setTimeout(onAbort, 250)
205
+ signal?.addEventListener('abort', onAbort, { once: true })
206
+ const inspect = () => {
207
+ if (finished) return
208
+ if (!current()) { finish(); return }
209
+ return new Promise(done => {
210
+ releaseQueue = done
211
+ const worker = this.client.worker
212
+ let started = false
213
+ this.pendingInspection = { id, finish, start: () => {
214
+ if (started) return
215
+ started = true
216
+ if (!current() || this.client.worker !== worker) { finish(); return }
217
+ try {
218
+ this.client.post({ type: 'observe', id, names: expected.entries.map(entry => entry.name) })
219
+ this.workerObservation = { id, worker, started: false }
220
+ } catch { finish() }
221
+ } }
222
+ try { this.client.post({ type: 'prepare', id }) } catch { finish() }
223
+ })
224
+ }
225
+ this.tail = this.tail.then(inspect, inspect)
226
+ })
227
+ }
228
+
138
229
  async execute(request, config) {
139
- const recoveryBoundaries = []
230
+ const recoveryBoundaries = this.initialRecoveryBoundary === undefined
231
+ ? []
232
+ : [this.initialRecoveryBoundary]
233
+ this.initialRecoveryBoundary = undefined
140
234
  const finishResult = result => recoveryBoundaries.length === 0
141
235
  ? result
142
236
  : { ...result, recoveryBoundaries: recoveryBoundaries.map(boundary => ({ ...boundary })) }
237
+ let currentGeneration
238
+ try {
239
+ currentGeneration = this.session?.surface?.replaceGeneration
240
+ } catch {
241
+ currentGeneration = undefined
242
+ }
243
+ if (currentGeneration !== undefined && currentGeneration !== this.surfaceGeneration) {
244
+ this.surfaceGeneration = currentGeneration
245
+ const visibleCallSeqs = visibleExecutableCallSeqs(this.session)
246
+ const contractsLiveState = [...this.liveCallSeqs].some(callSeq => (
247
+ !(visibleCallSeqs instanceof Set) || !visibleCallSeqs.has(callSeq)
248
+ ))
249
+ if ((config.durableReplay && !this.replayed) || contractsLiveState) {
250
+ const worker = this.client.worker
251
+ if (worker !== undefined) await this.client.reset(worker)
252
+ this.rollbackToDurable()
253
+ if (config.durableReplay) {
254
+ this.recoveryNotice = undefined
255
+ try {
256
+ this.history = recoverJournal(this.session, request.callSeq, {
257
+ visibleCallSeqs,
258
+ })
259
+ } catch (error) {
260
+ const boundary = recoveryBoundaryForHistory(this.history, undefined, { reset: true })
261
+ if (boundary !== undefined) recoveryBoundaries.push(boundary)
262
+ this.history = emptyHistory()
263
+ this.recoveryNotice = recoveryDiagnostic(1)
264
+ this.initialRecoveryBoundary = undefined
265
+ }
266
+ if (this.history.volatileSuffix.length > 0) {
267
+ this.recoveryNotice = recoveryDiagnostic(this.history.volatileSuffix.length)
268
+ }
269
+ this.initialRecoveryBoundary = recoveryBoundaryForHistory(this.history)
270
+ if (this.initialRecoveryBoundary !== undefined) {
271
+ recoveryBoundaries.push(this.initialRecoveryBoundary)
272
+ this.initialRecoveryBoundary = undefined
273
+ }
274
+ } else {
275
+ this.history = emptyHistory()
276
+ this.replayed = true
277
+ this.recoveryNotice = recoveryDiagnostic(1)
278
+ }
279
+ }
280
+ }
143
281
  if (!this.replayed && config.durableReplay) {
144
282
  let skipped = 0
145
283
  while (!this.replayed) {
@@ -153,44 +291,28 @@ class SessionKernel {
153
291
  this.completeJournal(request.journal, 'noop', error.result)
154
292
  return finishResult(error.result)
155
293
  }
156
- if (!(error instanceof ReplayFailure)) {
157
- const result = {
158
- logs: [],
159
- error: {
160
- kind: 'recovery',
161
- message: `cannot reconstruct REPL from session log: ${messageOf(error)}`,
162
- },
163
- }
164
- this.completeJournal(request.journal, 'noop', result)
165
- return finishResult(result)
166
- }
167
- const frontier = error.node.parent === undefined
168
- ? undefined
169
- : this.history.nodes[error.node.parent]
170
- const previousPathLength = pathToHead(this.history).length
171
294
  try {
172
- const boundary = {
173
- failedCallSeq: error.node.callSeq,
174
- frontierCallSeq: frontier?.callSeq ?? null,
175
- }
295
+ if (!(error instanceof ReplayFailure)) throw error
296
+ const previousPathLength = pathToHead(this.history).length
297
+ const boundary = recoveryBoundaryForHistory(this.history, error.node)
176
298
  const recovered = recoverJournal(this.session, request.callSeq, {
177
299
  extraBoundaries: [boundary],
300
+ visibleCallSeqs: visibleExecutableCallSeqs(this.session),
178
301
  })
302
+ const nextPathLength = pathToHead(recovered).length
303
+ if (nextPathLength >= previousPathLength) throw new Error('recovery did not contract the historical frontier')
179
304
  recoveryBoundaries.push(boundary)
180
305
  this.history = recovered
181
- } catch (boundaryError) {
182
- const result = {
183
- logs: [],
184
- error: {
185
- kind: 'recovery',
186
- message: `cannot apply REPL recovery boundary: ${messageOf(boundaryError)}`,
187
- },
188
- }
189
- this.completeJournal(request.journal, 'noop', result)
190
- return finishResult(result)
306
+ skipped += previousPathLength - nextPathLength
307
+ } catch {
308
+ // Unproved historical metadata cannot gate the current request.
309
+ // A failed or noncontracting recovery has no reusable frontier.
310
+ const boundary = recoveryBoundaryForHistory(this.history, undefined, { reset: true })
311
+ if (boundary !== undefined) recoveryBoundaries.push(boundary)
312
+ this.history = emptyHistory()
313
+ this.replayed = true
314
+ skipped++
191
315
  }
192
- const nextPathLength = pathToHead(this.history).length
193
- skipped += Math.max(1, previousPathLength - nextPathLength)
194
316
  this.durability = durabilityState()
195
317
  }
196
318
  }
@@ -207,7 +329,8 @@ class SessionKernel {
207
329
  if (result.error === undefined) {
208
330
  this.failures.reset()
209
331
  } else {
210
- const hint = this.failures.hint(result.error)
332
+ const primary = request.journal?.diagnostics.find(item => item.severity === 'error')
333
+ const hint = this.failures.hint(result.error, primary?.stateEffect)
211
334
  if (hint !== undefined) {
212
335
  result.logs = [...result.logs, renderDiagnostic(hint, request.program)]
213
336
  if (request.journal !== undefined) request.journal.diagnostics.push(hint)
@@ -222,7 +345,12 @@ class SessionKernel {
222
345
  for (const node of path) {
223
346
  try {
224
347
  const result = await this.cellExecutor.executeCell(
225
- { ...request, program: node.code, journal: undefined },
348
+ {
349
+ ...request,
350
+ program: node.code,
351
+ journal: undefined,
352
+ ...(node.userBindings === undefined ? { userBindings: undefined } : { userBindings: node.userBindings }),
353
+ },
226
354
  node.journal,
227
355
  config,
228
356
  )
@@ -245,6 +373,7 @@ class SessionKernel {
245
373
  throw new ReplayFailure(node, error)
246
374
  }
247
375
  }
376
+ for (const node of path) this.liveCallSeqs.add(node.callSeq)
248
377
  }
249
378
 
250
379
  completeJournal(journal, status, result, volatileReason, diagnostics = [], completion = undefined) {
@@ -263,21 +392,25 @@ class SessionKernel {
263
392
  journal.operations = journal.operations.filter(operation => operation.action !== 'save')
264
393
  }
265
394
  if (status === 'discarded' || status === 'noop') {
395
+ journal.userBindingNames = null
266
396
  journal.calls.length = 0
267
397
  journal.operations.length = 0
268
398
  }
269
399
  }
270
400
 
271
401
  rollbackToDurable() {
402
+ this.workerObservation = undefined
272
403
  this.durability = durabilityState()
273
404
  this.replayed = false
274
405
  this.bindingCatalog = new BindingCatalog()
406
+ this.liveCallSeqs.clear()
275
407
  }
276
408
 
277
409
  settleCell(active, result, terminate = false) {
278
410
  /* c8 ignore next */
279
411
  if (this.active !== active) return
280
- const { request, journal, replay, prepared, worker } = active
412
+ active.settled = true
413
+ const { request, journal, replay, worker } = active
281
414
  clearInterval(active.computeTimer)
282
415
  clearTimeout(active.wallTimer)
283
416
  request.signal?.removeEventListener('abort', active.onAbort)
@@ -285,6 +418,10 @@ class SessionKernel {
285
418
  result = { ...result, rewrites: active.rewrites }
286
419
  }
287
420
  if (journal !== undefined && replay === undefined) {
421
+ journal.userBindingsFingerprint = terminate
422
+ ? null
423
+ : active.userBindingSnapshot?.fingerprint ?? null
424
+ journal.userBindingNames = terminate ? null : active.userBindingNames ?? null
288
425
  if (terminate) {
289
426
  const volatileReason = active.pendingBindings.values().next().value ?? active.durability.reason
290
427
  this.completeJournal(journal, 'discarded', result, volatileReason, active.diagnostics)
@@ -303,25 +440,58 @@ class SessionKernel {
303
440
  this.tentatives.set(journal, {
304
441
  callSeq: request.callSeq,
305
442
  program: request.program,
306
- bindingCatalog: active.appliedBindingCatalog
307
- ?? active.priorBindingCatalog.advance(prepared, request.program),
443
+ bindingCatalog: active.appliedBindingCatalog,
444
+ userBindings: active.userBindingSnapshot,
308
445
  worker,
309
446
  })
447
+ this.unsettledCells++
310
448
  }
311
449
  }
312
- if (replay !== undefined && !terminate) {
450
+ if (!terminate) {
313
451
  this.bindingCatalog = active.appliedBindingCatalog
314
- ?? active.priorBindingCatalog.advance(prepared, request.program)
452
+ }
453
+ if (replay === undefined && !terminate) {
454
+ // A live cell establishes this worker's timeline even when historical
455
+ // replay was disabled. Only a reset may reopen its recovery boundary.
456
+ this.replayed = true
457
+ this.liveCallSeqs.add(request.callSeq ?? request.sourceCallSeq)
315
458
  }
316
459
  this.active = undefined
317
- if (terminate) void this.client.reset(worker)
318
- active.finish(result)
460
+ if (terminate) {
461
+ this.workerObservation = undefined
462
+ // A refused reclamation stays recorded on this kernel instead of becoming
463
+ // an unhandled rejection; the kernel keeps the worker until dispose.
464
+ this.resetWorker(worker)
465
+ }
466
+ if (!terminate && active.observing && replay === undefined) {
467
+ // Computation and journal settlement are complete. Observation cannot change either.
468
+ // done with observing=true precedes synchronous observation in the same worker turn.
469
+ this.workerObservation = { id: active.id, worker, started: true }
470
+ const timer = setTimeout(() => this.finishObservation({ id: active.id }), 250)
471
+ this.pendingObservation = { id: active.id, timer, journal, finish: () => active.finish(result) }
472
+ } else active.finish(result)
473
+ }
474
+
475
+ finishObservation(message) {
476
+ const inspection = this.pendingInspection
477
+ if (inspection !== undefined && inspection.id === message.id) {
478
+ inspection.finish(message.observation)
479
+ return
480
+ }
481
+ const pending = this.pendingObservation
482
+ if (pending === undefined || pending.id !== message.id) return
483
+ this.pendingObservation = undefined
484
+ clearTimeout(pending.timer)
485
+ const tentative = this.tentatives.get(pending.journal)
486
+ if (tentative !== undefined) tentative.observation = message.observation
487
+ pending.finish()
319
488
  }
320
489
 
321
490
  finalizeJournal(journal, confirmed) {
322
491
  const tentative = this.tentatives.get(journal)
323
492
  if (tentative === undefined) return
324
493
  this.tentatives.delete(journal)
494
+ this.unsettledCells--
325
495
  if (!confirmed) {
326
496
  if (journal.status === 'durable' || journal.status === 'volatile') {
327
497
  const reason = journal.volatileReason ?? 'run_code journal was not preserved in the final tool result'
@@ -329,7 +499,6 @@ class SessionKernel {
329
499
  type: 'volatile',
330
500
  reason,
331
501
  })
332
- this.bindingCatalog = tentative.bindingCatalog
333
502
  }
334
503
  return
335
504
  }
@@ -339,11 +508,11 @@ class SessionKernel {
339
508
  code: tentative.program,
340
509
  journal: normalized,
341
510
  ...(tentative.callSeq === undefined ? {} : { callSeq: tentative.callSeq }),
511
+ ...(tentative.userBindings === undefined ? {} : { userBindings: tentative.userBindings }),
342
512
  parent: this.history.head,
343
513
  })
344
514
  const index = this.history.nodes.push(node) - 1
345
515
  this.history.head = index
346
- this.bindingCatalog = tentative.bindingCatalog
347
516
  this.finishStateOperations(journal.operations, index, tentative.worker)
348
517
  return
349
518
  }
@@ -352,7 +521,6 @@ class SessionKernel {
352
521
  type: 'volatile',
353
522
  reason: journal.volatileReason,
354
523
  })
355
- this.bindingCatalog = tentative.bindingCatalog
356
524
  this.finishStateOperations(journal.operations, undefined, tentative.worker)
357
525
  }
358
526
  }
@@ -366,7 +534,11 @@ class SessionKernel {
366
534
  if (tentative === undefined) {
367
535
  return createReplMemorySnapshot(this.bindingCatalog.snapshot())
368
536
  }
369
- return createReplMemorySnapshot(tentative.bindingCatalog.snapshot())
537
+ return createReplMemorySnapshot(tentative.bindingCatalog.snapshot(), tentative.observation)
538
+ }
539
+
540
+ userBindingsFor(journal) {
541
+ return this.tentatives.get(journal)?.userBindings
370
542
  }
371
543
 
372
544
  finishStateOperations(operations, index, worker) {
@@ -375,12 +547,26 @@ class SessionKernel {
375
547
  this.history.checkpoints = transition.checkpoints
376
548
  if (transition.restored) {
377
549
  this.rollbackToDurable()
378
- void this.client.reset(worker)
550
+ this.resetWorker(worker)
379
551
  }
380
552
  }
381
553
 
382
- async dispose() {
554
+ dispose() {
555
+ if (this.disposal !== undefined) return this.disposal
556
+ const operation = this.#dispose()
557
+ this.disposal = operation
558
+ operation.then(
559
+ () => { if (this.disposal === operation) this.disposal = undefined },
560
+ () => { if (this.disposal === operation) this.disposal = undefined },
561
+ )
562
+ return operation
563
+ }
564
+
565
+ async #dispose() {
383
566
  this.disposed = true
567
+ this.pendingInspection?.finish()
568
+ this.workerObservation = undefined
569
+ // Keep any refused reclamation observable through this kernel's dispose.
384
570
  const worker = this.client.worker
385
571
  if (worker !== undefined) {
386
572
  /* c8 ignore next */
@@ -388,6 +574,18 @@ class SessionKernel {
388
574
  }
389
575
  await this.client.dispose()
390
576
  await this.tail
577
+ await Promise.all([...this.reclamationAttempts])
578
+ // A successful client disposal proves its owner no longer retains the
579
+ // helpers. Report every earlier refusal once, then let a later terminal
580
+ // retry release this already reclaimed kernel.
581
+ const reclamationFailures = [...this.reclamationRecords.values()]
582
+ .filter(record => record.failed)
583
+ .map(record => record.reason)
584
+ this.reclamationRecords.clear()
585
+ if (reclamationFailures.length === 1) throw reclamationFailures[0]
586
+ if (reclamationFailures.length > 1) {
587
+ throw new AggregateError(reclamationFailures, 'ptc-plus session kernel reclamation failed')
588
+ }
391
589
  }
392
590
  }
393
591
 
@@ -418,7 +616,13 @@ export class SessionRuntime {
418
616
  this.pendingNoops = new Map()
419
617
  this.settlements = new WeakSet()
420
618
  this.disposed = false
619
+ this.userBindingsCwd = options.userBindingsCwd ?? process.cwd()
620
+ if (typeof this.userBindingsCwd !== 'string' || !isAbsolute(this.userBindingsCwd)) {
621
+ throw new TypeError('userBindingsCwd must be an absolute path')
622
+ }
421
623
  this.withInitiator = typeof options.withInitiator === 'function' ? options.withInitiator : undefined
624
+ this.observeSession = options.observeSession ?? (() => false)
625
+ this.disposal = undefined
422
626
  }
423
627
 
424
628
  async run(sessionContext, request) {
@@ -435,6 +639,13 @@ export class SessionRuntime {
435
639
  this.config = resolved
436
640
  }
437
641
 
642
+ async observe(sessionId, memory, signal) {
643
+ if (this.disposed || !this.config.replViewEnabled) return undefined
644
+ const { observation: _observation, ...expected } = normalizeReplMemorySnapshot(memory)
645
+ if (!expected.available || expected.entries.length === 0) return undefined
646
+ return this.kernels.get(sessionId)?.observe(expected, signal)
647
+ }
648
+
438
649
  async runTentative(sessionContext, request) {
439
650
  const completed = result => Object.freeze({ result, settlement: undefined })
440
651
  if (this.disposed) return completed({ logs: [], error: { kind: 'abort', message: 'PTC runtime disposed' } })
@@ -458,10 +669,13 @@ export class SessionRuntime {
458
669
  sourceCallSeq = liveToolCallSeq(session, callId, 'run_code')
459
670
  callSeq = persistedCallSeq ?? sourceCallSeq
460
671
  } else {
461
- try {
462
- sourceCallSeq = liveToolCallSeq(session, callId, 'run_code')
463
- } catch {
464
- sourceCallSeq = undefined
672
+ sourceCallSeq = persistedCallSeq
673
+ if (sourceCallSeq === undefined) {
674
+ try {
675
+ sourceCallSeq = liveToolCallSeq(session, callId, 'run_code')
676
+ } catch {
677
+ sourceCallSeq = undefined
678
+ }
465
679
  }
466
680
  }
467
681
  } catch (error) {
@@ -472,9 +686,13 @@ export class SessionRuntime {
472
686
  let history
473
687
  try {
474
688
  history = cellConfig.durableReplay
475
- ? recoverJournal(session, callSeq)
689
+ ? recoverJournal(session, callSeq, {
690
+ visibleCallSeqs: visibleExecutableCallSeqs(session),
691
+ })
476
692
  : emptyHistory()
477
693
  } catch (error) {
694
+ // Malformed PTC metadata is contracted by the ordered recovery owner.
695
+ // Failure to read DSH's session source cannot prove a reset boundary.
478
696
  return completed({ logs: [], error: { kind: 'recovery', message: `cannot reconstruct REPL from session log: ${messageOf(error)}` } })
479
697
  }
480
698
  kernel = new SessionKernel({
@@ -482,15 +700,19 @@ export class SessionRuntime {
482
700
  history,
483
701
  cwd,
484
702
  session,
703
+ userBindingsCwd: this.userBindingsCwd,
485
704
  withInitiator: this.withInitiator,
705
+ observeValues: () => this.config.replViewEnabled && this.observeSession(sessionId),
486
706
  })
487
707
  this.kernels.set(sessionId, kernel)
488
708
  }
709
+ const policies = executionPolicies(cellConfig)
489
710
  const journal = createJournal(
490
711
  /* c8 ignore next */
491
712
  this.pendingNoops.get(sessionId) ?? [],
492
- cellConfig.looseTopLevelRedeclarations ? 'loose' : 'strict',
493
- rewritePolicy(cellConfig),
713
+ policies.bindingPolicy,
714
+ policies.rewritesEnabled,
715
+ policies.languageSemantics,
494
716
  )
495
717
  const workerReservation = kernel.reserveWorkerConfiguration(cellConfig)
496
718
  let result
@@ -504,6 +726,7 @@ export class SessionRuntime {
504
726
  kernel,
505
727
  sessionId,
506
728
  replMemory: kernel.replMemoryFor(journal),
729
+ userBindings: kernel.userBindingsFor(journal),
507
730
  ...(result.recoveryBoundaries === undefined
508
731
  ? {}
509
732
  : { recoveryBoundaries: result.recoveryBoundaries }),
@@ -548,14 +771,36 @@ export class SessionRuntime {
548
771
  const kernel = this.kernels.get(id)
549
772
  this.pendingNoops.delete(id)
550
773
  if (kernel === undefined) return
551
- this.kernels.delete(id)
774
+ // The entry survives a failed dispose so a repeated dispose reports the same
775
+ // unreclaimed instance instead of appearing to succeed.
552
776
  await kernel.dispose()
777
+ this.kernels.delete(id)
778
+ }
779
+
780
+ dispose() {
781
+ if (this.disposal !== undefined) return this.disposal
782
+ const operation = this.#dispose()
783
+ this.disposal = operation
784
+ operation.then(
785
+ () => { if (this.disposal === operation) this.disposal = undefined },
786
+ () => { if (this.disposal === operation) this.disposal = undefined },
787
+ )
788
+ return operation
553
789
  }
554
790
 
555
- async dispose() {
791
+ async #dispose() {
556
792
  this.disposed = true
557
- const kernels = [...this.kernels.values()]
558
- this.kernels.clear()
559
- await Promise.all(kernels.map(kernel => kernel.dispose()))
793
+ const entries = [...this.kernels]
794
+ const results = await Promise.allSettled(entries.map(async ([id, kernel]) => {
795
+ await kernel.dispose()
796
+ this.kernels.delete(id)
797
+ }))
798
+ const failures = []
799
+ for (const result of results) {
800
+ if (result.status === 'rejected') failures.push(result.reason)
801
+ }
802
+ if (failures.length > 0) {
803
+ throw new AggregateError(failures, 'ptc-plus session runtime disposal failed')
804
+ }
560
805
  }
561
806
  }