dsh-ptc-plus 0.3.2 → 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 (199) hide show
  1. package/README.en.md +65 -78
  2. package/README.md +65 -78
  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 +30596 -742
  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 +5 -3
  25. package/docs/adr/0007-separate-worker-transport-from-session-semantics.md +1 -1
  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 +3 -1
  31. package/docs/adr/0014-persist-cell-rewrite-policy.md +6 -2
  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 +5 -3
  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 +12 -0
  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 +73 -21
  48. package/docs/binding-review-placement-design.md +200 -0
  49. package/docs/capability-projection.md +1 -1
  50. package/docs/client-ui.md +95 -19
  51. package/docs/durability-design.md +25 -10
  52. package/docs/evaluation.md +78 -6
  53. package/docs/installation.md +41 -6
  54. package/docs/publishing.md +5 -3
  55. package/docs/runtime-reference.md +72 -25
  56. package/docs/semantic-validation.md +29 -0
  57. package/docs/stateful-binding-updates-proposal.md +755 -0
  58. package/docs/user-bindings.en.md +64 -0
  59. package/docs/user-bindings.md +64 -0
  60. package/docs/value-wire.md +3 -3
  61. package/docs/verification.md +57 -0
  62. package/index.js +369 -92
  63. package/internal/ast-traversal.js +5 -2
  64. package/internal/binding-descriptors.js +1 -0
  65. package/internal/binding-pattern-traversal.js +25 -0
  66. package/internal/binding-pattern.js +26 -29
  67. package/internal/binding-source-analysis.js +391 -0
  68. package/internal/binding-update-policy.js +66 -0
  69. package/internal/bounded-worker-round.js +56 -0
  70. package/internal/callable-definition-environment.js +42 -0
  71. package/internal/callable-source-catalog.js +129 -0
  72. package/internal/callable-source-encoding.js +47 -0
  73. package/internal/callable-source-facts.js +459 -0
  74. package/internal/callable-source-format.js +2 -0
  75. package/internal/callable-source-lowering.js +123 -0
  76. package/internal/cell-analysis-contract.js +25 -0
  77. package/internal/cell-analysis.js +240 -290
  78. package/internal/cell-completion.js +15 -0
  79. package/internal/cell-error.js +7 -0
  80. package/internal/cell-parser.js +66 -0
  81. package/internal/cell-rewriter.js +3 -677
  82. package/internal/commonjs-export-evidence.js +63 -0
  83. package/internal/commonjs-source-evidence.js +72 -0
  84. package/internal/compiler-amaro.cjs +2 -0
  85. package/internal/compiler-compression.js +12 -0
  86. package/internal/compiler-data.js +58 -0
  87. package/internal/compiler-descriptors.js +54 -0
  88. package/internal/compiler-dynamic-cache.js +48 -0
  89. package/internal/compiler-entry.js +35 -0
  90. package/internal/compiler-generated-names.js +17 -0
  91. package/internal/compiler-intrinsics.js +84 -0
  92. package/internal/compiler-module-links.js +34 -0
  93. package/internal/compiler-operations.js +61 -0
  94. package/internal/compiler-platform-factory.js +84 -0
  95. package/internal/compiler-platform.js +136 -0
  96. package/internal/compiler-record-roles.js +53 -0
  97. package/internal/compiler-region-output.js +259 -0
  98. package/internal/compiler-resource-lowering.js +103 -0
  99. package/internal/compiler-scope-facts.js +115 -0
  100. package/internal/compiler-service.js +109 -0
  101. package/internal/compiler-source-cache.js +32 -0
  102. package/internal/compiler-source-pieces.js +93 -0
  103. package/internal/compiler-source-regions.js +132 -0
  104. package/internal/compiler-storage-source.js +54 -0
  105. package/internal/compiler-text.js +15 -0
  106. package/internal/compiler-typescript.cjs +2 -0
  107. package/internal/config-spec.js +83 -27
  108. package/internal/cordis-tools-owner.js +130 -21
  109. package/internal/diagnostic.js +27 -1
  110. package/internal/direct-surface-owner.js +76 -23
  111. package/internal/dynamic-binding-evidence.js +16 -0
  112. package/internal/dynamic-environment-compiler.js +779 -0
  113. package/internal/dynamic-environment-integration.js +87 -0
  114. package/internal/dynamic-environment-runtime.js +362 -0
  115. package/internal/dynamic-native-calls.js +150 -0
  116. package/internal/dynamic-native-runtime.js +248 -0
  117. package/internal/dynamic-scope-analysis.js +90 -0
  118. package/internal/edit-transport-owner.js +27 -2
  119. package/internal/execution-seam-compat.js +193 -0
  120. package/internal/failure-reporting.js +118 -18
  121. package/internal/host-rpc.js +66 -0
  122. package/internal/isolated-worker.js +548 -0
  123. package/internal/kernel-child.js +139 -0
  124. package/internal/kernel-worker.js +904 -120
  125. package/internal/language-semantics.js +14 -0
  126. package/internal/legacy-binding-catalog.js +48 -0
  127. package/internal/legacy-cell-analysis.js +600 -0
  128. package/internal/legacy-cell-parser.js +50 -0
  129. package/internal/legacy-cell-rewriter.js +631 -0
  130. package/internal/legacy-repl-convenience.js +291 -0
  131. package/internal/managed-module-operations.js +170 -0
  132. package/internal/module-compilation.js +321 -0
  133. package/internal/module-runtime-platform.js +19 -0
  134. package/internal/module-transform-contract.js +12 -0
  135. package/internal/native-await-declarations.js +68 -0
  136. package/internal/native-root-compilation.js +54 -0
  137. package/internal/native-root-dynamic.js +67 -0
  138. package/internal/recovery-tips.js +27 -15
  139. package/internal/rejected-cell-editor.js +13 -8
  140. package/internal/repl-convenience.js +2 -289
  141. package/internal/repl-memory-projection.js +81 -12
  142. package/internal/repl-observation-interest.js +70 -0
  143. package/internal/repl-rewrite-contract.js +6 -0
  144. package/internal/repl-scope-normalizer.js +2091 -0
  145. package/internal/repl-value-observer.js +114 -0
  146. package/internal/rpc-contract.js +52 -0
  147. package/internal/runtime-bridge-owner.js +172 -28
  148. package/internal/runtime-config.js +48 -52
  149. package/internal/runtime-contexts.js +82 -10
  150. package/internal/runtime-messages.js +111 -0
  151. package/internal/session-cell-executor.js +401 -87
  152. package/internal/session-journal-recovery.js +684 -0
  153. package/internal/session-journal-schema.js +106 -0
  154. package/internal/session-journal.js +184 -654
  155. package/internal/session-log-view.js +42 -9
  156. package/internal/session-runtime.js +256 -95
  157. package/internal/session-state.js +247 -71
  158. package/internal/source-position-map.js +211 -71
  159. package/internal/stateful-module-compiler.js +154 -0
  160. package/internal/stateful-module-runtime.js +303 -0
  161. package/internal/stateful-root-compiler.js +598 -0
  162. package/internal/stateful-root-runtime.js +340 -0
  163. package/internal/typert-codec-compat.js +28 -0
  164. package/internal/typescript-transform.js +29 -0
  165. package/internal/user-binding-authoring.js +70 -0
  166. package/internal/user-binding-console-worker.js +184 -0
  167. package/internal/user-binding-console.js +148 -0
  168. package/internal/user-binding-draft-projection.js +335 -0
  169. package/internal/user-binding-model-context.js +31 -0
  170. package/internal/user-binding-runner.js +56 -0
  171. package/internal/user-bindings-owner.js +1065 -0
  172. package/internal/user-bindings-store.js +293 -0
  173. package/internal/user-bindings.js +353 -0
  174. package/internal/value-wire-schema.js +28 -0
  175. package/internal/value-wire.js +29 -20
  176. package/internal/worker-client.js +57 -67
  177. package/internal/worker-environment.js +45 -0
  178. package/internal/worker-output-relay.js +29 -0
  179. package/internal/worker-shutdown.js +14 -0
  180. package/package.json +75 -7
  181. package/src/client-activity.js +88 -53
  182. package/src/client-authoring-view.js +580 -0
  183. package/src/client-binding-review.js +230 -0
  184. package/src/client-bindings-data.js +29 -0
  185. package/src/client-catalog.js +127 -0
  186. package/src/client-code-editor.js +84 -0
  187. package/src/client-console.js +112 -0
  188. package/src/client-copy.js +370 -0
  189. package/src/client-feature-gates.js +62 -0
  190. package/src/client-host-compat.js +150 -0
  191. package/src/client-indicator-view.js +267 -0
  192. package/src/client-repl-view.js +292 -0
  193. package/src/client-rpc.js +20 -0
  194. package/src/client-settings-view.js +221 -0
  195. package/src/client-styles.js +106 -0
  196. package/src/client-tool-view.js +108 -0
  197. package/src/client-transport.js +159 -0
  198. package/src/client-workbench.js +521 -0
  199. package/src/client.js +233 -615
