dsh-ptc-plus 0.4.8 → 0.5.0

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 (139) hide show
  1. package/README.en.md +61 -123
  2. package/README.md +61 -123
  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-repl-workspace-en.png +0 -0
  6. package/assets/ptc-plus-repl-workspace-zh.png +0 -0
  7. package/assets/ptc-plus-settings-en.png +0 -0
  8. package/assets/ptc-plus-settings-zh.png +0 -0
  9. package/client.cjs +42336 -0
  10. package/cordis.patch.yml +13 -2
  11. package/docs/README.md +7 -2
  12. package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +31 -179
  13. package/docs/adr/0008-define-agent-operating-contract.md +5 -1
  14. package/docs/adr/0009-bound-expensive-acceptance-default.md +2 -2
  15. package/docs/adr/0016-archive-resolved-review-findings.md +1 -1
  16. package/docs/adr/0017-track-the-latest-dsh-public-surface.md +2 -2
  17. package/docs/adr/0019-plugin-settings-and-kill-switch.md +14 -3
  18. package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +2 -2
  19. package/docs/adr/0022-stage-npm-releases-with-oidc.md +4 -2
  20. package/docs/adr/0023-global-user-bindings.md +3 -1
  21. package/docs/adr/0026-persist-the-delivery-verdict-against-the-verified-candidate.md +2 -0
  22. package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +101 -35
  23. package/docs/adr/0030-serve-both-typert-codec-generations-from-one-codec.md +3 -3
  24. package/docs/adr/0031-close-semantic-obligations-by-owner.md +1 -1
  25. package/docs/adr/0032-own-session-log-facts-in-a-host-only-projection.md +27 -0
  26. package/docs/adr/0033-gate-client-visual-usability-in-a-browser.md +23 -0
  27. package/docs/adr/0034-keep-program-values-separate-from-output-projections.md +29 -0
  28. package/docs/adr/0035-run-coverage-only-before-a-new-release.md +27 -0
  29. package/docs/adr/0036-separate-focused-development-from-delivery-verification.md +29 -0
  30. package/docs/adr/0037-keep-tool-hover-previews-passive.md +52 -0
  31. package/docs/adr/0038-freeze-session-capability-plans.md +71 -0
  32. package/docs/adr/0039-verify-documentation-without-runtime-suites.md +29 -0
  33. package/docs/adr/0040-separate-capability-configurations-from-session-observations.md +39 -0
  34. package/docs/adr/0041-record-cell-execution-directories.md +65 -0
  35. package/docs/adr/0042-persist-provider-availability-and-handback.md +41 -0
  36. package/docs/adr/0043-compose-capability-and-context-policies.md +37 -0
  37. package/docs/adr/README.md +6 -0
  38. package/docs/architecture.md +25 -27
  39. package/docs/binding-review-placement-design.md +29 -21
  40. package/docs/capability-planning-components.md +93 -0
  41. package/docs/capability-projection.md +7 -0
  42. package/docs/client-ui.md +88 -29
  43. package/docs/client-visual-contract.md +65 -0
  44. package/docs/context-management.md +22 -0
  45. package/docs/context-optimization-plan.md +353 -0
  46. package/docs/context-public-interface-coverage.md +25 -0
  47. package/docs/durability-design.md +31 -14
  48. package/docs/evaluation.md +41 -6
  49. package/docs/evaluations/command-goat-deepseek-v4.1-flash-20261006.md +126 -0
  50. package/docs/evaluations/command-goat-deepseek-v4.1-flash.md +281 -0
  51. package/docs/installation.md +50 -8
  52. package/docs/program-data-plane.md +5 -3
  53. package/docs/publishing.md +5 -5
  54. package/docs/release-coverage.md +73 -0
  55. package/docs/release-notes.md +1 -1
  56. package/docs/review-procedure.md +56 -0
  57. package/docs/runtime-reference.md +48 -16
  58. package/docs/semantic-validation.md +14 -2
  59. package/docs/stateful-binding-updates-proposal.md +2 -2
  60. package/docs/user-bindings.en.md +2 -2
  61. package/docs/user-bindings.md +3 -3
  62. package/docs/value-wire.md +11 -1
  63. package/docs/verification-optimization-work.md +52 -15
  64. package/docs/verification.md +30 -86
  65. package/index.js +147 -23
  66. package/internal/binding-toggle-receipt.js +24 -0
  67. package/internal/capability-command-receipt.js +29 -0
  68. package/internal/capability-environment.js +1247 -0
  69. package/internal/capability-plan.js +285 -0
  70. package/internal/capability-policy.js +96 -0
  71. package/internal/capability-statistics-store.js +180 -0
  72. package/internal/capability-statistics.js +210 -0
  73. package/internal/compiler-platform.js +5 -3
  74. package/internal/compiler-service.js +2 -9
  75. package/internal/config-spec.js +79 -9
  76. package/internal/cordis-tools-owner.js +22 -19
  77. package/internal/direct-surface-owner.js +29 -29
  78. package/internal/dynamic-environment-runtime.js +9 -9
  79. package/internal/edit-transport-owner.js +183 -139
  80. package/internal/execution-provider-owner.js +160 -0
  81. package/internal/execution-seam-compat.js +76 -120
  82. package/internal/isolated-worker.js +12 -8
  83. package/internal/kernel-worker.js +104 -32
  84. package/internal/native-root-dynamic.js +12 -13
  85. package/internal/program-bindings.js +190 -0
  86. package/internal/recovery-tips.js +53 -23
  87. package/internal/rejected-cell-editor.js +8 -8
  88. package/internal/repl-observation-interest.js +11 -3
  89. package/internal/rpc-contract.js +1 -0
  90. package/internal/runtime-bridge-owner.js +351 -82
  91. package/internal/runtime-config.js +58 -0
  92. package/internal/runtime-contexts.js +85 -16
  93. package/internal/runtime-intrinsics.js +1 -0
  94. package/internal/runtime-messages.js +41 -0
  95. package/internal/sdk-projection.js +10 -82
  96. package/internal/session-cell-executor.js +42 -19
  97. package/internal/session-events.js +74 -4
  98. package/internal/session-journal-recovery.js +904 -184
  99. package/internal/session-journal-schema.js +6 -3
  100. package/internal/session-journal.js +31 -2
  101. package/internal/session-log-view.js +189 -43
  102. package/internal/session-runtime.js +175 -23
  103. package/internal/settings-compat.js +23 -6
  104. package/internal/stateful-module-compiler.js +3 -2
  105. package/internal/stateful-module-runtime.js +7 -3
  106. package/internal/stateful-root-runtime.js +9 -4
  107. package/internal/user-binding-authoring.js +2 -7
  108. package/internal/user-bindings-owner.js +47 -7
  109. package/internal/validated-parse-repair.js +1 -1
  110. package/internal/value-wire.js +68 -21
  111. package/internal/worker-cwd-virtualization.js +20 -18
  112. package/internal/worker-realm-surfaces.js +3 -3
  113. package/locale/en.json +6 -0
  114. package/locale/zh.json +6 -0
  115. package/package.json +55 -38
  116. package/src/client-authoring-view.js +286 -168
  117. package/src/client-binding-review.js +4 -1
  118. package/src/client-binding-toggle.js +18 -0
  119. package/src/client-capabilities-view.js +1341 -0
  120. package/src/client-capability-sort.js +29 -0
  121. package/src/client-code-editor.js +6 -0
  122. package/src/client-copy.js +881 -8
  123. package/src/client-feature-gates.js +15 -5
  124. package/src/client-host-compat.js +23 -2
  125. package/src/client-indicator-view.js +4 -6
  126. package/src/client-primitives.js +280 -0
  127. package/src/client-repl-view.js +23 -16
  128. package/src/client-settings-view.js +412 -69
  129. package/src/client-styles.js +193 -41
  130. package/src/client-tool-hover.js +185 -0
  131. package/src/client-tool-view.js +45 -11
  132. package/src/client-transport.js +44 -9
  133. package/src/client-workbench.js +9 -10
  134. package/src/client.js +148 -68
  135. package/assets/ptc-plus-bindings-en.png +0 -0
  136. package/assets/ptc-plus-bindings-zh.png +0 -0
  137. package/assets/ptc-plus-repair-en.png +0 -0
  138. package/assets/ptc-plus-repair-zh.png +0 -0
  139. package/client.js +0 -31857
