dsh-ptc-plus 0.5.1 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/README.en.md +10 -4
  2. package/README.md +10 -4
  3. package/client.cjs +1623 -104
  4. package/compiler-core.cjs +132 -132
  5. package/compiler-core.cjs.map +3 -3
  6. package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +2 -0
  7. package/docs/adr/0017-track-the-latest-dsh-public-surface.md +4 -2
  8. package/docs/adr/0019-plugin-settings-and-kill-switch.md +2 -2
  9. package/docs/adr/0023-global-user-bindings.md +12 -0
  10. package/docs/adr/0027-run-the-session-kernel-in-a-killable-helper-process.md +11 -1
  11. package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +1 -1
  12. package/docs/adr/0030-serve-both-typert-codec-generations-from-one-codec.md +2 -2
  13. package/docs/adr/0032-own-session-log-facts-in-a-host-only-projection.md +31 -1
  14. package/docs/adr/0044-coexist-with-the-official-persistent-repl.md +4 -4
  15. package/docs/adr/0045-customize-model-visible-prompt-text.md +38 -0
  16. package/docs/adr/0046-gate-the-commit-with-the-semantic-evidence-tier.md +35 -0
  17. package/docs/adr/0047-store-journal-argument-digests-in-the-projection.md +88 -0
  18. package/docs/adr/0048-supersede-a-rewritten-cell.md +48 -0
  19. package/docs/adr/0049-prove-a-settlement-against-its-published-record.md +53 -0
  20. package/docs/adr/0050-read-host-surfaces-through-their-published-contract.md +48 -0
  21. package/docs/adr/README.md +7 -1
  22. package/docs/architecture.md +3 -3
  23. package/docs/client-ui.md +5 -2
  24. package/docs/durability-design.md +15 -2
  25. package/docs/installation.md +9 -9
  26. package/docs/releases/v0.5.2.md +91 -0
  27. package/docs/releases/v0.5.3.md +77 -0
  28. package/docs/runtime-reference.md +43 -3
  29. package/docs/unmeasured-cell-recovery-repair-proposal.md +387 -0
  30. package/docs/user-bindings.en.md +15 -0
  31. package/docs/user-bindings.md +15 -0
  32. package/docs/v0.5.3-goals.md +117 -0
  33. package/docs/verification.md +7 -3
  34. package/index.js +100 -31
  35. package/internal/binding-adoption.js +136 -0
  36. package/internal/bounded-literal.js +85 -0
  37. package/internal/capability-command-receipt.js +18 -4
  38. package/internal/capability-delivery-proof.js +70 -0
  39. package/internal/capability-environment.js +200 -11
  40. package/internal/capability-plan.js +11 -8
  41. package/internal/cell-error.js +13 -0
  42. package/internal/config-spec.js +126 -6
  43. package/internal/cordis-tools-owner.js +2 -1
  44. package/internal/direct-surface-owner.js +39 -21
  45. package/internal/edit-transport-owner.js +81 -15
  46. package/internal/execution-provider-owner.js +6 -0
  47. package/internal/execution-seam-compat.js +25 -0
  48. package/internal/host-tool-sdk.js +308 -0
  49. package/internal/isolated-worker.js +134 -17
  50. package/internal/kernel-worker.js +167 -3
  51. package/internal/legacy-cell-analysis.js +14 -2
  52. package/internal/legacy-repl-convenience.js +16 -3
  53. package/internal/official-repl-priority.js +31 -10
  54. package/internal/program-bindings.js +12 -1
  55. package/internal/projection-retention.js +238 -0
  56. package/internal/prompt-text.js +858 -0
  57. package/internal/recovery-tips.js +50 -11
  58. package/internal/rejected-cell-editor.js +3 -2
  59. package/internal/repl-memory-projection.js +21 -2
  60. package/internal/runtime-bridge-owner.js +114 -9
  61. package/internal/runtime-config.js +2 -0
  62. package/internal/runtime-contexts.js +14 -5
  63. package/internal/runtime-messages.js +48 -15
  64. package/internal/session-cell-executor.js +132 -11
  65. package/internal/session-journal-recovery.js +1204 -116
  66. package/internal/session-journal-schema.js +10 -5
  67. package/internal/session-journal.js +214 -14
  68. package/internal/session-log-view.js +133 -26
  69. package/internal/session-runtime.js +746 -38
  70. package/internal/session-state.js +27 -0
  71. package/internal/stateful-root-compiler.js +11 -0
  72. package/internal/stateful-root-runtime.js +17 -0
  73. package/internal/user-binding-console.js +7 -0
  74. package/internal/user-bindings-owner.js +112 -6
  75. package/internal/user-bindings.js +2 -2
  76. package/internal/validated-parse-repair.js +6 -1
  77. package/internal/worker-client.js +27 -4
  78. package/package.json +16 -6
  79. package/src/client-activity.js +24 -4
  80. package/src/client-capabilities-view.js +504 -27
  81. package/src/client-copy.js +187 -2
  82. package/src/client-indicator-view.js +2 -1
  83. package/src/client-repl-view.js +35 -11
  84. package/src/client-styles.js +64 -2
  85. package/src/client.js +3 -1
