dsh-ptc-plus 0.4.2 → 0.4.8

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 (89) hide show
  1. package/README.en.md +170 -76
  2. package/README.md +171 -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 +599 -310
  7. package/compiler-core.cjs +162 -160
  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 +49 -11
  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/0017-track-the-latest-dsh-public-surface.md +5 -1
  15. package/docs/adr/0019-plugin-settings-and-kill-switch.md +3 -3
  16. package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +3 -1
  17. package/docs/adr/0021-separate-host-bootstrap-from-session-runtime.md +14 -3
  18. package/docs/adr/0023-global-user-bindings.md +1 -1
  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 +4 -0
  21. package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +26 -6
  22. package/docs/adr/0030-serve-both-typert-codec-generations-from-one-codec.md +2 -2
  23. package/docs/adr/0031-close-semantic-obligations-by-owner.md +29 -0
  24. package/docs/adr/README.md +4 -4
  25. package/docs/architecture.md +8 -5
  26. package/docs/binding-review-placement-design.md +1 -1
  27. package/docs/client-ui.md +8 -4
  28. package/docs/durability-design.md +7 -7
  29. package/docs/installation.md +48 -12
  30. package/docs/publishing.md +4 -4
  31. package/docs/release-notes.md +109 -0
  32. package/docs/runtime-reference.md +25 -12
  33. package/docs/semantic-validation.md +7 -3
  34. package/docs/user-bindings.en.md +7 -6
  35. package/docs/user-bindings.md +6 -5
  36. package/docs/value-wire.md +1 -1
  37. package/docs/verification.md +43 -2
  38. package/index.js +39 -34
  39. package/internal/async-local-storage-intrinsics.js +12 -7
  40. package/internal/callable-source-facts.js +37 -1
  41. package/internal/cell-analysis.js +64 -9
  42. package/internal/config-spec.js +1 -1
  43. package/internal/cordis-tools-owner.js +81 -15
  44. package/internal/direct-surface-owner.js +331 -7
  45. package/internal/edit-transport-owner.js +80 -20
  46. package/internal/execution-seam-compat.js +192 -30
  47. package/internal/kernel-worker.js +162 -72
  48. package/internal/message-sources.js +14 -0
  49. package/internal/module-compilation.js +2 -1
  50. package/internal/rejected-cell-editor.js +11 -21
  51. package/internal/repl-memory-projection.js +10 -0
  52. package/internal/repl-scope-normalizer.js +72 -35
  53. package/internal/repl-value-observer.js +141 -45
  54. package/internal/runtime-config.js +62 -2
  55. package/internal/runtime-intrinsics.js +3 -0
  56. package/internal/runtime-messages.js +6 -4
  57. package/internal/session-cell-executor.js +24 -1
  58. package/internal/session-journal-recovery.js +267 -67
  59. package/internal/session-journal.js +466 -40
  60. package/internal/session-log-view.js +5 -4
  61. package/internal/session-runtime.js +45 -21
  62. package/internal/session-state.js +7 -29
  63. package/internal/settings-compat.js +66 -1
  64. package/internal/source-position-map.js +19 -0
  65. package/internal/user-binding-console-worker.js +23 -9
  66. package/internal/user-binding-draft-projection.js +3 -2
  67. package/internal/user-bindings-owner.js +2 -1
  68. package/internal/user-bindings.js +6 -1
  69. package/internal/validated-parse-repair.js +6 -2
  70. package/internal/value-wire.js +95 -30
  71. package/internal/worker-client.js +99 -8
  72. package/internal/worker-cwd-virtualization.js +10 -9
  73. package/internal/worker-output-capture.js +187 -0
  74. package/internal/worker-output-fence.js +26 -0
  75. package/internal/worker-realm-surfaces.js +197 -0
  76. package/internal/worker-repl-realm.js +10 -4
  77. package/package.json +42 -4
  78. package/src/client-activity.js +4 -1
  79. package/src/client-authoring-view.js +121 -62
  80. package/src/client-console.js +4 -2
  81. package/src/client-copy.js +4 -2
  82. package/src/client-feature-gates.js +7 -9
  83. package/src/client-host-compat.js +104 -27
  84. package/src/client-repl-view.js +3 -1
  85. package/src/client-styles.js +12 -1
  86. package/src/client-tool-view.js +3 -1
  87. package/src/client-transport.js +2 -2
  88. package/src/client-workbench.js +10 -7
  89. package/src/client.js +56 -17
@@ -23,7 +23,7 @@ import { supportsNativeUsing, transformTypeScriptSource } from './typescript-tra
23
23
  import { createDynamicScopeAnalysis, varInitializerTarget } from './dynamic-scope-analysis.js'
24
24
  import { visitSource } from './compiler-source-regions.js'
25
25
  import { analyzeSourceDeclarations, resolveSourceBinding, COMMONJS_PARAMETERS } from './compiler-scope-facts.js'