@@ -1,39 +1,11 @@
1
- /**
2
- * Own how PTC Plus attaches to the host's program-execution seam.
3
- *
4
- * DSH registers one execution seam per generation. The current release
5
- * registers `ctx.ptcRuntime` (`@deepseek-ai/dsh-ptc-runtime`); the preceding one
6
- * registered `ctx.codeRuntime` (`@deepseek-ai/dsh-code-runtime`). Both run one
7
- * model-written program against host async bindings. They differ in the service
8
- * name, in the call shape (`resolve(request)` then `run(spec)` against a single
9
- * `run(request)`), and in the capability descriptors a provider publishes.
10
- * Which seam is live is decided by the registered service, never by a version
11
- * string, so a deployment on either generation obtains the same behavior.
12
- *
13
- * PTC Plus replaces the execution of session cells with its own persistent
14
- * REPL. The object DSH describes to the model is therefore the plugin's
15
- * execution, not the wrapped provider's, so `takeOver()` answers the
16
- * descriptors the plugin can honor and withholds the ones it cannot. The REPL
17
- * runs every cell in one long-lived worker rather than a fresh process per
18
- * call, applies the session's configured budgets rather than a per-call host
19
- * deadline, and performs no file confinement. DSH gates `timeoutMs` on
20
- * `timeout` and resolves or escalates file policy on `sandboxMode`, so
21
- * withholding those descriptors keeps DSH's authority decisions with DSH
22
- * instead of having this plugin accept an input it would then ignore.
23
- *
24
- * A seam-neutral request carries exactly `program`, `bindings`, and `signal`:
25
- * the fields PTC Plus consumes. A resolved spec's directory, deadline, and
26
- * authority describe the provider whose execution was replaced, and are neither
27
- * forwarded to the session kernel nor re-derived here.
28
- */
29
-
30
- /** Cordis service name registered by the current DSH generation. */
1
+ /** Resolved-spec Cordis execution contract. */
31
2
  export const EXECUTION_SEAM_SERVICE = 'ptcRuntime'
32
3
 
33
- /** Cordis service name registered by the preceding DSH generation. */
4
+ /** Request-only Cordis execution contract. */
34
5
  export const LEGACY_EXECUTION_SEAM_SERVICE = 'codeRuntime'
35
6
 
36
7
  const SEAM_SERVICES = Object.freeze([EXECUTION_SEAM_SERVICE, LEGACY_EXECUTION_SEAM_SERVICE])
8
+ const PLUGIN_EXECUTION_SEAM = Symbol.for('dsh-ptc-plus.execution-seam')
37
9
 
38
10
  /**
39
11
  * How long the plugin waits for the host's execution seam before it reports the
@@ -48,34 +20,12 @@ const SEAM_SERVICES = Object.freeze([EXECUTION_SEAM_SERVICE, LEGACY_EXECUTION_SE
48
20
  */