@@ -1,21 +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,
10
+ } from './session-journal.js'
11
+ import {
12
+ pathToHead,
13
+ recoveryBoundaryForHistory,
9
14
  recoverJournal,
10
15
  visibleExecutableCallSeqs,
11
- } from './session-journal.js'
16
+ } from './session-journal-recovery.js'
17
+ import { valueLimitsFromConfig } from './value-wire-schema.js'
12
18
  import { normalizeBindingDescriptors } from './binding-descriptors.js'
13
19
  import { resolveConfig } from './runtime-config.js'
14
20
  import { WorkerClient } from './worker-client.js'
21
+ import { compilerWorkerCache } from './compiler-service.js'
15
22
  import { BindingCatalog, durabilityState, transitionDurability } from './session-state.js'
16
23
  import { SessionCellExecutor } from './session-cell-executor.js'
24
+ import { executionPolicies } from './binding-update-policy.js'
17
25
  import {
18
26
  createReplMemorySnapshot,
27
+ normalizeReplMemorySnapshot,
19
28
  unavailableReplMemorySnapshot,
20
29
  } from './repl-memory-projection.js'
21
30
 
@@ -34,21 +43,6 @@ function recoveryDiagnostic(count) {
34
43
  })
35
44
  }
36
45
 
37
- function rewritePolicy(config) {
38
- return {
39
- autoRewriteImports: config.autoRewriteImports,
40
- autoStripExports: config.autoStripExports,
41
- autoSplitRedeclarations: config.autoSplitRedeclarations,
42
- }
43
- }
44
-
45
- function bindingPolicy(config) {
46
- return {
47
- variableRedeclarations: config.looseTopLevelRedeclarations,
48
- functionClassRedeclarations: config.looseTopLevelFunctionClassRedeclarations,
49
- }
50
- }
51
-
52
46
  function resolvedRuntimeConfig(config) {
53
47
  return Object.freeze(resolveConfig(config))
54
48
  }