@@ -26,9 +26,11 @@ import {
26
26
  import { assertStateName, LIVE_USER_BINDINGS_REUSE_POLICY } from './session-journal.js'
27
27
  import { LIVE_USER_BINDINGS_SHADOW_POLICY, normalizeUserBindingNames } from './session-journal-schema.js'
28
28
  import { PreflightError, prepareProgram } from './compiler-service.js'
29
+ import { UNANSWERED_SAMPLE_CODE } from './isolated-worker.js'
29
30
  import { executionPolicies } from './binding-update-policy.js'
31
+ import { resultEnvelopeText, resolvePromptTextConfig, rewriteDescriptions } from './prompt-text.js'
30
32
  import { createReplMemorySnapshot } from './repl-memory-projection.js'
31
- import { ModuleRewriteError } from './cell-error.js'
33
+ import { CellAdmissionBlocked, ModuleRewriteError } from './cell-error.js'
32
34
  import { mapSourcePosition } from './source-position-map.js'
33
35
  import { durabilityState, transitionDurability } from './session-state.js'
34
36
  import { dynamicBindingOrigin } from './dynamic-binding-evidence.js'
@@ -38,7 +40,6 @@ import {
38
40
  selectUserBindingsSnapshot,
39
41
  } from './user-bindings.js'
40
42
 
41
- const OUTPUT_LIMIT_MESSAGE = bytes => `output exceeded ${bytes} bytes; this cell was discarded and the worker reset. Only bindings in the verified recovery frontier can be restored. External effects may already have occurred and are not undone. For future cells, bound console output and return a smaller value; do not assume this cell's values remain available.`
42
43
 
43
44
  function earlyResult(kind, message) {
44
45
  return { logs: [], error: { kind, message } }
@@ -94,7 +95,7 @@ function parseDiagnostic(error, source, position, repair) {
94
95
  : ['this cell was not executed; correct the reported syntax and retry only this cell with run_code']
95
96
  : [
96
97
  `this cell was not executed; validated syntax repair: append ${JSON.stringify(repair.delimiter)} at the end of this cell`,
97
- `when this correction matches your intent and tools.edit_run_code is available in the program SDK, use ${repair.invocation} inside a new run_code cell with its target guard; the accepted edit runs the complete corrected cell after that new cell finishes; validation proves syntax/preflight acceptance, not intended behavior; otherwise submit corrected source in run_code`,
98
+ `when this correction matches your intent, tools.edit_run_code can apply it to call_seq ${repair.targetCallSeq} (same anchor with ${JSON.stringify(repair.delimiter)} appended); the accepted edit reruns the corrected cell after this one, otherwise submit corrected source in run_code`,
98
99
  ],
99
100
  })
100
101
  }