49
21
  const SEAM_ATTACHMENT_TIMEOUT_MS = 30_000
50
22
 
51
- /**
52
- * Descriptors the plugin replaces with the capabilities of its own execution.
53
- * `executionInstructions` describes the wrapped provider's program model, which
54
- * no longer applies; `timeout` and `sandboxMode` gate host inputs the plugin
55
- * cannot honor. Withdrawal is unconditional because the rule is a property of
56
- * the plugin's execution, not of a generation: the preceding generation's
57
- * contract defines none of these members and nothing in its install closure
58
- * reads them, so withdrawing there is inert, while a provider that published
59
- * them stays unable to outlive the takeover.
60
- */
61
- const WITHHELD_DESCRIPTORS = Object.freeze(['executionInstructions', 'sandboxMode', 'timeout'])
62
-
63
23
  /** Read a registered service without declaring an injection requirement. */
64
24
  function readService(ctx, name) {
65
25
  if (typeof ctx?.get === 'function') return ctx.get(name)
66
26
  return ctx?.[name]
67
27
  }
68
28
 
69
- function descriptorsMatch(left, right) {
70
- if (left === undefined || right === undefined) return left === right
71
- return left.value === right.value
72
- && left.get === right.get
73
- && left.set === right.set
74
- && left.writable === right.writable
75
- && left.enumerable === right.enumerable
76
- && left.configurable === right.configurable
77
- }
78
-
79
29
  /**
80
30
  * Build the seam handle for one live execution service.
81
31
  * @param {object} service - the registered `ctx.ptcRuntime` or `ctx.codeRuntime`.
@@ -85,6 +35,7 @@ export function createExecutionSeam(service, serviceName) {
85
35
  if (service === null || typeof service !== 'object') {
86
36
  throw new TypeError(`ptc-plus: ${serviceName} must be an execution service object`)
87
37
  }
38
+ if (typeof service[PLUGIN_EXECUTION_SEAM] === 'function') return service[PLUGIN_EXECUTION_SEAM]()
88
39
  const current = serviceName === EXECUTION_SEAM_SERVICE
89
40
  const upstreamRun = service.run
90
41
  if (typeof upstreamRun !== 'function') {
@@ -95,90 +46,95 @@ export function createExecutionSeam(service, serviceName) {
95
46
  throw new TypeError('ptc-plus: ptcRuntime.resolve must be a function')
96
47
  }
97
48
  let installed
98
- return Object.freeze({
49
+ const sourceListeners = new Set()
50
+ let available = true
51
+ const requireAvailable = () => {
52
+ if (!available) throw new Error(`ptc-plus: original ${serviceName} generation is unavailable`)
53
+ }
54
+ const requireCompatible = () => {
55
+ requireAvailable()
56
+ if (installed !== undefined && service.language !== installed.language) {
57
+ throw new Error(`ptc-plus: current original language ${JSON.stringify(service.language)} is incompatible with the active ${JSON.stringify(installed.language)} runtime`)
58
+ }
59
+ }
60
+ const provider = Object.freeze({
61
+ get language() { requireCompatible(); return service.language },
62
+ get isolation() { requireCompatible(); return installed === undefined ? service.isolation : 'worker-thread' },
63
+ get executionInstructions() { requireCompatible(); return installed === undefined ? service.executionInstructions ?? '' : '' },
64
+ get sandboxMode() { requireCompatible(); return installed === undefined ? service.sandboxMode : undefined },
65
+ get timeout() { requireCompatible(); return installed === undefined ? service.timeout : undefined },
66
+ resolve(request) {
67
+ requireCompatible()
68
+ return current ? upstreamResolve.call(service, request) : request
69
+ },
70
+ run(input) {
71
+ requireCompatible()
72
+ return installed === undefined ? upstreamRun.call(service, input) : installed.execute(input)
73
+ },
74
+ [PLUGIN_EXECUTION_SEAM]() { return seam },
75
+ })
76
+ const seam = Object.freeze({
99
77
  serviceName,
100
- language: service.language,
78
+ get language() { requireAvailable(); return service.language },
79
+ provider,
80
+ get active() { return installed !== undefined && service.language === installed.language },
81
+ onSourceChange(listener) {
82
+ sourceListeners.add(listener)
83
+ return () => sourceListeners.delete(listener)
84
+ },
85
+ sourceChanged() {
86
+ return Promise.all([...sourceListeners].map(listener => listener())).then(() => undefined)
87
+ },
101
88
  /**
102
- * Run one program through the provider this plugin wraps. The current
103
- * generation resolves the request before running it, exactly as its own
104
- * consumer does; the preceding generation accepts the request directly.
89
+ * Run one program through the provider this plugin wraps. The resolved-spec
90
+ * contract resolves the request before running it, exactly as its consumer
91
+ * does; the request-only contract accepts the request directly.
105
92
  */
106
93
  invokeUpstream(request) {
94
+ requireCompatible()
107
95
  return current
108
96
  ? upstreamRun.call(service, upstreamResolve.call(service, request))
109
97
  : upstreamRun.call(service, request)
110
98
  },
