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
@@ -10,12 +10,14 @@ import {
10
10
  reduceStateOperations,
11
11
  } from './session-journal.js'
12
12
  import {
13
+ foldSessionTimeline,
13
14
  pathToHead,
14
15
  recoveryBoundaryForHistory,
15
16
  recoverJournal,
16
17
  visibleExecutableCallSeqs,
17
18
  } from './session-journal-recovery.js'
18
19
  import { valueLimitsFromConfig } from './value-wire-schema.js'
20
+ import { encodeValueAtBoundary, valueWiresEqual } from './value-wire.js'
19
21
  import { normalizeBindingDescriptors } from './binding-descriptors.js'
20
22
  import { resolveConfig } from './runtime-config.js'
21
23
  import { WorkerClient } from './worker-client.js'
@@ -26,6 +28,7 @@ import { executionPolicies } from './binding-update-policy.js'
26
28
  import {
27
29
  createReplMemorySnapshot,
28
30
  normalizeReplMemorySnapshot,
31
+ REPL_MEMORY_META_KEY,
29
32
  unavailableReplMemorySnapshot,
30
33
  } from './repl-memory-projection.js'
31
34
 
@@ -34,8 +37,12 @@ const UNAVAILABLE_SURFACE_GENERATION = Symbol('unavailableSurfaceGeneration')
34
37
 
35
38
  function surfaceGeneration(session, provenanceRequired = true) {
36
39
  try {
37
- return session?.surface?.replaceGeneration
38
- ?? (provenanceRequired ? UNAVAILABLE_SURFACE_GENERATION : undefined)
40
+ const surface = session?.surface
41
+ const generation = surface?.replaceGeneration
42
+ if (generation === undefined) return provenanceRequired ? UNAVAILABLE_SURFACE_GENERATION : undefined
43
+ return surface.contentGeneration === undefined
44
+ ? generation
45
+ : `${generation}:${surface.contentGeneration}`
39
46
  } catch {
40
47
  return provenanceRequired ? UNAVAILABLE_SURFACE_GENERATION : undefined
41
48
  }
@@ -96,6 +103,7 @@ class SessionKernel {
96
103
  this.recoveryNotice = history.volatileSuffix.length === 0
97
104
  ? undefined
98
105
  : recoveryDiagnostic(history.volatileSuffix.length)
106
+ this.recoveryNoticeShown = false
99
107
  this.active = undefined
100
108
  this.sequence = 0
101
109
  this.tail = Promise.resolve()
@@ -144,17 +152,25 @@ class SessionKernel {
144
152
  } catch (error) {
145
153
  record.failed = true
146
154
  record.reason = error
147
- return
155
+ return Promise.resolve(record)
148
156
  }
149
157
  const attempt = Promise.resolve(reset).then(
150
- () => { this.reclamationRecords.delete(sequence) },
158
+ () => { this.reclamationRecords.delete(sequence); return record },
151
159
  error => {
152
160
  record.failed = true
153
161
  record.reason = error
162
+ return record
154
163
  },
155
164
  )
156
165
  this.reclamationAttempts.add(attempt)
157
166
  attempt.then(() => { this.reclamationAttempts.delete(attempt) })
167
+ return attempt
168
+ }
169
+
170
+ async prepareWorker() {
171
+ await Promise.all([...this.reclamationAttempts])
172
+ const failures = [...this.reclamationRecords.values()].filter(record => record.failed).map(record => record.reason)
173
+ if (failures.length) throw new AggregateError(failures, 'previous computation helper reclamation failed; dispose the session before continuing')
158
174
  }
159
175
 
160
176
  valueLimits(config = this.config) {
@@ -333,14 +349,12 @@ class SessionKernel {
333
349
  }
334
350
  if (skipped > 0) this.recoveryNotice = recoveryDiagnostic(skipped)
335
351
  }
336
- const leadingDiagnostics = this.recoveryNotice === undefined ? [] : [this.recoveryNotice]
352
+ const leadingDiagnostics = this.recoveryNotice === undefined || this.recoveryNoticeShown
353
+ ? [] : [this.recoveryNotice]
354
+ this.recoveryNoticeShown = this.recoveryNotice !== undefined
337
355
  this.recoveryNotice = undefined
338
356
  if (request.journal !== undefined) request.journal.diagnostics.push(...leadingDiagnostics)
339
357
  const result = await this.cellExecutor.executeCell(request, undefined, config)
340
- if (leadingDiagnostics.length > 0) {
341
- const rendered = leadingDiagnostics.map(item => renderDiagnostic(item, request.program))
342
- result.logs = [...rendered, ...result.logs]
343
- }
344
358
  if (result.error === undefined) {
345
359
  this.failures.reset()
346
360
  } else {
@@ -351,17 +365,29 @@ class SessionKernel {
351
365
  if (request.journal !== undefined) request.journal.diagnostics.push(hint)
352
366
  }
353
367
  }
368
+ if (leadingDiagnostics.length > 0) {
369
+ const rendered = leadingDiagnostics.map(item => renderDiagnostic(item, request.program))
370
+ if (result.error === undefined) result.logs = [...rendered, ...result.logs]
371
+ else result.error = { ...result.error, message: [result.error.message, ...rendered].join('\n') }
372
+ }
354
373
  return finishResult(result)
355
374
  }
