dsh-ptc-plus 0.4.8 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/README.en.md +61 -123
  2. package/README.md +61 -123
  3. package/assets/dsh-ptc-plus-banner-en.webp +0 -0
  4. package/assets/dsh-ptc-plus-banner-zh.webp +0 -0
  5. package/assets/ptc-plus-repl-workspace-en.png +0 -0
  6. package/assets/ptc-plus-repl-workspace-zh.png +0 -0
  7. package/assets/ptc-plus-settings-en.png +0 -0
  8. package/assets/ptc-plus-settings-zh.png +0 -0
  9. package/client.cjs +42336 -0
  10. package/cordis.patch.yml +13 -2
  11. package/docs/README.md +7 -2
  12. package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +31 -179
  13. package/docs/adr/0008-define-agent-operating-contract.md +5 -1
  14. package/docs/adr/0009-bound-expensive-acceptance-default.md +2 -2
  15. package/docs/adr/0016-archive-resolved-review-findings.md +1 -1
  16. package/docs/adr/0017-track-the-latest-dsh-public-surface.md +2 -2
  17. package/docs/adr/0019-plugin-settings-and-kill-switch.md +14 -3
  18. package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +2 -2
  19. package/docs/adr/0022-stage-npm-releases-with-oidc.md +4 -2
  20. package/docs/adr/0023-global-user-bindings.md +3 -1
  21. package/docs/adr/0026-persist-the-delivery-verdict-against-the-verified-candidate.md +2 -0
  22. package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +101 -35
  23. package/docs/adr/0030-serve-both-typert-codec-generations-from-one-codec.md +3 -3
  24. package/docs/adr/0031-close-semantic-obligations-by-owner.md +1 -1
  25. package/docs/adr/0032-own-session-log-facts-in-a-host-only-projection.md +27 -0
  26. package/docs/adr/0033-gate-client-visual-usability-in-a-browser.md +23 -0
  27. package/docs/adr/0034-keep-program-values-separate-from-output-projections.md +29 -0
  28. package/docs/adr/0035-run-coverage-only-before-a-new-release.md +27 -0
  29. package/docs/adr/0036-separate-focused-development-from-delivery-verification.md +29 -0
  30. package/docs/adr/0037-keep-tool-hover-previews-passive.md +52 -0
  31. package/docs/adr/0038-freeze-session-capability-plans.md +71 -0
  32. package/docs/adr/0039-verify-documentation-without-runtime-suites.md +29 -0
  33. package/docs/adr/0040-separate-capability-configurations-from-session-observations.md +39 -0
  34. package/docs/adr/0041-record-cell-execution-directories.md +65 -0
  35. package/docs/adr/0042-persist-provider-availability-and-handback.md +41 -0
  36. package/docs/adr/0043-compose-capability-and-context-policies.md +37 -0
  37. package/docs/adr/README.md +6 -0
  38. package/docs/architecture.md +25 -27
  39. package/docs/binding-review-placement-design.md +29 -21
  40. package/docs/capability-planning-components.md +93 -0
  41. package/docs/capability-projection.md +7 -0
  42. package/docs/client-ui.md +88 -29
  43. package/docs/client-visual-contract.md +65 -0
  44. package/docs/context-management.md +22 -0
  45. package/docs/context-optimization-plan.md +353 -0
  46. package/docs/context-public-interface-coverage.md +25 -0
  47. package/docs/durability-design.md +31 -14
  48. package/docs/evaluation.md +41 -6
  49. package/docs/evaluations/command-goat-deepseek-v4.1-flash-20261006.md +126 -0
  50. package/docs/evaluations/command-goat-deepseek-v4.1-flash.md +281 -0
  51. package/docs/installation.md +50 -8
  52. package/docs/program-data-plane.md +5 -3
  53. package/docs/publishing.md +5 -5
  54. package/docs/release-coverage.md +73 -0
  55. package/docs/release-notes.md +1 -1
  56. package/docs/review-procedure.md +56 -0
  57. package/docs/runtime-reference.md +48 -16
  58. package/docs/semantic-validation.md +14 -2
  59. package/docs/stateful-binding-updates-proposal.md +2 -2
  60. package/docs/user-bindings.en.md +2 -2
  61. package/docs/user-bindings.md +3 -3
  62. package/docs/value-wire.md +11 -1
  63. package/docs/verification-optimization-work.md +52 -15
  64. package/docs/verification.md +30 -86
  65. package/index.js +147 -23
  66. package/internal/binding-toggle-receipt.js +24 -0
  67. package/internal/capability-command-receipt.js +29 -0
  68. package/internal/capability-environment.js +1247 -0
  69. package/internal/capability-plan.js +285 -0
  70. package/internal/capability-policy.js +96 -0
  71. package/internal/capability-statistics-store.js +180 -0
  72. package/internal/capability-statistics.js +210 -0
  73. package/internal/compiler-platform.js +5 -3
  74. package/internal/compiler-service.js +2 -9
  75. package/internal/config-spec.js +79 -9
  76. package/internal/cordis-tools-owner.js +22 -19
  77. package/internal/direct-surface-owner.js +29 -29
  78. package/internal/dynamic-environment-runtime.js +9 -9
  79. package/internal/edit-transport-owner.js +183 -139
  80. package/internal/execution-provider-owner.js +160 -0
  81. package/internal/execution-seam-compat.js +76 -120
  82. package/internal/isolated-worker.js +12 -8
  83. package/internal/kernel-worker.js +104 -32
  84. package/internal/native-root-dynamic.js +12 -13
  85. package/internal/program-bindings.js +190 -0
  86. package/internal/recovery-tips.js +53 -23
  87. package/internal/rejected-cell-editor.js +8 -8
  88. package/internal/repl-observation-interest.js +11 -3
  89. package/internal/rpc-contract.js +1 -0
  90. package/internal/runtime-bridge-owner.js +351 -82
  91. package/internal/runtime-config.js +58 -0
  92. package/internal/runtime-contexts.js +85 -16
  93. package/internal/runtime-intrinsics.js +1 -0
  94. package/internal/runtime-messages.js +41 -0
  95. package/internal/sdk-projection.js +10 -82
  96. package/internal/session-cell-executor.js +42 -19
  97. package/internal/session-events.js +74 -4
  98. package/internal/session-journal-recovery.js +904 -184
  99. package/internal/session-journal-schema.js +6 -3
  100. package/internal/session-journal.js +31 -2
  101. package/internal/session-log-view.js +189 -43
  102. package/internal/session-runtime.js +175 -23
  103. package/internal/settings-compat.js +23 -6
  104. package/internal/stateful-module-compiler.js +3 -2
  105. package/internal/stateful-module-runtime.js +7 -3
  106. package/internal/stateful-root-runtime.js +9 -4
  107. package/internal/user-binding-authoring.js +2 -7
  108. package/internal/user-bindings-owner.js +47 -7
  109. package/internal/validated-parse-repair.js +1 -1
  110. package/internal/value-wire.js +68 -21
  111. package/internal/worker-cwd-virtualization.js +20 -18
  112. package/internal/worker-realm-surfaces.js +3 -3
  113. package/locale/en.json +6 -0
  114. package/locale/zh.json +6 -0
  115. package/package.json +55 -38
  116. package/src/client-authoring-view.js +286 -168
  117. package/src/client-binding-review.js +4 -1
  118. package/src/client-binding-toggle.js +18 -0
  119. package/src/client-capabilities-view.js +1341 -0
  120. package/src/client-capability-sort.js +29 -0
  121. package/src/client-code-editor.js +6 -0
  122. package/src/client-copy.js +881 -8
  123. package/src/client-feature-gates.js +15 -5
  124. package/src/client-host-compat.js +23 -2
  125. package/src/client-indicator-view.js +4 -6
  126. package/src/client-primitives.js +280 -0
  127. package/src/client-repl-view.js +23 -16
  128. package/src/client-settings-view.js +412 -69
  129. package/src/client-styles.js +193 -41
  130. package/src/client-tool-hover.js +185 -0
  131. package/src/client-tool-view.js +45 -11
  132. package/src/client-transport.js +44 -9
  133. package/src/client-workbench.js +9 -10
  134. package/src/client.js +148 -68
  135. package/assets/ptc-plus-bindings-en.png +0 -0
  136. package/assets/ptc-plus-bindings-zh.png +0 -0
  137. package/assets/ptc-plus-repair-en.png +0 -0
  138. package/assets/ptc-plus-repair-zh.png +0 -0
  139. package/client.js +0 -31857