111
- /**
112
- * Install the plugin's execution entry, and its capability descriptors, on
113
- * the live service. Only one takeover is live at a time.
114
- * @param {(request: object) => Promise<object>} execute - the plugin's entry.
115
- * @returns {() => void} restores every descriptor this call replaced.
116
- */
117
- takeOver(execute) {
118
- if (installed !== undefined) throw new Error('ptc-plus: execution seam already taken over')
119
- const saved = new Map()
120
- const written = new Map()
121
- const requested = new Map()
122
- requested.set('run', {
123
- configurable: true,
124
- writable: true,
125
- value: current
126
- ? spec => execute({
127
- program: spec.program,
128
- bindings: spec.bindings,
129
- signal: spec.signal,
130
- })
131
- : request => execute(request),
132
- })
133
- for (const name of WITHHELD_DESCRIPTORS) {
134
- requested.set(name, { configurable: true, value: name === 'executionInstructions' ? '' : undefined })
135
- }
136
- const extensible = Object.isExtensible(service)
137
- for (const [name, descriptor] of requested) {
138
- const previous = Object.getOwnPropertyDescriptor(service, name)
139
- saved.set(name, previous)
140
- const next = { enumerable: previous?.enumerable ?? false, ...descriptor }
141
- requested.set(name, next)
142
- const probe = {}
143
- if (previous !== undefined) Object.defineProperty(probe, name, previous)
144
- if (!extensible) Object.preventExtensions(probe)
145
- Object.defineProperty(probe, name, next)
146
- }
147
- try {
148
- for (const [name, descriptor] of requested) {
149
- Object.defineProperty(service, name, descriptor)
150
- written.set(name, Object.getOwnPropertyDescriptor(service, name))
151
- }
152
- } catch (error) {
153
- for (const name of [...written.keys()].reverse()) {
154
- if (!descriptorsMatch(Object.getOwnPropertyDescriptor(service, name), written.get(name))) continue
155
- const descriptor = saved.get(name)
156
- if (descriptor === undefined) delete service[name]
157
- else Object.defineProperty(service, name, descriptor)
158
- }
159
- throw error
160
- }
99
+ runUpstream(spec) {
100
+ requireCompatible()
101
+ return upstreamRun.call(service, spec)
102
+ },
103
+ installExecute(execute) {
104
+ requireAvailable()
105
+ if (typeof execute !== 'function') throw new TypeError('ptc-plus: execution entry must be a function')
106
+ if (installed !== undefined) throw new Error('ptc-plus: execution seam already installed')
107
+ const entry = { execute, language: service.language }
108
+ installed = entry
161
109
  const restore = () => {
162
- if (installed !== restore) return
110
+ if (installed !== entry) return
163
111
  installed = undefined
164
- for (const [name, descriptor] of saved) {
165
- if (!descriptorsMatch(Object.getOwnPropertyDescriptor(service, name), written.get(name))) continue
166
- if (descriptor === undefined) delete service[name]
167
- else Object.defineProperty(service, name, descriptor)
168
- }
169
112
  }
170
- installed = restore
171
113
  return restore
172
114
  },
115
+ retire() {
116
+ available = false
117
+ installed = undefined
118
+ sourceListeners.clear()
119
+ },
173
120
  })
121
+ return seam
122
+ }
123
+
124
+ function registeredExecutionSeam(service, serviceName) {
125
+ const seam = createExecutionSeam(service, serviceName)
126
+ if (typeof service[PLUGIN_EXECUTION_SEAM] !== 'function') {
127
+ throw new Error(`ptc-plus: ${serviceName} requires the public execution-provider bundle composition`)
128
+ }
129
+ return seam
174
130
  }
175
131
 
176
132
  /**
177
133
  * Wait for the host's execution seam and attach the plugin to the live one.
178
134
  *
179
- * The current service is preferred whenever both are live; the preceding
180
- * generation attaches only when the current one is absent, and a current
181
- * service registered later retires the live legacy injection before attaching.
135
+ * The resolved-spec service is preferred whenever both contracts are live; the
136
+ * request-only service attaches when it is absent. A resolved-spec service
137
+ * registered later retires the request-only injection before attaching.
182
138
  * The injected scope requires the plugin's own core
183
139
  * services as well, so the callback receives a context the plugin can work in,
184
140
  * and everything the plugin registers there belongs to the seam the host
@@ -259,7 +215,7 @@ export function installExecutionSeam(ctx, {
259
215
  entry = { name, scope }
260
216
  live = entry
261
217
  registerLifecycle(entry, name, scope)
262
- const attached = attach(scope, createExecutionSeam(scope?.[name] ?? readService(ctx, name), name))
218
+ const attached = attach(scope, registeredExecutionSeam(scope?.[name] ?? readService(ctx, name), name))
263
219
  entry.attached = attached
264
220
  return attached
265
221
  })
@@ -292,7 +248,7 @@ export function installExecutionSeam(ctx, {
292
248
  const entry = { name, scope }
293
249
  live = entry
294
250
  try {
295
- entry.attached = attach(scope, createExecutionSeam(scope?.[name] ?? readService(ctx, name), name))
251
+ entry.attached = attach(scope, registeredExecutionSeam(scope?.[name] ?? readService(ctx, name), name))
296
252
  } catch (error) {
297
253
  live = undefined
298
254
  throw error
@@ -15,6 +15,17 @@ const ISOLATED_STOP_MS = 5000
15
15
  /** Bound on the helper's output pipes really ending after its process exited. */
16
16
  const ISOLATED_OUTPUT_MS = 5000
17
17
 