26
- import { callableMarkerPosition } from './callable-source-facts.js'
26
+ import { callableMarkerPosition, restoreMappedCallableSourceMarks } from './callable-source-facts.js'
27
27
  import { createSourcePieces } from './compiler-source-pieces.js'
28
28
  import { indexSourceRegions, validateRegionSource, sourceFeatures } from './compiler-region-output.js'
29
29
 
@@ -424,7 +424,7 @@ export function lowerStatefulDecorators(result, target, intrinsicContext) {
424
424
 
425
425
  /** Erase type-only declarations and lower runtime TypeScript values before
426
426
  * logical ownership and module export/link plans consume the source. */
427
- export function normalizeTypeScriptValues(code, sourceMap, target) {
427
+ export function normalizeTypeScriptValues(code, sourceMap, target, callableSources) {
428
428
  const tree=parse(code,{...parserOptionsForTarget(target),errorRecovery:true})
429
429
  let parameterProperties = false
430
430
  let tokens
@@ -521,20 +521,15 @@ export function normalizeTypeScriptValues(code, sourceMap, target) {
521
521
  if(enumEdits.length>0) source=applySourceEdits(source,identitySourceMap(source.length),enumEdits).code
522
522
  const transformed=transformTypeScriptSource(source,{module:target!=='commonjs',sourceMap:true,
523
523
  transform:{tsEnumIsMutable:true,noEmptyExport:true}})
524
- const nativeMap=new SourceMap(JSON.parse(transformed.map))
525
- const starts=[0]
526
- for(let index=0;index<source.length;index++) if(source[index]==='\n') starts.push(index+1)
527
- const mappings=[]
528
- let line=1,column=1
529
- for(let index=0;index<transformed.code.length;index++) {
530
- const origin=nativeMap.findOrigin(line,column)
531
- const start=Math.min(source.length,Math.max(0,(starts[origin.lineNumber-1]??0)+(origin.columnNumber??1)-1))
532
- mappings.push({generatedStart:index,generatedEnd:index+1,originalStart:path.node.start+start,
533
- originalEnd:Math.min(owner.end,path.node.start+start+1)})
534
- if(transformed.code[index]==='\n'){line++;column=1}else column++
535
- }
536
- const exported=owner!==path.node&&transformed.code.trim()!==''?`\nexport {${path.node.id.name}};`:''
537
- edits.push({start:owner.start,end:owner.end,text:transformed.code+exported,mappings})
524
+ let mapped=mappedSourceTransform(source,identitySourceMap(source.length),transformed)
525
+ mapped=restoreMappedCallableSourceMarks(mapped,source,transformed,callableSources,parserOptionsForTarget(target))
526
+ const mappings=[...mapped.sourceMap].map(mapping=>({
527
+ ...mapping,
528
+ originalStart:path.node.start+mapping.originalStart,
529
+ originalEnd:Math.min(owner.end,path.node.start+mapping.originalEnd),
530
+ }))
531
+ const exported=owner!==path.node&&mapped.code.trim()!==''?`\nexport {${path.node.id.name}};`:''
532
+ edits.push({start:owner.start,end:owner.end,text:mapped.code+exported,mappings})
538
533
  path.skip()
539
534
  },
540
535
  })
@@ -1130,23 +1125,46 @@ export function preserveStatementBoundaries(code, sourceMap = identitySourceMap(
1130
1125
  * The root compiler owns Program bindings and calls this pass after lowering
1131
1126
  * those declarations. protected-v1 keeps native local binding policy.
1132
1127
  */
1128
+ const normalizationFailureInputs = new WeakMap()
1129
+
1130
+ function retainNormalizationFailureInput(error, input) {
1131
+ if ((typeof error === 'object' && error !== null) || typeof error === 'function') {
1132
+ if (!normalizationFailureInputs.has(error)) normalizationFailureInputs.set(error, input)
1133
+ }
1134
+ }
1135
+
1136
+ /** Latest mapped normalization stage established before an error was thrown. */
1137
+ export function normalizationFailureInput(error) {
1138
+ return (typeof error === 'object' && error !== null) || typeof error === 'function'
1139
+ ? normalizationFailureInputs.get(error) : undefined
1140
+ }
1141
+
1133
1142
  export function normalizeStatefulScopes(code, sourceMap = identitySourceMap(code.length), options = {}) {
1134
- const { outputPlan, ...metadata } = normalizeStatefulScopeInput(code, sourceMap, options)
1135
- let result = metadata
1136
- if (outputPlan) {
1137
- // Release declaration ASTs and scope ancestry before materializing the
1138
- // expanded output. This phase retains only numeric pieces and source facts.
1139
- const { pieces, root, code, sourceMap, regionFacts } = outputPlan
1140
- options.onPhase?.('mapping')
1141
- const output = pieces.emit(root)
1142
- result = { ...metadata, ...applySourceEdits(code, sourceMap,
1143
- [{ start: 0, end: code.length, ...output }]),
1144
- sourceRegions: { ...regionFacts, regions: output.regions } }
1143
+ let diagnosticInput = { code, sourceMap }
1144
+ try {
1145
+ const { outputPlan, ...metadata } = normalizeStatefulScopeInput(code, sourceMap, options)
1146
+ let result = metadata
1147
+ if (outputPlan) {
1148
+ // Release declaration ASTs and scope ancestry before materializing the
1149
+ // expanded output. This phase retains only numeric pieces and source facts.
1150
+ const { pieces, root, code, sourceMap, regionFacts } = outputPlan
1151
+ diagnosticInput = { code, sourceMap }
1152
+ options.onPhase?.('mapping')
1153
+ const output = pieces.emit(root)
1154
+ result = { ...metadata, ...applySourceEdits(code, sourceMap,
1155
+ [{ start: 0, end: code.length, ...output }]),
1156
+ sourceRegions: { ...regionFacts, regions: output.regions } }
1157
+ }
1158
+ diagnosticInput = result
1159
+ options.onPhase?.('validation')
1160
+ if (options.target === 'module' || options.target === 'commonjs') validateRegionSource(result, parserOptionsForTarget(options.target))
1161
+ result = options.deferDecorators ? result : lowerStatefulDecorators(result, options.target, options.intrinsicContext)
1162
+ diagnosticInput = result
1163
+ return options.deferResources ? result : lowerStatefulResources(result, options)
1164
+ } catch (error) {
1165
+ retainNormalizationFailureInput(error, diagnosticInput)
1166
+ throw error
1145
1167
  }
1146
- options.onPhase?.('validation')
1147
- if (options.target === 'module' || options.target === 'commonjs') validateRegionSource(result, parserOptionsForTarget(options.target))
1148
- result = options.deferDecorators ? result : lowerStatefulDecorators(result, options.target, options.intrinsicContext)
1149
- return options.deferResources ? result : lowerStatefulResources(result, options)
1150
1168
  }
1151
1169
 
1152
1170
  /** Resource lifetime is lowered only after declaration owners have consumed
@@ -1227,15 +1245,33 @@ export function hasModuleResources(code) {
1227
1245
  node.type === 'VariableDeclaration' && (node.kind === 'using' || node.kind === 'await using'))
1228
1246
  }
1229
1247
 
1230
- function normalizeStatefulScopeInput(code, sourceMap, { mode = 'stateful-v1', target, moduleImport, intrinsicContext, onPhase,
1231
- nativeJavaScript = false, nativeUsing = supportsNativeUsing() } = {}) {
1248
+ function normalizeStatefulScopeInput(code, sourceMap, options) {
1249
+ let diagnosticInput = { code, sourceMap }
1250
+ try {
1251
+ return normalizeStatefulScopeInputUnchecked(code, sourceMap, options, input => { diagnosticInput = input })
1252
+ } catch (error) {
1253
+ retainNormalizationFailureInput(error, diagnosticInput)
1254
+ throw error
1255
+ }
1256
+ }
1257
+
1258
+ function normalizeStatefulScopeInputUnchecked(code, sourceMap, { mode = 'stateful-v1', target, moduleImport, intrinsicContext, onPhase,
1259
+ callableSources, nativeJavaScript = false, nativeUsing = supportsNativeUsing() } = {}, updateDiagnosticInput) {
1232
1260
  if (mode !== 'stateful-v1' && mode !== 'protected-v1') throw new TypeError('ptc-plus: unsupported scope semantics')
1233
1261
  onPhase?.('typescript')
1234
- const typescript = nativeJavaScript ? { code, sourceMap }
1235
- : lowerLegacyParameterDecorators(normalizeTypeScriptValues(code,sourceMap,target),target,intrinsicContext)
1262
+ let typescript
1263
+ if (nativeJavaScript) {
1264
+ typescript = { code, sourceMap }
1265
+ } else {
1266
+ const normalizedTypescript = normalizeTypeScriptValues(code,sourceMap,target,callableSources)
1267
+ updateDiagnosticInput(normalizedTypescript)
1268
+ typescript = lowerLegacyParameterDecorators(normalizedTypescript,target,intrinsicContext)
1269
+ }
1270
+ updateDiagnosticInput(typescript)
1236
1271
  const separated = preserveStatementBoundaries(typescript.code, typescript.sourceMap, parserOptionsForTarget(target))
1237
1272
  code=separated.code
1238
1273
  sourceMap=separated.sourceMap
1274
+ updateDiagnosticInput(separated)
1239
1275
  const finish = result => {
1240
1276
  result = { ...result, deferredHelpers: typescript.deferredHelpers,
1241
1277
  internalBindings: new Set([...typescript.internalBindings ?? [], ...result.internalBindings ?? []]) }
@@ -1269,6 +1305,7 @@ function normalizeStatefulScopeInput(code, sourceMap, { mode = 'stateful-v1', ta
1269
1305
  const annex = introduceAnnexBVars(code, sourceMap, target, onPhase)
1270
1306
  code = annex.code
1271
1307
  sourceMap = annex.sourceMap
1308
+ updateDiagnosticInput(annex)
1272
1309
  onPhase?.('facts')
1273
1310
  const facts = analyzeLogicalScopes(code, { target, compact: true, compactNames: target === 'module' || target === 'commonjs', onPhase })
1274
1311
  onPhase?.('planning')
@@ -1,116 +1,212 @@
1
1
  import { types } from 'node:util'
2
- import { isContext, runInContext, runInThisContext } from 'node:vm'
3
- import { parse } from 'acorn'
2
+ import { readFileSync } from 'node:fs'
3
+ import { createRequire } from 'node:module'
4
+ import { createContext, isContext, runInContext, runInThisContext } from 'node:vm'
4
5
  import { randomUUID } from 'node:crypto'
5
- import { walkAst, SKIP_AST_CHILDREN } from './ast-traversal.js'
6
- import { bindingNodes } from './binding-pattern-traversal.js'
6
+ import { runtimeIntrinsics as internal } from './runtime-intrinsics.js'
7
+ import { deleteWorkerRealmProperty, WORKER_REALM_MUTATION } from './worker-realm-surfaces.js'
7
8
 
8
9
  const MAX_ENTRIES = 128
9
10
  const MAX_TEXT = 512
10
11
  const MAX_PROPERTIES = 5
11
- const ownDescriptor = Object.getOwnPropertyDescriptor
12
- const isArray = Array.isArray
13
12
  const isProxy = types.isProxy
14
- const stringify = JSON.stringify
15
13
  const now = Date.now
16
- const bigintText = BigInt.prototype.toString
17
14
  const BIGINT_LIMIT = 10n ** 128n
15
+ const IDENTIFIER = /^[$_\p{ID_Start}][$\u200c\u200d\p{ID_Continue}]*$/u
16
+ const AST_METADATA_KEYS = new internal.Set(['start', 'end', 'loc', 'range'])
17
+ const {
18
+ Set: PrivateSet,
19
+ appendArray,
20
+ bigintToString,
21
+ isArray,
22
+ jsonStringify,
23
+ min,
24
+ objectCreate,
25
+ objectGetOwnPropertyDescriptor: ownDescriptor,
26
+ objectHasOwn,
27
+ objectIs,
28
+ objectKeys,
29
+ regexpTest,
30
+ replaceAllString,
31
+ setAdd,
32
+ setHas,
33
+ sliceString,
34
+ toString,
35
+ } = internal
18
36
  const unreadable = () => ({ status: 'unreadable', text: '', truncated: false })
19
37
  const runInRealm = (source, context, options) => isContext(context)
20
38
  ? runInContext(source, context, options)
21
39
  : runInThisContext(source, options)
22
40
 
41
+ function createPrivateParser() {
42
+ const require = createRequire(import.meta.url)
43
+ const exports = objectCreate(null)
44
+ const module = objectCreate(null)
45
+ module.exports = exports
46
+ const context = createContext({ exports, module })
47
+ runInContext(readFileSync(require.resolve('acorn'), 'utf8'), context,
48
+ { filename: require.resolve('acorn') })
49
+ return module.exports.parse
50
+ }
51
+
52
+ const parse = createPrivateParser()
53
+
54
+ function parseProgram(program) {
55
+ const options = objectCreate(null)
56
+ options.ecmaVersion = 'latest'
57
+ options.allowAwaitOutsideFunction = true
58
+ return parse(program, options)
59
+ }
60
+
61
+ function containsAwait(node) {
62
+ if (node === null || typeof node !== 'object') return false
63
+ if (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression'
64
+ || node.type === 'ArrowFunctionExpression') return false
65
+ if (node.type === 'AwaitExpression' || node.await === true) return true
66
+ const keys = objectKeys(node)
67
+ for (let index = 0; index < keys.length; index += 1) {
68
+ const key = keys[index]
69
+ if (setHas(AST_METADATA_KEYS, key)) continue
70
+ const value = node[key]
71
+ if (isArray(value)) {
72
+ for (let child = 0; child < value.length; child += 1) {
73
+ if (containsAwait(value[child])) return true
74
+ }
75
+ } else if (containsAwait(value)) return true
76
+ }
77
+ return false
78
+ }
79
+
80
+ function recordBindingNames(pattern, names) {
81
+ if (pattern === null || typeof pattern !== 'object') return
82
+ if (pattern.type === 'Identifier') {
83
+ setAdd(names, pattern.name)
84
+ return
85
+ }
86
+ if (pattern.type === 'RestElement') return recordBindingNames(pattern.argument, names)
87
+ if (pattern.type === 'AssignmentPattern') return recordBindingNames(pattern.left, names)
88
+ if (pattern.type === 'ArrayPattern') {
89
+ for (let index = 0; index < pattern.elements.length; index += 1) {
90
+ recordBindingNames(pattern.elements[index], names)
91
+ }
92
+ return
93
+ }
94
+ if (pattern.type === 'ObjectPattern') {
95
+ for (let index = 0; index < pattern.properties.length; index += 1) {
96
+ const property = pattern.properties[index]
97
+ recordBindingNames(property.type === 'RestElement' ? property.argument : property.value, names)
98
+ }
99
+ }
100
+ }
101
+
23
102
  function primitiveText(value) {
24
- if (typeof value === 'string') return stringify(value.slice(0, MAX_TEXT))
103
+ if (typeof value === 'string') return jsonStringify(sliceString(value, 0, MAX_TEXT))
25
104
  if (typeof value === 'bigint') return value > -BIGINT_LIMIT && value < BIGINT_LIMIT
26
- ? `${bigintText.call(value)}n` : '[bigint: more than 128 digits]'
105
+ ? `${bigintToString(value)}n` : '[bigint: more than 128 digits]'
27
106
  if (typeof value === 'symbol' || typeof value === 'function') return `[${typeof value}: unreadable]`
28
107
  if (value === null) return 'null'
29
108
  if (typeof value === 'object') return isProxy(value) ? '[proxy: unreadable]' : '[object]'
30
- return Object.is(value, -0) ? '-0' : String(value)
109
+ return objectIs(value, -0) ? '-0' : toString(value)
31
110
  }
32
111
 
33
112
  /** Inspect own descriptors only, rejecting proxies before any reflective operation. */
34
113
  export function previewBindingValue(value) {
35
- if (['function', 'symbol'].includes(typeof value)) return unreadable()
114
+ if (typeof value === 'function' || typeof value === 'symbol') return unreadable()
36
115
  if (value === null || typeof value !== 'object') {
37
116
  const text = primitiveText(value)
38
- return { status: 'readable', text: text.slice(0, MAX_TEXT), truncated: text.length > MAX_TEXT
117
+ return { status: 'readable', text: sliceString(text, 0, MAX_TEXT), truncated: text.length > MAX_TEXT
39
118
  || (typeof value === 'bigint' && (value <= -BIGINT_LIMIT || value >= BIGINT_LIMIT)) }
40
119
  }
41
120
  // Only arrays provide a finite slot range without enumerating the whole object.
42
121
  if (isProxy(value) || !isArray(value)) return unreadable()
43
- const keys = Array.from({ length: Math.min(ownDescriptor(value, 'length').value, MAX_PROPERTIES) }, (_value, index) => String(index))
44
- const properties = keys.map(key => {
122
+ const keys = []
123
+ const length = min(ownDescriptor(value, 'length').value, MAX_PROPERTIES)
124
+ for (let index = 0; index < length; index += 1) appendArray(keys, toString(index))
125
+ const properties = []
126
+ for (let index = 0; index < keys.length; index += 1) {
127
+ const key = keys[index]
45
128
  const descriptor = ownDescriptor(value, key)
46
129
  const text = descriptor === undefined ? '[empty]'
47
- : Object.hasOwn(descriptor, 'value') ? primitiveText(descriptor.value) : '[accessor: unreadable]'
48
- return `${stringify(key)}: ${text}`
49
- })
50
- const text = `array { ${properties.join(', ')} }`
130
+ : objectHasOwn(descriptor, 'value') ? primitiveText(descriptor.value) : '[accessor: unreadable]'
131
+ appendArray(properties, `${jsonStringify(key)}: ${text}`)
132
+ }
133
+ let text = 'array { '
134
+ for (let index = 0; index < properties.length; index += 1) {
135
+ if (index > 0) text += ', '
136
+ text += properties[index]
137
+ }
138
+ text += ' }'
51
139
  // Uninspected own properties remain unknown even when all indexed slots fit.
52
- return { status: 'readable', text: text.slice(0, MAX_TEXT), truncated: true }
140
+ return { status: 'readable', text: sliceString(text, 0, MAX_TEXT), truncated: true }
53
141
  }
54
142
 
55
143
  /** Probe the actual evaluator without consulting a Node version or any session value. */
56
144
  export async function supportsAwaitLexicals(context, evaluate) {
57
- const prefix = `__ptc_observation_${randomUUID().replaceAll('-', '')}`
145
+ const prefix = `__ptc_observation_${replaceAllString(randomUUID(), '-', '')}`
58
146
  const names = [`${prefix}_let`, `${prefix}_const`]
59
147
  try {
60
148
  await evaluate(`let ${names[0]} = await Promise.resolve(41); const ${names[1]} = 42;`)
61
- return names.every((name, index) => ownDescriptor(context, name) === undefined
62
- && runInRealm(name, context, { timeout: 25 }) === index + 41)
149
+ for (let index = 0; index < names.length; index += 1) {
150
+ if (ownDescriptor(context, names[index]) !== undefined
151
+ || runInRealm(names[index], context, { timeout: 25 }) !== index + 41) return false
152
+ }
153
+ return true
63
154
  } catch {
64
155
  return false
65
156
  } finally {
66
157
  // Lexical probe names are private and never enter the binding inventory.
67
- for (const name of names) delete context[name]
158
+ for (let index = 0; index < names.length; index += 1) {
159
+ deleteWorkerRealmProperty(
160
+ WORKER_REALM_MUTATION.RESTORE,
161
+ 'await lexical probe',
162
+ context,
163
+ names[index],
164
+ )
165
+ }
68
166
  }
69
167
  }
70
168
 
71
169
  /** Successful programs prove storage; await lexical reads additionally require a runtime probe. */
72
170
  export function createReplValueObserver(context, { awaitLexicals = false } = {}) {
73
- const lexicals = new Set()
74
- const globals = new Set()
171
+ const lexicals = new PrivateSet()
172
+ const globals = new PrivateSet()
75
173
  return {
76
174
  record(program) {
77
175
  try {
78
- const tree = parse(program, { ecmaVersion: 'latest', allowAwaitOutsideFunction: true })
79
- let awaits = false
80
- walkAst(tree, node => {
81
- if (['FunctionDeclaration', 'FunctionExpression', 'ArrowFunctionExpression'].includes(node.type)) return SKIP_AST_CHILDREN
82
- if (node.type === 'AwaitExpression' || node.await === true) awaits = true
83
- })
84
- if (awaits && !awaitLexicals) return
85
- for (const statement of tree.body) {
176
+ const tree = parseProgram(program)
177
+ if (containsAwait(tree) && !awaitLexicals) return
178
+ for (let statementIndex = 0; statementIndex < tree.body.length; statementIndex += 1) {
179
+ const statement = tree.body[statementIndex]
86
180
  if (statement.type !== 'VariableDeclaration') continue
87
- for (const declaration of statement.declarations) {
88
- for (const binding of bindingNodes(declaration.id)) {
89
- (statement.kind === 'var' ? globals : lexicals).add(binding.name)
90
- }
181
+ const names = statement.kind === 'var' ? globals : lexicals
182
+ for (let index = 0; index < statement.declarations.length; index += 1) {
183
+ recordBindingNames(statement.declarations[index].id, names)
91
184
  }
92
185
  }
93
186
  } catch {}
94
187
  },
95
188
  observe(names) {
96
189
  const at = now()
97
- const entries = names.slice(0, MAX_ENTRIES).map(name => {
190
+ const entries = []
191
+ const length = min(names.length, MAX_ENTRIES)
192
+ for (let index = 0; index < length; index += 1) {
193
+ const name = names[index]
98
194
  let preview = unreadable()
99
195
  try {
100
- if (lexicals.has(name)) {
196
+ if (setHas(lexicals, name)) {
101
197
  // Lexical storage shadows global properties, even while uninitialized.
102
- if (/^[$_\p{ID_Start}][$\u200c\u200d\p{ID_Continue}]*$/u.test(name) && now() - at < 100) {
198
+ if (regexpTest(IDENTIFIER, name) && now() - at < 100) {
103
199
  preview = previewBindingValue(runInRealm(name, context, { timeout: 25, displayErrors: false }))
104
200
  }
105
- } else if (globals.has(name)) {
201
+ } else if (setHas(globals, name)) {
106
202
  const descriptor = ownDescriptor(context, name)
107
- if (descriptor !== undefined && Object.hasOwn(descriptor, 'value')) {
203
+ if (descriptor !== undefined && objectHasOwn(descriptor, 'value')) {
108
204
  preview = previewBindingValue(descriptor.value)
109
205
  }
110
206
  }
111
207
  } catch {}
112
- return { name, ...preview }
113
- })
208
+ appendArray(entries, { name, ...preview })
209
+ }
114
210
  return { at, entries }
115
211
  },
116
212
  }
@@ -43,8 +43,36 @@ function assertField(field, value) {
43
43
  * @param config - raw config object.
44
44
  * @returns a resolved config object.
45
45
  */
46
+ /**
47
+ * A volatile schema field validates to a live `{ get }` handle. A field that
48
+ * has no schema default arrives as a handle with an undefined live value and
49
+ * keeps the runtime default.
50
+ */
51
+ function liveConfigValue(value, fallback) {
52
+ if (value !== null && typeof value === 'object'
53
+ && typeof value.get === 'function' && Object.keys(value).length === 1) {
54
+ const live = value.get()
55
+ return live === undefined ? fallback : live
56
+ }
57
+ return value
58
+ }
59
+
46
60
  export function resolveConfig(config = {}) {
61
+ const provided = new Set()
62
+ for (const field of CONFIG_FIELDS) {
63
+ if (!Object.hasOwn(config, field.key)) continue
64
+ const value = config[field.key]
65
+ if (value !== null && typeof value === 'object'
66
+ && typeof value.get === 'function' && Object.keys(value).length === 1) {
67
+ if (value.get() !== undefined) provided.add(field.key)
68
+ } else {
69
+ provided.add(field.key)
70
+ }
71
+ }
47
72
  const resolved = { ...CONFIG_DEFAULTS, ...config }
73
+ for (const field of CONFIG_FIELDS) {
74
+ resolved[field.key] = liveConfigValue(resolved[field.key], CONFIG_DEFAULTS[field.key])
75
+ }
48
76
  for (const field of CONFIG_FIELDS) assertField(field, resolved[field.key])
49
77
  // Migrate complete old policies; mixed choices retain their explicit
50
78
  // compatibility state until the user selects the unified language.
@@ -52,8 +80,8 @@ export function resolveConfig(config = {}) {
52
80
  'looseTopLevelRedeclarations', 'looseTopLevelFunctionClassRedeclarations',
53
81
  'autoRewriteImports', 'autoStripExports', 'autoSplitRedeclarations',
54
82
  ]
55
- if (!Object.hasOwn(config, 'bindingUpdates')
56
- && legacyKeys.some(key => Object.hasOwn(config, key))) {
83
+ if (!provided.has('bindingUpdates')
84
+ && legacyKeys.some(key => provided.has(key))) {
57
85
  if (legacyKeys.every(key => resolved[key] === true)) {
58
86
  resolved.bindingUpdates = 'stateful'
59
87
  } else if (legacyKeys.slice(0, 2).every(key => resolved[key] === false)
@@ -65,6 +93,38 @@ export function resolveConfig(config = {}) {
65
93
  return resolved
66
94
  }
67
95
 
96
+ /** Whether a validated config carries live volatile field handles. */
97
+ export function hasVolatileFields(config) {
98
+ if (config === null || typeof config !== 'object') return false
99
+ return Object.values(config).some(value => value !== null && typeof value === 'object'
100
+ && typeof value.get === 'function' && Object.keys(value).length === 1)
101
+ }
102
+
103
+ /**
104
+ * Follow the host's volatile-only commit into the running fiber.
105
+ *
106
+ * The loader updates the retained handles in place and emits
107
+ * `loader/volatile-update` on the owning fiber; listeners on a child scope are
108
+ * filtered out, so the listener belongs to the plugin's own context and is
109
+ * released with the runtime scope it serves.
110
+ *
111
+ * @param host - the plugin's own context, owning the loader event listener.
112
+ * @param scope - the runtime scope whose disposal removes the listener.
113
+ * @param config - the validated config carrying the volatile handles.
114
+ * @param listener - receives the newly resolved plain configuration.
115
+ * @returns whether a volatile listener was installed.
116
+ */
117
+ export function watchVolatileConfig(host, scope, config, listener) {
118
+ if (!hasVolatileFields(config) || typeof host?.on !== 'function') return false
119
+ const update = () => listener(resolveConfig(config))
120
+ if (typeof scope?.effect === 'function') {
121
+ scope.effect(() => host.on('loader/volatile-update', update), 'ptc-plus volatile settings lifecycle')
122
+ } else {
123
+ host.on('loader/volatile-update', update)
124
+ }
125
+ return true
126
+ }
127
+
68
128
  /**
69
129
  * Validate one wall-clock ceiling against the platform timer limit.
70
130
  * @param value - the configured maximum wall time in milliseconds.
@@ -27,6 +27,7 @@ export const runtimeIntrinsics = {
27
27
  defineProperty: compilerDescriptors.reflectDefineProperty,
28
28
  has: Reflect.has,
29
29
  ownKeys: Reflect.ownKeys,
30
+ set: Reflect.set,
30
31
  },
31
32
  Error, TypeError, Array, Map, Set, WeakMap, WeakSet, RegExp, Proxy,
32
33
  objectCreate: Object.create,
@@ -46,6 +47,8 @@ export const runtimeIntrinsics = {
46
47
  toBigInt: BigInt,
47
48
  toNumber: Number,
48
49
  toString: String,
50
+ bigintToString: uncurry(BigInt.prototype.toString),
51
+ jsonStringify: JSON.stringify,
49
52
  mapGet: uncurry(Map.prototype.get),
50
53
  mapHas: uncurry(Map.prototype.has),
51
54
  mapSet: uncurry(Map.prototype.set),
@@ -1,5 +1,7 @@
1
1
  import { createUserMessage } from '@deepseek-ai/dsh-llm'
2
2
 
3
+ import { isPtcMessageSource, PTC_MESSAGE_SOURCE_KIND } from './message-sources.js'
4
+
3
5
  export const PTC_DELIVERY_CONTEXT = 'tools:ptc-plus-message-delivery'
4
6
  export const PTC_BINDING_CATALOG = 'tools:ptc-plus-user-binding-defaults'
5
7
  export const PTC_STATE_NAMES = Object.freeze([
@@ -54,7 +56,7 @@ function stateText(sections, prefix = STATE_PREFIX) {
54
56
  /** Recognize only bounded, formed PTC records, never authoring tasks or Skills. */
55
57
  export function readRuntimeMessage(message) {
56
58
  const source = message?.source
57
- if (source?.kind !== 'plugin' || source.plugin !== 'ptc-plus'
59
+ if (!isPtcMessageSource(source)
58
60
  || !Array.isArray(message.content) || message.content.length !== 1
59
61
  || message.content[0]?.type !== 'text') return undefined
60
62
  const text = message.content[0].text
@@ -87,7 +89,7 @@ export function runtimeBindingCatalogMessage(context) {
87
89
  throw new Error('invalid PTC binding catalog')
88
90
  }
89
91
  return createUserMessage({
90
- source: { kind: 'plugin', plugin: 'ptc-plus', form: 'catalog' },
92
+ source: { kind: PTC_MESSAGE_SOURCE_KIND, form: 'catalog' },
91
93
  content: [{ type: 'text', text: `${CATALOG_PREFIX}\n\n${context?.text ?? EMPTY_CATALOG}` }],
92
94
  })
93
95
  }
@@ -96,14 +98,14 @@ export function runtimeStateMessage(value) {
96
98
  const sections = stateSections(value)
97
99
  if (sections === undefined) throw new Error('invalid PTC runtime state sections')
98
100
  return createUserMessage({
99
- source: { kind: 'plugin', plugin: 'ptc-plus', form: 'snapshot', sections },
101
+ source: { kind: PTC_MESSAGE_SOURCE_KIND, form: 'snapshot', sections },
100
102
  content: [{ type: 'text', text: stateText(sections) }],
101
103
  })
102
104
  }
103
105
 
104
106
  export function runtimeNoticeMessage(tip) {
105
107
  const message = createUserMessage({
106
- source: { kind: 'plugin', plugin: 'ptc-plus', form: 'notice', summary: tip.name },
108
+ source: { kind: PTC_MESSAGE_SOURCE_KIND, form: 'notice', summary: tip.name },
107
109
  content: [{ type: 'text', text: tip.text }],
108
110
  })
109
111
  if (readRuntimeMessage(message) === undefined) throw new Error('invalid PTC recovery notice')
@@ -1,3 +1,5 @@
1
+ import { resolve } from 'node:path'
2
+ import { pathToFileURL } from 'node:url'
1
3
  import {
2
4
  decodeValue,
3
5
  encodeValue,
@@ -415,6 +417,8 @@ export class SessionCellExecutor {
415
417
  const priorBindingCatalog = userBindingPlan.catalog
416
418
  const catalog = priorBindingCatalog.inputs()
417
419
  const { bindingPolicy, rewritesEnabled, moduleSemantics, languageSemantics, moduleTransform } = executionPolicies(config, replayRecord)
420
+ const moduleDirectory = kernel.cwd ?? process.cwd()
421
+ const moduleFilename = resolve(moduleDirectory, 'repl')
418
422
  const prepareCell = program => prepareProgram(program, {
419
423
  knownBindings: catalog.knownBindings,
420
424
  bindingPolicy,
@@ -430,6 +434,11 @@ export class SessionCellExecutor {
430
434
  dynamicOrigins: catalog.dynamicOrigins,
431
435
  establishedRoots: catalog.establishedRoots,
432
436
  moduleSemantics,
437
+ importMeta: {
438
+ url: pathToFileURL(moduleFilename).href,
439
+ filename: moduleFilename,
440
+ dirname: moduleDirectory,
441
+ },
433
442
  })
434
443
  let prepared
435
444
  try {
@@ -712,9 +721,10 @@ export class SessionCellExecutor {
712
721
  volatile: this.handleVolatile,
713
722
  call: this.handleCall,
714
723
  'output-limit': this.handleOutputLimit,
724
+ 'worker-output-error': this.handleWorkerOutputError,
715
725
  done: this.handleDone,
716
726
  }[message.type]
717
- handler?.call(this, message)
727
+ return handler?.call(this, message)
718
728
  }
719
729
 
720
730
  handleVolatile(message) {
@@ -742,6 +752,19 @@ export class SessionCellExecutor {
742
752
  }, true)
743
753
  }
744
754
 
755
+ handleWorkerOutputError(message) {
756
+ const active = this.kernel.active
757
+ if (active?.id !== message.id) return false
758
+ const detail = typeof message.message === 'string'
759
+ ? message.message
760
+ : 'the kernel returned an invalid output-attribution event'
761
+ active.resolve({
762
+ logs: [],
763
+ error: { kind: 'worker-exit', message: `worker output attribution failed: ${detail}` },
764
+ }, true)
765
+ return true
766
+ }
767
+
745
768
  handleDone(message) {
746
769
  const kernel = this.kernel
747
770
  if (kernel.active?.id !== message.id) return