dsh-ptc-plus 0.4.1 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/README.en.md +164 -78
  2. package/README.md +165 -79
  3. package/assets/dsh-ptc-plus-banner-en.webp +0 -0
  4. package/assets/dsh-ptc-plus-banner-zh.webp +0 -0
  5. package/assets/ptc-plus-icon.svg +5 -0
  6. package/client.js +854 -336
  7. package/compiler-core.cjs +161 -157
  8. package/compiler-core.cjs.map +3 -3
  9. package/docs/README.md +4 -1
  10. package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +8 -7
  11. package/docs/adr/0008-define-agent-operating-contract.md +2 -2
  12. package/docs/adr/0010-session-log-derived-recovery-tips.md +7 -0
  13. package/docs/adr/0016-archive-resolved-review-findings.md +3 -3
  14. package/docs/adr/0019-plugin-settings-and-kill-switch.md +3 -3
  15. package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +2 -0
  16. package/docs/adr/0021-separate-host-bootstrap-from-session-runtime.md +14 -3
  17. package/docs/adr/0023-global-user-bindings.md +7 -7
  18. package/docs/adr/0025-use-versioned-logical-binding-identities.md +3 -3
  19. package/docs/adr/0026-persist-the-delivery-verdict-against-the-verified-candidate.md +23 -7
  20. package/docs/adr/0027-run-the-session-kernel-in-a-killable-helper-process.md +9 -5
  21. package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +26 -6
  22. package/docs/adr/0031-close-semantic-obligations-by-owner.md +29 -0
  23. package/docs/adr/README.md +4 -4
  24. package/docs/architecture.md +15 -10
  25. package/docs/binding-review-placement-design.md +1 -1
  26. package/docs/client-ui.md +9 -5
  27. package/docs/durability-design.md +12 -9
  28. package/docs/installation.md +27 -9
  29. package/docs/runtime-reference.md +23 -12
  30. package/docs/semantic-validation.md +7 -3
  31. package/docs/stateful-binding-updates-proposal.md +8 -4
  32. package/docs/user-bindings.en.md +10 -7
  33. package/docs/user-bindings.md +9 -6
  34. package/docs/value-wire.md +1 -1
  35. package/docs/verification-optimization-work.md +41 -0
  36. package/docs/verification.md +53 -2
  37. package/index.js +39 -34
  38. package/internal/async-local-storage-intrinsics.js +43 -0
  39. package/internal/binding-declaration-contract.js +8 -0
  40. package/internal/binding-source-analysis.js +381 -64
  41. package/internal/binding-update-policy.js +7 -1
  42. package/internal/callable-source-facts.js +37 -1
  43. package/internal/cell-analysis.js +85 -17
  44. package/internal/compiler-intrinsics.js +9 -43
  45. package/internal/compiler-module-links.js +7 -4
  46. package/internal/config-spec.js +1 -1
  47. package/internal/cordis-tools-owner.js +20 -10
  48. package/internal/dynamic-environment-integration.js +8 -4
  49. package/internal/dynamic-environment-runtime.js +24 -3
  50. package/internal/dynamic-native-calls.js +4 -1
  51. package/internal/dynamic-native-runtime.js +15 -1
  52. package/internal/edit-transport-owner.js +80 -20
  53. package/internal/execution-seam-compat.js +192 -30
  54. package/internal/failure-reporting.js +38 -30
  55. package/internal/isolated-worker.js +17 -10
  56. package/internal/kernel-worker.js +645 -393
  57. package/internal/message-sources.js +14 -0
  58. package/internal/module-compilation.js +7 -5
  59. package/internal/module-transform-contract.js +32 -2
  60. package/internal/record-utils.js +24 -15
  61. package/internal/repl-memory-projection.js +10 -0
  62. package/internal/repl-scope-normalizer.js +72 -35
  63. package/internal/repl-value-observer.js +145 -46
  64. package/internal/runtime-config.js +62 -2
  65. package/internal/runtime-intrinsics.js +104 -0
  66. package/internal/runtime-messages.js +6 -4
  67. package/internal/session-cell-executor.js +26 -2
  68. package/internal/session-journal-recovery.js +267 -67
  69. package/internal/session-journal-schema.js +30 -13
  70. package/internal/session-journal.js +485 -44
  71. package/internal/session-log-view.js +5 -4
  72. package/internal/session-runtime.js +46 -21
  73. package/internal/session-state.js +7 -29
  74. package/internal/settings-compat.js +66 -1
  75. package/internal/source-position-map.js +38 -0
  76. package/internal/stateful-root-compiler.js +3 -1
  77. package/internal/typescript-transform.js +4 -2
  78. package/internal/user-binding-console-worker.js +115 -41
  79. package/internal/user-binding-draft-projection.js +3 -2
  80. package/internal/user-bindings-owner.js +2 -1
  81. package/internal/user-bindings.js +16 -3
  82. package/internal/validated-parse-repair.js +6 -2
  83. package/internal/value-wire.js +189 -94
  84. package/internal/worker-client.js +99 -8
  85. package/internal/worker-cwd-virtualization.js +82 -54
  86. package/internal/worker-environment.js +20 -0
  87. package/internal/worker-output-capture.js +187 -0
  88. package/internal/worker-output-fence.js +26 -0
  89. package/internal/worker-realm-surfaces.js +197 -0
  90. package/internal/worker-repl-realm.js +140 -0
  91. package/package.json +37 -4
  92. package/src/client-activity.js +12 -2
  93. package/src/client-authoring-view.js +121 -62
  94. package/src/client-console.js +4 -2
  95. package/src/client-copy.js +4 -2
  96. package/src/client-feature-gates.js +7 -9
  97. package/src/client-host-compat.js +104 -27
  98. package/src/client-repl-view.js +3 -1
  99. package/src/client-styles.js +12 -1
  100. package/src/client-tool-view.js +3 -1
  101. package/src/client-transport.js +2 -2
  102. package/src/client-workbench.js +4 -3
  103. package/src/client.js +56 -17