18
+ export function isolatedWorkerEntry(entry) {
19
+ if (!(entry instanceof URL) && (typeof entry !== 'string' || entry.length === 0)) {
20
+ throw new TypeError('isolated worker entry must be a URL or a non-empty string filename')
21
+ }
22
+ return entry instanceof URL
23
+ ? entry.href
24
+ : entry.slice(0, 5).toLowerCase() === 'file:'
25
+ ? new URL(entry).href
26
+ : pathToFileURL(resolve(entry)).href
27
+ }
28
+
18
29
  /**
19
30
  * A worker-shaped transport whose worker lives in a killable helper process.
20
31
  *
@@ -29,17 +40,10 @@ export class IsolatedWorker extends EventEmitter {
29
40
  super()
30
41
  // The entry follows the worker constructor's contract: a URL or a string
31
42
  // filename. Anything else fails here, before a helper process exists.
32
- if (!(entry instanceof URL) && (typeof entry !== 'string' || entry.length === 0)) {
33
- throw new TypeError('isolated worker entry must be a URL or a non-empty string filename')
34
- }
35
43
  // The init message crosses advanced IPC, which cannot rebuild a URL object, so
36
44
  // the boundary normalizes it to one absolute file URL string. A path follows
37
45
  // the worker constructor's rule: resolved from the current working directory.
38
- const entrySpec = entry instanceof URL
39
- ? entry.href
40
- : entry.slice(0, 5).toLowerCase() === 'file:'
41
- ? new URL(entry).href
42
- : pathToFileURL(resolve(entry)).href
46
+ const entrySpec = isolatedWorkerEntry(entry)
43
47
  this.stdout = new PassThrough()
44
48
  this.stderr = new PassThrough()
45
49
  this.protocol = protocol
@@ -8,13 +8,13 @@ import { USER_BINDING_TRANSFORM, LEGACY_USER_BINDING_TRANSFORM } from './module-
8
8
  import { isAbsolute, resolve } from 'node:path'
9
9
  import repl from 'node:repl'
10
10
  import { PassThrough, Writable } from 'node:stream'
11
- import { pathToFileURL } from 'node:url'
11
+ import { pathToFileURL } from './module-runtime-platform.js'
12
12
  import { MessageChannel, parentPort, workerData } from 'node:worker_threads'
13
13
  import { synchronizeBuiltinEsmExports } from './builtin-esm-sync.js'
14
14
  import { createPrivateAsyncLocalStorage } from './async-local-storage-intrinsics.js'
15
15
  import { createExceptionOriginScope, errorDetails, messageOf, programBindingError } from './failure-reporting.js'
16
16
  import { DURABLE_IMPORTS, FORBIDDEN_IMPORTS } from './module-policy.js'
17
- import { decodeValue, encodeValue } from './value-wire.js'
17
+ import { decodeValue, encodeValue, encodeValueAtBoundary, valueEncodingFailure } from './value-wire.js'
18
18
  import { LEGACY_USER_BINDINGS_REUSE_POLICY, LIVE_USER_BINDINGS_SHADOW_POLICY, normalizeUserBindingNames } from './session-journal-schema.js'
19
19
  import { installWorkerCwdVirtualization } from './worker-cwd-virtualization.js'
20
20
  import {
@@ -114,6 +114,15 @@ const output = new PassThrough()
114
114
  output.resume()
115
115
  const evaluationScope = createPrivateAsyncLocalStorage()
116
116
  const sessionCwd = typeof workerData?.cwd === 'string' ? workerData.cwd : undefined
117
+ // The module source base is fixed when a cell starts; the operation directory
118
+ // follows the Session's working directory while the cell runs, so a confirmed
119
+ // `tools.working_directory` change rebases the rest of the cell exactly as the
120
+ // Host tools, which resolve at call time, already do.
121
+ let executionCwd = sessionCwd
122
+ let cellOperationCwd = sessionCwd
123
+ const resolveModuleFilename = resolve
124
+ const isAbsoluteDirectory = isAbsolute
125
+ const nativeCreateRequire = createRequire
117
126
  if (sessionCwd !== undefined && !isAbsolute(sessionCwd)) {
118
127
  throw new Error(`ptc-plus session cwd must be absolute, got ${jsonStringify(sessionCwd)}`)
119
128
  }
@@ -145,7 +154,7 @@ const completionObserver = createCellCompletionObserver(runInWorkerReplRealm('Fu
145
154
  let replModuleRealm
146
155
  const statefulRoots = createStatefulRootRuntime({
147
156
  refresh: name => refreshLegacyPublication(name),
148
- importModule: (source, options) => managedModuleImport(replParent, source, options, replModuleRealm),
157
+ importModule: moduleImportFor(),
149
158
  errors: runInWorkerReplRealm('({ ReferenceError, TypeError })'),
150
159
  dynamicIntrinsics: runInWorkerReplRealm('({intrinsicEval:eval,realmFunction:Function,globalObject:globalThis,errors:{ReferenceError,TypeError,SyntaxError},reflect:Reflect,object:Object})'),
151
160
  readAmbient: name => runInWorkerReplRealm(name),
@@ -169,7 +178,7 @@ const statefulRoots = createStatefulRootRuntime({
169
178
  const nativeRootDynamic = createNativeRootDynamic({
170
179
  logicalReference: (name, writable) => statefulRoots.legacyReference(name, writable),
171
180
  intrinsics: runInWorkerReplRealm('({intrinsicEval:eval,realmFunction:Function,globalObject:globalThis,errors:{ReferenceError,TypeError,SyntaxError},reflect:Reflect,object:Object})'),
172
- importModule: (source, options) => managedModuleImport(replParent, source, options, replModuleRealm),
181
+ importModule: moduleImportFor(),
173
182
  read: name => runInWorkerReplRealm(name),
174
183
  typeOf: name => runInWorkerReplRealm(`typeof ${name}`),
175
184
  write(name, value, strict) {
@@ -181,11 +190,11 @@ const nativeRootDynamic = createNativeRootDynamic({
181
190
  let valueObserver
182
191
  const REPL_IMPORT_CANARY = 'data:text/javascript,export default 1'
183
192
  let replParent
184
- const sessionReplParent = sessionCwd === undefined ? undefined : pathToFileURL(resolve(sessionCwd, 'repl')).href
185
- const staticAdapterParents = new Set()
193
+ const cellModuleParent = () => executionCwd === undefined ? replParent : pathToFileURL(resolveModuleFilename(executionCwd, 'repl')).href
194
+ const staticAdapterParents = new Map()
186
195
  const userBindingModuleParents = new Map()
187
196
  const userModuleCompilation = createUserModuleCompilationHooks({
188
- transformForParent: parent => (parent === replParent || parent === sessionReplParent) && activeExecution !== undefined
197
+ transformForParent: parent => (parent === replParent || parent === cellModuleParent()) && activeExecution !== undefined
189
198
  ? activeExecution.moduleTransform : undefined,
190
199
  })
191
200
  registerHooks({
@@ -193,10 +202,11 @@ registerHooks({
193
202
  if (specifier === REPL_IMPORT_CANARY && replParent === undefined) replParent = context.parentURL
194
203
  return userModuleCompilation.resolve(specifier, context, (source, resolvedContext) => {
195
204
  const userBindingParent = mapGet(userBindingModuleParents, resolvedContext.parentURL)
205
+ const adapterParent = mapGet(staticAdapterParents, resolvedContext.parentURL)
196
206
  return nextResolve(source, userBindingParent !== undefined
197
207
  ? { ...resolvedContext, parentURL: userBindingParent }
198
- : resolvedContext.parentURL === replParent || setHas(staticAdapterParents, resolvedContext.parentURL)
199
- ? { ...resolvedContext, parentURL: sessionReplParent ?? replParent }
208
+ : adapterParent !== undefined
209
+ ? { ...resolvedContext, parentURL: adapterParent }
200
210
  : resolvedContext)
201
211
  })
202
212
  },
@@ -356,23 +366,53 @@ installWorkerCwdVirtualization(sessionCwd, originalRequire, reason => {
356
366
  return
357
367
  }
358
368
  markVolatile(reason)
359
- })
369
+ }, () => cellOperationCwd)
360
370
  synchronizeBuiltinEsmExports()
361
- const providedRequire = sessionReplParent === undefined ? originalRequire : createRequire(sessionReplParent)
362
- function selectProvidedRequire(args) {
363
- const specifier = args[0]
364
- if (setHas(FORBIDDEN_IMPORTS, specifier)) throw new Error(`module ${specifier} is forbidden because it exposes kernel control`)
365
- if (!setHas(DURABLE_IMPORTS, specifier)) markVolatile(`require(${jsonStringify(specifier)})`)
366
- return managedRequire(activeExecution?.languageSemantics === 'legacy-v1' ? replParent : sessionReplParent ?? replParent,
367
- activeExecution?.languageSemantics === 'legacy-v1' ? originalRequire : providedRequire)
371
+ function recordCellModuleResolution(transform) {
372
+ // An escaped loader retains its source generation. Native resolution still
373
+ // selects the URL; the module compiler owns adaptation before its first load.
374
+ return (parent, source, attributes, resolution) => userModuleCompilation.recordResolution(
375
+ parent, source, attributes, resolution, transform)
368
376
  }
369
- defineWorkerRealmProperty(WORKER_REALM_MUTATION.STABLE, 'cell module metadata', context, 'require', {
370
- configurable: true,
371
- value: new Proxy(providedRequire, {
372
- apply: (_, receiver, args) => privateReflect.apply(selectProvidedRequire(args), receiver, args),
373
- construct: (_, args, newTarget) => privateReflect.construct(selectProvidedRequire(args), args, newTarget),
374
- }),
375
- })
377
+
378
+ function moduleImportFor(parent, recordResolution) {
379
+ return (source, options) => managedModuleImport(parent ?? replParent, source, options, replModuleRealm, recordResolution)
380
+ }
381
+
382
+ function cellImportModuleFor(parent, transform) {
383
+ return moduleImportFor(parent, recordCellModuleResolution(transform))
384
+ }
385
+
386
+ function providedRequireFor(parent, legacy, transform, explicitCwd) {
387
+ const recordResolution = recordCellModuleResolution(transform)
388
+ const nativeRequire = legacy || !explicitCwd ? originalRequire : nativeCreateRequire(parent)
389
+ const select = args => {
390
+ const specifier = args[0]
391
+ if (setHas(FORBIDDEN_IMPORTS, specifier)) throw new Error(`module ${specifier} is forbidden because it exposes kernel control`)
392
+ if (!setHas(DURABLE_IMPORTS, specifier)) markVolatile(`require(${jsonStringify(specifier)})`)
393
+ return managedRequire(legacy ? replParent : parent ?? replParent, legacy ? originalRequire : nativeRequire, recordResolution)
394
+ }
395
+ return new Proxy(nativeRequire, {
396
+ apply: (_, receiver, args) => privateReflect.apply(select(args), receiver, args),
397
+ construct: (_, args, newTarget) => privateReflect.construct(select(args), args, newTarget),
398
+ })
399
+ }
400
+ let providedRequireParent = cellModuleParent()
401
+ let providedRequireLegacy = false
402
+ let providedRequireTransform
403
+ let providedRequireExplicitCwd
404
+ function installProvidedRequire(parent, legacy = false, transform, explicitCwd = executionCwd !== undefined) {
405
+ defineWorkerRealmProperty(WORKER_REALM_MUTATION.STABLE, 'cell module metadata', context, 'require', {
406
+ configurable: true,
407
+ value: providedRequireFor(parent, legacy, transform, explicitCwd),
408
+ })
409
+ providedRequireParent = parent
410
+ providedRequireLegacy = legacy
411
+ providedRequireTransform = transform
412
+ providedRequireExplicitCwd = explicitCwd
413
+ }
414
+ installProvidedRequire(providedRequireParent)
415
+
376
416
  function evaluate(program, completionSignal, asyncCompletion = false) {
377
417
  return new ControlPromise((resolve, reject) => {
378
418
  let settled = false
@@ -483,14 +523,16 @@ function staticAdapterSource(load) {
483
523
  }
484
524
 
485
525
  async function loadStaticModule(load) {
526
+ const parent = cellModuleParent()
527
+ const importModule = cellImportModuleFor(parent, activeExecution?.moduleTransform)
486
528
  if (load.operation === 'native-dynamic') return { namespace: nativeRootDynamic.environment(load.callableSources,
487
529
  load.awaitRoot === true, filterArray(load.logicalRoots, name => statefulRoots.has(name)
488
- && rootBindingStorage(name, moduleRuntimeIntrinsics.includes(load.nativeLexicals, name)) !== 'lexical'), load.writableRoots) }
530
+ && rootBindingStorage(name, moduleRuntimeIntrinsics.includes(load.nativeLexicals, name)) !== 'lexical'), load.writableRoots, importModule) }
489
531
  if (load.operation === 'import') {
490
- return { namespace: (source, options) => managedModuleImport(replParent, source, options, replModuleRealm) }
532
+ return { namespace: importModule }
491
533
  }
492
534
  const adapter = `data:text/javascript,${encodeURIComponent(staticAdapterSource(load))}#${++nextStaticAdapterId}`
493
- setAdd(staticAdapterParents, adapter)
535
+ mapSet(staticAdapterParents, adapter, cellModuleParent())
494
536
  userModuleCompilation.mark(adapter, { compiled: true,
495
537
  transform: activeExecution?.moduleTransform })
496
538
  try {
@@ -499,7 +541,7 @@ async function loadStaticModule(load) {
499
541
  const namespace = readModuleImport(adapter, load.source, null, () => completion.value.namespace, load.options?.with)
500
542
  return { namespace }
501
543
  } finally {
502
- setDelete(staticAdapterParents, adapter)
544
+ mapDelete(staticAdapterParents, adapter)
503
545
  }
504
546
  }
505
547
 
@@ -510,6 +552,18 @@ function hasExecutionLease(runId) {
510
552
  // BoundError is the error class installed by this cell's wrapper, not a name
511
553
  // looked up later: the cell can replace or delete the context binding while a
512
554
  // call is pending, and the call must keep its submitted error identity.
555
+ // A confirmed Session directory change rebases the rest of the cell: Host tools
556
+ // resolve against the session service at call time, and this worker's relative
557
+ // operations now resolve against the same directory. The cell's module source
558
+ // base (imports, import.meta, provided require) stays fixed at its start.
559
+ function adoptConfirmedCellDirectory(call, value) {
560
+ if (call.global !== 'tools' || call.member !== 'working_directory') return
561
+ if (activeRun !== call.runId) return
562
+ const cwd = value?.cwd
563
+ if (typeof cwd !== 'string' || cwd.length === 0 || !isAbsoluteDirectory(cwd)) return
564
+ cellOperationCwd = cwd
565
+ }
566
+
513
567
  function callHost(runId, global, member, args, BoundError) {
514
568
  if (!hasExecutionLease(runId)) {
515
569
  return promiseReject(programBindingError('lease', 'PTC execution lease expired'))
@@ -528,11 +582,11 @@ function callHost(runId, global, member, args, BoundError) {
528
582
  const settled = new ControlPromise(resolve => {
529
583
  moduleRuntimeIntrinsics.Reflect.apply(moduleRuntimeIntrinsics.promiseThen, result, [resolve, resolve])
530
584
  })
531
- mapSet(pending, id, { ...settle, runId, valueLimits, settled, member, BoundError })
585
+ mapSet(pending, id, { ...settle, runId, valueLimits, settled, global, member, BoundError })
532
586
  try {
533
587
  channel.postMessage({
534
588
  type: 'call', runId, id, global, member,
535
- args: encodeValue(args, valueLimits),
589
+ args: encodeValueAtBoundary(args, valueLimits, 'parameter'),
536
590
  })
537
591
  } catch (error) {
538
592
  mapDelete(pending, id)
@@ -1213,7 +1267,7 @@ async function closeExecution(execution) {
1213
1267
  }
1214
1268
 
1215
1269
  function failureOutcome(error, phase, program) {
1216
- if (phase === 'encode') return { invalidOutput: messageOf(error) }
1270
+ if (phase === 'encode') return { invalidOutput: messageOf(error), valueFailure: valueEncodingFailure(error) }
1217
1271
  const failure = error instanceof StaticImportFailure ? error.error : error
1218
1272
  // The V8 frame is reported in the executed text; validate it there so an
1219
1273
  // out-of-range frame is an explicit absence instead of a mapped position.
@@ -1225,6 +1279,7 @@ function failureOutcome(error, phase, program) {
1225
1279
  return {
1226
1280
  error: detail.message,
1227
1281
  errorName: detail.name,
1282
+ valueFailure: valueEncodingFailure(failure),
1228
1283
  ...(detail.toolName === undefined ? {} : { toolName: detail.toolName }),
1229
1284
  ...(error instanceof StaticImportFailure ? { moduleLoadFailed: true } : {}),
1230
1285
  ...(position === undefined ? {} : { position }),
@@ -1306,6 +1361,18 @@ async function sendCompletion(message, execution, userBindings, committedRedecla
1306
1361
 
1307
1362
  async function runCell(message) {
1308
1363
  if (activeExecution !== undefined) throw new Error('kernel received overlapping cells')
1364
+ if (message.cwd !== undefined && (typeof message.cwd !== 'string' || !isAbsoluteDirectory(message.cwd))) {
1365
+ throw new Error('ptc-plus cell cwd must be absolute')
1366
+ }
1367
+ executionCwd = message.cwd ?? sessionCwd
1368
+ cellOperationCwd = executionCwd
1369
+ const parent = cellModuleParent()
1370
+ const legacyRequire = message.languageSemantics === 'legacy-v1'
1371
+ const explicitCwd = executionCwd !== undefined
1372
+ if (parent !== providedRequireParent || legacyRequire !== providedRequireLegacy || message.moduleTransform !== providedRequireTransform
1373
+ || explicitCwd !== providedRequireExplicitCwd) {
1374
+ installProvidedRequire(parent, legacyRequire, message.moduleTransform, explicitCwd)
1375
+ }
1309
1376
  await writeOutputFence(nativeStdoutWrite, outputFenceMarker(message.outputFence, 'stdout', 'start'))
1310
1377
  await writeOutputFence(nativeStderrWrite, outputFenceMarker(message.outputFence, 'stderr', 'start'))
1311
1378
  await new ControlPromise(resolve => {
@@ -1368,6 +1435,7 @@ async function runCell(message) {
1368
1435
  context, message.rootRuntimeName, {
1369
1436
  configurable: true,
1370
1437
  value: statefulRoots.begin({ ...message.rootBindings,
1438
+ importModule: cellImportModuleFor(parent, message.moduleTransform),
1371
1439
  legacyLexicals,
1372
1440
  legacyWritable,
1373
1441
  legacyObjects: filterArray(legacyWritable, name => mapGet(legacyStorage, name) !== 'lexical'),
@@ -1486,7 +1554,11 @@ channel.on('message', (message) => {
1486
1554
  if (call === undefined || call.runId !== message.runId) return
1487
1555
  mapDelete(pending, message.id)
1488
1556
  try {
1489
- if (message.ok) call.resolve(decodeValue(message.value, call.valueLimits))
1557
+ if (message.ok) {
1558
+ const value = decodeValue(message.value, call.valueLimits)
1559
+ adoptConfirmedCellDirectory(call, value)
1560
+ call.resolve(value)
1561
+ }
1490
1562
  else if (call.BoundError === undefined) {
1491
1563
  const error = new Error(message.error)
1492
1564
  if (message.cause !== undefined) error.ptcCause = message.cause
@@ -6,26 +6,25 @@ const { Map, Set, mapGet, mapSet, mapHas, mapDelete, setHas, setAdd, appendArray
6
6
  /** Native storage and catalog-proved logical roots share a captured legacy view. */