356
375
 
357
376
  async replayHistory(request, config) {
358
377
  this.bindingCatalog = new BindingCatalog()
378
+ const limits = this.valueLimits(config)
379
+ const emptyArgsWire = encodeValueAtBoundary({}, limits, 'parameter')
359
380
  const path = pathToHead(this.history)
360
381
  for (const node of path) {
382
+ const bindings = replayBindingNamespaces(request.bindings, node.journal, limits, emptyArgsWire)
383
+ const bindingDescriptors = bindings === request.bindings
384
+ ? request.bindingDescriptors : normalizeBindingDescriptors(bindings)
361
385
  try {
362
386
  const result = await this.cellExecutor.executeCell(
363
387
  {
364
388
  ...request,
389
+ bindings: bindingDescriptors.namespaces,
390
+ bindingDescriptors,
365
391
  program: node.code,
366
392
  journal: undefined,
367
393
  ...(node.userBindings === undefined ? { userBindings: undefined } : { userBindings: node.userBindings }),
@@ -476,7 +502,17 @@ class SessionKernel {
476
502
  this.workerObservation = undefined
477
503
  // A refused reclamation stays recorded on this kernel instead of becoming
478
504
  // an unhandled rejection; the kernel keeps the worker until dispose.
479
- this.resetWorker(worker)
505
+ void this.resetWorker(worker).then(record => {
506
+ if (record.failed) {
507
+ result = { ...result, error: { ...result.error,
508
+ message: `${result.error?.message ?? 'cell terminated'}; helper reclamation failed: ${String(record.reason?.message ?? record.reason)}` } }
509
+ if (journal !== undefined && replay === undefined) {
510
+ this.completeJournal(journal, 'discarded', result, journal.volatileReason, active.diagnostics)
511
+ }
512
+ }
513
+ active.finish(result)
514
+ })
515
+ return
480
516
  }
481
517
  if (!terminate && active.observing && replay === undefined) {
482
518
  // Computation and journal settlement are complete. Observation cannot change either.
@@ -621,11 +657,47 @@ function sessionOf(sessionContext) {
621
657
  session,
622
658
  callId: sessionContext.callId,
623
659
  persistedCallSeq: sessionContext.persistedCallSeq,
660
+ sourceCallSeq: sessionContext.sourceCallSeq,
624
661
  cwd: typeof session?.header?.cwd === 'string' ? session.header.cwd : undefined,
625
662
  surfaceProvenanceRequired: true,
626
663
  }
627
664
  }
628
665
 
666
+ /**
667
+ * Recorded-value replay settles a host call from its journal entry without
668
+ * dispatching it. A member the Host no longer supplies must therefore stay
669
+ * visible to the replayed cell as a replay-only stub; otherwise a routine
670
+ * capability removal would make an otherwise provable cell unreconstructable
671
+ * and contract the durable frontier. Zero-argument calls keep their recorded
672
+ * empty-object form so the args wire still matches the journal.
673
+ */
674
+ function replayBindingNamespaces(namespaces, journal, limits, emptyArgsWire) {
675
+ const calls = journal?.calls ?? []
676
+ let changed = false
677
+ const next = namespaces.map(namespace => {
678
+ const recorded = calls.filter(call => call.global === namespace.global)
679
+ const missing = [...new Set(recorded.map(call => call.member))]
680
+ .filter(member => !namespace.members.includes(member))
681
+ if (missing.length === 0) return namespace
682
+ changed = true
683
+ const functions = { ...namespace.functions }
684
+ const emptyObjectMembers = [...(namespace.emptyObjectMembers ?? [])]
685
+ for (const member of missing) {
686
+ // The replay executor settles or rejects every host-binding call from the
687
+ // recorded journal, so the placeholder keeps the member visible without
688
+ // ever running; the throw only guards a dispatch path that would bypass
689
+ // recorded-value interception.
690
+ /* c8 ignore next -- replay interception precedes invocation */
691
+ functions[member] = () => { throw new Error(`replay-only member ${namespace.global}.${member}`) }
692
+ if (recorded.some(call => call.member === member && valueWiresEqual(call.args, emptyArgsWire, limits))) {
693
+ emptyObjectMembers.push(member)
694
+ }
695
+ }
696
+ return { ...namespace, functions, emptyObjectMembers }
697
+ })
698
+ return changed ? next : namespaces
699
+ }
700
+
629
701
  export class SessionRuntime {
630
702
  constructor(config = {}, options = {}) {
631
703
  this.config = resolvedRuntimeConfig(config)
@@ -642,6 +714,7 @@ export class SessionRuntime {
642
714
  this.withInitiator = typeof options.withInitiator === 'function' ? options.withInitiator : undefined
643
715
  this.observeSession = options.observeSession ?? (() => false)
644
716
  this.readSessionLog = options.readSessionLog ?? (async session => session)
717
+ this.sessionForId = options.sessionForId
645
718
  this.disposal = undefined
646
719
  }
647
720
 
@@ -686,10 +759,32 @@ export class SessionRuntime {
686
759
  if (this.disposed || !this.config.replViewEnabled) return undefined
687
760
  const { observation: _observation, ...expected } = normalizeReplMemorySnapshot(memory)
688
761
  if (!expected.available || expected.entries.length === 0) return undefined
689
- return this.kernels.get(sessionId)?.observe(expected, signal)
762
+ const kernel = this.kernels.get(sessionId)
763
+ const session = kernel?.session ?? this.sessionForId?.(sessionId)
764
+ if (session !== undefined && kernel?.surfaceProvenanceRequired !== false) {
765
+ const observed = await this.readSessionLog(session, signal)
766
+ signal?.throwIfAborted()
767
+ const visible = visibleExecutableCallSeqs(observed)
768
+ if (kernel !== undefined) {
769
+ if ([...kernel.liveCallSeqs].some(seq => !visible.has(seq))) return unavailableReplMemorySnapshot()
770
+ kernel.surfaceGeneration = surfaceGeneration(session)
771
+ } else {
772
+ const timeline = foldSessionTimeline(observed.events)
773
+ const history = recoverJournal(observed, undefined, { visibleCallSeqs: visible })
774
+ const head = history.nodes[history.head]
775
+ if (head === undefined) return unavailableReplMemorySnapshot()
776
+ // A cold inventory can describe a proved recorded frontier without
777
+ // instantiating a worker. It cannot describe a discarded suffix.
778
+ const resultSeq = timeline.results.get(head.callSeq)?.eventSeq
779
+ const result = observed.events.find(event => event.type === 'tool/result' && event.seq === resultSeq)
780
+ const recorded = result?.data?.meta?.[REPL_MEMORY_META_KEY]?.memory
781
+ if (!isDeepStrictEqual(recorded, memory)) return unavailableReplMemorySnapshot()
782
+ }
783
+ }
784
+ return kernel?.observe(expected, signal)
690
785
  }
691
786
 
692
- async runTentative(sessionContext, request) {
787
+ async runTentative(sessionContext, request, { programCompletion = false } = {}) {
693
788
  const completed = result => Object.freeze({ result, settlement: undefined })
694
789
  if (this.disposed) return completed({ logs: [], error: { kind: 'abort', message: 'PTC runtime disposed' } })
695
790
  let bindingDescriptors
@@ -699,11 +794,15 @@ export class SessionRuntime {
699
794
  return completed({ logs: [], error: { kind: 'exception', message: messageOf(error) } })
700
795
  }
701
796
  request = { ...request, bindings: bindingDescriptors.namespaces, bindingDescriptors }
797
+ if (request.cwd !== undefined && (typeof request.cwd !== 'string' || !isAbsolute(request.cwd))) {
798
+ return completed({ logs: [], error: { kind: 'exception', message: 'PTC execution cwd must be an absolute path' } })
799
+ }
702
800
  const {
703
801
  id: sessionId,
704
802
  session,
705
803
  callId,
706
804
  persistedCallSeq,
805
+ sourceCallSeq: provedSourceCallSeq,
707
806
  cwd,
708
807
  surfaceProvenanceRequired,
709
808
  } = sessionOf(sessionContext)
@@ -726,11 +825,15 @@ export class SessionRuntime {
726
825
  && !isCanonicalSequence(persistedCallSeq)) {
727
826
  throw new Error('persisted tool call sequence must be a non-negative safe integer')
728
827
  }
828
+ if (provedSourceCallSeq !== undefined
829
+ && !isCanonicalSequence(provedSourceCallSeq)) {
830
+ throw new Error('source tool call sequence must be a non-negative safe integer')
831
+ }
729
832
  if (cellConfig.durableReplay) {
730
- sourceCallSeq = liveToolCallSeq(sessionLog, callId, 'run_code')
833
+ sourceCallSeq = provedSourceCallSeq ?? liveToolCallSeq(sessionLog, callId, 'run_code')
731
834
  callSeq = persistedCallSeq ?? sourceCallSeq
732
835
  } else {
733
- sourceCallSeq = persistedCallSeq
836
+ sourceCallSeq = provedSourceCallSeq ?? persistedCallSeq
734
837
  if (sourceCallSeq === undefined) {
735
838
  try {
736
839
  sourceCallSeq = liveToolCallSeq(sessionLog, callId, 'run_code')
@@ -779,11 +882,12 @@ export class SessionRuntime {
779
882
  policies.rewritesEnabled,
780
883
  policies.languageSemantics,
781
884
  policies.moduleTransform,
885
+ request.cwd ?? cwd ?? null,
782
886
  )
783
887
  const workerReservation = kernel.reserveWorkerConfiguration(cellConfig)
784
888
  let result
785
889
  try {
786
- result = await kernel.run({ ...request, journal, callSeq, sourceCallSeq, sessionLog }, cellConfig)
890
+ result = await kernel.run({ ...request, journal, callSeq, sourceCallSeq, sessionLog, programCompletion }, cellConfig)
787
891
  } finally {
788
892
  kernel.releaseWorkerConfiguration(workerReservation)
789
893
  }
@@ -36,12 +36,16 @@ export function hostOwnsSettingsDocument(settingsModule) {
36
36
  * @returns the Cordis schema field for that definition.
37
37
  */
38
38
  export function configFieldSchema({ Schema, field, settingsModule }) {
39
- const base = field.type === 'boolean'
40
- ? Schema.boolean().default(field.default)
39
+ const schema = field.type === 'boolean'
40
+ ? Schema.boolean()
41
+ : field.type === 'string'
42
+ ? Schema.string()
41
43
  : field.type === 'enum'
42
- // Keep omission visible until resolveConfig migrates legacy policies.
43
44
  ? Schema.union(field.options.map(option => Schema.const(option)))
44
- : Schema.number().step(1).min(field.min).max(field.max).default(field.default)
45
+ : Schema.number().step(1).min(field.min).max(field.max)
46
+ // Migration must distinguish omitted legacy inputs from explicit new choices.
47
+ // Runtime defaults belong to resolveConfig; the Host must preserve omission.
48
+ const base = field.preserveOmission || field.type === 'enum' ? schema : schema.default(field.default)
45
49
  const described = base.description(field.description)
46
50
  if (!hostOwnsSettingsDocument(settingsModule)) return described
47
51
  if (typeof described.volatile !== 'function') {
@@ -68,7 +72,10 @@ export function installSettingsSectionCompat({
68
72
  const writeNamespace = sectionInstalled ? namespace : (ownerFiber ?? ctx.fiber)?.entry?.options?.id
69
73
  const writer = typeof provider?.update === 'function'
70
74
  && typeof writeNamespace === 'string' && writeNamespace.length > 0
71
- ? Object.freeze({ update: patch => provider.update(writeNamespace, patch) }) : undefined
75
+ ? Object.freeze({
76
+ update: (patch, expectedRevision) => provider.update(writeNamespace, patch, expectedRevision),
77
+ read: () => provider.describe?.().find(descriptor => descriptor.ns === writeNamespace),
78
+ }) : undefined
72
79
  const unbind = onProvider(provider, writer)
73
80
  if (typeof unbind === 'function') settingsContext.effect(() => unbind, 'ptc-plus settings writer')
74
81
  if (typeof provider?.installSection === 'function') {
@@ -96,9 +96,9 @@ export function createUserModuleCompilationHooks({ transformForParent = () => un
96
96
  }
97
97
  }
98
98
  }
99
- const recordResolution = (parentUrl, specifier, importAttributes, result) => {
99
+ const recordResolution = (parentUrl, specifier, importAttributes, result,
100
+ transform = mapGet(modules, parentUrl) ?? transformForParent(parentUrl)) => {
100
101
  recordStatefulModuleResolution(parentUrl, specifier, importAttributes, result.url)
101
- const transform = mapGet(modules, parentUrl) ?? transformForParent(parentUrl)
102
102
  // Runtime links are outside the user compilation graph. Skipping only
103
103
  // their load would still propagate user transforms to their dependencies.
104
104
  if (transform !== undefined && !startsWith(result.url, 'node:')
@@ -106,6 +106,7 @@ export function createUserModuleCompilationHooks({ transformForParent = () => un
106
106
  }
107
107
  return {
108
108
  mark,
109
+ recordResolution,
109
110
  resolve(specifier, context, nextResolve) {
110
111
  const evidence = commonJsEvidence.resolve(specifier, context, recordResolution)
111
112
  if (evidence !== undefined) return evidence
@@ -198,7 +198,7 @@ const nativeModuleRealm = {
198
198
  importModule: (source, options) => import(source, options),
199
199
  }
200
200
 
201
- export function managedModuleImport(parentUrl, specifier, options, realm = nativeModuleRealm) {
201
+ export function managedModuleImport(parentUrl, specifier, options, realm = nativeModuleRealm, recordResolution) {
202
202
  return new realm.Promise((resolve, reject) => {
203
203
  // Template conversion follows native import's ToString (including Symbols).
204
204
  const source = realm.stringify(specifier)
@@ -216,6 +216,7 @@ export function managedModuleImport(parentUrl, specifier, options, realm = nativ
216
216
  if (request !== url) return nextResolve(request, context)
217
217
  const resolution = nextResolve(source, { ...context, parentURL: parentUrl })
218
218
  const attributes = resolution.importAttributes ?? context.importAttributes
219
+ recordResolution?.(parentUrl, source, attributes, resolution)
219
220
  return { url: resolveManagedModuleAdapter(resolution, attributes), format: 'module',
220
221
  importAttributes: attributes, shortCircuit: true }
221
222
  })
@@ -259,13 +260,16 @@ export function loadManagedModuleAdapter(url) {
259
260
  }
260
261
 
261
262
  /** Only PTC-provided require paths decode formally compiled ESM returns. */
262
- export function managedRequire(parentUrl, nativeRequire) {
263
+ export function managedRequire(parentUrl, nativeRequire, recordResolution) {
263
264
  const acquire = (target, receiver, args) => {
264
265
  let url
265
266
  let value
266
267
  const dispose = registerModuleResolutionHook((source, context, nextResolve) => {
267
268
  const result = nextResolve(source, context)
268
- if (source === args[0] && url === undefined) url = result.url
269
+ if (source === args[0] && url === undefined) {
270
+ url = result.url
271
+ recordResolution?.(parentUrl, source, context.importAttributes, result)
272
+ }
269
273
  return result
270
274
  })
271
275
  try {
@@ -13,7 +13,7 @@ export function createStatefulRootRuntime({ readAmbient, typeofAmbient, writeAmb
13
13
  const entries = new Map()
14
14
  const intrinsics = captureCompilerIntrinsics(dynamicIntrinsics?.realmFunction)
15
15
  errors ??= intrinsics
16
- const dynamicRuntime = createDynamicEnvironmentRuntime({ ...dynamicIntrinsics, importModule })
16
+ const defaultDynamicRuntime = createDynamicEnvironmentRuntime({ ...dynamicIntrinsics, importModule })
17
17
  const moduleRuntime = dynamicIntrinsics === undefined ? undefined : createDynamicEnvironmentRuntime()
18
18
  let commit
19
19
  const read = name => {
@@ -59,7 +59,11 @@ export function createStatefulRootRuntime({ readAmbient, typeofAmbient, writeAmb
59
59
  return value
60
60
  }
61
61
  const begin = ({ declared = [], known = [], readOnly = [], legacyImports = [], legacyWritable = [], legacyLexicals = [], legacyObjects = [],
62
- declarationKinds = [], declaredKinds = [], callableSources = [], languageSemantics = 'stateful-v1', committed }) => {
62
+ declarationKinds = [], declaredKinds = [], callableSources = [], languageSemantics = 'stateful-v1', committed,
63
+ importModule: cellImportModule = importModule }) => {
64
+ // Each cell owns dynamic-source resolution; shared logical roots remain live.
65
+ const dynamicRuntime = createDynamicEnvironmentRuntime({ ...dynamicIntrinsics,
66
+ importModule: cellImportModule }).installIntrinsics()
63
67
  dynamicRuntime.registerSources(callableSources)
64
68
  commit = committed
65
69
  const kinds = new Map()
@@ -127,7 +131,7 @@ export function createStatefulRootRuntime({ readAmbient, typeofAmbient, writeAmb
127
131
  const result = {
128
132
  values,
129
133
  intrinsics,
130
- importModule,
134
+ importModule: cellImportModule,
131
135
  dynamic({ realm = false } = {}) {
132
136
  const pendingKinds = new Map()
133
137
  for (let index = 0; index < declaredKinds.length; index++) mapSet(pendingKinds, declaredKinds[index][0], declaredKinds[index][1])
@@ -155,6 +159,7 @@ export function createStatefulRootRuntime({ readAmbient, typeofAmbient, writeAmb
155
159
  const varFrame = { has: name => variableKind(kindFor(name)), get: name => varFrame.has(name) ? descriptor(name, 'var') : undefined,
156
160
  set() {} }
157
161
  return dynamicRuntime.environment({
162
+ importModule: cellImportModule,
158
163
  varFrame, frames: [{ kind: 'lexical', bindings: lexicalFrame }],
159
164
  declareVars: realm ? (names, functionNames) => {
160
165
  const globals = []
@@ -288,7 +293,7 @@ export function createStatefulRootRuntime({ readAmbient, typeofAmbient, writeAmb
288
293
  // Only new-generation environments select intrinsic interfaces. Native
289
294
  // global/prototype properties and saved legacy identities remain intact.
290
295
  if (dynamicIntrinsics !== undefined) {
291
- dynamicRuntime.installIntrinsics()
296
+ defaultDynamicRuntime.installIntrinsics()
292
297
  moduleRuntime.installIntrinsics()
293
298
  }
294
299
  return result
@@ -37,14 +37,9 @@ export const BINDING_SUBMISSION = deepFreeze({
37
37
  })
38
38
 
39
39
  export const BINDING_AUTHORING_SDK = `
40
- During /binding, submit a draft for user review from run_code using the supplied requestId. Submission does not save, enable or execute it.
40
+ /binding only: copy its requestId; submission does not save, enable or run the draft.
41
41
  \`\`\`ts
42
- declare namespace code {
43
- function submitBindingDraft(args: {
44
- requestId: string;
45
- entry: { id: string; name: string; scope: "namespace" | "top-level"; symbols?: string[]; purpose: string; source: string; modelContext?: { includeDeclaration?: boolean; instructions?: string } };
46
- }): Promise<{ accepted: true; id: string; requestId: string }>;
47
- }
42
+ declare namespace code { function submitBindingDraft(args: { requestId: string; entry: { id: string; name: string; scope: "namespace" | "top-level"; symbols?: string[]; purpose: string; source: string; modelContext?: { includeDeclaration?: boolean; instructions?: string } } }): Promise<{ accepted: true; id: string; requestId: string }> }
48
43
  \`\`\``
49
44
 
50
45
  export function bindingAuthoringInstructions(requestId, cwd) {
@@ -1,4 +1,5 @@
1
1
  import { PTC_MESSAGE_SOURCE_KIND } from './message-sources.js'
2
+ import { bindingToggleMessage } from './binding-toggle-receipt.js'
2
3
  import { RPC_CONTRACTS } from './rpc-contract.js'
3
4
  import { randomUUID } from 'node:crypto'
4
5
  import { dirname } from 'node:path'
@@ -378,13 +379,29 @@ export function createUserBindingsOwner(ctx, options = {}) {
378
379
  }
379
380
  // Fixed endpoint tables grouped by domain; the dispatcher only looks up the
380
381
  // operation, rejects unknown endpoints and hands over the validated input.
382
+ const setEnabled = async (input, requested) => {
383
+ const next = await store.setEnabled(input.id, requested, input.expectedRevision)
384
+ const entry = next.entries.find(entry => entry.id === input.id)
385
+ if (entry === undefined) throw new Error('saved binding missing from confirmed catalog')
386
+ const sessions = new Set((ctx.agents?.list?.() ?? []).map(agent => agent.session).filter(Boolean))
387
+ for (const session of sessions) {
388
+ try {
389
+ // Before the first accepted step these are initial settings, not changes
390
+ // to an environment the model has received. The catalog owns first delivery.
391
+ if (session.deriveMessages()[0]?.role !== 'system') continue
392
+ const message = createUserMessage(bindingToggleMessage(entry, next.revision))
393
+ session.append('user/message', message, { surfaceOp: 'append' })
394
+ } catch (error) { reportContainedFailure('ptc-plus: binding configuration saved but its session receipt could not be appended', error) }
395
+ }
396
+ return next
397
+ }
381
398
  const storageEndpoints = Object.freeze({
382
399
  list: () => store.list(),
383
400
  load: input => store.entry(input.id),
384
401
  reload: () => store.reload(),
385
402
  save: saveEntry,
386
- enable: input => store.setEnabled(input.id, true, input.expectedRevision),
387
- disable: input => store.setEnabled(input.id, false, input.expectedRevision),
403
+ enable: input => setEnabled(input, true),
404
+ disable: input => setEnabled(input, false),
388
405
  remove: input => store.remove(input.id, input.expectedRevision),
389
406
  import: input => store.importFile(input.path, input.expectedRevision, input.options),
390
407
  validate: input => validateEntry(input.entry),
@@ -53,7 +53,7 @@ function renderInvocation(argumentsValue) {
53
53
  const json = JSON.stringify(argumentsValue)
54
54
  .replaceAll('\u2028', '\\u2028')
55
55
  .replaceAll('\u2029', '\\u2029')
56
- return `edit_run_code(${json})`
56
+ return `await tools.edit_run_code(${json})`
57
57
  }
58
58
 
59
59
  /** Requires exactly one valid suffix across all one- and two-token EOF closures. */
@@ -1,3 +1,4 @@
1
+ import { types } from 'node:util'
1
2
  import { assertFields, isRecord } from './record-utils.js'
2
3
  import { runtimeIntrinsics as internal } from './runtime-intrinsics.js'
3
4
  import {
@@ -23,6 +24,9 @@ const intrinsicFunctionToString = Function.prototype.toString
23
24
  const reflectApply = Reflect.apply
24
25
  const bufferByteLength = internal.bufferByteLength
25
26
  const jsonStringify = JSON.stringify
27
+ const { isProxy, isDate, isMap, isSet, isRegExp, isAnyArrayBuffer, isTypedArray,
28
+ isDataView, isNativeError, isPromise, isWeakMap, isWeakSet } = types
29
+ const encodingFailures = new WeakMap()
26
30
 
27
31
  function limitsOf(options = {}) {
28
32
  const limits = { ...DEFAULT_VALUE_LIMITS, ...options }
@@ -36,19 +40,16 @@ function limitsOf(options = {}) {
36
40
  }
37
41
 
38
42
  function hasIntrinsicConstructor(prototype, name) {
43
+ if (isProxy(prototype)) return false
39
44
  const constructor = Object.getOwnPropertyDescriptor(prototype, 'constructor')?.value
40
- if (typeof constructor !== 'function') return false
41
- try {
42
- return constructor.name === name
43
- && constructor.prototype === prototype
44
- && reflectApply(intrinsicFunctionToString, constructor, []) === `function ${name}() { [native code] }`
45
- } catch {
46
- return false
47
- }
45
+ if (typeof constructor !== 'function' || isProxy(constructor)) return false
46
+ return Object.getOwnPropertyDescriptor(constructor, 'name')?.value === name
47
+ && Object.getOwnPropertyDescriptor(constructor, 'prototype')?.value === prototype
48
+ && reflectApply(intrinsicFunctionToString, constructor, []) === `function ${name}() { [native code] }`
48
49
  }
49
50
 
50
51
  function isIntrinsicObjectPrototype(value) {
51
- return Object.getPrototypeOf(value) === null && hasIntrinsicConstructor(value, 'Object')
52
+ return !isProxy(value) && Object.getPrototypeOf(value) === null && hasIntrinsicConstructor(value, 'Object')
52
53
  }
53
54
 
54
55
  function plainObjectPrototype(value) {
@@ -66,8 +67,53 @@ function hasPlainArrayPrototype(value) {
66
67
  && isIntrinsicObjectPrototype(objectPrototype)
67
68
  }
68
69
 
69
- function invalid(path, detail) {
70
- throw new TypeError(`value at ${path} is not PTC Value V1: ${detail}`)
70
+ function invalid(path, detail, kind = 'shape', legacyDetail = detail, legacyMessage = `value at ${path} is not PTC Value V1: ${legacyDetail}`) {
71
+ const error = new TypeError(`value at ${path} is not PTC Value V1: ${detail}`)
72
+ weakMapSet(encodingFailures, error, { kind, path, detail, legacyMessage })
73
+ throw error
74
+ }
75
+
76
+ /** Brand checks do not read constructor, toStringTag, getters, or Proxy traps. */
77
+ function unsupportedObject(value) {
78
+ if (isProxy(value)) return 'Proxy'
79
+ if (isDate(value)) return 'Date'
80
+ if (isMap(value)) return 'Map'
81
+ if (isSet(value)) return 'Set'
82
+ if (isRegExp(value)) return 'RegExp'
83
+ if (isAnyArrayBuffer(value)) return 'ArrayBuffer/SharedArrayBuffer'
84
+ if (isTypedArray(value)) return 'TypedArray/Buffer'
85
+ if (isDataView(value)) return 'DataView'
86
+ if (isNativeError(value)) return 'Error'
87
+ if (isPromise(value)) return 'Promise'
88
+ if (isWeakMap(value)) return 'WeakMap'
89
+ if (isWeakSet(value)) return 'WeakSet'
90
+ return 'unknown prototype'
91
+ }
92
+
93
+ export function valueEncodingFailure(error) {
94
+ return weakMapGet(encodingFailures, error)
95
+ }
96
+
97
+ export function valueEncodingHelp(failure) {
98
+ return failure?.kind === 'budget'
99
+ ? 'reduce the transferred graph to fit the reported value budget'
100
+ : 'select needed data explicitly as supported primitives, plain objects or arrays; keep unsupported live values in bindings (Date: explicit ISO text or timestamp; Map/Set: explicit entries or values); do not JSON.stringify a graph containing BigInt'
101
+ }
102
+
103
+ /** Only codec-owned failures acquire boundary facts; owner exceptions remain untouched. */
104
+ export function encodeValueAtBoundary(value, options, boundary) {
105
+ try {
106
+ return encodeValue(value, options)
107
+ } catch (error) {
108
+ const failure = valueEncodingFailure(error)
109
+ if (failure === undefined) throw error
110
+ const fact = boundary === 'parameter'
111
+ ? 'program binding parameter was not dispatched'
112
+ : 'program binding call completed, but its result could not be transferred; external effects may already have occurred; do not repeat the operation solely to obtain its result'
113
+ const wrapped = new TypeError(`${fact}: ${error.message}; ${valueEncodingHelp(failure)}`)
114
+ weakMapSet(encodingFailures, wrapped, failure)
115
+ throw wrapped
116
+ }
71
117
  }
72
118
 
73
119
  function stringBytes(value) {
@@ -86,7 +132,7 @@ export function encodeValue(value, options = {}) {
86
132
 
87
133
  const accountText = (text, path) => {
88
134
  textBytes += stringBytes(text)
89
- if (textBytes > limits.maxStringBytes) invalid(path, `string budget exceeds ${limits.maxStringBytes} bytes`)
135
+ if (textBytes > limits.maxStringBytes) invalid(path, `string budget exceeds ${limits.maxStringBytes} bytes`, 'budget')
90
136
  }
91
137
 
92
138
  const atom = (current, path) => {
@@ -106,14 +152,14 @@ export function encodeValue(value, options = {}) {
106
152
  if (typeof current === 'bigint') {
107
153
  const text = toString(current)
108
154
  const digits = text[0] === '-' ? text.length - 1 : text.length
109
- if (digits > limits.maxBigIntDigits) invalid(path, `BigInt exceeds ${limits.maxBigIntDigits} digits`)
155
+ if (digits > limits.maxBigIntDigits) invalid(path, `BigInt exceeds ${limits.maxBigIntDigits} digits`, 'budget')
110
156
  accountText(text, path)
111
157
  return { tag: 'bigint', value: text }
112
158
  }
113
- if (typeof current !== 'object') invalid(path, typeof current)
159
+ if (typeof current !== 'object') invalid(path, typeof current, 'unsupported')
114
160
  const prior = weakMapGet(seen, current)
115
161
  if (prior !== undefined) return { tag: 'reference', index: prior }
116
- if (nodes.length >= limits.maxNodes) invalid(path, `node budget exceeds ${limits.maxNodes}`)
162
+ if (nodes.length >= limits.maxNodes) invalid(path, `node budget exceeds ${limits.maxNodes}`, 'budget')
117
163
  const index = nodes.length
118
164
  weakMapSet(seen, current, index)
119
165
  appendArray(nodes, undefined)
@@ -127,10 +173,10 @@ export function encodeValue(value, options = {}) {
127
173
  const current = item.value
128
174
  if (isArray(current)) {
129
175
  if (!hasPlainArrayPrototype(current)) invalid(item.path, 'non-plain array')
130
- if (current.length > limits.maxArrayLength) invalid(item.path, `array length exceeds ${limits.maxArrayLength}`)
176
+ if (current.length > limits.maxArrayLength) invalid(item.path, `array length exceeds ${limits.maxArrayLength}`, 'budget')
131
177
  arraySlotCount += current.length
132
178
  if (arraySlotCount > limits.maxArrayLength) {
133
- invalid(item.path, `array slot budget exceeds ${limits.maxArrayLength}`)
179
+ invalid(item.path, `array slot budget exceeds ${limits.maxArrayLength}`, 'budget')
134
180
  }
135
181
  const entries = []
136
182
  const keys = Reflect.ownKeys(current)
@@ -147,7 +193,7 @@ export function encodeValue(value, options = {}) {
147
193
  invalid(`${item.path}[${key}]`, 'array index must be an enumerable data property')
148
194
  }
149
195
  edgeCount += 1
150
- if (edgeCount > limits.maxEdges) invalid(item.path, `edge budget exceeds ${limits.maxEdges}`)
196
+ if (edgeCount > limits.maxEdges) invalid(item.path, `edge budget exceeds ${limits.maxEdges}`, 'budget')
151
197
  appendArray(entries, [index, atom(descriptor.value, `${item.path}[${key}]`)])
152
198
  }
153
199
  nodes[item.index] = { type: 'array', length: current.length, entries }
@@ -155,7 +201,7 @@ export function encodeValue(value, options = {}) {
155
201
  }
156
202
 
157
203
  const prototype = plainObjectPrototype(current)
158
- if (prototype === undefined) invalid(item.path, 'non-plain object')
204
+ if (prototype === undefined) invalid(item.path, `non-plain object (${unsupportedObject(current)})`, 'unsupported', 'non-plain object')
159
205
  const entries = []
160
206
  const keys = Reflect.ownKeys(current)
161
207
  for (let keyIndex = 0; keyIndex < keys.length; keyIndex += 1) {
@@ -167,7 +213,7 @@ export function encodeValue(value, options = {}) {
167
213
  }
168
214
  accountText(key, `${item.path}.${key}`)
169
215
  edgeCount += 1
170
- if (edgeCount > limits.maxEdges) invalid(item.path, `edge budget exceeds ${limits.maxEdges}`)
216
+ if (edgeCount > limits.maxEdges) invalid(item.path, `edge budget exceeds ${limits.maxEdges}`, 'budget')
171
217
  appendArray(entries, [key, atom(descriptor.value, `${item.path}.${key}`)])
172
218
  }
173
219
  nodes[item.index] = { type: 'object', prototype, entries }
@@ -442,7 +488,8 @@ function renderCanonicalValue(normalized, limits) {
442
488
  let bytes = 0
443
489
  const append = (text) => {
444
490
  bytes += stringBytes(text)
445
- if (bytes > limits.maxStringBytes) throw new TypeError(`rendered PTC value exceeds ${limits.maxStringBytes} bytes`)
491
+ if (bytes > limits.maxStringBytes) invalid('$', `rendered PTC value exceeds ${limits.maxStringBytes} bytes`, 'budget', undefined,
492
+ `rendered PTC value exceeds ${limits.maxStringBytes} bytes`)
446
493
  appendArray(chunks, text)
447
494
  }
448
495
  const emitted = new Set()