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.
- package/README.en.md +10 -4
- package/README.md +10 -4
- package/client.cjs +1623 -104
- package/compiler-core.cjs +132 -132
- package/compiler-core.cjs.map +3 -3
- package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +2 -0
- package/docs/adr/0017-track-the-latest-dsh-public-surface.md +4 -2
- package/docs/adr/0019-plugin-settings-and-kill-switch.md +2 -2
- package/docs/adr/0023-global-user-bindings.md +12 -0
- package/docs/adr/0027-run-the-session-kernel-in-a-killable-helper-process.md +11 -1
- package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +1 -1
- package/docs/adr/0030-serve-both-typert-codec-generations-from-one-codec.md +2 -2
- package/docs/adr/0032-own-session-log-facts-in-a-host-only-projection.md +31 -1
- package/docs/adr/0044-coexist-with-the-official-persistent-repl.md +4 -4
- package/docs/adr/0045-customize-model-visible-prompt-text.md +38 -0
- package/docs/adr/0046-gate-the-commit-with-the-semantic-evidence-tier.md +35 -0
- package/docs/adr/0047-store-journal-argument-digests-in-the-projection.md +88 -0
- package/docs/adr/0048-supersede-a-rewritten-cell.md +48 -0
- package/docs/adr/0049-prove-a-settlement-against-its-published-record.md +53 -0
- package/docs/adr/0050-read-host-surfaces-through-their-published-contract.md +48 -0
- package/docs/adr/README.md +7 -1
- package/docs/architecture.md +3 -3
- package/docs/client-ui.md +5 -2
- package/docs/durability-design.md +15 -2
- package/docs/installation.md +9 -9
- package/docs/releases/v0.5.2.md +91 -0
- package/docs/releases/v0.5.3.md +77 -0
- package/docs/runtime-reference.md +43 -3
- package/docs/unmeasured-cell-recovery-repair-proposal.md +387 -0
- package/docs/user-bindings.en.md +15 -0
- package/docs/user-bindings.md +15 -0
- package/docs/v0.5.3-goals.md +117 -0
- package/docs/verification.md +7 -3
- package/index.js +100 -31
- package/internal/binding-adoption.js +136 -0
- package/internal/bounded-literal.js +85 -0
- package/internal/capability-command-receipt.js +18 -4
- package/internal/capability-delivery-proof.js +70 -0
- package/internal/capability-environment.js +200 -11
- package/internal/capability-plan.js +11 -8
- package/internal/cell-error.js +13 -0
- package/internal/config-spec.js +126 -6
- package/internal/cordis-tools-owner.js +2 -1
- package/internal/direct-surface-owner.js +39 -21
- package/internal/edit-transport-owner.js +81 -15
- package/internal/execution-provider-owner.js +6 -0
- package/internal/execution-seam-compat.js +25 -0
- package/internal/host-tool-sdk.js +308 -0
- package/internal/isolated-worker.js +134 -17
- package/internal/kernel-worker.js +167 -3
- package/internal/legacy-cell-analysis.js +14 -2
- package/internal/legacy-repl-convenience.js +16 -3
- package/internal/official-repl-priority.js +31 -10
- package/internal/program-bindings.js +12 -1
- package/internal/projection-retention.js +238 -0
- package/internal/prompt-text.js +858 -0
- package/internal/recovery-tips.js +50 -11
- package/internal/rejected-cell-editor.js +3 -2
- package/internal/repl-memory-projection.js +21 -2
- package/internal/runtime-bridge-owner.js +114 -9
- package/internal/runtime-config.js +2 -0
- package/internal/runtime-contexts.js +14 -5
- package/internal/runtime-messages.js +48 -15
- package/internal/session-cell-executor.js +132 -11
- package/internal/session-journal-recovery.js +1204 -116
- package/internal/session-journal-schema.js +10 -5
- package/internal/session-journal.js +214 -14
- package/internal/session-log-view.js +133 -26
- package/internal/session-runtime.js +746 -38
- package/internal/session-state.js +27 -0
- package/internal/stateful-root-compiler.js +11 -0
- package/internal/stateful-root-runtime.js +17 -0
- package/internal/user-binding-console.js +7 -0
- package/internal/user-bindings-owner.js +112 -6
- package/internal/user-bindings.js +2 -2
- package/internal/validated-parse-repair.js +6 -1
- package/internal/worker-client.js +27 -4
- package/package.json +16 -6
- package/src/client-activity.js +24 -4
- package/src/client-capabilities-view.js +504 -27
- package/src/client-copy.js +187 -2
- package/src/client-indicator-view.js +2 -1
- package/src/client-repl-view.js +35 -11
- package/src/client-styles.js +64 -2
- 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
|
|
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.
|
|
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
|
|
572
|
-
//
|
|
573
|
-
//
|
|
574
|
-
|
|
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:
|
|
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:
|
|
975
|
+
error: { kind: 'output-limit', message: resultEnvelopeText(resolvePromptTextConfig(active.config), active.config.maxOutputBytes) },
|
|
855
976
|
}, true)
|
|
856
977
|
return
|
|
857
978
|
}
|