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.
- package/README.en.md +61 -123
- package/README.md +61 -123
- package/assets/dsh-ptc-plus-banner-en.webp +0 -0
- package/assets/dsh-ptc-plus-banner-zh.webp +0 -0
- package/assets/ptc-plus-repl-workspace-en.png +0 -0
- package/assets/ptc-plus-repl-workspace-zh.png +0 -0
- package/assets/ptc-plus-settings-en.png +0 -0
- package/assets/ptc-plus-settings-zh.png +0 -0
- package/client.cjs +42336 -0
- package/cordis.patch.yml +13 -2
- package/docs/README.md +7 -2
- package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +31 -179
- package/docs/adr/0008-define-agent-operating-contract.md +5 -1
- package/docs/adr/0009-bound-expensive-acceptance-default.md +2 -2
- package/docs/adr/0016-archive-resolved-review-findings.md +1 -1
- package/docs/adr/0017-track-the-latest-dsh-public-surface.md +2 -2
- package/docs/adr/0019-plugin-settings-and-kill-switch.md +14 -3
- package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +2 -2
- package/docs/adr/0022-stage-npm-releases-with-oidc.md +4 -2
- package/docs/adr/0023-global-user-bindings.md +3 -1
- package/docs/adr/0026-persist-the-delivery-verdict-against-the-verified-candidate.md +2 -0
- package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +101 -35
- package/docs/adr/0030-serve-both-typert-codec-generations-from-one-codec.md +3 -3
- package/docs/adr/0031-close-semantic-obligations-by-owner.md +1 -1
- package/docs/adr/0032-own-session-log-facts-in-a-host-only-projection.md +27 -0
- package/docs/adr/0033-gate-client-visual-usability-in-a-browser.md +23 -0
- package/docs/adr/0034-keep-program-values-separate-from-output-projections.md +29 -0
- package/docs/adr/0035-run-coverage-only-before-a-new-release.md +27 -0
- package/docs/adr/0036-separate-focused-development-from-delivery-verification.md +29 -0
- package/docs/adr/0037-keep-tool-hover-previews-passive.md +52 -0
- package/docs/adr/0038-freeze-session-capability-plans.md +71 -0
- package/docs/adr/0039-verify-documentation-without-runtime-suites.md +29 -0
- package/docs/adr/0040-separate-capability-configurations-from-session-observations.md +39 -0
- package/docs/adr/0041-record-cell-execution-directories.md +65 -0
- package/docs/adr/0042-persist-provider-availability-and-handback.md +41 -0
- package/docs/adr/0043-compose-capability-and-context-policies.md +37 -0
- package/docs/adr/README.md +6 -0
- package/docs/architecture.md +25 -27
- package/docs/binding-review-placement-design.md +29 -21
- package/docs/capability-planning-components.md +93 -0
- package/docs/capability-projection.md +7 -0
- package/docs/client-ui.md +88 -29
- package/docs/client-visual-contract.md +65 -0
- package/docs/context-management.md +22 -0
- package/docs/context-optimization-plan.md +353 -0
- package/docs/context-public-interface-coverage.md +25 -0
- package/docs/durability-design.md +31 -14
- package/docs/evaluation.md +41 -6
- package/docs/evaluations/command-goat-deepseek-v4.1-flash-20261006.md +126 -0
- package/docs/evaluations/command-goat-deepseek-v4.1-flash.md +281 -0
- package/docs/installation.md +50 -8
- package/docs/program-data-plane.md +5 -3
- package/docs/publishing.md +5 -5
- package/docs/release-coverage.md +73 -0
- package/docs/release-notes.md +1 -1
- package/docs/review-procedure.md +56 -0
- package/docs/runtime-reference.md +48 -16
- package/docs/semantic-validation.md +14 -2
- package/docs/stateful-binding-updates-proposal.md +2 -2
- package/docs/user-bindings.en.md +2 -2
- package/docs/user-bindings.md +3 -3
- package/docs/value-wire.md +11 -1
- package/docs/verification-optimization-work.md +52 -15
- package/docs/verification.md +30 -86
- package/index.js +147 -23
- package/internal/binding-toggle-receipt.js +24 -0
- package/internal/capability-command-receipt.js +29 -0
- package/internal/capability-environment.js +1247 -0
- package/internal/capability-plan.js +285 -0
- package/internal/capability-policy.js +96 -0
- package/internal/capability-statistics-store.js +180 -0
- package/internal/capability-statistics.js +210 -0
- package/internal/compiler-platform.js +5 -3
- package/internal/compiler-service.js +2 -9
- package/internal/config-spec.js +79 -9
- package/internal/cordis-tools-owner.js +22 -19
- package/internal/direct-surface-owner.js +29 -29
- package/internal/dynamic-environment-runtime.js +9 -9
- package/internal/edit-transport-owner.js +183 -139
- package/internal/execution-provider-owner.js +160 -0
- package/internal/execution-seam-compat.js +76 -120
- package/internal/isolated-worker.js +12 -8
- package/internal/kernel-worker.js +104 -32
- package/internal/native-root-dynamic.js +12 -13
- package/internal/program-bindings.js +190 -0
- package/internal/recovery-tips.js +53 -23
- package/internal/rejected-cell-editor.js +8 -8
- package/internal/repl-observation-interest.js +11 -3
- package/internal/rpc-contract.js +1 -0
- package/internal/runtime-bridge-owner.js +351 -82
- package/internal/runtime-config.js +58 -0
- package/internal/runtime-contexts.js +85 -16
- package/internal/runtime-intrinsics.js +1 -0
- package/internal/runtime-messages.js +41 -0
- package/internal/sdk-projection.js +10 -82
- package/internal/session-cell-executor.js +42 -19
- package/internal/session-events.js +74 -4
- package/internal/session-journal-recovery.js +904 -184
- package/internal/session-journal-schema.js +6 -3
- package/internal/session-journal.js +31 -2
- package/internal/session-log-view.js +189 -43
- package/internal/session-runtime.js +175 -23
- package/internal/settings-compat.js +23 -6
- package/internal/stateful-module-compiler.js +3 -2
- package/internal/stateful-module-runtime.js +7 -3
- package/internal/stateful-root-runtime.js +9 -4
- package/internal/user-binding-authoring.js +2 -7
- package/internal/user-bindings-owner.js +47 -7
- package/internal/validated-parse-repair.js +1 -1
- package/internal/value-wire.js +68 -21
- package/internal/worker-cwd-virtualization.js +20 -18
- package/internal/worker-realm-surfaces.js +3 -3
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +55 -38
- package/src/client-authoring-view.js +286 -168
- package/src/client-binding-review.js +4 -1
- package/src/client-binding-toggle.js +18 -0
- package/src/client-capabilities-view.js +1341 -0
- package/src/client-capability-sort.js +29 -0
- package/src/client-code-editor.js +6 -0
- package/src/client-copy.js +881 -8
- package/src/client-feature-gates.js +15 -5
- package/src/client-host-compat.js +23 -2
- package/src/client-indicator-view.js +4 -6
- package/src/client-primitives.js +280 -0
- package/src/client-repl-view.js +23 -16
- package/src/client-settings-view.js +412 -69
- package/src/client-styles.js +193 -41
- package/src/client-tool-hover.js +185 -0
- package/src/client-tool-view.js +45 -11
- package/src/client-transport.js +44 -9
- package/src/client-workbench.js +9 -10
- package/src/client.js +148 -68
- package/assets/ptc-plus-bindings-en.png +0 -0
- package/assets/ptc-plus-bindings-zh.png +0 -0
- package/assets/ptc-plus-repair-en.png +0 -0
- package/assets/ptc-plus-repair-zh.png +0 -0
- 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
|
-
|
|
38
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
329
|
+
const recovered = recoverJournal(observedSession, request.callSeq, {
|
|
313
330
|
extraBoundaries: [boundary],
|
|
314
|
-
visibleCallSeqs: visibleExecutableCallSeqs(
|
|
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
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
713
|
-
visibleCallSeqs: visibleExecutableCallSeqs(
|
|
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
|
|
40
|
-
? Schema.boolean()
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|