7
7
  export function createNativeRootDynamic({ intrinsics, importModule, read, write, typeOf, remove, logicalReference }) {
8
8
  const interfaceOwner = {}
9
- const runtime = createDynamicEnvironmentRuntime({ ...intrinsics, importModule, interfaceOwner }).installIntrinsics()
10
9
  const descriptor = name => ({ kind: 'var', get: () => read(name),
11
10
  set: (value, strict) => write(name, value, strict), delete: () => remove(name), typeof: () => typeOf(name) })
12
- const environment = runtime.environment({
13
- ambient(name, strict) {
14
- return runtime.reference(name, descriptor(name), undefined, strict)
15
- },
16
- declareVars: runtime.declareGlobals,
17
- createVar: descriptor,
18
- })
19
- runtime.setRootEnvironment(() => environment)
20
11
  return {
21
- environment(callableSources, awaitRoot = false, logicalRoots = [], writableRoots = []) {
12
+ environment(callableSources, awaitRoot = false, logicalRoots = [], writableRoots = [], cellImportModule = importModule) {
22
13
  if (logicalRoots.length === 0) {
23
- runtime.registerSources(callableSources)
24
- return awaitRoot ? environment.awaitActivation() : environment
14
+ const cellRuntime = createDynamicEnvironmentRuntime({ ...intrinsics,
15
+ importModule: cellImportModule, interfaceOwner }).installIntrinsics()
16
+ const captured = cellRuntime.environment({
17
+ ambient: (name, strict) => cellRuntime.reference(name, descriptor(name), undefined, strict),
18
+ declareVars: cellRuntime.declareGlobals,
19
+ createVar: descriptor,
20
+ })
21
+ cellRuntime.setRootEnvironment(() => captured)
22
+ cellRuntime.registerSources(callableSources)
23
+ return awaitRoot ? captured.awaitActivation() : captured
25
24
  }
26
25
  // Each captured legacy environment keeps its own root view. Installing a
27
26
  // later bridge must not change escaped eval/Function calls from old cells.
28
- const bridgedRuntime = createDynamicEnvironmentRuntime({ ...intrinsics, importModule, interfaceOwner }).installIntrinsics()
27
+ const bridgedRuntime = createDynamicEnvironmentRuntime({ ...intrinsics, importModule: cellImportModule, interfaceOwner }).installIntrinsics()
29
28
  const writable = new Set()
30
29
  for (let index = 0; index < writableRoots.length; index++) setAdd(writable, writableRoots[index])
31
30
  const bindings = new Map(), varFrame = new Map(), roots = new Map()