dsh-ptc-plus 0.4.10 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/README.en.md +65 -127
  2. package/README.md +65 -127
  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 +42702 -0
  10. package/cordis.patch.yml +1 -0
  11. package/docs/README.md +6 -2
  12. package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +31 -198
  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 +4 -4
  17. package/docs/adr/0019-plugin-settings-and-kill-switch.md +1 -1
  18. package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +1 -1
  19. package/docs/adr/0022-stage-npm-releases-with-oidc.md +4 -2
  20. package/docs/adr/0023-global-user-bindings.md +2 -0
  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 +25 -23
  23. package/docs/adr/0031-close-semantic-obligations-by-owner.md +1 -1
  24. package/docs/adr/0034-keep-program-values-separate-from-output-projections.md +29 -0
  25. package/docs/adr/0035-run-coverage-only-before-a-new-release.md +27 -0
  26. package/docs/adr/0036-separate-focused-development-from-delivery-verification.md +29 -0
  27. package/docs/adr/0037-keep-tool-hover-previews-passive.md +52 -0
  28. package/docs/adr/0038-freeze-session-capability-plans.md +71 -0
  29. package/docs/adr/0039-verify-documentation-without-runtime-suites.md +29 -0
  30. package/docs/adr/0040-separate-capability-configurations-from-session-observations.md +39 -0
  31. package/docs/adr/0041-record-cell-execution-directories.md +65 -0
  32. package/docs/adr/0042-persist-provider-availability-and-handback.md +41 -0
  33. package/docs/adr/0043-compose-capability-and-context-policies.md +37 -0
  34. package/docs/adr/0044-coexist-with-the-official-persistent-repl.md +35 -0
  35. package/docs/adr/README.md +8 -0
  36. package/docs/architecture.md +23 -25
  37. package/docs/capability-planning-components.md +93 -0
  38. package/docs/capability-projection.md +7 -0
  39. package/docs/client-ui.md +70 -23
  40. package/docs/client-visual-contract.md +26 -9
  41. package/docs/context-management.md +22 -0
  42. package/docs/context-optimization-plan.md +353 -0
  43. package/docs/context-public-interface-coverage.md +25 -0
  44. package/docs/durability-design.md +20 -15
  45. package/docs/evaluation.md +41 -6
  46. package/docs/evaluations/command-goat-deepseek-v4.1-flash-20261006.md +126 -0
  47. package/docs/evaluations/command-goat-deepseek-v4.1-flash.md +281 -0
  48. package/docs/installation.md +33 -10
  49. package/docs/program-data-plane.md +5 -3
  50. package/docs/publishing.md +7 -7
  51. package/docs/release-coverage.md +73 -0
  52. package/docs/release-notes.md +1 -1
  53. package/docs/review-procedure.md +56 -0
  54. package/docs/runtime-reference.md +53 -16
  55. package/docs/semantic-validation.md +14 -2
  56. package/docs/stateful-binding-updates-proposal.md +2 -2
  57. package/docs/user-bindings.md +1 -1
  58. package/docs/value-wire.md +11 -1
  59. package/docs/verification-optimization-work.md +26 -15
  60. package/docs/verification.md +30 -88
  61. package/index.js +146 -20
  62. package/internal/binding-toggle-receipt.js +24 -0
  63. package/internal/capability-command-receipt.js +31 -0
  64. package/internal/capability-environment.js +1253 -0
  65. package/internal/capability-plan.js +285 -0
  66. package/internal/capability-policy.js +96 -0
  67. package/internal/capability-statistics-store.js +180 -0
  68. package/internal/capability-statistics.js +214 -0
  69. package/internal/config-spec.js +89 -10
  70. package/internal/direct-surface-owner.js +27 -29
  71. package/internal/dynamic-environment-runtime.js +9 -9
  72. package/internal/edit-transport-owner.js +195 -140
  73. package/internal/execution-provider-owner.js +40 -12
  74. package/internal/execution-seam-compat.js +26 -8
  75. package/internal/kernel-worker.js +104 -32
  76. package/internal/native-root-dynamic.js +12 -13
  77. package/internal/official-repl-priority.js +51 -0
  78. package/internal/program-bindings.js +190 -0
  79. package/internal/recovery-tips.js +3 -2
  80. package/internal/rejected-cell-editor.js +8 -8
  81. package/internal/repl-observation-interest.js +11 -3
  82. package/internal/rpc-contract.js +1 -0
  83. package/internal/runtime-bridge-owner.js +275 -34
  84. package/internal/runtime-config.js +58 -0
  85. package/internal/runtime-contexts.js +74 -5
  86. package/internal/runtime-intrinsics.js +1 -0
  87. package/internal/runtime-messages.js +41 -0
  88. package/internal/sdk-projection.js +10 -82
  89. package/internal/session-cell-executor.js +42 -19
  90. package/internal/session-events.js +34 -0
  91. package/internal/session-journal-recovery.js +234 -11
  92. package/internal/session-journal-schema.js +6 -3
  93. package/internal/session-journal.js +31 -2
  94. package/internal/session-log-view.js +1 -1
  95. package/internal/session-runtime.js +119 -15
  96. package/internal/settings-compat.js +12 -5
  97. package/internal/stateful-module-compiler.js +3 -2
  98. package/internal/stateful-module-runtime.js +7 -3
  99. package/internal/stateful-root-runtime.js +9 -4
  100. package/internal/user-binding-authoring.js +2 -7
  101. package/internal/user-bindings-owner.js +19 -2
  102. package/internal/validated-parse-repair.js +1 -1
  103. package/internal/value-wire.js +68 -21
  104. package/internal/worker-cwd-virtualization.js +20 -18
  105. package/internal/worker-realm-surfaces.js +3 -3
  106. package/package.json +49 -30
  107. package/src/client-activity.js +3 -2
  108. package/src/client-authoring-view.js +67 -30
  109. package/src/client-binding-review.js +2 -1
  110. package/src/client-binding-toggle.js +18 -0
  111. package/src/client-capabilities-view.js +1555 -0
  112. package/src/client-capability-sort.js +29 -0
  113. package/src/client-copy.js +923 -8
  114. package/src/client-feature-gates.js +3 -0
  115. package/src/client-indicator-view.js +4 -6
  116. package/src/client-primitives.js +33 -2
  117. package/src/client-repl-view.js +20 -13
  118. package/src/client-settings-view.js +411 -41
  119. package/src/client-styles.js +112 -15
  120. package/src/client-tool-hover.js +185 -0
  121. package/src/client-tool-view.js +45 -9
  122. package/src/client-transport.js +44 -9
  123. package/src/client-workbench.js +6 -7
  124. package/src/client.js +76 -22
  125. package/client.js +0 -32479