@@ -366,6 +367,35 @@ function stateArguments(value) {
366
367
  export class SessionCellExecutor {
367
368
  constructor(kernel) {
368
369
  this.kernel = kernel
370
+ this.pendingRestore = undefined
371
+ }
372
+
373
+ /**
374
+ * Install recorded values into the worker without running any program.
375
+ *
376
+ * Recovery restores a cell the journal recorded as volatile from the values
377
+ * that cell produced. Sending its source instead would redispatch whatever
378
+ * ambient source made it volatile, so this path carries values only: the
379
+ * worker commits each one as a declaration and reports the names it could
380
+ * not install. A missing worker is reported the same way rather than thrown,
381
+ * because the caller has to tell the model which bindings it lost.
382
+ */
383
+ async restoreBindings(values, config = this.kernel.config) {
384
+ const kernel = this.kernel
385
+ const failedAll = reason => values.map(entry => ({ name: entry.name, reason }))
386
+ if (kernel.disposed) return { restored: [], failed: failedAll('session kernel disposed') }
387
+ if (kernel.client.worker === undefined) return { restored: [], failed: failedAll('no live worker') }
388
+ const id = ++kernel.sequence
389
+ const valueLimits = kernel.valueLimits(config)
390
+ return new Promise(resolve => {
391
+ this.pendingRestore = { id, resolve }
392
+ try {
393
+ kernel.client.post({ type: 'restore', id, values, valueLimits })
394
+ } catch (error) {
395
+ this.pendingRestore = undefined
396
+ resolve({ restored: [], failed: failedAll(error instanceof Error ? error.message : String(error)) })
397
+ }
398
+ })
369
399
  }
370
400
 
371
401
  async executeCell(request, replayRecord = undefined, config = this.kernel.config) {
@@ -380,6 +410,24 @@ export class SessionCellExecutor {
380
410
  kernel.completeJournal(request.journal, 'noop', result)
381
411
  return result
382
412
  }
413
+ // A restore-only request installs recorded values: it must not compile or run
414
+ // the node's program. Preparing it first could reject source that is dead by
415
+ // definition — the node is not on the path to the head, so it may reference
416
+ // bindings a recorded rewind ended — and a rejected preparation is what made
417
+ // the off-path repair install nothing. The catalog advances from the recorded
418
+ // names instead, so later cells still compile against them.
419
+ if (request.restoreValues !== undefined) {
420
+ try {
421
+ await kernel.prepareWorker()
422
+ await kernel.ensureWorker(config.maxOldGenerationSizeMb)
423
+ } catch (error) {
424
+ if (error instanceof CellAdmissionBlocked) throw error
425
+ return earlyResult('worker-exit', messageOf(error))
426
+ }
427
+ const outcome = await this.restoreBindings(request.restoreValues, config)
428
+ kernel.bindingCatalog = kernel.bindingCatalog.restore(outcome.restored)
429
+ return { logs: [], restored: outcome.restored, failed: outcome.failed }
430
+ }
383
431
 
384
432
  let userBindings
385
433
  const userBindingsShadowPolicy = replayRecord?.userBindingsShadowPolicy ?? LIVE_USER_BINDINGS_SHADOW_POLICY
@@ -425,6 +473,7 @@ export class SessionCellExecutor {
425
473
  const moduleDirectory = executionCwd ?? process.cwd()
426
474
  const moduleFilename = resolve(moduleDirectory, 'repl')
427
475
  const prepareCell = program => prepareProgram(program, {
476
+ rewriteDescriptions: rewriteDescriptions(resolvePromptTextConfig(config)),
428
477
  knownBindings: catalog.knownBindings,
429
478
  bindingPolicy,
430
479
  languageSemantics,
@@ -483,8 +532,12 @@ export class SessionCellExecutor {
483
532
  let worker
484
533
  try {
485
534
  await kernel.prepareWorker()
486
- worker = await kernel.client.ensure(config.maxOldGenerationSizeMb)
535
+ worker = await kernel.ensureWorker(config.maxOldGenerationSizeMb)
487
536
  } catch (error) {
537
+ // An admission block is the kernel's own lifecycle fact, so the kernel
538
+ // settles the cell: it must not be read as a failure of recorded history
539
+ // or of this source.
540
+ if (error instanceof CellAdmissionBlocked) throw error
488
541
  const result = earlyResult('worker-exit', messageOf(error))
489
542
  kernel.completeJournal(request.journal, 'discarded', result)
490
543
  kernel.rollbackToDurable()
@@ -510,6 +563,7 @@ export class SessionCellExecutor {
510
563
  const active = {
511
564
  id,
512
565
  started: false,
566
+ dispatched: false,
513
567
  request: { ...request, bindings: bindings.namespaces },
514
568
  finish: resolve,
515
569
  computeTimer: undefined,
@@ -561,23 +615,57 @@ export class SessionCellExecutor {
561
615
  // cache. Work that completed before this request must not be charged to
562
616
  // the new cell, and work after the sample must remain counted.
563
617
  active.wallTimer = setTimeout(() => {
618
+ // A cell that never reached the runtime has executed nothing, so this
619
+ // failure cannot discard an earlier settlement or justify rebuilding the
620
+ // worker on its own; SessionRuntime owns how many such cells it keeps.
621
+ if (active.dispatched !== true) {
622
+ kernel.abandonCell(active, earlyResult(
623
+ 'budget-unavailable',
624
+ `wall-clock ceiling reached (${config.maxWallMs}ms) before this cell was dispatched; no code from it ran; retry the cell or raise maxWallMs`,
625
+ ))
626
+ return
627
+ }
564
628
  active.resolve(earlyResult('timeout', `wall-clock ceiling reached (${config.maxWallMs}ms); split long-running work into smaller cells`), true)
565
629
  }, config.maxWallMs)
566
630
  active.budgetsReady = (async () => {
567
631
  let started
568
632
  try {
633
+ // How long this environment may wait for an answer belongs to the
634
+ // cell's configuration; the transport reads its bound per request.
635
+ if (Number.isSafeInteger(config.sampleBoundMs) && config.sampleBoundMs > 0) {
636
+ worker.sampleBoundMs = config.sampleBoundMs
637
+ }
569
638
  started = await worker.sampleUtilization()
570
639
  } catch (error) {
571
- // No fresh baseline means this request cannot be measured. Use the
572
- // existing worker-exit route instead of pretending the last cache
573
- // sample is this request's starting point.
574
- active.resolve(earlyResult('worker-exit', messageOf(error)), true)
640
+ // No fresh baseline means this request cannot be measured: the
641
+ // helper's periodic cache may predate work that already finished, so
642
+ // it is not a substitute start point. A helper that is still trusted
643
+ // has not failed, and no frame was dispatched yet, so the unmeasured
644
+ // cell settles without execution. Only a helper the transport reports
645
+ // as lost takes the worker-exit route.
646
+ if (safeProperty(error, 'code') === UNANSWERED_SAMPLE_CODE) {
647
+ kernel.abandonCell(active, earlyResult(
648
+ 'budget-unavailable',
649
+ `${messageOf(error)}; this cell was not dispatched and no code from it ran; retry the cell`,
650
+ ))
651
+ } else {
652
+ active.resolve(earlyResult('worker-exit', messageOf(error)), true)
653
+ }
575
654
  return
576
655
  }
577
656
  if (active.settled) return
578
657
  active.computeTimer = setInterval(() => {
579
658
  if (active.settled) return
580
659
  if (worker.performance.eventLoopUtilization(started).active > config.computeMs) {
660
+ // Nothing from this cell was dispatched, so an exhausted budget
661
+ // measures earlier work on this worker, not this cell's source.
662
+ if (active.dispatched !== true) {
663
+ kernel.abandonCell(active, earlyResult(
664
+ 'budget-unavailable',
665
+ `compute budget exhausted (${config.computeMs}ms event-loop active) before this cell was dispatched; no code from this cell ran; a callback from earlier work may still be blocking the worker; retry the cell or restart the session`,
666
+ ))
667
+ return
668
+ }
581
669
  active.resolve(earlyResult('timeout', `compute budget exhausted (${config.computeMs}ms event-loop active, including synchronous blocking); this measure does not establish CPU use or the specific cause. Use asynchronous waiting for long waits, or a DSH-owned managed job if available. This cell was discarded and the worker reset; external effects or processes may continue.`), true)
582
670
  }
583
671
  }, Math.min(100, config.computeMs))
@@ -604,6 +692,12 @@ export class SessionCellExecutor {
604
692
  returnSignal: prepared.returnSignal,
605
693
  asyncCompletion: prepared.asyncCompletion,
606
694
  commitSignal: prepared.commitSignal,
695
+ // Compile-time evidence that a binding's initializer is a bounded
696
+ // object literal. Nothing records from it yet; settlement will use
697
+ // it to decide whether a value can be captured without walking it.
698
+ declarationEvidence: prepared.declarations
699
+ .filter(declaration => declaration.boundedLiteral !== undefined)
700
+ .map(declaration => [declaration.name, declaration.boundedLiteral]),
607
701
  maxOutputBytes: config.maxOutputBytes,
608
702
  valueLimits,
609
703
  durability,
@@ -624,6 +718,9 @@ export class SessionCellExecutor {
624
718
  ...request.bindingDescriptors.reservedNames,
625
719
  ])],
626
720
  })
721
+ // The run frame is with the worker now, so a later failure may have
722
+ // executed this cell's source and cannot settle as an unmeasured cell.
723
+ active.dispatched = true
627
724
  } catch (error) {
628
725
  active.resolve(earlyResult('worker-exit', messageOf(error)), true)
629
726
  }
@@ -689,7 +786,7 @@ export class SessionCellExecutor {
689
786
  throw new Error(`cannot save a durable REPL state from a volatile segment; volatile reason: ${active.durability.reason ?? 'replayability is unproved'}. This save rejection leaves live bindings usable in the current worker; continue computation from them. It does not undo external effects.`)
690
787
  }
691
788
  active.control.names.add(name)
692
- active.journal.operations.push({ action, name })
789
+ active.journal.operations.push({ action, name, kind: 'checkpoint' })
693
790
  return { action, name, saved: true }
694
791
  }
695
792
  if (action === 'delete') {
@@ -703,6 +800,18 @@ export class SessionCellExecutor {
703
800
  }
704
801
 
705
802
  onMessage(message) {
803
+ if (message?.type === 'restored') {
804
+ const pending = this.pendingRestore
805
+ if (pending === undefined || pending.id !== message.id) return
806
+ this.pendingRestore = undefined
807
+ const restored = Array.isArray(message.restored) ? message.restored.filter(name => typeof name === 'string') : []
808
+ const failed = Array.isArray(message.failed)
809
+ ? message.failed.filter(entry => entry !== null && typeof entry === 'object' && typeof entry.name === 'string')
810
+ .map(entry => ({ name: entry.name, reason: typeof entry.reason === 'string' ? entry.reason : 'the worker did not report a reason' }))
811
+ : []
812
+ pending.resolve({ restored, failed })
813
+ return
814
+ }
706
815
  if (message?.type === 'observation-started') {
707
816
  const kernel = this.kernel
708
817
  const observation = kernel.workerObservation
@@ -759,7 +868,7 @@ export class SessionCellExecutor {
759
868
  const logs = Array.isArray(message.logs) && message.logs.every(log => typeof log === 'string') ? message.logs : []
760
869
  active.resolve({
761
870
  logs: limitLogs(logs),
762
- error: { kind: 'output-limit', message: OUTPUT_LIMIT_MESSAGE(active.config.maxOutputBytes) },
871
+ error: { kind: 'output-limit', message: resultEnvelopeText(resolvePromptTextConfig(active.config), active.config.maxOutputBytes) },
763
872
  }, true)
764
873
  }
765
874
 
@@ -791,6 +900,18 @@ export class SessionCellExecutor {
791
900
  reason: typeof message.volatileReason === 'string' ? message.volatileReason : undefined,
792
901
  })
793
902
  }
903
+ // The worker sends the values this settlement produced, durable or volatile, so
904
+ // the journal can keep them; recovery installs them instead of re-running the
905
+ // cell when its replay path does not reach it. A value the worker could not
906
+ // transfer arrives in `unrecorded` and is named in the record.
907
+ const recorded = message.bindingValues ?? message.volatileBindingValues
908
+ if (recorded !== undefined && typeof recorded === 'object'
909
+ && Array.isArray(recorded.values) && Array.isArray(recorded.unrecorded)
910
+ && recorded.values.every(entry => entry !== null && typeof entry === 'object'
911
+ && typeof entry.name === 'string' && entry.wire !== undefined)
912
+ && recorded.unrecorded.every(name => typeof name === 'string')) {
913
+ active.bindingValues = recorded
914
+ }
794
915
  if (active.replay !== undefined && active.durability.status !== 'durable') {
795
916
  active.resolve({ logs, error: { kind: 'recovery', message: 'durable history requested a volatile capability during replay' } }, true)
796
917
  return
@@ -851,7 +972,7 @@ export class SessionCellExecutor {
851
972
  if (bytes > active.config.maxOutputBytes) {
852
973
  active.resolve({
853
974
  logs: limitLogs(logs),
854
- error: { kind: 'output-limit', message: OUTPUT_LIMIT_MESSAGE(active.config.maxOutputBytes) },
975
+ error: { kind: 'output-limit', message: resultEnvelopeText(resolvePromptTextConfig(active.config), active.config.maxOutputBytes) },
855
976
  }, true)
856
977
  return
857
978
  }