@@ -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) {
@@ -244,6 +260,7 @@ class SessionKernel {
244
260
  }
245
261
 
246
262
  async execute(request, config) {
263
+ const observedSession = request.sessionLog ?? this.session
247
264
  const recoveryBoundaries = this.initialRecoveryBoundary === undefined
248
265
  ? []
249
266
  : [this.initialRecoveryBoundary]
@@ -256,7 +273,7 @@ class SessionKernel {
256
273
  if (this.surfaceProvenanceRequired
257
274
  && (currentGeneration !== this.surfaceGeneration || hasLiveState)) {
258
275
  this.surfaceGeneration = currentGeneration
259
- const visibleCallSeqs = visibleExecutableCallSeqs(this.session)
276
+ const visibleCallSeqs = visibleExecutableCallSeqs(observedSession)
260
277
  const contractsLiveState = [...this.liveCallSeqs].some(callSeq => (
261
278
  !(visibleCallSeqs instanceof Set) || !visibleCallSeqs.has(callSeq)
262
279
  ))
@@ -267,7 +284,7 @@ class SessionKernel {
267
284
  if (config.durableReplay) {
268
285
  this.recoveryNotice = undefined
269
286
  try {
270
- this.history = recoverJournal(this.session, request.callSeq, {
287
+ this.history = recoverJournal(observedSession, request.callSeq, {
271
288
  visibleCallSeqs,
272
289
  })
273
290
  } catch (error) {
@@ -309,9 +326,9 @@ class SessionKernel {
309
326
  if (!(error instanceof ReplayFailure)) throw error
310
327
  const previousPathLength = pathToHead(this.history).length
311
328
  const boundary = recoveryBoundaryForHistory(this.history, error.node)
312
- const recovered = recoverJournal(this.session, request.callSeq, {
329
+ const recovered = recoverJournal(observedSession, request.callSeq, {
313
330
  extraBoundaries: [boundary],
314
- visibleCallSeqs: visibleExecutableCallSeqs(this.session),
331
+ visibleCallSeqs: visibleExecutableCallSeqs(observedSession),
315
332
  })
316
333
  const nextPathLength = pathToHead(recovered).length
317
334
  if (nextPathLength >= previousPathLength) throw new Error('recovery did not contract the historical frontier')
@@ -332,14 +349,12 @@ class SessionKernel {
332
349
  }
333
350
  if (skipped > 0) this.recoveryNotice = recoveryDiagnostic(skipped)
334
351
  }
335
- const leadingDiagnostics = this.recoveryNotice === undefined ? [] : [this.recoveryNotice]
352
+ const leadingDiagnostics = this.recoveryNotice === undefined || this.recoveryNoticeShown
353
+ ? [] : [this.recoveryNotice]
354
+ this.recoveryNoticeShown = this.recoveryNotice !== undefined
336
355
  this.recoveryNotice = undefined
337
356
  if (request.journal !== undefined) request.journal.diagnostics.push(...leadingDiagnostics)
338
357
  const result = await this.cellExecutor.executeCell(request, undefined, config)
339
- if (leadingDiagnostics.length > 0) {
340
- const rendered = leadingDiagnostics.map(item => renderDiagnostic(item, request.program))
341
- result.logs = [...rendered, ...result.logs]
342
- }
343
358
  if (result.error === undefined) {
344
359
  this.failures.reset()
345
360
  } else {
@@ -350,17 +365,29 @@ class SessionKernel {
350
365
  if (request.journal !== undefined) request.journal.diagnostics.push(hint)
351
366
  }
352
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
+ }
353
373
  return finishResult(result)
354
374
  }
355
375
 
356
376
  async replayHistory(request, config) {
357
377
  this.bindingCatalog = new BindingCatalog()
378
+ const limits = this.valueLimits(config)
379
+ const emptyArgsWire = encodeValueAtBoundary({}, limits, 'parameter')
358
380
  const path = pathToHead(this.history)
359
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)
360
385
  try {
361
386
  const result = await this.cellExecutor.executeCell(
362
387
  {
363
388
  ...request,
389
+ bindings: bindingDescriptors.namespaces,
390
+ bindingDescriptors,
364
391
  program: node.code,
365
392
  journal: undefined,
366
393
  ...(node.userBindings === undefined ? { userBindings: undefined } : { userBindings: node.userBindings }),
@@ -475,7 +502,17 @@ class SessionKernel {
475
502
  this.workerObservation = undefined
476
503
  // A refused reclamation stays recorded on this kernel instead of becoming
477
504
  // an unhandled rejection; the kernel keeps the worker until dispose.
478
- 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
479
516
  }
480
517
  if (!terminate && active.observing && replay === undefined) {
481
518
  // Computation and journal settlement are complete. Observation cannot change either.
@@ -620,17 +657,55 @@ function sessionOf(sessionContext) {
620
657
  session,
621
658
  callId: sessionContext.callId,
622
659
  persistedCallSeq: sessionContext.persistedCallSeq,
660
+ sourceCallSeq: sessionContext.sourceCallSeq,
623
661
  cwd: typeof session?.header?.cwd === 'string' ? session.header.cwd : undefined,
624
662
  surfaceProvenanceRequired: true,
625
663
  }
626
664
  }
627
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
+
628
701
  export class SessionRuntime {
629
702
  constructor(config = {}, options = {}) {
630
703
  this.config = resolvedRuntimeConfig(config)
631
704
  this.kernels = new Map()
632
705
  this.pendingNoops = new Map()
633
706
  this.settlements = new WeakSet()
707
+ this.preparingCells = new Set()
708
+ this.submissions = new WeakSet()
634
709
  this.disposed = false
635
710
  this.userBindingsCwd = options.userBindingsCwd ?? process.cwd()
636
711
  if (typeof this.userBindingsCwd !== 'string' || !isAbsolute(this.userBindingsCwd)) {
@@ -638,6 +713,8 @@ export class SessionRuntime {
638
713
  }
639
714
  this.withInitiator = typeof options.withInitiator === 'function' ? options.withInitiator : undefined
640
715
  this.observeSession = options.observeSession ?? (() => false)
716
+ this.readSessionLog = options.readSessionLog ?? (async session => session)
717
+ this.sessionForId = options.sessionForId
641
718
  this.disposal = undefined
642
719
  }
643
720
 
@@ -647,8 +724,31 @@ export class SessionRuntime {
647
724
  return execution.result
648
725
  }
649
726
 
727
+ beginSubmission(sessionContext, signal) {
728
+ const admission = { sessionId: sessionOf(sessionContext).id, config: this.config,
729
+ signal, cancelled: this.disposed }
730
+ this.submissions.add(admission)
731
+ if (!this.disposed) this.preparingCells.add(admission)
732
+ return admission
733
+ }
734
+
735
+ releaseSubmission(admission) {
736
+ this.preparingCells.delete(admission)
737
+ this.submissions.delete(admission)
738
+ }
739
+
740
+ assertSubmission(admission) {
741
+ if (this.disposed) throw new Error('PTC runtime disposed')
742
+ if (admission.cancelled || !this.submissions.has(admission)) throw new Error('PTC session disposed')
743
+ }
744
+
650
745
  reconfigure(config) {
651
746
  const resolved = resolvedRuntimeConfig(config)
747
+ for (const admission of this.preparingCells) {
748
+ if (!admission.signal?.aborted && resolved.maxOldGenerationSizeMb !== admission.config.maxOldGenerationSizeMb) {
749
+ throw new Error('ptc-plus: maxOldGenerationSizeMb cannot change while a session worker is active; retry after the session is disposed')
750
+ }
751
+ }
652
752
  const kernels = [...this.kernels.values()]
653
753
  for (const kernel of kernels) kernel.assertReconfigurationAllowed(resolved)
654
754
  for (const kernel of kernels) kernel.reconfigure(resolved)
@@ -659,13 +759,34 @@ export class SessionRuntime {
659
759
  if (this.disposed || !this.config.replViewEnabled) return undefined
660
760
  const { observation: _observation, ...expected } = normalizeReplMemorySnapshot(memory)
661
761
  if (!expected.available || expected.entries.length === 0) return undefined
662
- 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)
663
785
  }
664
786
 
665
- async runTentative(sessionContext, request) {
787
+ async runTentative(sessionContext, request, { programCompletion = false } = {}) {
666
788
  const completed = result => Object.freeze({ result, settlement: undefined })
667
789
  if (this.disposed) return completed({ logs: [], error: { kind: 'abort', message: 'PTC runtime disposed' } })
668
- const cellConfig = this.config
669
790
  let bindingDescriptors
670
791
  try {
671
792
  bindingDescriptors = normalizeBindingDescriptors(request?.bindings)
@@ -673,44 +794,67 @@ export class SessionRuntime {
673
794
  return completed({ logs: [], error: { kind: 'exception', message: messageOf(error) } })
674
795
  }
675
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
+ }
676
800
  const {
677
801
  id: sessionId,
678
802
  session,
679
803
  callId,
680
804
  persistedCallSeq,
805
+ sourceCallSeq: provedSourceCallSeq,
681
806
  cwd,
682
807
  surfaceProvenanceRequired,
683
808
  } = sessionOf(sessionContext)
684
809
  let callSeq
685
810
  let sourceCallSeq
811
+ let sessionLog
812
+ const admission = sessionContext?.submission ?? this.beginSubmission(sessionContext, request.signal)
813
+ if (!this.submissions.has(admission) || admission.sessionId !== sessionId) {
814
+ return completed({ logs: [], error: { kind: 'exception', message: 'PTC submission does not belong to this session runtime' } })
815
+ }
816
+ const cellConfig = admission.config
817
+ const aborted = () => this.disposed || admission.cancelled
818
+ const abortResult = () => completed({ logs: [], error: { kind: 'abort',
819
+ message: request.signal?.aborted ? String(request.signal.reason)
820
+ : this.disposed ? 'PTC runtime disposed' : 'PTC session disposed' } })
686
821
  try {
822
+ sessionLog = sessionContext?.sessionLog ?? await this.readSessionLog(session, request.signal)
823
+ if (aborted()) return abortResult()
687
824
  if (persistedCallSeq !== undefined
688
825
  && !isCanonicalSequence(persistedCallSeq)) {
689
826
  throw new Error('persisted tool call sequence must be a non-negative safe integer')
690
827
  }
828
+ if (provedSourceCallSeq !== undefined
829
+ && !isCanonicalSequence(provedSourceCallSeq)) {
830
+ throw new Error('source tool call sequence must be a non-negative safe integer')
831
+ }
691
832
  if (cellConfig.durableReplay) {
692
- sourceCallSeq = liveToolCallSeq(session, callId, 'run_code')
833
+ sourceCallSeq = provedSourceCallSeq ?? liveToolCallSeq(sessionLog, callId, 'run_code')
693
834
  callSeq = persistedCallSeq ?? sourceCallSeq
694
835
  } else {
695
- sourceCallSeq = persistedCallSeq
836
+ sourceCallSeq = provedSourceCallSeq ?? persistedCallSeq
696
837
  if (sourceCallSeq === undefined) {
697
838
  try {
698
- sourceCallSeq = liveToolCallSeq(session, callId, 'run_code')
839
+ sourceCallSeq = liveToolCallSeq(sessionLog, callId, 'run_code')
699
840
  } catch {
700
841
  sourceCallSeq = undefined
701
842
  }
702
843
  }
703
844
  }
704
845
  } catch (error) {
846
+ if (aborted() || request.signal?.aborted) return abortResult()
705
847
  return completed({ logs: [], error: { kind: 'recovery', message: `cannot identify current run_code call in session log: ${messageOf(error)}` } })
848
+ } finally {
849
+ this.releaseSubmission(admission)
706
850
  }
707
851
  let kernel = this.kernels.get(sessionId)
708
852
  if (kernel === undefined) {
709
853
  let history
710
854
  try {
711
855
  history = cellConfig.durableReplay
712
- ? recoverJournal(session, callSeq, {
713
- visibleCallSeqs: visibleExecutableCallSeqs(session),
856
+ ? recoverJournal(sessionLog, callSeq, {
857
+ visibleCallSeqs: visibleExecutableCallSeqs(sessionLog),
714
858
  })
715
859
  : emptyHistory()
716
860
  } catch (error) {
@@ -738,11 +882,12 @@ export class SessionRuntime {
738
882
  policies.rewritesEnabled,
739
883
  policies.languageSemantics,
740
884
  policies.moduleTransform,
885
+ request.cwd ?? cwd ?? null,
741
886
  )
742
887
  const workerReservation = kernel.reserveWorkerConfiguration(cellConfig)
743
888
  let result
744
889
  try {
745
- result = await kernel.run({ ...request, journal, callSeq, sourceCallSeq }, cellConfig)
890
+ result = await kernel.run({ ...request, journal, callSeq, sourceCallSeq, sessionLog, programCompletion }, cellConfig)
746
891
  } finally {
747
892
  kernel.releaseWorkerConfiguration(workerReservation)
748
893
  }
@@ -793,6 +938,12 @@ export class SessionRuntime {
793
938
 
794
939
  async disposeSession(sessionId) {
795
940
  const id = String(sessionId)
941
+ for (const admission of this.preparingCells) {
942
+ if (admission.sessionId === id) {
943
+ admission.cancelled = true
944
+ this.preparingCells.delete(admission)
945
+ }
946
+ }
796
947
  const kernel = this.kernels.get(id)
797
948
  this.pendingNoops.delete(id)
798
949
  if (kernel === undefined) return
@@ -815,6 +966,7 @@ export class SessionRuntime {
815
966
 
816
967
  async #dispose() {
817
968
  this.disposed = true
969
+ this.preparingCells.clear()
818
970
  const entries = [...this.kernels]
819
971
  const results = await Promise.allSettled(entries.map(async ([id, kernel]) => {
820
972
  await kernel.dispose()
@@ -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') {
@@ -63,7 +67,17 @@ export function installSettingsSectionCompat({
63
67
  }) {
64
68
  ctx.inject(['settings'], settingsContext => {
65
69
  const provider = settingsContext.settings
66
- onProvider(provider)
70
+ const sectionInstalled = typeof provider?.installSection === 'function'
71
+ || typeof settingsModule?.installSettingsSection === 'function'
72
+ const writeNamespace = sectionInstalled ? namespace : (ownerFiber ?? ctx.fiber)?.entry?.options?.id
73
+ const writer = typeof provider?.update === 'function'
74
+ && typeof writeNamespace === 'string' && writeNamespace.length > 0
75
+ ? Object.freeze({
76
+ update: (patch, expectedRevision) => provider.update(writeNamespace, patch, expectedRevision),
77
+ read: () => provider.describe?.().find(descriptor => descriptor.ns === writeNamespace),
78
+ }) : undefined
79
+ const unbind = onProvider(provider, writer)
80
+ if (typeof unbind === 'function') settingsContext.effect(() => unbind, 'ptc-plus settings writer')
67
81
  if (typeof provider?.installSection === 'function') {
68
82
  provider.installSection(ctx, namespace, schema, entry, hooks)
69
83
  return
@@ -79,7 +93,10 @@ export function installSettingsSectionCompat({
79
93
  // fiber, so the owner is the plugin's entry context rather than the
80
94
  // scope this installation happens to run in.
81
95
  const dispose = provider.configure({ auto: false }, ownerFiber ?? ctx.fiber)
82
- ctx.effect?.(() => dispose, 'ptc-plus settings page policy')
96
+ // The policy belongs to the settings surface that accepted it, so its
97
+ // disposer is released when that surface goes away instead of
98
+ // accumulating on the plugin scope across settings reloads.
99
+ settingsContext.effect?.(() => dispose, 'ptc-plus settings page policy')
83
100
  }
84
101
  return
85
102
  }
@@ -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) {