@@ -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()
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Official persistent-REPL priority.
3
+ *
4
+ * The official `persistent-in-memory` execution mode is a live provider
5
+ * capability, not a release fact: a provider reports `programFormat` `'repl'`
6
+ * exactly when it retains a REPL per state owner. While the user selects the
7
+ * official REPL priority and that provider is live, the plugin yields PTC
8
+ * session execution to the official implementation and installs no session
9
+ * runtime: `run_code`, its descriptors and its guidance stay official. The
10
+ * decision is re-evaluated on every configuration change and host source
11
+ * change, so either side can take over without restarting the host.
12
+ */
13
+
14
+ /**
15
+ * Decide whether this configuration yields PTC session execution to the
16
+ * official provider.
17
+ * @param {object} config - resolved plugin configuration.
18
+ * @param {object} provider - the live provider surface registered for the seam.
19
+ * @returns {boolean} true when the official persistent REPL owns execution.
20
+ */
21
+ export function defersToOfficialRepl(config, provider) {
22
+ if (config?.officialReplPriority !== true) return false
23
+ try {
24
+ return provider?.programFormat === 'repl'
25
+ } catch {
26
+ // An unreadable capability descriptor cannot prove official ownership;
27
+ // the plugin keeps its execution responsibility instead of guessing.
28
+ return false
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Runtime record for an attachment that yields execution to the official
34
+ * provider. It owns no execution resources: the official provider releases
35
+ * anything it retained through its own lifecycle, and the plugin's settings,
36
+ * storage and UI owners remain available so the user can switch back.
37
+ */
38
+ export function createDeferredRuntime() {
39
+ return Object.freeze({
40
+ cancelCordisActivation() {
41
+ // No Cordis companion is installed while execution is deferred.
42
+ },
43
+ contextsForRequest: () => [],
44
+ async reconfigure() {
45
+ // This attachment has no owners to reconfigure.
46
+ },
47
+ async dispose() {
48
+ // Nothing is owned here; disposal returns immediately.
49
+ },
50
+ })
51
+ }
@@ -1,5 +1,19 @@
1
1
  /** Deterministic capability metadata derived from the current DSH tool view. */
2
2
  import { record, text } from './record-utils.js'
3
+ import { createHash } from 'node:crypto'
4
+ import { snapshotJsonValue } from '@deepseek-ai/dsh-util-values'
5
+
6
+ /** Derive SDK contracts from the public, scope-qualified tool registry. */
7
+ export function toolSdkSchemas(tools, scope) {
8
+ return tools.schemas(scope).filter(schema => schema.name !== 'run_code').map(schema => {
9
+ const definition = tools.get(schema.name, scope)
10
+ const output = snapshotJsonValue(definition?.output?.schema)
11
+ if (output === undefined) {
12
+ throw new TypeError(`ptc-plus: tool "${schema.name}" output schema must be lossless JSON before SDK projection`)
13
+ }
14
+ return { ...schema, output }
15
+ })
16
+ }
3
17
 
4
18
  const COMPLETENESS = new Set(['complete', 'bounded', 'incremental', 'open-world', 'unknown'])
5
19
  const SOURCE_KINDS = new Set(['authored', 'runtime', 'tests', 'docs'])
@@ -185,3 +199,179 @@ export function capabilityInspect(metadata, symbols = undefined, budget = 50) {
185
199
  budget,
186
200
  }
187
201
  }
202
+
203
+ const SEARCH_DEFAULT_BYTES = 16_384
204
+ const SEARCH_MIN_BYTES = 512
205
+ const SEARCH_STOP_WORDS = new Set('a an and are as at be by can could do for from how i in is it me of on or please that the this to use using with would you 我 帮 帮我 请 的 了 和 与 在 如何 怎么 可以'.split(' '))
206
+ const SEARCH_SEGMENTER = new Intl.Segmenter('zh', { granularity: 'word' })
207
+
208
+ function taskTerms(value) {
209
+ const separated = value.normalize('NFKC')
210
+ .replace(/(\p{Lu}+)(\p{Lu}\p{Ll})/gu, '$1 $2')
211
+ .replace(/([\p{Ll}\p{N}])(\p{Lu})/gu, '$1 $2')
212
+ .toLowerCase()
213
+ .replace(/[^\p{L}\p{N}]+/gu, ' ')
214
+ return [...new Set([...SEARCH_SEGMENTER.segment(separated)]
215
+ .filter(part => part.isWordLike && !SEARCH_STOP_WORDS.has(part.segment))
216
+ .map(part => part.segment))]
217
+ }
218
+
219
+ function searchKeywords(value) {
220
+ if (value === undefined) return ''
221
+ if (typeof value === 'string') return value
222
+ if (Array.isArray(value) && value.every(item => typeof item === 'string')) return value.join(' ')
223
+ throw new TypeError('ptc-plus: capability search keywords must be a string or string array')
224
+ }
225
+
226
+ // Search budgets measure JSON, so non-JSON metadata cannot be silently converted or dropped.
227
+ function searchJson(value) {
228
+ const ancestors = new Set()
229
+ const visit = (item) => {
230
+ if (item === null || typeof item === 'string' || typeof item === 'boolean') return
231
+ if (typeof item === 'number' && Number.isFinite(item)) return
232
+ if (typeof item !== 'object') throw new TypeError('ptc-plus: capability search metadata must be lossless JSON data')
233
+ if (ancestors.has(item)) throw new TypeError('ptc-plus: capability search metadata must be lossless JSON data')
234
+ const prototype = Object.getPrototypeOf(item)
235
+ if (prototype !== (Array.isArray(item) ? Array.prototype : Object.prototype) && prototype !== null) {
236
+ throw new TypeError('ptc-plus: capability search metadata must be lossless JSON data')
237
+ }
238
+ ancestors.add(item)
239
+ for (const key of Reflect.ownKeys(item)) {
240
+ if (Array.isArray(item) && key === 'length') continue
241
+ const descriptor = Object.getOwnPropertyDescriptor(item, key)
242
+ if (typeof key !== 'string' || !descriptor.enumerable || !Object.hasOwn(descriptor, 'value')) {
243
+ throw new TypeError('ptc-plus: capability search metadata must be lossless JSON data')
244
+ }
245
+ visit(descriptor.value)
246
+ }
247
+ if (Array.isArray(item) && (Object.keys(item).length !== item.length
248
+ || Object.keys(item).some(key => !/^(0|[1-9][0-9]*)$/.test(key) || Number(key) >= item.length))) {
249
+ throw new TypeError('ptc-plus: capability search metadata must be lossless JSON data')
250
+ }
251
+ ancestors.delete(item)
252
+ }
253
+ visit(value)
254
+ return JSON.stringify(value, (_key, item) => item !== null && typeof item === 'object' && !Array.isArray(item)
255
+ ? Object.fromEntries(Object.keys(item).sort().map(key => [key, item[key]])) : item)
256
+ }
257
+
258
+ function searchDigest(value) {
259
+ return createHash('sha256').update(value).digest('base64url')
260
+ }
261
+
262
+ function measuredSearchPage(value) {
263
+ value.usedBytes = 0
264
+ let bytes = Buffer.byteLength(JSON.stringify(value), 'utf8')
265
+ while (value.usedBytes !== bytes) {
266
+ value.usedBytes = bytes
267
+ bytes = Buffer.byteLength(JSON.stringify(value), 'utf8')
268
+ }
269
+ return value
270
+ }
271
+
272
+ /** Derive one request-local lexical index from owner metadata; never owns invocation. */
273
+ export function createCapabilitySearcher(metadata) {
274
+ const seen = new Set()
275
+ const snapshot = JSON.parse(searchJson(metadata))
276
+ const catalog = metadataEntries(snapshot).flatMap((entry) => {
277
+ const { members, ...owner } = entry
278
+ return members.map((member) => {
279
+ const symbol = `${entry.namespace}.${member.name}`
280
+ if (seen.has(symbol)) throw new TypeError(`ptc-plus: capability search has ambiguous symbol ${symbol}`)
281
+ seen.add(symbol)
282
+ const value = metadataSnapshot(JSON.parse(searchJson({ symbol, owner, contract: member })), 'capability search metadata')
283
+ const names = new Set(taskTerms(symbol))
284
+ const terms = new Set(taskTerms([
285
+ symbol, member.description ?? '', owner.description ?? '',
286
+ searchKeywords(member.keywords), searchKeywords(owner.keywords),
287
+ ].join(' ')))
288
+ return { value, names, terms }
289
+ })
290
+ }).sort((left, right) => left.value.symbol < right.value.symbol ? -1 : left.value.symbol > right.value.symbol ? 1 : 0)
291
+ const catalogId = searchDigest(JSON.stringify(catalog.map(item => item.value)))
292
+
293
+ return (args) => {
294
+ const params = record(args, 'capability search arguments')
295
+ const allowed = new Set(['query', 'budgetBytes', 'limit', 'cursor'])
296
+ if (Reflect.ownKeys(params).some(key => typeof key !== 'string' || !allowed.has(key)
297
+ || !Object.prototype.propertyIsEnumerable.call(params, key))) {
298
+ throw new TypeError('ptc-plus: capability search has an unknown argument')
299
+ }
300
+ const query = text(params.query, 'capability search query').normalize('NFKC').trim().toLowerCase()
301
+ const queryTerms = taskTerms(params.query)
302
+ const budgetBytes = params.budgetBytes === undefined ? SEARCH_DEFAULT_BYTES : params.budgetBytes
303
+ const limit = params.limit === undefined ? 10 : params.limit
304
+ if (!Number.isSafeInteger(budgetBytes) || budgetBytes < SEARCH_MIN_BYTES) {
305
+ throw new TypeError(`ptc-plus: capability search budgetBytes must be an integer of at least ${SEARCH_MIN_BYTES}`)
306
+ }
307
+ if (!Number.isSafeInteger(limit) || limit < 1) throw new TypeError('ptc-plus: capability search limit must be a positive integer')
308
+ const queryId = searchDigest(JSON.stringify({ query, terms: queryTerms }))
309
+ const ranked = catalog.flatMap((candidate) => {
310
+ const { symbol, owner, contract } = candidate.value
311
+ const coverage = queryTerms.filter(term => candidate.terms.has(term)).length
312
+ const nameCoverage = queryTerms.filter(term => candidate.names.has(term)).length
313
+ const rank = symbol.normalize('NFKC').toLowerCase() === query ? 0
314
+ : owner.namespace.normalize('NFKC').toLowerCase() === query ? 1
315
+ : contract.name.normalize('NFKC').toLowerCase() === query ? 2
316
+ : coverage === 0 ? undefined
317
+ : nameCoverage === queryTerms.length ? 3
318
+ : coverage === queryTerms.length ? 4 : 5
319
+ return rank === undefined ? [] : [{ ...candidate, rank, coverage, nameCoverage }]
320
+ }).sort((left, right) => left.rank - right.rank || right.coverage - left.coverage
321
+ || right.nameCoverage - left.nameCoverage
322
+ || (left.value.symbol < right.value.symbol ? -1 : left.value.symbol > right.value.symbol ? 1 : 0))
323
+
324
+ let offset = 0
325
+ if (params.cursor !== undefined) {
326
+ if (typeof params.cursor !== 'string' || !/^[A-Za-z0-9_-]+$/.test(params.cursor)) {
327
+ throw new TypeError('ptc-plus: capability search cursor is invalid')
328
+ }
329
+ let cursor
330
+ try { cursor = JSON.parse(Buffer.from(params.cursor, 'base64url').toString('utf8')) } catch {
331
+ throw new TypeError('ptc-plus: capability search cursor is invalid')
332
+ }
333
+ if (cursor?.version !== 1 || cursor.catalog !== catalogId || cursor.query !== queryId
334
+ || !Number.isSafeInteger(cursor.offset) || cursor.offset < 0 || cursor.offset >= ranked.length
335
+ || Buffer.from(JSON.stringify(cursor)).toString('base64url') !== params.cursor) {
336
+ throw new TypeError('ptc-plus: capability search cursor is invalid or catalog/query changed')
337
+ }
338
+ offset = cursor.offset
339
+ }
340
+
341
+ const page = (matches, oversized, next) => measuredSearchPage({
342
+ matches, oversized, total: ranked.length, remaining: ranked.length - next,
343
+ cursor: next < ranked.length ? Buffer.from(JSON.stringify({
344
+ version: 1, catalog: catalogId, query: queryId, offset: next,
345
+ })).toString('base64url') : null,
346
+ budgetBytes, unit: 'utf8-json-bytes', usedBytes: 0,
347
+ })
348
+ let result = page([], [], offset)
349
+ for (let next = offset; next < ranked.length && next - offset < limit; next += 1) {
350
+ const candidate = ranked[next].value
351
+ const expanded = page([...result.matches, candidate], result.oversized, next + 1)
352
+ if (expanded.usedBytes <= budgetBytes) {
353
+ result = expanded
354
+ continue
355
+ }
356
+ const alone = page([candidate], [], next + 1)
357
+ if (alone.usedBytes <= budgetBytes) break
358
+ const omitted = {
359
+ symbol: candidate.symbol, requiredBytes: alone.usedBytes,
360
+ inspect: { symbols: [candidate.symbol], budget: 1 },
361
+ }
362
+ const bounded = page(result.matches, [...result.oversized, omitted], next + 1)
363
+ if (bounded.usedBytes > budgetBytes) {
364
+ if (next === offset) throw new RangeError(`ptc-plus: capability search needs at least ${bounded.usedBytes} budgetBytes for its continuation and inspect reference`)
365
+ break
366
+ }
367
+ result = bounded
368
+ }
369
+ if (result.usedBytes > budgetBytes) throw new RangeError(`ptc-plus: capability search needs at least ${result.usedBytes} budgetBytes for its continuation`)
370
+ return result
371
+ }
372
+ }
373
+
374
+ /** Return whole owner contracts within a UTF-8 JSON byte budget, including the envelope. */
375
+ export function capabilitySearch(metadata, args) {
376
+ return createCapabilitySearcher(metadata)(args)
377
+ }
@@ -2,6 +2,7 @@ import { PTC_STATE_NAMES, recoveryTipIdentity } from './runtime-messages.js'
2
2
 
3
3
  const PLATFORM_CAUSE_CODES = new Set(['EACCES', 'EINVAL', 'ENOTDIR', 'ENOENT', 'UNKNOWN'])
4
4
  const TIP_CONTEXT_PREFIX = 'tools:ptc-plus-tip/'
5
+ const PLATFORM_FACTS = '`return { platform: process.platform, arch: process.arch, node: process.version, cwd: process.cwd(), execPath: process.execPath }`'
5
6
  const TIP_PREFIXES = Object.freeze({
6
7
  'repeated-binding-failure': 'The same binding failure has recurred.',
7
8
  'platform-command-failure': 'An executable, shell, or path failed in the current execution world.',
@@ -93,8 +94,8 @@ function tipCandidate(view) {
93
94
  function renderTip(id, detailed, candidate = {}) {
94
95
  if (id === 'platform-command-failure') {
95
96
  return detailed
96
- ? `${TIP_PREFIXES[id]} Re-check the active execution world and the actual executable before retrying. Use direct argv for a normal executable; use a shell only when its syntax or resolution is required. Windows, WSL, POSIX, and package shims have different paths and launch rules.`
97
- : `${TIP_PREFIXES[id]} Inspect the executable or path in the current execution world and choose direct argv or a shell only when required; do not assume Windows, WSL, POSIX, or one shell.`
97
+ ? `${TIP_PREFIXES[id]} Re-check the active execution world and the actual executable before retrying. First inspect bounded runtime facts with ${PLATFORM_FACTS}; use the result to choose the executable and path. Use direct argv for a normal executable; use a shell only when its syntax or resolution is required. Windows, WSL, POSIX, and package shims have different paths and launch rules. Do not dump process.env.`
98
+ : `${TIP_PREFIXES[id]} Inspect bounded runtime facts with ${PLATFORM_FACTS}, then choose the executable and path in the current execution world. Use direct argv or a shell only when required; do not assume Windows, WSL, POSIX, or one shell. Do not dump process.env.`
98
99
  }
99
100
  if (!candidate.capability) {
100
101
  return `${TIP_PREFIXES[id]} Inspect the visible cell source and results for the local name, scope, initialization, or declaration conflict. Correct that expression before continuing; capability discovery does not list session-local variables.${detailed ? ' Earlier statements may have run; use current expression evidence without assuming a failed declaration initialized its binding.' : ''}`
@@ -255,39 +255,39 @@ function assemble(source, replacements) {
255
255
  export function editRunCodeSchema() {
256
256
  return {
257
257
  name: 'edit_run_code',
258
- description: 'Edit the most recent eligible cell captured at dispatch and run the complete corrected cell. A successful edit becomes the next target. This reruns the entire cell, including prior effects; use it after pre-execution rejection or when repeating those effects is safe under the called API\'s retry rules. Otherwise continue in a new run_code cell using retained values. A diagnostic may supply a localized correction; use it when it matches your intent and preserve its expected_target_call_seq. Send exactly one edits or regex_edits array: edits replaces unique literal fragments; regex_edits replaces a counted set of pattern matches. All ranges must be non-overlapping in the original cell. Regex replacements use JavaScript $1/$<name> captures, not \\1.',
258
+ description: 'Call this from within a run_code cell. It accepts an edit of the most recent eligible cell captured at nested dispatch and returns { accepted, scheduled, target_call_seq } when scheduled. The corrected cell runs after the current run_code cell releases its worker; its result is reported with that cell, and a successful edit becomes the next target. This reruns the entire target cell, including prior effects; use it after pre-execution rejection or when repeating those effects is safe under the called API\'s retry rules. Otherwise continue in a new run_code cell using retained values. A diagnostic may supply a localized correction; use it when it matches your intent and preserve its expected_target_call_seq. Send exactly one edits or regex_edits array: edits replaces unique literal fragments; regex_edits replaces a counted set of pattern matches. All ranges must be non-overlapping in the original cell. Regex replacements use JavaScript $1/$<name> captures, not \\1.',
259
259
  parameters: {
260
260
  type: 'object',
261
261
  additionalProperties: false,
262
262
  properties: {
263
263
  edits: {
264
- type: 'array', minItems: 1, maxItems: EDIT_LIMITS.exactEdits,
264
+ type: 'array',
265
265
  description: 'Atomic exact replacements, all resolved against the original target cell.',
266
266
  items: {
267
267
  type: 'object', additionalProperties: false,
268
268
  properties: {
269
- old_string: { type: 'string', minLength: 1, description: 'Exact text in the target cell. It must occur exactly once.' },
269
+ old_string: { type: 'string', description: 'Nonempty exact text in the target cell. It must occur exactly once.' },
270
270
  new_string: { type: 'string', description: 'Literal replacement text. Use an empty string to delete the match.' },
271
271
  },
272
272
  required: ['old_string', 'new_string'],
273
273
  },
274
274
  },
275
275
  regex_edits: {
276
- type: 'array', minItems: 1, maxItems: EDIT_LIMITS.regexEdits,
276
+ type: 'array',
277
277
  description: 'Atomic regular-expression replacements, all matched against the original target cell with JavaScript pattern and replacement semantics; each replacement is a JavaScript template (see replacement).',
278
278
  items: {
279
279
  type: 'object', additionalProperties: false,
280
280
  properties: {
281
- pattern: { type: 'string', minLength: 1, description: 'JavaScript regular-expression source without delimiters or flags (for example \\d+px, not /\\d+px/). Zero-length matches are rejected.' },
282
- flags: { type: 'string', pattern: '^(?!.*(.).*\\1)[gimsu]*$', description: 'Unique JavaScript g, i, m, s, or u flags. Global matching is always applied.' },
281
+ pattern: { type: 'string', description: 'Nonempty JavaScript regular-expression source without delimiters or flags (for example \\d+px, not /\\d+px/). Zero-length matches are rejected.' },
282
+ flags: { type: 'string', description: 'Unique JavaScript g, i, m, s, or u flags. Global matching is always applied.' },
283
283
  replacement: { type: 'string', description: 'JavaScript replacement template. Reference captures as $1, $2, ... or $<name> (for example "<$1>" inserts capture 1); backslash escapes such as \\1 are NOT capture references and stay literal text; write a literal dollar sign as $$.' },
284
- expected_matches: { type: 'integer', minimum: 1, maximum: EDIT_LIMITS.regexMatches, description: 'Exact match count required before any replacement is applied.' },
284
+ expected_matches: { type: 'integer', description: 'Positive exact match count required before any replacement is applied.' },
285
285
  },
286
286
  required: ['pattern', 'flags', 'replacement', 'expected_matches'],
287
287
  },
288
288
  },
289
289
  [EXPECTED_TARGET_CALL_SEQ]: {
290
- type: 'integer', minimum: 0,
290
+ type: 'integer',
291
291
  description: 'Optional target precondition copied from a validated diagnostic. The edit is rejected if the captured cell has another call sequence.',
292
292
  },
293
293
  },