dsh-ptc-plus 0.4.2 → 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 (83) hide show
  1. package/README.en.md +166 -76
  2. package/README.md +167 -77
  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 +593 -306
  7. package/compiler-core.cjs +162 -160
  8. package/compiler-core.cjs.map +3 -3
  9. package/docs/README.md +3 -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 +1 -1
  18. package/docs/adr/0026-persist-the-delivery-verdict-against-the-verified-candidate.md +23 -7
  19. package/docs/adr/0027-run-the-session-kernel-in-a-killable-helper-process.md +4 -0
  20. package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +26 -6
  21. package/docs/adr/0031-close-semantic-obligations-by-owner.md +29 -0
  22. package/docs/adr/README.md +4 -4
  23. package/docs/architecture.md +6 -5
  24. package/docs/binding-review-placement-design.md +1 -1
  25. package/docs/client-ui.md +8 -4
  26. package/docs/durability-design.md +7 -7
  27. package/docs/installation.md +26 -8
  28. package/docs/runtime-reference.md +18 -11
  29. package/docs/semantic-validation.md +7 -3
  30. package/docs/user-bindings.en.md +7 -6
  31. package/docs/user-bindings.md +6 -5
  32. package/docs/value-wire.md +1 -1
  33. package/docs/verification.md +38 -1
  34. package/index.js +39 -34
  35. package/internal/async-local-storage-intrinsics.js +12 -7
  36. package/internal/callable-source-facts.js +37 -1
  37. package/internal/cell-analysis.js +64 -9
  38. package/internal/config-spec.js +1 -1
  39. package/internal/cordis-tools-owner.js +20 -10
  40. package/internal/edit-transport-owner.js +80 -20
  41. package/internal/execution-seam-compat.js +192 -30
  42. package/internal/kernel-worker.js +162 -72
  43. package/internal/message-sources.js +14 -0
  44. package/internal/module-compilation.js +2 -1
  45. package/internal/repl-memory-projection.js +10 -0
  46. package/internal/repl-scope-normalizer.js +72 -35
  47. package/internal/repl-value-observer.js +141 -45
  48. package/internal/runtime-config.js +62 -2
  49. package/internal/runtime-intrinsics.js +3 -0
  50. package/internal/runtime-messages.js +6 -4
  51. package/internal/session-cell-executor.js +24 -1
  52. package/internal/session-journal-recovery.js +267 -67
  53. package/internal/session-journal.js +466 -40
  54. package/internal/session-log-view.js +5 -4
  55. package/internal/session-runtime.js +45 -21
  56. package/internal/session-state.js +7 -29
  57. package/internal/settings-compat.js +66 -1
  58. package/internal/source-position-map.js +19 -0
  59. package/internal/user-binding-console-worker.js +23 -9
  60. package/internal/user-binding-draft-projection.js +3 -2
  61. package/internal/user-bindings-owner.js +2 -1
  62. package/internal/user-bindings.js +6 -1
  63. package/internal/validated-parse-repair.js +6 -2
  64. package/internal/value-wire.js +95 -30
  65. package/internal/worker-client.js +99 -8
  66. package/internal/worker-cwd-virtualization.js +10 -9
  67. package/internal/worker-output-capture.js +187 -0
  68. package/internal/worker-output-fence.js +26 -0
  69. package/internal/worker-realm-surfaces.js +197 -0
  70. package/internal/worker-repl-realm.js +10 -4
  71. package/package.json +36 -4
  72. package/src/client-activity.js +4 -1
  73. package/src/client-authoring-view.js +121 -62
  74. package/src/client-console.js +4 -2
  75. package/src/client-copy.js +4 -2
  76. package/src/client-feature-gates.js +7 -9
  77. package/src/client-host-compat.js +104 -27
  78. package/src/client-repl-view.js +3 -1
  79. package/src/client-styles.js +12 -1
  80. package/src/client-tool-view.js +3 -1
  81. package/src/client-transport.js +2 -2
  82. package/src/client-workbench.js +4 -3
  83. package/src/client.js +56 -17
package/index.js CHANGED
@@ -14,12 +14,12 @@ import { PTC_DELIVERY_CONTEXT } from './internal/runtime-messages.js'
14
14
  import { createCordisToolsOwner } from './internal/cordis-tools-owner.js'
15
15
  import { createEditTransportOwner, EDIT_RUN_CODE } from './internal/edit-transport-owner.js'
16
16
  import { createRuntimeBridgeOwner, RUN_CODE } from './internal/runtime-bridge-owner.js'
17
- import { resolveConfig } from './internal/runtime-config.js'
17
+ import { resolveConfig, watchVolatileConfig } from './internal/runtime-config.js'
18
18
  import {
19
19
  CONFIG_FIELDS,
20
20
  SETTINGS_NAMESPACE,
21
21
  } from './internal/config-spec.js'