@@ -72,15 +66,10 @@ class ReplayCancelled extends Error {
72
66
  }
73
67
 
74
68
  class SessionKernel {
75
- constructor({ config, history, cwd, session, withInitiator }) {
69
+ constructor({ config, history, cwd, session, userBindingsCwd, withInitiator, observeValues }) {
76
70
  this.config = config
77
71
  this.history = history
78
- this.initialRecoveryBoundary = history.available === false && history.volatileSuffix.length > 0
79
- ? {
80
- failedCallSeq: history.volatileSuffix[0].seq,
81
- frontierCallSeq: history.head === undefined ? null : history.nodes[history.head]?.callSeq ?? null,
82
- }
83
- : undefined
72
+ this.initialRecoveryBoundary = recoveryBoundaryForHistory(history)
84
73
  this.surfaceGeneration = undefined
85
74
  try {
86
75
  this.surfaceGeneration = session?.surface?.replaceGeneration
@@ -88,6 +77,8 @@ class SessionKernel {
88
77
  this.surfaceGeneration = undefined
89
78
  }
90
79
  this.cwd = cwd
80
+ this.userBindingsCwd = userBindingsCwd
81
+ this.observeValues = observeValues
91
82
  this.session = session
92
83
  this.withInitiator = withInitiator
93
84
  this.durability = durabilityState()
@@ -101,28 +92,56 @@ class SessionKernel {
101
92
  this.sequence = 0
102
93
  this.tail = Promise.resolve()
103
94
  this.tentatives = new WeakMap()
95
+ this.unsettledCells = 0
96
+ this.pendingInspection = undefined
97
+ this.pendingObservation = undefined
98
+ this.workerObservation = undefined
104
99
  this.workerReservations = new Set()
105
100
  this.cellExecutor = new SessionCellExecutor(this)
106
101
  this.client = new WorkerClient({
107
102
  workerUrl: WORKER_URL,
108
103
  cwd,
104
+ compilerCache: compilerWorkerCache,
109
105
  onMessage: message => this.cellExecutor.onMessage(message),
110
- onFailure: message => this.active?.resolve({
111
- logs: [], error: { kind: 'worker-exit', message },
112
- }, true),
106
+ onFailure: message => {
107
+ this.pendingInspection?.finish()
108
+ this.workerObservation = undefined
109
+ this.active?.resolve({ logs: [], error: { kind: 'worker-exit', message } }, true)
110
+ },
113
111
  })
114
112
  this.failures = createFailureTracker()
113
+ this.reclamationSequence = 0
114
+ this.reclamationRecords = new Map()
115
+ this.reclamationAttempts = new Set()
115
116
  this.disposed = false
117
+ this.disposal = undefined
116
118
  }
117
119
 
118
- valueLimits(config = this.config) {
119
- return {
120
- maxNodes: config.maxValueNodes,
121
- maxEdges: config.maxValueEdges,
122
- maxArrayLength: config.maxValueArrayLength,
123
- maxBigIntDigits: config.maxValueBigIntDigits,
124
- 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
125
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)
126
145
  }
127
146
 
128
147
  assertReconfigurationAllowed(config) {
@@ -156,6 +175,57 @@ class SessionKernel {
156
175
  return result
157
176
  }
158
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
+
159
229
  async execute(request, config) {
160
230
  const recoveryBoundaries = this.initialRecoveryBoundary === undefined
161
231
  ? []
@@ -187,6 +257,8 @@ class SessionKernel {
187
257
  visibleCallSeqs,
188
258
  })
189
259
  } catch (error) {
260
+ const boundary = recoveryBoundaryForHistory(this.history, undefined, { reset: true })
261
+ if (boundary !== undefined) recoveryBoundaries.push(boundary)
190
262
  this.history = emptyHistory()
191
263
  this.recoveryNotice = recoveryDiagnostic(1)
192
264
  this.initialRecoveryBoundary = undefined
@@ -194,14 +266,7 @@ class SessionKernel {
194
266
  if (this.history.volatileSuffix.length > 0) {
195
267
  this.recoveryNotice = recoveryDiagnostic(this.history.volatileSuffix.length)
196
268
  }
197
- this.initialRecoveryBoundary = this.history.available === false && this.history.volatileSuffix.length > 0
198
- ? {
199
- failedCallSeq: this.history.volatileSuffix[0].seq,
200
- frontierCallSeq: this.history.head === undefined
201
- ? null
202
- : this.history.nodes[this.history.head]?.callSeq ?? null,
203
- }
204
- : undefined
269
+ this.initialRecoveryBoundary = recoveryBoundaryForHistory(this.history)
205
270
  if (this.initialRecoveryBoundary !== undefined) {
206
271
  recoveryBoundaries.push(this.initialRecoveryBoundary)
207
272
  this.initialRecoveryBoundary = undefined
@@ -226,45 +291,28 @@ class SessionKernel {
226
291
  this.completeJournal(request.journal, 'noop', error.result)
227
292
  return finishResult(error.result)
228
293
  }
229
- if (!(error instanceof ReplayFailure)) {
230
- const result = {
231
- logs: [],
232
- error: {
233
- kind: 'recovery',
234
- message: `cannot reconstruct REPL from session log: ${messageOf(error)}`,
235
- },
236
- }
237
- this.completeJournal(request.journal, 'noop', result)
238
- return finishResult(result)
239
- }
240
- const frontier = error.node.parent === undefined
241
- ? undefined
242
- : this.history.nodes[error.node.parent]
243
- const previousPathLength = pathToHead(this.history).length
244
294
  try {
245
- const boundary = {
246
- failedCallSeq: error.node.callSeq,
247
- frontierCallSeq: frontier?.callSeq ?? null,
248
- }
295
+ if (!(error instanceof ReplayFailure)) throw error
296
+ const previousPathLength = pathToHead(this.history).length
297
+ const boundary = recoveryBoundaryForHistory(this.history, error.node)
249
298
  const recovered = recoverJournal(this.session, request.callSeq, {
250
299
  extraBoundaries: [boundary],
251
300
  visibleCallSeqs: visibleExecutableCallSeqs(this.session),
252
301
  })
302
+ const nextPathLength = pathToHead(recovered).length
303
+ if (nextPathLength >= previousPathLength) throw new Error('recovery did not contract the historical frontier')
253
304
  recoveryBoundaries.push(boundary)
254
305
  this.history = recovered
255
- } catch (boundaryError) {
256
- const result = {
257
- logs: [],
258
- error: {
259
- kind: 'recovery',
260
- message: `cannot apply REPL recovery boundary: ${messageOf(boundaryError)}`,
261
- },
262
- }
263
- this.completeJournal(request.journal, 'noop', result)
264
- 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++
265
315
  }
266
- const nextPathLength = pathToHead(this.history).length
267
- skipped += Math.max(1, previousPathLength - nextPathLength)
268
316
  this.durability = durabilityState()
269
317
  }
270
318
  }
@@ -281,7 +329,8 @@ class SessionKernel {
281
329
  if (result.error === undefined) {
282
330
  this.failures.reset()
283
331
  } else {
284
- 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)
285
334
  if (hint !== undefined) {
286
335
  result.logs = [...result.logs, renderDiagnostic(hint, request.program)]
287
336
  if (request.journal !== undefined) request.journal.diagnostics.push(hint)
@@ -296,7 +345,12 @@ class SessionKernel {
296
345
  for (const node of path) {
297
346
  try {
298
347
  const result = await this.cellExecutor.executeCell(
299
- { ...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
+ },
300
354
  node.journal,
301
355
  config,
302
356
  )
@@ -338,12 +392,14 @@ class SessionKernel {
338
392
  journal.operations = journal.operations.filter(operation => operation.action !== 'save')
339
393
  }
340
394
  if (status === 'discarded' || status === 'noop') {
395
+ journal.userBindingNames = null
341
396
  journal.calls.length = 0
342
397
  journal.operations.length = 0
343
398
  }
344
399
  }
345
400
 
346
401
  rollbackToDurable() {
402
+ this.workerObservation = undefined
347
403
  this.durability = durabilityState()
348
404
  this.replayed = false
349
405
  this.bindingCatalog = new BindingCatalog()
@@ -353,7 +409,8 @@ class SessionKernel {
353
409
  settleCell(active, result, terminate = false) {
354
410
  /* c8 ignore next */
355
411
  if (this.active !== active) return
356
- const { request, journal, replay, prepared, worker } = active
412
+ active.settled = true
413
+ const { request, journal, replay, worker } = active
357
414
  clearInterval(active.computeTimer)
358
415
  clearTimeout(active.wallTimer)
359
416
  request.signal?.removeEventListener('abort', active.onAbort)
@@ -361,6 +418,10 @@ class SessionKernel {
361
418
  result = { ...result, rewrites: active.rewrites }
362
419
  }
363
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
364
425
  if (terminate) {
365
426
  const volatileReason = active.pendingBindings.values().next().value ?? active.durability.reason
366
427
  this.completeJournal(journal, 'discarded', result, volatileReason, active.diagnostics)
@@ -379,28 +440,58 @@ class SessionKernel {
379
440
  this.tentatives.set(journal, {
380
441
  callSeq: request.callSeq,
381
442
  program: request.program,
382
- bindingCatalog: active.appliedBindingCatalog
383
- ?? active.priorBindingCatalog.advance(prepared, request.program),
443
+ bindingCatalog: active.appliedBindingCatalog,
444
+ userBindings: active.userBindingSnapshot,
384
445
  worker,
385
446
  })
447
+ this.unsettledCells++
386
448
  }
387
449
  }
388
- if (replay !== undefined && !terminate) {
450
+ if (!terminate) {
389
451
  this.bindingCatalog = active.appliedBindingCatalog
390
- ?? active.priorBindingCatalog.advance(prepared, request.program)
391
452
  }
392
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
393
457
  this.liveCallSeqs.add(request.callSeq ?? request.sourceCallSeq)
394
458
  }
395
459
  this.active = undefined
396
- if (terminate) void this.client.reset(worker)
397
- 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()
398
488
  }
399
489
 
400
490
  finalizeJournal(journal, confirmed) {
401
491
  const tentative = this.tentatives.get(journal)
402
492
  if (tentative === undefined) return
403
493
  this.tentatives.delete(journal)
494
+ this.unsettledCells--
404
495
  if (!confirmed) {
405
496
  if (journal.status === 'durable' || journal.status === 'volatile') {
406
497
  const reason = journal.volatileReason ?? 'run_code journal was not preserved in the final tool result'
@@ -408,7 +499,6 @@ class SessionKernel {
408
499
  type: 'volatile',
409
500
  reason,
410
501
  })
411
- this.bindingCatalog = tentative.bindingCatalog
412
502
  }
413
503
  return
414
504
  }
@@ -418,11 +508,11 @@ class SessionKernel {
418
508
  code: tentative.program,
419
509
  journal: normalized,
420
510
  ...(tentative.callSeq === undefined ? {} : { callSeq: tentative.callSeq }),
511
+ ...(tentative.userBindings === undefined ? {} : { userBindings: tentative.userBindings }),
421
512
  parent: this.history.head,
422
513
  })
423
514
  const index = this.history.nodes.push(node) - 1
424
515
  this.history.head = index
425
- this.bindingCatalog = tentative.bindingCatalog
426
516
  this.finishStateOperations(journal.operations, index, tentative.worker)
427
517
  return
428
518
  }
@@ -431,7 +521,6 @@ class SessionKernel {
431
521
  type: 'volatile',
432
522
  reason: journal.volatileReason,
433
523
  })
434
- this.bindingCatalog = tentative.bindingCatalog
435
524
  this.finishStateOperations(journal.operations, undefined, tentative.worker)
436
525
  }
437
526
  }
@@ -445,7 +534,11 @@ class SessionKernel {
445
534
  if (tentative === undefined) {
446
535
  return createReplMemorySnapshot(this.bindingCatalog.snapshot())
447
536
  }
448
- 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
449
542
  }
450
543
 
451
544
  finishStateOperations(operations, index, worker) {
@@ -454,12 +547,26 @@ class SessionKernel {
454
547
  this.history.checkpoints = transition.checkpoints
455
548
  if (transition.restored) {
456
549
  this.rollbackToDurable()
457
- void this.client.reset(worker)
550
+ this.resetWorker(worker)
458
551
  }
459
552
  }
460
553
 
461
- 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() {
462
566
  this.disposed = true
567
+ this.pendingInspection?.finish()
568
+ this.workerObservation = undefined
569
+ // Keep any refused reclamation observable through this kernel's dispose.
463
570
  const worker = this.client.worker
464
571
  if (worker !== undefined) {
465
572
  /* c8 ignore next */
@@ -467,6 +574,18 @@ class SessionKernel {
467
574
  }
468
575
  await this.client.dispose()
469
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
+ }
470
589
  }
471
590
  }
472
591
 
@@ -497,7 +616,13 @@ export class SessionRuntime {
497
616
  this.pendingNoops = new Map()
498
617
  this.settlements = new WeakSet()
499
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
+ }
500
623
  this.withInitiator = typeof options.withInitiator === 'function' ? options.withInitiator : undefined
624
+ this.observeSession = options.observeSession ?? (() => false)
625
+ this.disposal = undefined
501
626
  }
502
627
 
503
628
  async run(sessionContext, request) {
@@ -514,6 +639,13 @@ export class SessionRuntime {
514
639
  this.config = resolved
515
640
  }
516
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
+
517
649
  async runTentative(sessionContext, request) {
518
650
  const completed = result => Object.freeze({ result, settlement: undefined })
519
651
  if (this.disposed) return completed({ logs: [], error: { kind: 'abort', message: 'PTC runtime disposed' } })
@@ -559,6 +691,8 @@ export class SessionRuntime {
559
691
  })
560
692
  : emptyHistory()
561
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.
562
696
  return completed({ logs: [], error: { kind: 'recovery', message: `cannot reconstruct REPL from session log: ${messageOf(error)}` } })
563
697
  }
564
698
  kernel = new SessionKernel({
@@ -566,15 +700,19 @@ export class SessionRuntime {
566
700
  history,
567
701
  cwd,
568
702
  session,
703
+ userBindingsCwd: this.userBindingsCwd,
569
704
  withInitiator: this.withInitiator,
705
+ observeValues: () => this.config.replViewEnabled && this.observeSession(sessionId),
570
706
  })
571
707
  this.kernels.set(sessionId, kernel)
572
708
  }
709
+ const policies = executionPolicies(cellConfig)
573
710
  const journal = createJournal(
574
711
  /* c8 ignore next */
575
712
  this.pendingNoops.get(sessionId) ?? [],
576
- bindingPolicy(cellConfig),
577
- rewritePolicy(cellConfig),
713
+ policies.bindingPolicy,
714
+ policies.rewritesEnabled,
715
+ policies.languageSemantics,
578
716
  )
579
717
  const workerReservation = kernel.reserveWorkerConfiguration(cellConfig)
580
718
  let result
@@ -588,6 +726,7 @@ export class SessionRuntime {
588
726
  kernel,
589
727
  sessionId,
590
728
  replMemory: kernel.replMemoryFor(journal),
729
+ userBindings: kernel.userBindingsFor(journal),
591
730
  ...(result.recoveryBoundaries === undefined
592
731
  ? {}
593
732
  : { recoveryBoundaries: result.recoveryBoundaries }),
@@ -632,14 +771,36 @@ export class SessionRuntime {
632
771
  const kernel = this.kernels.get(id)
633
772
  this.pendingNoops.delete(id)
634
773
  if (kernel === undefined) return
635
- 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.
636
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
637
789
  }
638
790
 
639
- async dispose() {
791
+ async #dispose() {
640
792
  this.disposed = true
641
- const kernels = [...this.kernels.values()]
642
- this.kernels.clear()
643
- 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
+ }
644
805
  }
645
806
  }