@@ -1,9 +1,12 @@
1
1
  import { mkdtemp, rm } from 'node:fs/promises'
2
+ import { randomBytes } from 'node:crypto'
2
3
  import { tmpdir } from 'node:os'
3
4
  import { isAbsolute, join } from 'node:path'
4
5
  import { messageOf, processExitDescription } from './failure-reporting.js'
5
6
  import { IsolatedOwner } from './isolated-worker.js'
6
7
  import { normalizeWorkerEnvironment } from './worker-environment.js'
8
+ import { WorkerOutputCapture } from './worker-output-capture.js'
9
+ import { stripOutputFenceMarkers } from './worker-output-fence.js'
7
10
 
8
11
  export { normalizeWorkerEnvironment }
9
12
 
@@ -11,16 +14,21 @@ const KERNEL_HELPER = new URL('./kernel-child.js', import.meta.url)
11
14
 
12
15
  /** Owns one session kernel's worker process, private port, and scratch directory. */
13
16
  export class WorkerClient {
14
- constructor({ workerUrl, cwd, onMessage, onFailure, compilerCache }) {
17
+ constructor({ workerUrl, cwd, onMessage, onFailure, onUnmatchedOutputFailure, compilerCache }) {
15
18
  this.workerUrl = workerUrl
16
19
  this.cwd = cwd
17
20
  this.onMessage = onMessage
18
21
  this.onFailure = onFailure
22
+ this.onUnmatchedOutputFailure = onUnmatchedOutputFailure ?? (() => false)
19
23
  this.compilerCache = compilerCache
20
24
  this.worker = undefined
21
25
  this.workerLimit = undefined
22
26
  this.workerReady = undefined
23
27
  this.port = undefined
28
+ this.outputCapture = undefined
29
+ this.outputCompletionPending = undefined
30
+ this.deferredOutputMessages = []
31
+ this.pendingOutputFailure = undefined
24
32
  this.scratchReady = undefined
25
33
  this.stderrTails = new WeakMap()
26
34
  // One owner keeps every started kernel transport, including startup failures
@@ -32,13 +40,18 @@ export class WorkerClient {
32
40
 
33
41
  /** Last non-empty stderr line of a failed worker, capped for diagnostics. */
34
42
  stderrDetail(worker) {
35
- const tail = this.stderrTails.get(worker)?.join('').trim()
43
+ const tail = stripOutputFenceMarkers(this.stderrTails.get(worker)?.join('')).trim()
36
44
  if (tail === undefined || tail.length === 0) return undefined
37
45
  const lastLine = tail.split(/\r?\n/).map(line => line.trim()).filter(line => line.length > 0).slice(-1)[0]
38
46
  return lastLine === undefined ? undefined : lastLine.slice(0, 300)
39
47
  }
40
48
 
41
49
  async ensure(maxOldGenerationSizeMb) {
50
+ if (this.pendingOutputFailure !== undefined) {
51
+ const message = this.pendingOutputFailure
52
+ this.pendingOutputFailure = undefined
53
+ throw new Error(`worker output attribution failed: ${message}`)
54
+ }
42
55
  if (this.workerLimit !== undefined && this.workerLimit !== maxOldGenerationSizeMb) {
43
56
  throw new Error('ptc-plus: session worker memory limit differs from the submitted cell configuration')
44
57
  }
@@ -86,15 +99,21 @@ export class WorkerClient {
86
99
  this.workerLimit = undefined
87
100
  throw error
88
101
  }
89
- worker.stdout.resume()
90
- worker.stderr.resume()
102
+ const outputCapture = new WorkerOutputCapture()
103
+ this.outputCapture = outputCapture
91
104
  const stderrTail = []
105
+ worker.stdout.on?.('data', chunk => {
106
+ this.handleCapturedOutput(worker, outputCapture.push('stdout', chunk))
107
+ })
92
108
  worker.stderr.on?.('data', (chunk) => {
93
109
  const text = typeof chunk === 'string' ? chunk : String(chunk ?? '')
94
110
  stderrTail.push(text)
95
111
  const bounded = Buffer.from(stderrTail.join('')).subarray(-2048).toString('utf8')
96
112
  stderrTail.splice(0, stderrTail.length, bounded)
113
+ this.handleCapturedOutput(worker, outputCapture.push('stderr', chunk))
97
114
  })
115
+ worker.stdout.resume()
116
+ worker.stderr.resume()
98
117
  this.stderrTails.set(worker, stderrTail)
99
118
  worker.on('error', error => this.fail(worker, `worker error: ${messageOf(error)}`))
100
119
  worker.on('exit', (code, signal) => {
@@ -135,9 +154,7 @@ export class WorkerClient {
135
154
  return
136
155
  }
137
156
  this.port = message.port
138
- this.port.on('message', (value) => {
139
- if (worker === this.worker) this.onMessage(value)
140
- })
157
+ this.port.on('message', value => this.handleWorkerMessage(worker, outputCapture, value))
141
158
  resolve(worker)
142
159
  })
143
160
  })
@@ -145,7 +162,17 @@ export class WorkerClient {
145
162
  }
146
163
 
147
164
  post(message) {
148
- this.port.postMessage(message)
165
+ if (message?.type !== 'run') {
166
+ this.port.postMessage(message)
167
+ return
168
+ }
169
+ const outputFence = randomBytes(24).toString('hex')
170
+ const failure = this.outputCapture?.begin(message.id, message.maxOutputBytes, outputFence)
171
+ if (failure !== undefined) {
172
+ this.handleCapturedOutput(this.worker, failure)
173
+ return
174
+ }
175
+ this.port.postMessage({ ...message, outputFence })
149
176
  }
150
177
 
151
178
  /** Best-effort reply for a request whose lease may outlive a worker reset. */
@@ -153,11 +180,68 @@ export class WorkerClient {
153
180
  this.port?.postMessage(message)
154
181
  }
155
182
 
183
+ handleWorkerMessage(worker, outputCapture, value) {
184
+ if (worker !== this.worker) return
185
+ const outputProtocol = value?.type === 'output-start'
186
+ || value?.type === 'late-output'
187
+ || value?.type === 'done'
188
+ if (this.outputCompletionPending !== undefined && !outputProtocol) {
189
+ this.deferredOutputMessages.push(value)
190
+ return
191
+ }
192
+ const captured = value?.type === 'output-start'
193
+ ? outputCapture.start(value)
194
+ : value?.type === 'late-output'
195
+ ? outputCapture.lateOutput()
196
+ : value?.type === 'done'
197
+ ? outputCapture.complete(value)
198
+ : value
199
+ if (value?.type === 'done' && captured === undefined) {
200
+ this.outputCompletionPending = value.id
201
+ return
202
+ }
203
+ this.handleCapturedOutput(worker, captured)
204
+ }
205
+
206
+ handleCapturedOutput(worker, message) {
207
+ if (message === undefined || worker !== this.worker) return
208
+ if (message.type === 'worker-output-started') {
209
+ this.port?.postMessage({
210
+ type: 'output-start-ack',
211
+ id: message.id,
212
+ outputFence: message.outputFence,
213
+ })
214
+ return
215
+ }
216
+ const releasesDeferred = message.type === 'done' && message.id === this.outputCompletionPending
217
+ const discardsDeferred = message.id === this.outputCompletionPending
218
+ && (message.type === 'worker-output-error' || message.type === 'output-limit')
219
+ if (releasesDeferred || discardsDeferred) this.outputCompletionPending = undefined
220
+ const consumed = this.onMessage(message) === true
221
+ if (message.type === 'worker-output-error' && worker === this.worker) {
222
+ const failureConsumed = consumed || this.onUnmatchedOutputFailure(
223
+ `worker output attribution failed: ${message.message}`,
224
+ ) === true
225
+ if (!failureConsumed) this.pendingOutputFailure = message.message
226
+ void this.reset(worker).catch(error => this.onFailure(`worker output reset failed: ${messageOf(error)}`))
227
+ return
228
+ }
229
+ if (releasesDeferred) {
230
+ const deferred = this.deferredOutputMessages
231
+ this.deferredOutputMessages = []
232
+ for (const value of deferred) this.onMessage(value)
233
+ } else if (discardsDeferred) this.deferredOutputMessages = []
234
+ }
235
+
156
236
  fail(worker, message) {
157
237
  if (worker !== this.worker) return
158
238
  this.worker = undefined
159
239
  this.workerLimit = undefined
160
240
  this.workerReady = undefined
241
+ this.outputCapture = undefined
242
+ this.outputCompletionPending = undefined
243
+ this.deferredOutputMessages = []
244
+ this.pendingOutputFailure = undefined
161
245
  this.port?.close()
162
246
  this.port = undefined
163
247
  const detail = this.stderrDetail(worker)
@@ -170,6 +254,9 @@ export class WorkerClient {
170
254
  this.worker = undefined
171
255
  this.workerLimit = undefined
172
256
  this.workerReady = undefined
257
+ this.outputCapture = undefined
258
+ this.outputCompletionPending = undefined
259
+ this.deferredOutputMessages = []
173
260
  this.port = undefined
174
261
  }
175
262
  await this.owner.stop(this.hostIds.get(worker))
@@ -184,6 +271,10 @@ export class WorkerClient {
184
271
  this.worker = undefined
185
272
  this.workerLimit = undefined
186
273
  this.workerReady = undefined
274
+ this.outputCapture = undefined
275
+ this.outputCompletionPending = undefined
276
+ this.deferredOutputMessages = []
277
+ this.pendingOutputFailure = undefined
187
278
  this.port = undefined
188
279
  if (worker !== undefined) this.hostIds.delete(worker)
189
280
  const failures = []
@@ -1,5 +1,15 @@
1
1
  import { isAbsolute, parse, resolve, sep } from 'node:path'
2
2
  import { promisify } from 'node:util'
3
+ import { runtimeIntrinsics as internal } from './runtime-intrinsics.js'
4
+ import {
5
+ WORKER_REALM_MUTATION,
6
+ assignWorkerRealmProperty,
7
+ installProjectedProcessCwd,
8
+ } from './worker-realm-surfaces.js'
9
+
10
+ const { Reflect: privateReflect, appendArray, bufferConcat, bufferIsBuffer, bufferToString,
11
+ copyArray, isArray, regexpTest } = internal
12
+ const TRAILING_SEPARATOR = /[\\/]$/
3
13
 
4
14
  const FILE_SYSTEM_PATH_ARGUMENTS = Object.freeze({
5
15
  access: [0], accessSync: [0], appendFile: [0], appendFileSync: [0],
@@ -34,7 +44,7 @@ const CHILD_PROCESS_PROJECTED_PROPERTIES = Object.freeze({
34
44
  function wrapBuiltinCallable(original, projectArguments, projectedProperties = []) {
35
45
  const wrapped = {
36
46
  invoke(...args) {
37
- return Reflect.apply(original, this, projectArguments(args))
47
+ return privateReflect.apply(original, this, projectArguments(args))
38
48
  },
39
49
  }.invoke
40
50
  const descriptors = Object.getOwnPropertyDescriptors(original)
@@ -56,83 +66,100 @@ function wrapBuiltinCallable(original, projectArguments, projectedProperties = [
56
66
  function wrapBuiltin(owner, name, projectArguments, projectedProperties) {
57
67
  const original = owner?.[name]
58
68
  if (typeof original !== 'function') return
59
- owner[name] = wrapBuiltinCallable(original, projectArguments, projectedProperties)
69
+ assignWorkerRealmProperty(WORKER_REALM_MUTATION.STABLE, 'cwd-projected Node builtins',
70
+ owner, name, wrapBuiltinCallable(original, projectArguments, projectedProperties))
71
+ }
72
+
73
+ export function projectChildProcessArguments(name, args, sessionCwd) {
74
+ const sessionChildOptions = options => (
75
+ options === null || typeof options !== 'object' || options.cwd !== undefined
76
+ ? options
77
+ : { ...options, cwd: sessionCwd }
78
+ )
79
+ const withDefaultOptions = (index) => {
80
+ const next = copyArray(args)
81
+ next[index] = sessionChildOptions(next[index]) ?? { cwd: sessionCwd }
82
+ return next
83
+ }
84
+ const insertDefaultOptions = (index) => {
85
+ const next = copyArray(args, 0, index)
86
+ appendArray(next, { cwd: sessionCwd })
87
+ return copyArray(args, index, args.length, next)
88
+ }
89
+ if (name === 'exec' || name === 'execSync') {
90
+ const next = copyArray(args)
91
+ if (typeof next[1] === 'function') return insertDefaultOptions(1)
92
+ else next[1] = sessionChildOptions(next[1]) ?? { cwd: sessionCwd }
93
+ return next
94
+ }
95
+ if (name === 'execFile' || name === 'execFileSync') {
96
+ if (args.length === 1 || typeof args[1] === 'function') {
97
+ return insertDefaultOptions(1)
98
+ }
99
+ if (typeof args[2] === 'function') {
100
+ if (args[1] !== undefined && !isArray(args[1])) return withDefaultOptions(1)
101
+ return insertDefaultOptions(2)
102
+ }
103
+ if (args[1] !== undefined && !isArray(args[1])) return withDefaultOptions(1)
104
+ return withDefaultOptions(2)
105
+ }
106
+ return args.length === 1 || (args[1] !== undefined && !isArray(args[1]))
107
+ ? withDefaultOptions(1)
108
+ : withDefaultOptions(2)
60
109
  }
61
110
 
62
111
  /** Install one session's cwd projection into Node builtins loaded by the worker. */
63
- export function installWorkerCwdVirtualization(sessionCwd, originalRequire) {
64
- if (sessionCwd === undefined) return
112
+ export function installWorkerCwdVirtualization(sessionCwd, originalRequire, markVolatile) {
113
+ if (sessionCwd === undefined) {
114
+ const nativeCwd = process.cwd
115
+ assignWorkerRealmProperty(WORKER_REALM_MUTATION.STABLE, 'process.cwd', process, 'cwd', () => {
116
+ markVolatile('process.cwd')
117
+ return privateReflect.apply(nativeCwd, process, [])
118
+ })
119
+ return
120
+ }
65
121
  const nativeResolve = resolve
66
- const bufferPrefix = Buffer.from(sessionCwd + (/[\\/]$/.test(sessionCwd) ? '' : sep))
122
+ const bufferPrefix = Buffer.from(sessionCwd + (regexpTest(TRAILING_SEPARATOR, sessionCwd) ? '' : sep))
67
123
  const sessionPath = (value) => {
68
124
  if (typeof value === 'string') return isAbsolute(value) ? value : nativeResolve(sessionCwd, value)
69
- if (Buffer.isBuffer(value)) {
70
- const absolute = isAbsolute(String.fromCharCode(...value.subarray(0, 3)))
71
- return absolute ? value : Buffer.concat([bufferPrefix, value])
125
+ if (bufferIsBuffer(value)) {
126
+ const absolute = isAbsolute(bufferToString(value, 'utf8', 0, 3))
127
+ return absolute ? value : bufferConcat([bufferPrefix, value])
72
128
  }
73
129
  return value
74
130
  }
75
131
  const sessionPathPrefix = (value) => {
76
132
  if (typeof value !== 'string' || isAbsolute(value) || parse(value).root !== '') return sessionPath(value)
77
- return sessionCwd + (/[\\/]$/.test(sessionCwd) ? '' : sep) + value
133
+ return sessionCwd + (regexpTest(TRAILING_SEPARATOR, sessionCwd) ? '' : sep) + value
78
134
  }
79
135
  const projectPaths = (args, indices) => {
80
- const next = [...args]
81
- for (const index of indices) next[index] = sessionPath(next[index])
136
+ const next = copyArray(args)
137
+ for (let offset = 0; offset < indices.length; offset += 1) {
138
+ const index = indices[offset]
139
+ next[index] = sessionPath(next[index])
140
+ }
82
141
  return next
83
142
  }
84
143
  const projectPrefix = (args) => {
85
- const next = [...args]
144
+ const next = copyArray(args)
86
145
  next[0] = sessionPathPrefix(next[0])
87
146
  return next
88
147
  }
89
- const sessionChildOptions = options => (
90
- options === null || typeof options !== 'object' || options.cwd !== undefined
91
- ? options
92
- : { ...options, cwd: sessionCwd }
93
- )
94
- const withDefaultOptions = (args, index) => {
95
- const next = [...args]
96
- next[index] = sessionChildOptions(next[index]) ?? { cwd: sessionCwd }
97
- return next
98
- }
99
- const childProcessOptions = (name, args) => {
100
- if (name === 'exec' || name === 'execSync') {
101
- const next = [...args]
102
- if (typeof next[1] === 'function') next.splice(1, 0, { cwd: sessionCwd })
103
- else next[1] = sessionChildOptions(next[1]) ?? { cwd: sessionCwd }
104
- return next
105
- }
106
- if (name === 'execFile' || name === 'execFileSync') {
107
- if (args.length === 1 || typeof args[1] === 'function') {
108
- const next = [...args]
109
- next.splice(1, 0, { cwd: sessionCwd })
110
- return next
111
- }
112
- if (typeof args[2] === 'function') {
113
- if (args[1] !== undefined && !Array.isArray(args[1])) return withDefaultOptions(args, 1)
114
- const next = [...args]
115
- next.splice(2, 0, { cwd: sessionCwd })
116
- return next
117
- }
118
- if (args[1] !== undefined && !Array.isArray(args[1])) return withDefaultOptions(args, 1)
119
- return withDefaultOptions(args, 2)
120
- }
121
- return args.length === 1 || (args[1] !== undefined && !Array.isArray(args[1]))
122
- ? withDefaultOptions(args, 1)
123
- : withDefaultOptions(args, 2)
124
- }
125
148
  const sessionGlobOptions = (args) => {
126
- const next = [...args]
127
- if (typeof next[1] === 'function') next.splice(1, 0, { cwd: sessionCwd })
128
- else if (next[1] === undefined) next[1] = { cwd: sessionCwd }
149
+ if (typeof args[1] === 'function') {
150
+ const next = copyArray(args, 0, 1)
151
+ appendArray(next, { cwd: sessionCwd })
152
+ return copyArray(args, 1, args.length, next)
153
+ }
154
+ const next = copyArray(args)
155
+ if (next[1] === undefined) next[1] = { cwd: sessionCwd }
129
156
  else if (next[1] !== null && typeof next[1] === 'object' && next[1].cwd === undefined) {
130
157
  next[1] = { ...next[1], cwd: sessionCwd }
131
158
  }
132
159
  return next
133
160
  }
134
161
 
135
- process.cwd = () => sessionCwd
162
+ installProjectedProcessCwd(process, sessionCwd)
136
163
  const fs = originalRequire('node:fs')
137
164
  const path = originalRequire('node:path')
138
165
  for (const [name, indices] of Object.entries(FILE_SYSTEM_PATH_ARGUMENTS)) {
@@ -147,9 +174,10 @@ export function installWorkerCwdVirtualization(sessionCwd, originalRequire) {
147
174
  wrapBuiltin(fs, name, sessionGlobOptions)
148
175
  wrapBuiltin(fs.promises, name, sessionGlobOptions)
149
176
  }
150
- wrapBuiltin(path, 'resolve', args => [sessionCwd, ...args])
177
+ wrapBuiltin(path, 'resolve', args => copyArray(args, 0, args.length, [sessionCwd]))
151
178
  const childProcess = originalRequire('node:child_process')
152
179
  for (const name of ['exec', 'execFile', 'execFileSync', 'execSync', 'fork', 'spawn', 'spawnSync']) {
153
- wrapBuiltin(childProcess, name, args => childProcessOptions(name, args), CHILD_PROCESS_PROJECTED_PROPERTIES[name])
180
+ wrapBuiltin(childProcess, name,
181
+ args => projectChildProcessArguments(name, args, sessionCwd), CHILD_PROCESS_PROJECTED_PROPERTIES[name])
154
182
  }
155
183
  }
@@ -23,6 +23,26 @@ export const HOST_ONLY_ENVIRONMENT_NAMES = new Set([
23
23
  'dsh_ptc_compiler_bytecode',
24
24
  ])
25
25
 
26
+ /** Detect whether the Host executable needs Electron's Node startup mode. */
27
+ export function isElectronHost(versions = process.versions) {
28
+ return typeof versions?.electron === 'string'
29
+ }
30
+
31
+ /** Overlay only the helper process environment needed to boot an Electron executable as Node. */
32
+ export function helperProcessEnvironment(source, platform = process.platform, electronHost = isElectronHost()) {
33
+ if (!electronHost) return source
34
+ const environment = { ...(source ?? process.env) }
35
+ if (platform === 'win32') {
36
+ for (const name of Object.keys(environment)) {
37
+ if (name !== 'ELECTRON_RUN_AS_NODE' && name.toLowerCase() === 'electron_run_as_node') {
38
+ delete environment[name]
39
+ }
40
+ }
41
+ }
42
+ environment.ELECTRON_RUN_AS_NODE = '1'
43
+ return environment
44
+ }
45
+
26
46
  /** Project the host environment once without losing Windows case variants. */
27
47
  export function normalizeWorkerEnvironment(source, platform = process.platform) {
28
48
  if (platform !== 'win32') {
@@ -0,0 +1,187 @@
1
+ import {
2
+ OUTPUT_FENCE_ALLOWANCE_BYTES,
3
+ outputFenceMarker,
4
+ validOutputFenceToken,
5
+ } from './worker-output-fence.js'
6
+
7
+ function bytes(value) {
8
+ return Buffer.isBuffer(value) ? Buffer.from(value) : Buffer.from(String(value ?? ''))
9
+ }
10
+
11
+ function nonEmptyText(value) {
12
+ return value.length === 0 ? [] : [value.toString('utf8')]
13
+ }
14
+
15
+ /** Correlate helper stdout/stderr with one kernel completion using dual fences. */
16
+ export class WorkerOutputCapture {
17
+ constructor() {
18
+ this.armed = false
19
+ this.round = undefined
20
+ this.settledId = undefined
21
+ }
22
+
23
+ begin(id, maxOutputBytes, outputFence) {
24
+ this.armed = true
25
+ if (this.round !== undefined) {
26
+ return { type: 'worker-output-error', id, message: 'worker output rounds overlapped' }
27
+ }
28
+ if (!validOutputFenceToken(outputFence)) {
29
+ return { type: 'worker-output-error', id, message: 'host created an invalid worker output fence' }
30
+ }
31
+ this.round = {
32
+ id,
33
+ maxOutputBytes,
34
+ outputFence,
35
+ stdout: [],
36
+ stderr: [],
37
+ bytes: 0,
38
+ started: false,
39
+ startMessage: false,
40
+ completion: undefined,
41
+ }
42
+ return undefined
43
+ }
44
+
45
+ push(channel, chunk) {
46
+ if (!this.armed) return undefined
47
+ if (this.round === undefined) {
48
+ this.armed = false
49
+ return {
50
+ type: 'worker-output-error',
51
+ id: this.settledId,
52
+ message: 'a settled cell produced descriptor output after its output fence; the worker was reset to stop unattributed output',
53
+ }
54
+ }
55
+ const value = bytes(chunk)
56
+ this.round[channel].push(value)
57
+ this.round.bytes += value.length
58
+ if (this.round.bytes > this.round.maxOutputBytes + OUTPUT_FENCE_ALLOWANCE_BYTES) {
59
+ const id = this.round.id
60
+ this.round = undefined
61
+ return { type: 'output-limit', id, logs: [] }
62
+ }
63
+ return this.round.started ? this.#finish() : this.#start()
64
+ }
65
+
66
+ start(message) {
67
+ if (this.round === undefined || message?.id !== this.round.id) {
68
+ const id = this.round?.id ?? message?.id
69
+ this.round = undefined
70
+ return { type: 'worker-output-error', id, message: 'kernel output start has no matching output round' }
71
+ }
72
+ if (message.outputFence !== this.round.outputFence) {
73
+ const id = this.round.id
74
+ this.round = undefined
75
+ return { type: 'worker-output-error', id, message: 'kernel output start has a mismatched output fence' }
76
+ }
77
+ if (this.round.startMessage) {
78
+ const id = this.round.id
79
+ this.round = undefined
80
+ return { type: 'worker-output-error', id, message: 'kernel repeated an output start' }
81
+ }
82
+ this.round.startMessage = true
83
+ return this.#start()
84
+ }
85
+
86
+ complete(message) {
87
+ if (this.round === undefined || message?.id !== this.round.id) {
88
+ const id = this.round?.id ?? message?.id
89
+ this.round = undefined
90
+ return { type: 'worker-output-error', id, message: 'kernel completion has no matching output round' }
91
+ }
92
+ if (!this.round.started || message.outputFence !== this.round.outputFence) {
93
+ const id = this.round.id
94
+ this.round = undefined
95
+ return { type: 'worker-output-error', id, message: 'kernel completion has a mismatched output fence' }
96
+ }
97
+ if (this.round.completion !== undefined) {
98
+ const id = this.round.id
99
+ this.round = undefined
100
+ return { type: 'worker-output-error', id, message: 'kernel repeated a completion' }
101
+ }
102
+ this.round.completion = message
103
+ return this.#finish()
104
+ }
105
+
106
+ lateOutput() {
107
+ if (this.round !== undefined) {
108
+ const id = this.round.id
109
+ this.round = undefined
110
+ return {
111
+ type: 'worker-output-error',
112
+ id,
113
+ message: 'a settled cell produced background output during this cell; the worker was reset to prevent cross-cell attribution',
114
+ }
115
+ }
116
+ if (this.armed) {
117
+ this.armed = false
118
+ return {
119
+ type: 'worker-output-error',
120
+ id: this.settledId,
121
+ message: 'a settled cell produced background output after its output fence; the worker was reset to prevent cross-cell attribution',
122
+ }
123
+ }
124
+ return undefined
125
+ }
126
+
127
+ #start() {
128
+ const round = this.round
129
+ if (round?.startMessage !== true) return undefined
130
+ const stdout = Buffer.concat(round.stdout)
131
+ const stderr = Buffer.concat(round.stderr)
132
+ const stdoutFence = Buffer.from(outputFenceMarker(round.outputFence, 'stdout', 'start'))
133
+ const stderrFence = Buffer.from(outputFenceMarker(round.outputFence, 'stderr', 'start'))
134
+ const stdoutAt = stdout.indexOf(stdoutFence)
135
+ const stderrAt = stderr.indexOf(stderrFence)
136
+ if (stdoutAt < 0 || stderrAt < 0) return undefined
137
+ const stdoutAfter = stdout.subarray(stdoutAt + stdoutFence.length)
138
+ const stderrAfter = stderr.subarray(stderrAt + stderrFence.length)
139
+ if (stdoutAt > 0 || stderrAt > 0 || stdoutAfter.length > 0 || stderrAfter.length > 0) {
140
+ const id = round.id
141
+ this.round = undefined
142
+ return {
143
+ type: 'worker-output-error',
144
+ id,
145
+ message: 'a settled cell produced descriptor output before the next cell started; the worker was reset to prevent cross-cell attribution',
146
+ }
147
+ }
148
+ round.stdout = []
149
+ round.stderr = []
150
+ round.bytes = 0
151
+ round.started = true
152
+ return { type: 'worker-output-started', id: round.id, outputFence: round.outputFence }
153
+ }
154
+
155
+ #finish() {
156
+ const round = this.round
157
+ if (round?.completion === undefined) return undefined
158
+ const stdout = Buffer.concat(round.stdout)
159
+ const stderr = Buffer.concat(round.stderr)
160
+ const stdoutFence = Buffer.from(outputFenceMarker(round.outputFence, 'stdout', 'end'))
161
+ const stderrFence = Buffer.from(outputFenceMarker(round.outputFence, 'stderr', 'end'))
162
+ const stdoutAt = stdout.indexOf(stdoutFence)
163
+ const stderrAt = stderr.indexOf(stderrFence)
164
+ if (stdoutAt < 0 || stderrAt < 0) return undefined
165
+ const stdoutAfter = stdout.subarray(stdoutAt + stdoutFence.length)
166
+ const stderrAfter = stderr.subarray(stderrAt + stderrFence.length)
167
+ if (stdoutAfter.length > 0 || stderrAfter.length > 0) {
168
+ this.round = undefined
169
+ this.armed = false
170
+ return {
171
+ type: 'worker-output-error',
172
+ id: round.id,
173
+ message: 'a settled cell produced descriptor output after its output fence; the worker was reset to prevent cross-cell attribution',
174
+ }
175
+ }
176
+ const descriptorLogs = [
177
+ ...nonEmptyText(stdout.subarray(0, stdoutAt)),
178
+ ...nonEmptyText(stderr.subarray(0, stderrAt)),
179
+ ]
180
+ const rawBytes = descriptorLogs.reduce((total, value) => total + Buffer.byteLength(JSON.stringify(value)), 0)
181
+ this.round = undefined
182
+ this.settledId = round.id
183
+ if (rawBytes > round.maxOutputBytes) return { type: 'output-limit', id: round.id, logs: [] }
184
+ const logs = Array.isArray(round.completion.logs) ? [...round.completion.logs, ...descriptorLogs] : descriptorLogs
185
+ return { ...round.completion, logs }
186
+ }
187
+ }
@@ -0,0 +1,26 @@
1
+ import { runtimeIntrinsics as internal } from './runtime-intrinsics.js'
2
+
3
+ const FENCE_PREFIX = '\u001eDSH_PTC_OUTPUT_FENCE_V1:'
4
+ const FENCE_SUFFIX = '\u001f'
5
+ const TOKEN = /^[a-f0-9]{48}$/u
6
+ const FENCE = /\u001eDSH_PTC_OUTPUT_FENCE_V1:(?:stdout|stderr):(?:start|end):[a-f0-9]{48}\u001f/gu
7
+ const { regexpTest, replaceString, toString } = internal
8
+
9
+ export const OUTPUT_FENCE_ALLOWANCE_BYTES = 512
10
+
11
+ export function validOutputFenceToken(token) {
12
+ return typeof token === 'string' && regexpTest(TOKEN, token)
13
+ }
14
+
15
+ export function outputFenceMarker(token, channel, phase) {
16
+ if (!validOutputFenceToken(token)
17
+ || (channel !== 'stdout' && channel !== 'stderr')
18
+ || (phase !== 'start' && phase !== 'end')) {
19
+ throw new TypeError('invalid worker output fence')
20
+ }
21
+ return `${FENCE_PREFIX}${channel}:${phase}:${token}${FENCE_SUFFIX}`
22
+ }
23
+
24
+ export function stripOutputFenceMarkers(value) {
25
+ return replaceString(toString(value ?? ''), FENCE, '')
26
+ }