22
- import { installSettingsSectionCompat } from './internal/settings-compat.js'
22
+ import { configFieldSchema, installSettingsSectionCompat } from './internal/settings-compat.js'
23
23
  import { installExecutionSeam } from './internal/execution-seam-compat.js'
24
24
  import { valueLimitsFromConfig } from './internal/value-wire-schema.js'
25
25
  import { createReplMemoryProjection } from './internal/repl-memory-projection.js'
@@ -35,19 +35,13 @@ const INSTALL_CLEANUP = Symbol('ptc-plus install cleanup')
35
35
  /** Plugin name used by loader diagnostics. */
36
36
  export const name = 'ptc-plus'
37
37
 
38
- /** Runtime limits and behavior exposed to Cordis configuration. */
39
- function configSchemaField(field) {
40
- const base = field.type === 'boolean'
41
- ? Schema.boolean().default(field.default)
42
- : field.type === 'enum'
43
- // Keep omission visible until resolveConfig migrates legacy policies.
44
- ? Schema.union(field.options.map(option => Schema.const(option)))
45
- : Schema.number().step(1).min(field.min).max(field.max).default(field.default)
46
- return base.description(field.description)
47
- }
48
-
49
38
  export const Config = Schema.object(Object.fromEntries(
50
- CONFIG_FIELDS.map(field => [field.key, configSchemaField(field)]),
39
+ CONFIG_FIELDS.map(field => [
40
+ field.key,
41
+ // Runtime limits and behavior exposed to Cordis configuration. The shape
42
+ // each field takes is owned by the installed settings generation.
43
+ configFieldSchema({ Schema, field, settingsModule: dshSettings }),
44
+ ]),
51
45
  ))
52
46
 
53
47
  /**
@@ -408,22 +402,27 @@ function installPtCRuntime(ctx, resolvedConfig, seam, toolSchemasForAgent, sessi
408
402
  if (exec.name === EDIT_RUN_CODE) return editTransport.handleResult(exec, result)
409
403
  if (exec.name === RUN_CODE) return runtimeBridge.handleResult(exec, result)
410
404
  }))
411
- disposers.push(ctx.on('agent/disposed', async ({ agent }) => {
412
- directSurface.disposeAgent(agent)
413
- editTransport.disposeAgent(agent)
414
- await Promise.all([
415
- runtimeBridge.disposeAgent(agent),
416
- userBindings.clearAgentPresentation(agent),
417
- ])
418
- }))
419
- disposers.push(ctx.on('session/disposed', async (session) => {
420
- directSurface.disposeSession(session)
421
- editTransport.disposeSession(session)
422
- await Promise.all([
423
- runtimeBridge.disposeSession(session),
424
- userBindings.clearSessionPresentation(session?.id ?? session),
425
- ])
426
- }))
405
+ const disposeLifecycleOwners = async (operations, message) => {
406
+ const results = await Promise.allSettled(operations.map(operation => (
407
+ Promise.resolve().then(operation)
408
+ )))
409
+ const failures = results
410
+ .filter(result => result.status === 'rejected')
411
+ .map(result => result.reason)
412
+ if (failures.length > 0) throw new AggregateError(failures, message)
413
+ }
414
+ disposers.push(ctx.on('agent/disposed', ({ agent }) => disposeLifecycleOwners([
415
+ () => directSurface.disposeAgent(agent),
416
+ () => editTransport.disposeAgent(agent),
417
+ () => runtimeBridge.disposeAgent(agent),
418
+ () => userBindings.clearAgentPresentation(agent),
419
+ ], 'ptc-plus: agent disposal failed')))
420
+ disposers.push(ctx.on('session/disposed', session => disposeLifecycleOwners([
421
+ () => directSurface.disposeSession(session),
422
+ () => editTransport.disposeSession(session),
423
+ () => runtimeBridge.disposeSession(session),
424
+ () => userBindings.clearSessionPresentation(session?.id ?? session),
425
+ ], 'ptc-plus: session disposal failed')))
427
426
  disposers.push(ctx.on('agent-preset/selected', (sessionId) => {
428
427
  directSurface.resetSessionComposition(sessionId)
429
428
  const agent = ctx.agents?.get?.(String(sessionId))
@@ -512,18 +511,22 @@ function installPtCRuntime(ctx, resolvedConfig, seam, toolSchemasForAgent, sessi
512
511
  }
513
512
 
514
513
  /** Register the session-bound REPL runtime. */
515
- export function apply(ctx, config = {}) {
514
+ export const apply = (ctx, config = {}) => {
516
515
  // Configuration is validated before the plugin waits for the host's seam, so
517
516
  // an unusable configuration fails at the load call rather than later.
518
517
  const resolvedConfig = resolveConfig(config)
519
518
  return installExecutionSeam(ctx, {
520
519
  services: inject,
521
- attach: (scope, seam) => applyWithExecutionSeam(scope, resolvedConfig, seam),
520
+ attach: (scope, seam) => applyWithExecutionSeam(scope, resolvedConfig, seam, ctx, config),
521
+ reportFailure: (error, seamName) => ctx.logger?.warn?.(
522
+ `ptc-plus: execution seam ${seamName} failed to attach`,
523
+ error,
524
+ ),
522
525
  })
523
526
  }
524
527
 
525
528
  /** Install the plugin into the scope of the seam the host registered. */
526
- function applyWithExecutionSeam(ctx, resolvedConfig, seam) {
529
+ function applyWithExecutionSeam(ctx, resolvedConfig, seam, volatileHost, volatileConfig) {
527
530
  const toolSchemasForAgent = agent => typeof ctx.tools.schemas === 'function'
528
531
  ? ctx.tools.schemas(agent)
529
532
  : []
@@ -767,7 +770,7 @@ function applyWithExecutionSeam(ctx, resolvedConfig, seam) {
767
770
  ctx.effect(() => ctx.on('agent/disposed', ({ agent }) => messageOwner.disposeAgent(agent)), 'ptc-plus message agent disposal')
768
771
  ctx.effect(() => () => messageOwner.dispose(), 'ptc-plus dynamic message lifecycle')
769
772
 
770
- let configSource = () => resolvedConfig
773
+ let configSource = () => resolveConfig(volatileConfig ?? resolvedConfig)
771
774
  let configurationGeneration = 0
772
775
  let settingsWriter
773
776
  let configurationRollback = false
@@ -826,6 +829,7 @@ function applyWithExecutionSeam(ctx, resolvedConfig, seam) {
826
829
  if (typeof ctx.inject === 'function') {
827
830
  installSettingsSectionCompat({
828
831
  ctx,
832
+ ownerFiber: volatileHost.fiber,
829
833
  settingsModule: dshSettings,
830
834
  namespace: SETTINGS_NAMESPACE,
831
835
  schema: Config,
@@ -841,5 +845,6 @@ function applyWithExecutionSeam(ctx, resolvedConfig, seam) {
841
845
  },
842
846
  })
843
847
  }
848
+ watchVolatileConfig(volatileHost, ctx, volatileConfig, () => reconcile())
844
849
  if (configurationGeneration === 0) return reconcile(settingsWriter === undefined)
845
850
  }
@@ -1,7 +1,7 @@
1
1
  import { AsyncLocalStorage } from 'node:async_hooks'
2
+ import { WORKER_REALM_MUTATION, defineWorkerRealmProperty } from './worker-realm-surfaces.js'
2
3
 
3
4
  const bind = Function.prototype.call.bind(Function.prototype.bind)
4
- const defineProperty = Object.defineProperty
5
5
  const getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor
6
6
  const apply = Reflect.apply
7
7
  const nativeGetStore = AsyncLocalStorage.prototype.getStore
@@ -10,9 +10,12 @@ const nativeRun = AsyncLocalStorage.prototype.run
10
10
  const nativeRunDescriptor = getOwnPropertyDescriptor(AsyncLocalStorage.prototype, 'run')
11
11
 
12
12
  function protect(scope) {
13
- defineProperty(scope, 'getStore', { value: bind(nativeGetStore, scope) })
14
- defineProperty(scope, 'enterWith', { value: bind(nativeEnterWith, scope) })
15
- defineProperty(scope, 'run', { value: bind(nativeRun, scope) })
13
+ defineWorkerRealmProperty(WORKER_REALM_MUTATION.STABLE, 'private AsyncLocalStorage receiver methods',
14
+ scope, 'getStore', { value: bind(nativeGetStore, scope) })
15
+ defineWorkerRealmProperty(WORKER_REALM_MUTATION.STABLE, 'private AsyncLocalStorage receiver methods',
16
+ scope, 'enterWith', { value: bind(nativeEnterWith, scope) })
17
+ defineWorkerRealmProperty(WORKER_REALM_MUTATION.STABLE, 'private AsyncLocalStorage receiver methods',
18
+ scope, 'run', { value: bind(nativeRun, scope) })
16
19
  return scope
17
20
  }
18
21
 
@@ -22,17 +25,19 @@ export function createPrivateAsyncLocalStorage() {
22
25
 
23
26
  export function protectNextAsyncLocalStorageRun(invoke) {
24
27
  let scope
25
- defineProperty(AsyncLocalStorage.prototype, 'run', {
28
+ defineWorkerRealmProperty(WORKER_REALM_MUTATION.TEMPORARY, 'AsyncLocalStorage.prototype.run',
29
+ AsyncLocalStorage.prototype, 'run', {
26
30
  ...nativeRunDescriptor,
27
31
  value: function (...args) {
28
32
  scope ??= protect(this)
29
33
  return apply(nativeRun, this, args)
30
34
  },
31
- })
35
+ })
32
36
  try {
33
37
  invoke()
34
38
  } finally {
35
- defineProperty(AsyncLocalStorage.prototype, 'run', nativeRunDescriptor)
39
+ defineWorkerRealmProperty(WORKER_REALM_MUTATION.RESTORE, 'AsyncLocalStorage.prototype.run',
40
+ AsyncLocalStorage.prototype, 'run', nativeRunDescriptor)
36
41
  }
37
42
  return scope
38
43
  }
@@ -43,6 +43,16 @@ function callableSourceStart(source, node) {
43
43
  return start + tokens.getToken().start
44
44
  }
45
45
 
46
+ function removeCallableSourceMarks(source, prefix, suffix, sources, options) {
47
+ const tree = parse(prefix + source + suffix, options)
48
+ const markers = new Set(sources.entries.map(entry => entry.marker))
49
+ const offset = prefix.length
50
+ const edits = tree.comments.filter(comment => markers.has(comment.value)
51
+ && comment.start >= offset && comment.end <= offset + source.length)
52
+ .map(comment => ({ start: comment.start - offset, end: comment.end - offset, text: '' }))
53
+ return edits.length === 0 ? source : applySourceEdits(source, identitySourceMap(source.length), edits).code
54
+ }
55
+
46
56
  export function callableMarkerPosition(node, source, comments) {
47
57
  if (node.type !== 'ArrowFunctionExpression') return node.body.start + 1
48
58
  let start = source.lastIndexOf('=>', node.body.start - 1)
@@ -129,6 +139,7 @@ export function markCallableSources(source, sourceMap = identitySourceMap(source
129
139
  const nativeFunctions = typedRecords.map(record => markedNodes.get(record.marker))
130
140
  .filter(node => t.isFunction(node) && !isMethod(node)).sort((left, right) => left.start - right.start)
131
141
  const nativeRecipes = new Map()
142
+ const aliasMarkers = []
132
143
  const nativeFunctionIndex = start => {
133
144
  let low = 0, high = nativeFunctions.length
134
145
  while (low < high) {
@@ -175,7 +186,17 @@ export function markCallableSources(source, sourceMap = identitySourceMap(source
175
186
  original = lowered.source
176
187
  const entries = [...originals].map(([key, text]) => [key, key === marker ? original : text])
177
188
  for (const [node, fact] of markedCallableOwners(lowered.emitted, entries)) {
178
- sources.aliases.push([lowered.emittedSource.slice(node.start, node.end), fact.original])
189
+ const generated = lowered.emittedSource.slice(node.start, node.end)
190
+ aliasMarkers.push([generated, fact.marker])
191
+ const generatedMethod = isMethod(node)
192
+ const markerFree = removeCallableSourceMarks(
193
+ generated,
194
+ generatedMethod ? 'class Source {' : '(',
195
+ generatedMethod ? '}' : ')',
196
+ sources,
197
+ options,
198
+ )
199
+ if (markerFree !== generated) aliasMarkers.push([markerFree, fact.marker])
179
200
  }
180
201
  }
181
202
  // Prefer source-preserving erasure. Runtime TypeScript constructs such as
@@ -202,6 +223,7 @@ export function markCallableSources(source, sourceMap = identitySourceMap(source
202
223
  } catch {
203
224
  lowerCallable()
204
225
  }
226
+ original = removeCallableSourceMarks(original, prefix, suffix, sources, options)
205
227
  entry.buffer = sources.buffers.length
206
228
  sources.buffers.push(original)
207
229
  entry.start = 0
@@ -209,6 +231,8 @@ export function markCallableSources(source, sourceMap = identitySourceMap(source
209
231
  originals.set(marker, original)
210
232
  if (!method && t.isFunction(node)) nativeRecipes.set(node, { node, start, end: node.end, text: original })
211
233
  }
234
+ const finalFacts = factMap(sources)
235
+ for (const [generated, marker] of aliasMarkers) sources.aliases.push([generated, originalText(finalFacts.get(marker))])
212
236
  return { ...marked, callableSources: sources, nativeJavaScript }
213
237
  }
214
238
 
@@ -273,6 +297,18 @@ export function collectCallableSources(code, sources, parserOptions = {}, transf
273
297
  }).concat(sources.aliases ?? [])
274
298
  }
275
299
 
300
+ /** Restore only callable marks whose transformed starts have exact source-map ownership. */
301
+ export function restoreMappedCallableSourceMarks(mapped, input, transformation, sources, parserOptions = {}) {
302
+ if (sources === undefined || sources.length === 0) return mapped
303
+ const { tree, owners } = callableSourceOwners(mapped.code, sources, parserOptions,
304
+ { code: input, map: transformation.map })
305
+ const edits = [...owners].map(([node, fact]) => {
306
+ const start = callableMarkerPosition(node, mapped.code, tree.comments)
307
+ return { start, end: start, text: `/*${fact.marker}*/` }
308
+ })
309
+ return edits.length === 0 ? mapped : applySourceEdits(mapped.code, mapped.sourceMap, edits)
310
+ }
311
+
276
312
  /** Emission ranges come from typed nodes and exact generator mappings. */
277
313
  export function regionCallableRanges(input, generated, sourceRanges, parserOptions) {
278
314
  if (!sourceRanges?.length) return []
@@ -20,7 +20,7 @@ import {
20
20
  import { applySourceEdits, mapSourcePosition, mapSourceSpan, sourceRangeHasOriginalText } from './source-position-map.js'
21
21
  import { SKIP_AST_CHILDREN, walkAst } from './ast-traversal.js'
22
22
  import { compileStatefulRoot } from './stateful-root-compiler.js'
23
- import { CELL_PARSER_PLUGINS, bindCompilerIntrinsics, lowerStatefulDecorators, lowerStatefulResources, lowerNativeLanguageSource, normalizeStatefulScopes } from './repl-scope-normalizer.js'
23
+ import { CELL_PARSER_PLUGINS, bindCompilerIntrinsics, lowerStatefulDecorators, lowerStatefulResources, lowerNativeLanguageSource, normalizationFailureInput, normalizeStatefulScopes } from './repl-scope-normalizer.js'
24
24
  import { markCallableSources, collectCallableSources } from './callable-source-facts.js'
25
25
  import { LegacyPreflightError, prepareLegacyProgram } from './legacy-cell-analysis.js'
26
26
  import { adaptLegacyModuleImports } from './managed-module-operations.js'
@@ -220,6 +220,49 @@ function isFunction(node) {
220
220
  || node.type === 'ArrowFunctionExpression'
221
221
  }
222
222
 
223
+ function lowerCellImportMeta(result, importMeta, originalSource) {
224
+ if (!result.code.includes('import')) return result
225
+ const tree = parseSource(result.code, {
226
+ sourceType: 'unambiguous',
227
+ errorRecovery: true,
228
+ allowAwaitOutsideFunction: true,
229
+ allowImportExportEverywhere: true,
230
+ allowReturnOutsideFunction: true,
231
+ allowUndeclaredExports: true,
232
+ plugins: CELL_PARSER_PLUGINS,
233
+ }).program
234
+ const references = []
235
+ walkAst(tree, (node) => {
236
+ if (node.type === 'MetaProperty' && node.meta?.name === 'import' && node.property?.name === 'meta') {
237
+ references.push(node)
238
+ }
239
+ })
240
+ if (references.length === 0) return result
241
+ if (importMeta === null || typeof importMeta !== 'object'
242
+ || typeof importMeta.url !== 'string' || typeof importMeta.filename !== 'string'
243
+ || typeof importMeta.dirname !== 'string') {
244
+ const node = references[0]
245
+ throw new ModuleRewriteError('import.meta requires the session module base', mapSourcePosition({
246
+ line: node.loc.start.line,
247
+ column: node.loc.start.column + 1,
248
+ }, result.code, originalSource, result.sourceMap))
249
+ }
250
+ const binding = createGeneratedNameAllocator(tree)('import_meta')
251
+ const directiveEnd = tree.directives.at(-1)?.end ?? tree.interpreter?.end ?? 0
252
+ const declaration = `\nconst ${binding}={__proto__:null,url:${JSON.stringify(importMeta.url)},filename:${JSON.stringify(importMeta.filename)},dirname:${JSON.stringify(importMeta.dirname)}};\n`
253
+ const edits = [
254
+ { start: directiveEnd, end: directiveEnd, text: declaration },
255
+ ...references.map(node => ({
256
+ start: node.start,
257
+ end: node.end,
258
+ text: binding,
259
+ mappings: [{ generatedStart: 0, generatedEnd: binding.length,
260
+ originalStart: node.start, originalEnd: node.end }],
261
+ })),
262
+ ]
263
+ return { ...result, ...applySourceEdits(result.code, result.sourceMap, edits), importMetaBinding: binding }
264
+ }
265
+
223
266
 
224
267
  function staticModuleClassification(moduleLoads) {
225
268
  const reasons = new Map()
@@ -282,14 +325,16 @@ export function classifyDurability(code, knownBindings = new Set(), {
282
325
  const reasons = new Map()
283
326
  const addReason = reason => reasons.set(renderDurabilityReason(reason), reason)
284
327
  for (const reason of staticModuleClassification(moduleLoads)) addReason(reason)
285
- const classifyModule = (source) => {
328
+ const classifyModule = (source, form) => {
286
329
  if (source?.type !== 'Literal' || typeof source.value !== 'string') {
287
330
  addReason(DYNAMIC_MODULE_REASON)
288
331
  return
289
332
  }
290
333
  const classification = classifyModuleSource(source.value)
291
334
  if (classification.status === 'forbidden') {
292
- throw new PreflightError(`cell import of ${source.value} is forbidden because it exposes kernel control`, source)
335
+ const span = sourceMap === undefined || originalSource === undefined ? declarationSpan(source)
336
+ : mapSourceSpan(declarationSpan(source), code, originalSource, sourceMap)
337
+ throw new PreflightError(`cell ${form} of ${source.value} is forbidden because it exposes kernel control`, undefined, span)
293
338
  }
294
339
  if (classification.reason !== undefined) addReason(classification.reason)
295
340
  }
@@ -327,7 +372,7 @@ export function classifyDurability(code, knownBindings = new Set(), {
327
372
  nestedScopes = [...scopes, loopBindings(node)]
328
373
  }
329
374
  if (node.type === 'ImportExpression') {
330
- classifyModule(node.source)
375
+ classifyModule(node.source, 'import')
331
376
  }
332
377
  const sourceOwnedThis = sourceMap === undefined
333
378
  || sourceRangeHasOriginalText(sourceMap, code, originalSource, node.start, node.end)
@@ -336,7 +381,7 @@ export function classifyDurability(code, knownBindings = new Set(), {
336
381
  }
337
382
  if (node.type === 'CallExpression' && node.callee?.type === 'Identifier' && node.callee.name === 'require'
338
383
  && !isBound('require', nestedScopes)) {
339
- classifyModule(node.arguments[0])
384
+ classifyModule(node.arguments[0], 'require')
340
385
  }
341
386
  if (node.type === 'Identifier' && isReferenceIdentifier(node, parent, parentKey)
342
387
  && !isBound(node.name, nestedScopes) && AMBIENT_GLOBALS.has(node.name)
@@ -462,21 +507,31 @@ export function prepareProgram(program, options = {}) {
462
507
  let normalized
463
508
  let prepared
464
509
  let callableSources
510
+ let diagnosticInput
465
511
  const intrinsicContext = { bindings: new Set() }
466
512
  try {
467
513
  const marked = markCallableSources(program, undefined, { plugins: CELL_PARSER_PLUGINS, allowImportExportEverywhere: true, allowUndeclaredExports: true },
468
514
  { lowerNativeSource: lowerNativeLanguageSource, nativeUsing: options.nativeUsing })
515
+ const moduleMetadata = lowerCellImportMeta(marked, options.importMeta, program)
516
+ diagnosticInput = moduleMetadata
469
517
  callableSources = marked.callableSources
470
- normalized = normalizeStatefulScopes(marked.code, marked.sourceMap, { mode: options.languageSemantics,
471
- nativeJavaScript: marked.nativeJavaScript, deferDecorators: true, deferResources: true, intrinsicContext })
518
+ normalized = normalizeStatefulScopes(moduleMetadata.code, moduleMetadata.sourceMap, { mode: options.languageSemantics,
519
+ callableSources, nativeJavaScript: marked.nativeJavaScript,
520
+ deferDecorators: true, deferResources: true, intrinsicContext })
521
+ if (moduleMetadata.importMetaBinding !== undefined) {
522
+ normalized.internalBindings = new Set(normalized.internalBindings)
523
+ normalized.internalBindings.add(moduleMetadata.importMetaBinding)
524
+ }
525
+ diagnosticInput = normalized
472
526
  prepared = compileStatefulRoot(normalized.code, { ...options, sourceMap: normalized.sourceMap,
473
527
  internalBindings: normalized.internalBindings, dynamicBindings: normalized.dynamicBindings,
474
528
  privateBindings: normalized.privateBindings, originalSource: program })
475
529
  } catch (error) {
476
530
  if (error instanceof ModuleRewriteError) throw error
477
531
  const position = error.loc === undefined ? undefined : { line: error.loc.line, column: error.loc.column + 1 }
478
- throw new ModuleRewriteError(error.message, normalized === undefined ? position
479
- : mapSourcePosition(position, normalized.code, program, normalized.sourceMap))
532
+ const failureInput = normalizationFailureInput(error) ?? diagnosticInput
533
+ throw new ModuleRewriteError(error.message, failureInput === undefined ? position
534
+ : mapSourcePosition(position, failureInput.code, program, failureInput.sourceMap))
480
535
  }
481
536
  prepared = { ...prepared, deferredHelpers: normalized.deferredHelpers }
482
537
  intrinsicContext.expression = `this[${JSON.stringify(prepared.rootRuntimeName)}].intrinsics`
@@ -96,7 +96,7 @@ export const CONFIG_FIELDS = Object.freeze([
96
96
  {
97
97
  key: 'userBindingsEnabled',
98
98
  type: 'boolean',
99
- default: false,
99
+ default: true,
100
100
  label: '启用全局用户 Binding',
101
101
  labelEn: 'Enable Global User Bindings',
102
102
  description: '管理 TypeScript helper,并将已启用条目作为跨会话的默认 REPL 绑定。',
@@ -133,12 +133,22 @@ function requireCompanionServices(agent) {
133
133
 
134
134
  async function companionSkillDirectory(agentPresets) {
135
135
  const preset = await agentPresets.resolve(CORDIS_PRESET_ID)
136
- if (typeof preset?.path !== 'string' || !isAbsolute(preset.path)) {
137
- throw new Error('ptc-plus: DSH cordis preset did not expose an absolute composition path')
138
- }
139
136
  if (typeof preset.broken === 'string' && preset.broken.length > 0) {
140
137
  throw new Error(`ptc-plus: DSH cordis preset is unavailable: ${preset.broken}`)
141
138
  }
139
+ // The generations that discover presets from their composition directories
140
+ // publish an absolute path, and the preset's sibling `skills` directory is
141
+ // then the companion Skill root. A generation that declares presets without
142
+ // any composition path publishes no root through this surface at all, so the
143
+ // report names that missing publication instead of claiming a shape failure
144
+ // on a healthy host. A published path that is not absolute is still a
145
+ // contract contradiction.
146
+ if (preset?.path === undefined) {
147
+ throw new Error('ptc-plus: the installed DSH agentPresets generation publishes no composition path for the "cordis" preset, so it does not publish the companion Skill root that cordisToolsEnabled mounts')
148
+ }
149
+ if (typeof preset.path !== 'string' || !isAbsolute(preset.path)) {
150
+ throw new Error('ptc-plus: DSH cordis preset did not expose an absolute composition path')
151
+ }
142
152
  return join(dirname(preset.path), 'skills')
143
153
  }
144
154
 
@@ -329,6 +339,7 @@ export function createCordisToolsOwner(
329
339
 
330
340
  const installAgent = (agent) => {
331
341
  if (disposed) return Promise.resolve()
342
+ if (withdrawn.has(agent)) return Promise.resolve()
332
343
  const mounted = mounts.get(agent)
333
344
  if (mounted !== undefined) {
334
345
  return mounted.withdrawn === true ? Promise.resolve() : mounted.activation
@@ -416,11 +427,11 @@ export function createCordisToolsOwner(
416
427
  /**
417
428
  * Install for a call the host makes on its own behalf.
418
429
  *
419
- * DSH 0.1.6 announces `agent/created` serially and rejects `agents.create()`
420
- * when a listener rejects, so a scope this owner cannot serve must withdraw
421
- * instead of failing an operation another plugin asked for. Install-time
422
- * enumeration stays strict: there the plugin speaks for what it claims to
423
- * have published, and `ready` reports the same failure.
430
+ * The public host lifecycle announces `agent/created` serially and rejects
431
+ * `agents.create()` when a listener rejects, so a scope this owner cannot
432
+ * serve must withdraw instead of failing an operation another plugin asked
433
+ * for. Install-time enumeration stays strict: there the plugin speaks for
434
+ * what it claims to have published, and `ready` reports the same failure.
424
435
  */
425
436
  const installForHost = (agent) => {
426
437
  let activation
@@ -451,8 +462,7 @@ export function createCordisToolsOwner(
451
462
  if (disposed) return
452
463
  for (const agent of [...pending]) {
453
464
  void Promise.resolve().then(() => installAgent(agent)).catch(error => {
454
- pending.delete(agent)
455
- ctx.logger?.warn?.('ptc-plus: deferred Cordis activation failed', error)
465
+ withdraw(agent, error)
456
466
  })
457
467
  }
458
468
  })
@@ -238,11 +238,34 @@ export function createEditTransportOwner(ctx, {
238
238
  }
239
239
  }
240
240
 
241
+ const disposeInstalled = (installed) => {
242
+ const failures = []
243
+ for (const key of ['disposePresentation', 'disposeRegistration']) {
244
+ const dispose = installed[key]
245
+ if (dispose === undefined) continue
246
+ try {
247
+ dispose()
248
+ installed[key] = undefined
249
+ } catch (error) {
250
+ failures.push(error)
251
+ }
252
+ }
253
+ if (failures.length > 0) {
254
+ throw new AggregateError(failures, 'ptc-plus: edit_run_code scope disposal failed')
255
+ }
256
+ }
257
+
241
258
  const uninstall = (agent) => {
242
259
  const installed = installedScopes.get(agent)
243
260
  if (installed === undefined) return
244
- installedScopes.delete(agent)
245
- installed.dispose()
261
+ try {
262
+ disposeInstalled(installed)
263
+ } finally {
264
+ if (installed.disposePresentation === undefined
265
+ && installed.disposeRegistration === undefined) {
266
+ installedScopes.delete(agent)
267
+ }
268
+ }
246
269
  }
247
270
 
248
271
  return Object.freeze({
@@ -251,11 +274,19 @@ export function createEditTransportOwner(ctx, {
251
274
  currentDurableReplay = nextConfig.durableReplay
252
275
  },
253
276
  isInstalled(agent) {
254
- return installedScopes.has(agent)
277
+ const installed = installedScopes.get(agent)
278
+ return installed?.disposePresentation !== undefined
279
+ && installed.disposeRegistration !== undefined
255
280
  },
256
281
  ensureInstalled(agent) {
257
282
  const installed = installedScopes.get(agent)
258
- if (installed !== undefined) return installed.definition
283
+ if (installed !== undefined) {
284
+ if (installed.disposePresentation !== undefined
285
+ && installed.disposeRegistration !== undefined) {
286
+ return installed.definition
287
+ }
288
+ uninstall(agent)
289
+ }
259
290
  const liveSchemas = typeof toolSchemasForAgent === 'function'
260
291
  ? toolSchemasForAgent(agent)
261
292
  : []
@@ -271,24 +302,31 @@ export function createEditTransportOwner(ctx, {
271
302
  if (typeof disposeRegistration !== 'function') {
272
303
  throw new Error('ptc-plus: DSH tools.register did not return a disposer')
273
304
  }
274
- let disposePresentation
305
+ const nextInstalled = {
306
+ definition,
307
+ disposePresentation: undefined,
308
+ disposeRegistration,
309
+ sessionId: sessionId(agent),
310
+ }
311
+ installedScopes.set(agent, nextInstalled)
275
312
  try {
276
- disposePresentation = scopedTools.presentAs('both')
313
+ const disposePresentation = scopedTools.presentAs('both')
277
314
  if (typeof disposePresentation !== 'function') {
278
315
  throw new Error('ptc-plus: DSH tools.presentAs did not return a disposer')
279
316
  }
317
+ nextInstalled.disposePresentation = disposePresentation
280
318
  } catch (error) {
281
- disposeRegistration()
319
+ try {
320
+ uninstall(agent)
321
+ } catch (cleanupError) {
322
+ throw new AggregateError(
323
+ [error, cleanupError],
324
+ 'ptc-plus: edit_run_code installation and rollback failed',
325
+ { cause: error },
326
+ )
327
+ }
282
328
  throw error
283
329
  }
284
- let active = true
285
- const dispose = () => {
286
- if (!active) return
287
- active = false
288
- disposePresentation()
289
- disposeRegistration()
290
- }
291
- installedScopes.set(agent, { definition, dispose, sessionId: sessionId(agent) })
292
330
  return definition
293
331
  },
294
332
  handleResult(exec, result) {
@@ -305,28 +343,50 @@ export function createEditTransportOwner(ctx, {
305
343
  releaseClaim(pending.id, pending.targetCallSeq)
306
344
  },
307
345
  disposeAgent(agent) {
308
- uninstall(agent)
346
+ let failure
347
+ try {
348
+ uninstall(agent)
349
+ } catch (error) {
350
+ failure = error
351
+ }
309
352
  const id = sessionId(agent)
310
353
  if (id !== undefined) {
311
354
  discardSettlements(id)
312
355
  editClaims.delete(id)
313
356
  }
357
+ if (failure !== undefined) throw failure
314
358
  },
315
359
  disposeSession(session) {
316
360
  const id = String(session.id)
361
+ const failures = []
317
362
  for (const [agent, installed] of installedScopes) {
318
363
  if (installed.sessionId !== id) continue
319
- installedScopes.delete(agent)
320
- installed.dispose()
364
+ try {
365
+ uninstall(agent)
366
+ } catch (error) {
367
+ failures.push(error)
368
+ }
321
369
  }
322
370
  discardSettlements(id)
323
371
  editClaims.delete(id)
372
+ if (failures.length > 0) {
373
+ throw new AggregateError(failures, 'ptc-plus: edit_run_code session disposal failed')
374
+ }
324
375
  },
325
376
  dispose() {
326
377
  discardSettlements()
327
378
  editClaims.clear()
328
- for (const installed of installedScopes.values()) installed.dispose()
329
- installedScopes.clear()
379
+ const failures = []
380
+ for (const agent of [...installedScopes.keys()]) {
381
+ try {
382
+ uninstall(agent)
383
+ } catch (error) {
384
+ failures.push(error)
385
+ }
386
+ }
387
+ if (failures.length > 0) {
388
+ throw new AggregateError(failures, 'ptc-plus: edit_run_code owner disposal failed')
389
+ }
330
390
  },
331
391
  })
332
392
  }