dsh-ptc-plus 0.5.0 → 0.5.2
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 +15 -3
- package/README.md +15 -3
- package/client.cjs +1983 -234
- 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 +2 -2
- package/docs/adr/0023-global-user-bindings.md +1 -1
- package/docs/adr/0027-run-the-session-kernel-in-a-killable-helper-process.md +11 -1
- package/docs/adr/0032-own-session-log-facts-in-a-host-only-projection.md +31 -1
- package/docs/adr/0038-freeze-session-capability-plans.md +1 -1
- package/docs/adr/0044-coexist-with-the-official-persistent-repl.md +35 -0
- 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/README.md +2 -0
- package/docs/architecture.md +2 -2
- package/docs/capability-planning-components.md +2 -2
- package/docs/client-ui.md +13 -10
- package/docs/client-visual-contract.md +1 -1
- package/docs/durability-design.md +7 -1
- package/docs/installation.md +5 -3
- package/docs/publishing.md +2 -2
- package/docs/releases/v0.5.2.md +79 -0
- package/docs/runtime-reference.md +42 -3
- package/docs/unmeasured-cell-recovery-repair-proposal.md +387 -0
- package/docs/verification.md +6 -2
- package/index.js +63 -19
- package/internal/bounded-literal.js +85 -0
- package/internal/capability-command-receipt.js +3 -1
- package/internal/capability-environment.js +171 -13
- package/internal/capability-plan.js +11 -8
- package/internal/capability-statistics.js +6 -2
- package/internal/cell-error.js +13 -0
- package/internal/config-spec.js +122 -2
- package/internal/direct-surface-owner.js +14 -10
- package/internal/edit-transport-owner.js +96 -17
- package/internal/execution-provider-owner.js +25 -0
- package/internal/execution-seam-compat.js +18 -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 +51 -0
- 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/runtime-bridge-owner.js +11 -8
- package/internal/runtime-config.js +2 -0
- package/internal/runtime-contexts.js +14 -5
- package/internal/runtime-messages.js +43 -15
- package/internal/sdk-projection.js +2 -2
- package/internal/session-cell-executor.js +132 -11
- package/internal/session-journal-recovery.js +1009 -99
- package/internal/session-journal-schema.js +7 -5
- package/internal/session-journal.js +177 -14
- package/internal/session-log-view.js +111 -24
- package/internal/session-runtime.js +674 -33
- 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 +8 -27
- 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 +5 -2
- package/src/client-activity.js +27 -6
- package/src/client-capabilities-view.js +774 -125
- package/src/client-copy.js +231 -18
- package/src/client-repl-view.js +6 -1
- package/src/client-styles.js +68 -2
- package/src/client-tool-view.js +9 -2
|
@@ -5,14 +5,46 @@ import { PassThrough } from 'node:stream'
|
|
|
5
5
|
import { pathToFileURL } from 'node:url'
|
|
6
6
|
import { helperProcessEnvironment, isElectronHost } from './worker-environment.js'
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Bound on the wait for one on-demand utilization answer.
|
|
10
|
+
*
|
|
11
|
+
* The helper answers by reading the inner worker's counters synchronously, so
|
|
12
|
+
* this bound covers delivery rather than helper work. It is deliberately not
|
|
13
|
+
* the shutdown release grace below: how long a stopping helper may keep running
|
|
14
|
+
* must not decide how long a cell waits for its measurement.
|
|
15
|
+
*/
|
|
16
|
+
export const ISOLATED_SAMPLE_MS = 1000
|
|
17
|
+
/**
|
|
18
|
+
* Sampling failure code for a sample whose answer this transport could not
|
|
19
|
+
* observe. The transport reports only that fact: whether the worker stays usable
|
|
20
|
+
* belongs to the caller, which alone knows how many consecutive measurements it
|
|
21
|
+
* may lose before the worker has to be replaced.
|
|
22
|
+
*/
|
|
23
|
+
export const UNANSWERED_SAMPLE_CODE = 'ptc-sample-unanswered'
|
|
24
|
+
/**
|
|
25
|
+
* Helper messages one sample may read out before it gives up on the answer.
|
|
26
|
+
*
|
|
27
|
+
* Reading stops at the first event-loop turn that delivers nothing, so this
|
|
28
|
+
* bound only ends a backlog larger than the bound itself, where the answer
|
|
29
|
+
* cannot be reached by reading until the channel empties.
|
|
30
|
+
*/
|
|
31
|
+
const SAMPLE_DRAIN_TURNS = 4096
|
|
8
32
|
/**
|
|
9
33
|
* Fallback bound for a helper that answers the stop request and then keeps the
|
|
10
34
|
* process alive with unrelated work of its own.
|
|
11
35
|
*/
|
|
12
36
|
const ISOLATED_RELEASE_MS = 1000
|
|
13
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* Default bound for each close stage of one helper and the transport default.
|
|
39
|
+
*
|
|
40
|
+
* Exit confirmation and the output close that follows are each bounded by it;
|
|
41
|
+
* the fixed release grace is capped so the bound still leaves a confirmation
|
|
42
|
+
* window for the kill. A transport instance carries a configurable
|
|
43
|
+
* `reclaimBoundMs`, and the close path reads that instance bound instead of
|
|
44
|
+
* this default.
|
|
45
|
+
*/
|
|
14
46
|
const ISOLATED_STOP_MS = 5000
|
|
15
|
-
/**
|
|
47
|
+
/** Default bound on the helper's output pipes really ending after its process exited. */
|
|
16
48
|
const ISOLATED_OUTPUT_MS = 5000
|
|
17
49
|
|
|
18
50
|
export function isolatedWorkerEntry(entry) {
|
|
@@ -36,7 +68,7 @@ export function isolatedWorkerEntry(entry) {
|
|
|
36
68
|
* so a kernel that never answers is reclaimed without ending the host.
|
|
37
69
|
*/
|
|
38
70
|
export class IsolatedWorker extends EventEmitter {
|
|
39
|
-
constructor({ helper, entry, workerData, resourceLimits, env, protocol = 'kernel' }) {
|
|
71
|
+
constructor({ helper, entry, workerData, resourceLimits, env, protocol = 'kernel', reclaimBoundMs = ISOLATED_STOP_MS }) {
|
|
40
72
|
super()
|
|
41
73
|
// The entry follows the worker constructor's contract: a URL or a string
|
|
42
74
|
// filename. Anything else fails here, before a helper process exists.
|
|
@@ -58,6 +90,29 @@ export class IsolatedWorker extends EventEmitter {
|
|
|
58
90
|
this.utilization = { idle: 0, active: 0 }
|
|
59
91
|
this.utilizationSequence = 0
|
|
60
92
|
this.utilizationRequests = new Map()
|
|
93
|
+
/**
|
|
94
|
+
* Count of messages this helper has produced.
|
|
95
|
+
*
|
|
96
|
+
* Reading a sample uses it to tell one event-loop turn that delivered
|
|
97
|
+
* something from one that delivered nothing; the count says nothing about
|
|
98
|
+
* when the helper wrote a message.
|
|
99
|
+
*/
|
|
100
|
+
this.helperMessages = 0
|
|
101
|
+
/**
|
|
102
|
+
* Bound this instance waits for one utilization answer.
|
|
103
|
+
*
|
|
104
|
+
* It is read per request, so a transport user may shorten or extend the wait
|
|
105
|
+
* for its own environment without changing the shutdown release grace.
|
|
106
|
+
*/
|
|
107
|
+
this.sampleBoundMs = ISOLATED_SAMPLE_MS
|
|
108
|
+
/**
|
|
109
|
+
* Bound this instance's close path waits for the killed helper to be
|
|
110
|
+
* confirmed gone and for its output pipes to end.
|
|
111
|
+
*
|
|
112
|
+
* It is read when the close starts, so a transport user may widen the wait
|
|
113
|
+
* for its own platform without changing the release grace.
|
|
114
|
+
*/
|
|
115
|
+
this.reclaimBoundMs = reclaimBoundMs
|
|
61
116
|
this.pendingErrors = []
|
|
62
117
|
this.performance = {
|
|
63
118
|
/** Utilization reported by the helper, in the shape callers already use. */
|
|
@@ -124,6 +179,7 @@ export class IsolatedWorker extends EventEmitter {
|
|
|
124
179
|
}
|
|
125
180
|
})
|
|
126
181
|
this.child.on('message', message => {
|
|
182
|
+
this.helperMessages += 1
|
|
127
183
|
if (message?.type === 'child-error') {
|
|
128
184
|
const error = new Error(message.message)
|
|
129
185
|
if (message.name !== undefined) error.name = message.name
|
|
@@ -139,11 +195,14 @@ export class IsolatedWorker extends EventEmitter {
|
|
|
139
195
|
}
|
|
140
196
|
if (message?.type === 'inner-worker-exit') { this.emit('inner-worker-exit', message.code); return }
|
|
141
197
|
if (message?.type === 'utilization-sample') {
|
|
198
|
+
// An answer for a request this transport already concluded is still the
|
|
199
|
+
// helper doing its work: keep the freshest value it reported.
|
|
200
|
+
this.utilization = message.utilization
|
|
142
201
|
const pending = this.utilizationRequests.get(message.id)
|
|
143
202
|
if (pending !== undefined) {
|
|
144
203
|
this.utilizationRequests.delete(message.id)
|
|
145
204
|
clearTimeout(pending.timer)
|
|
146
|
-
|
|
205
|
+
if (pending.decision !== undefined) clearImmediate(pending.decision)
|
|
147
206
|
pending.resolve(message.utilization)
|
|
148
207
|
}
|
|
149
208
|
return
|
|
@@ -225,25 +284,81 @@ export class IsolatedWorker extends EventEmitter {
|
|
|
225
284
|
}
|
|
226
285
|
const id = ++this.utilizationSequence
|
|
227
286
|
return new Promise((resolve, reject) => {
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
287
|
+
const pending = { resolve, reject, timer: undefined, decision: undefined }
|
|
288
|
+
pending.timer = setTimeout(() => {
|
|
289
|
+
// A blocked host runs its expired timers before it reads its IPC pipe, so
|
|
290
|
+
// the answer may be waiting unread here. Read the channel out first and
|
|
291
|
+
// only then conclude that no answer exists.
|
|
292
|
+
pending.decision = setImmediate(() => this.#readOutSample(id, pending))
|
|
293
|
+
}, this.sampleBoundMs)
|
|
294
|
+
this.utilizationRequests.set(id, pending)
|
|
233
295
|
try {
|
|
234
296
|
this.child.send({ type: 'sample-utilization', id })
|
|
235
297
|
} catch (error) {
|
|
236
|
-
clearTimeout(timer)
|
|
298
|
+
clearTimeout(pending.timer)
|
|
237
299
|
this.utilizationRequests.delete(id)
|
|
238
300
|
reject(error)
|
|
239
301
|
}
|
|
240
302
|
})
|
|
241
303
|
}
|
|
242
304
|
|
|
305
|
+
/**
|
|
306
|
+
* Read the helper's channel out before one unanswered sample is concluded.
|
|
307
|
+
*
|
|
308
|
+
* Node delivers one helper message per event-loop turn, and the answer this
|
|
309
|
+
* request waits for is written after everything the helper wrote before it, so
|
|
310
|
+
* it can sit behind queued messages. Each turn therefore either delivers the
|
|
311
|
+
* answer or another message the helper wrote; only a turn that delivers no
|
|
312
|
+
* message shows that the channel holds nothing more this read could observe.
|
|
313
|
+
*
|
|
314
|
+
* A message the parser still holds in parts is not delivered on any of those
|
|
315
|
+
* turns, so a partially received message ahead of the answer delays it past
|
|
316
|
+
* this read-out; the sample then reports no answer, which is all this
|
|
317
|
+
* transport can prove about it.
|
|
318
|
+
*/
|
|
319
|
+
#readOutSample(id, pending) {
|
|
320
|
+
let delivered = this.helperMessages
|
|
321
|
+
let turns = 0
|
|
322
|
+
const turn = () => {
|
|
323
|
+
if (this.utilizationRequests.get(id) !== pending) return
|
|
324
|
+
if (turns >= SAMPLE_DRAIN_TURNS) {
|
|
325
|
+
this.utilizationRequests.delete(id)
|
|
326
|
+
pending.decision = undefined
|
|
327
|
+
pending.reject(this.#unansweredSample('backlog'))
|
|
328
|
+
return
|
|
329
|
+
}
|
|
330
|
+
if (this.helperMessages === delivered) {
|
|
331
|
+
this.utilizationRequests.delete(id)
|
|
332
|
+
pending.decision = undefined
|
|
333
|
+
pending.reject(this.#unansweredSample('silent-channel'))
|
|
334
|
+
return
|
|
335
|
+
}
|
|
336
|
+
delivered = this.helperMessages
|
|
337
|
+
turns += 1
|
|
338
|
+
pending.decision = setImmediate(turn)
|
|
339
|
+
}
|
|
340
|
+
pending.decision = setImmediate(turn)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Report one sample whose answer this transport could not observe.
|
|
345
|
+
*
|
|
346
|
+
* The transport states the observation only. Whether the caller keeps its
|
|
347
|
+
* worker is decided where the consecutive measurement failures are counted.
|
|
348
|
+
*/
|
|
349
|
+
#unansweredSample(reason) {
|
|
350
|
+
const error = new Error(reason === 'backlog'
|
|
351
|
+
? `kernel helper wrote at least ${SAMPLE_DRAIN_TURNS} messages ahead of this utilization sample, so its answer could not be reached before the read-out bound`
|
|
352
|
+
: 'kernel helper did not answer utilization sampling before its deadline')
|
|
353
|
+
error.code = UNANSWERED_SAMPLE_CODE
|
|
354
|
+
return error
|
|
355
|
+
}
|
|
356
|
+
|
|
243
357
|
/** Fail every request that still needs the helper's utilization answer. */
|
|
244
358
|
#rejectUtilizationRequests(error) {
|
|
245
359
|
for (const pending of this.utilizationRequests.values()) {
|
|
246
360
|
clearTimeout(pending.timer)
|
|
361
|
+
if (pending.decision !== undefined) clearImmediate(pending.decision)
|
|
247
362
|
pending.reject(error)
|
|
248
363
|
}
|
|
249
364
|
this.utilizationRequests.clear()
|
|
@@ -268,12 +383,14 @@ export class IsolatedWorker extends EventEmitter {
|
|
|
268
383
|
|
|
269
384
|
async #stop() {
|
|
270
385
|
if (this.exited) return
|
|
271
|
-
//
|
|
272
|
-
//
|
|
273
|
-
//
|
|
386
|
+
// The configured bound is this close's exit-confirmation deadline, anchored
|
|
387
|
+
// at the close start so an acknowledgement never postpones the kill. The
|
|
388
|
+
// fixed release grace is capped to leave a grace-sized confirmation window
|
|
389
|
+
// for the kill, so a bound at or just above the grace is never consumed
|
|
390
|
+
// before the kill; the output pipes then get the same bound from their exit.
|
|
274
391
|
const started = Date.now()
|
|
275
|
-
const
|
|
276
|
-
const
|
|
392
|
+
const confirmBy = started + this.reclaimBoundMs
|
|
393
|
+
const releaseAt = started + Math.min(ISOLATED_RELEASE_MS, Math.max(0, this.reclaimBoundMs - ISOLATED_RELEASE_MS))
|
|
277
394
|
const exited = this.exitedPromise
|
|
278
395
|
const onReleased = message => {
|
|
279
396
|
if (message?.type === 'worker-message' && message.value?.type === 'shutdown-released') this.released = true
|
|
@@ -287,7 +404,7 @@ export class IsolatedWorker extends EventEmitter {
|
|
|
287
404
|
} catch {
|
|
288
405
|
// The channel is closed; the kill path below owns the instance.
|
|
289
406
|
}
|
|
290
|
-
if (!await leftWithin(this, remaining(
|
|
407
|
+
if (!await leftWithin(this, remaining(releaseAt))) {
|
|
291
408
|
let killFailure
|
|
292
409
|
if (!this.exited) {
|
|
293
410
|
try {
|
|
@@ -410,7 +527,7 @@ export class IsolatedOwner {
|
|
|
410
527
|
// synchronously finish or throw and clear the marker for a later retry.
|
|
411
528
|
const settling = Promise.resolve().then(async () => {
|
|
412
529
|
if (record.neverStarted !== true && record.transport !== undefined) {
|
|
413
|
-
const closed = await outputClosed(record.transport, ISOLATED_OUTPUT_MS)
|
|
530
|
+
const closed = await outputClosed(record.transport, record.transport?.reclaimBoundMs ?? ISOLATED_OUTPUT_MS)
|
|
414
531
|
if (!closed) {
|
|
415
532
|
// Output never drained: keep the instance and say so instead of
|
|
416
533
|
// pretending the pipes ended naturally.
|
|
@@ -42,7 +42,7 @@ import { installProgramAmbientResolver } from './dynamic-environment-runtime.js'
|
|
|
42
42
|
const { Object, Map, Set, Proxy, Error,
|
|
43
43
|
mapGet, mapSet, mapHas, mapDelete, mapClear, mapSize, mapForEach,
|
|
44
44
|
setHas, setAdd, setDelete, setClear, setForEach,
|
|
45
|
-
everyArray, someArray, appendArray, copyArray, join, sliceString, endsWith,
|
|
45
|
+
everyArray, someArray, isArray, min, appendArray, copyArray, join, sliceString, endsWith,
|
|
46
46
|
bufferConcat, bufferIsBuffer, bufferToString,
|
|
47
47
|
Reflect: privateReflect, promiseResolve, promiseReject } = moduleRuntimeIntrinsics
|
|
48
48
|
const PublicPromise = globalThis.Promise
|
|
@@ -114,6 +114,11 @@ const output = new PassThrough()
|
|
|
114
114
|
output.resume()
|
|
115
115
|
const evaluationScope = createPrivateAsyncLocalStorage()
|
|
116
116
|
const sessionCwd = typeof workerData?.cwd === 'string' ? workerData.cwd : undefined
|
|
117
|
+
/** Fallback bound for the startup conformance probe when the host config omits one. */
|
|
118
|
+
const DEFAULT_STARTUP_PROBE_MS = 5000
|
|
119
|
+
const startupProbeMs = Number.isSafeInteger(workerData?.startupProbeMs) && workerData.startupProbeMs > 0
|
|
120
|
+
? workerData.startupProbeMs
|
|
121
|
+
: DEFAULT_STARTUP_PROBE_MS
|
|
117
122
|
// The module source base is fixed when a cell starts; the operation directory
|
|
118
123
|
// follows the Session's working directory while the cell runs, so a confirmed
|
|
119
124
|
// `tools.working_directory` change rebases the rest of the cell exactly as the
|
|
@@ -232,6 +237,9 @@ const CONFORMANCE_CELL = `"use strict";
|
|
|
232
237
|
}`
|
|
233
238
|
let activeRun
|
|
234
239
|
let activeExecution
|
|
240
|
+
// Compile-time proof that a binding's initializer is a bounded object literal,
|
|
241
|
+
// keyed by binding name for the running cell.
|
|
242
|
+
let declaredBoundedLiterals = new Map()
|
|
235
243
|
let suppressReplCwdObservation = false
|
|
236
244
|
let nextCallId = 0
|
|
237
245
|
let nextStaticAdapterId = 0
|
|
@@ -964,7 +972,13 @@ async function activatePerNameUserBindings(snapshot, shadowedNames, cwd, reusePo
|
|
|
964
972
|
let evaluated
|
|
965
973
|
const activation = { id: entry.id, called: false, failed: false }
|
|
966
974
|
try {
|
|
967
|
-
if (entry.durability === 'volatile')
|
|
975
|
+
if (entry.durability === 'volatile') {
|
|
976
|
+
// Name the binding and its first cause instead of the whole reason list:
|
|
977
|
+
// the model reads this to decide what it can no longer redo.
|
|
978
|
+
const reasons = String(entry.volatileReason ?? 'non-replayable source').split(', ')
|
|
979
|
+
const more = reasons.length > 1 ? ` (+${reasons.length - 1} more)` : ''
|
|
980
|
+
markVolatile(`user binding ${jsonStringify(entry.id)} depends on ${reasons[0]}${more}`)
|
|
981
|
+
}
|
|
968
982
|
evaluated = await userBindingActivationScope.run(activation, () => evaluateUserBinding(entry, cwd, snapshot.transform))
|
|
969
983
|
const namespace = evaluated.namespace
|
|
970
984
|
for (let symbolIndex = 0; symbolIndex < entry.symbols.length; symbolIndex += 1) {
|
|
@@ -1297,6 +1311,131 @@ function observeBindings(names) {
|
|
|
1297
1311
|
}) }
|
|
1298
1312
|
}
|
|
1299
1313
|
|
|
1314
|
+
/**
|
|
1315
|
+
* The logical binding a committed root target names.
|
|
1316
|
+
*
|
|
1317
|
+
* A root target is `root:<sourceStart>:<name>` with an optional position suffix
|
|
1318
|
+
* the compiler appends for a repeated name. Only the logical name exists on the
|
|
1319
|
+
* recovery side, and it is the name the model reads, so the record keeps that
|
|
1320
|
+
* instead of the compiler's internal target.
|
|
1321
|
+
*/
|
|
1322
|
+
function logicalBindingName(target) {
|
|
1323
|
+
const match = /^root:\d+:([^:]+)/u.exec(target)
|
|
1324
|
+
return match === null ? target : match[1]
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
/** Cap one recorded binding: 64 KiB of encoded wire. */
|
|
1328
|
+
const RECORDED_BINDING_WIRE_LIMIT = 64 * 1024
|
|
1329
|
+
/** Cap one settlement's recorded bindings: 1 MiB of encoded wire in total. */
|
|
1330
|
+
/** Cap the graph a recording pass may walk before the value becomes unrecorded. */
|
|
1331
|
+
const RECORDED_VALUE_NODES = 2048
|
|
1332
|
+
const RECORDED_VALUE_EDGES = 4096
|
|
1333
|
+
|
|
1334
|
+
const RECORDED_CELL_WIRE_LIMIT = 1024 * 1024
|
|
1335
|
+
|
|
1336
|
+
/**
|
|
1337
|
+
* Encode the values a settlement produced.
|
|
1338
|
+
*
|
|
1339
|
+
* Every settlement records what it declared, durable or volatile: a durable
|
|
1340
|
+
* cell still needs its values when recovery cannot replay it (a rewind moved
|
|
1341
|
+
* the head past it, or the log proves it but the path does not reach it), and a
|
|
1342
|
+
* volatile cell cannot be replayed at all. The record keeps values as wire so
|
|
1343
|
+
* recovery installs them without running the cell again. Two caps keep the
|
|
1344
|
+
* journal bounded: 64 KiB per binding and 1 MiB per settlement. A value over a
|
|
1345
|
+
* cap, or one the codec cannot transfer, is reported by name instead of
|
|
1346
|
+
* silently disappearing from the record.
|
|
1347
|
+
*/
|
|
1348
|
+
/**
|
|
1349
|
+
* Whether recording one binding can be attempted without walking an unbounded
|
|
1350
|
+
* value. The codec enumerates the value's own keys before it can apply any
|
|
1351
|
+
* budget, and enumerating a graph with millions of keys exhausts a worker heap
|
|
1352
|
+
* before a cap could reject it. So only shapes that are cheap to size are
|
|
1353
|
+
* recorded: primitives, and arrays whose length is already known. A plain
|
|
1354
|
+
* object, function or class instance is reported by name as unrecorded — its
|
|
1355
|
+
* binding still exists in the journal's operations, but its value is not
|
|
1356
|
+
* captured.
|
|
1357
|
+
*/
|
|
1358
|
+
function recordingAffordable(value) {
|
|
1359
|
+
if (value === null) return true
|
|
1360
|
+
const kind = typeof value
|
|
1361
|
+
if (kind === 'number' || kind === 'string' || kind === 'boolean' || kind === 'bigint' || kind === 'undefined') return true
|
|
1362
|
+
if (isArray(value)) return value.length <= RECORDED_VALUE_NODES
|
|
1363
|
+
return false
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
function collectBindingValues(names, limits = {}) {
|
|
1367
|
+
const values = []
|
|
1368
|
+
const unrecorded = []
|
|
1369
|
+
const seen = new Set()
|
|
1370
|
+
let recorded = 0
|
|
1371
|
+
for (const name of names) {
|
|
1372
|
+
const logical = logicalBindingName(name)
|
|
1373
|
+
if (setHas(seen, logical)) continue
|
|
1374
|
+
const local = statefulRoots.localValue(name) ?? statefulRoots.localValue(logical)
|
|
1375
|
+
if (local === undefined) {
|
|
1376
|
+
setAdd(seen, logical)
|
|
1377
|
+
unrecorded.push(logical)
|
|
1378
|
+
continue
|
|
1379
|
+
}
|
|
1380
|
+
setAdd(seen, logical)
|
|
1381
|
+
try {
|
|
1382
|
+
// A recording pass must stay cheap. The codec starts by enumerating the
|
|
1383
|
+
// value's own keys, so a huge declaration (millions of keys) would exhaust
|
|
1384
|
+
// the worker heap before any budget check could reject it. Walk the value
|
|
1385
|
+
// cheaply first — array length, or a `for…in` that stops after the cap
|
|
1386
|
+
// without materializing keys — and record the name as unrecorded instead.
|
|
1387
|
+
if (!recordingAffordable(local.value)) {
|
|
1388
|
+
unrecorded.push(logical)
|
|
1389
|
+
continue
|
|
1390
|
+
}
|
|
1391
|
+
const wire = encodeValue(local.value, {
|
|
1392
|
+
...limits,
|
|
1393
|
+
maxNodes: min(limits.maxNodes ?? RECORDED_VALUE_NODES, RECORDED_VALUE_NODES),
|
|
1394
|
+
maxEdges: min(limits.maxEdges ?? RECORDED_VALUE_EDGES, RECORDED_VALUE_EDGES),
|
|
1395
|
+
maxArrayLength: min(limits.maxArrayLength ?? RECORDED_VALUE_NODES, RECORDED_VALUE_NODES),
|
|
1396
|
+
maxStringBytes: min(limits.maxStringBytes ?? RECORDED_BINDING_WIRE_LIMIT, RECORDED_BINDING_WIRE_LIMIT),
|
|
1397
|
+
})
|
|
1398
|
+
const size = jsonStringify(wire).length
|
|
1399
|
+
if (size > RECORDED_BINDING_WIRE_LIMIT || recorded + size > RECORDED_CELL_WIRE_LIMIT) {
|
|
1400
|
+
unrecorded.push(logical)
|
|
1401
|
+
continue
|
|
1402
|
+
}
|
|
1403
|
+
recorded += size
|
|
1404
|
+
values.push({ name: logical, wire })
|
|
1405
|
+
} catch {
|
|
1406
|
+
unrecorded.push(logical)
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
return { values, unrecorded }
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
/**
|
|
1413
|
+
* Install the values recovery recorded for a volatile cell.
|
|
1414
|
+
*
|
|
1415
|
+
* The worker never runs the restoration cell's source: each binding is decoded
|
|
1416
|
+
* and committed directly, so an ambient read, a timer or a child process
|
|
1417
|
+
* reached by the original cell is not dispatched a second time. A value the
|
|
1418
|
+
* codec cannot decode is reported by name so recovery can say which binding it
|
|
1419
|
+
* could not bring back.
|
|
1420
|
+
*/
|
|
1421
|
+
function restoreRecordedValues(message) {
|
|
1422
|
+
const limits = message.valueLimits
|
|
1423
|
+
const restored = []
|
|
1424
|
+
const failed = []
|
|
1425
|
+
for (const entry of message.values ?? []) {
|
|
1426
|
+
const name = entry?.name
|
|
1427
|
+
if (typeof name !== 'string' || name === '') continue
|
|
1428
|
+
try {
|
|
1429
|
+
const value = decodeValue(entry.wire, limits)
|
|
1430
|
+
if (statefulRoots.restoreValue(name, value)) restored.push(name)
|
|
1431
|
+
else failed.push({ name, reason: 'the store cannot publish a session identity for this name' })
|
|
1432
|
+
} catch (error) {
|
|
1433
|
+
failed.push({ name, reason: error instanceof Error ? error.message : String(error) })
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
return { restored, failed }
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1300
1439
|
function refreshLegacyPublication(name) {
|
|
1301
1440
|
const publications = activeExecution?.nativePublications
|
|
1302
1441
|
const publication = publications === undefined ? undefined : mapGet(publications, name)
|
|
@@ -1334,12 +1473,16 @@ async function sendCompletion(message, execution, userBindings, committedRedecla
|
|
|
1334
1473
|
// after this point is background output and carries the attribution
|
|
1335
1474
|
// diagnostic instead of being attached to a later cell.
|
|
1336
1475
|
execution.open = false
|
|
1476
|
+
const bindingValues = collectBindingValues(committedRedeclarations, execution.valueLimits)
|
|
1337
1477
|
channel.postMessage({
|
|
1338
1478
|
type: 'done',
|
|
1339
1479
|
id: message.id,
|
|
1340
1480
|
outputFence: message.outputFence,
|
|
1341
1481
|
logs: execution.logs,
|
|
1342
1482
|
...completionDurability(execution),
|
|
1483
|
+
...(bindingValues.values.length === 0 && bindingValues.unrecorded.length === 0
|
|
1484
|
+
? {}
|
|
1485
|
+
: { bindingValues }),
|
|
1343
1486
|
committedRedeclarations: setValuesArray(committedRedeclarations),
|
|
1344
1487
|
...(message.rootBindings === undefined ? {} : { rootBindingFacts: statefulRoots.facts() }),
|
|
1345
1488
|
...(perName ? { userBindingNames: normalizeUserBindingNames(mapArray(mapEntriesArray(userBindingSources), entry => ({
|
|
@@ -1398,6 +1541,11 @@ async function runCell(message) {
|
|
|
1398
1541
|
volatileReason: message.volatileReason,
|
|
1399
1542
|
}
|
|
1400
1543
|
activeExecution = execution
|
|
1544
|
+
// Compile-time evidence that a declaration's initializer is a bounded object
|
|
1545
|
+
// literal, keyed by binding name. Settlement uses it to decide whether a value
|
|
1546
|
+
// can be recorded without walking an object the codec cannot size; nothing
|
|
1547
|
+
// reads it yet, so this commit changes no recording behaviour.
|
|
1548
|
+
declaredBoundedLiterals = mapFromArray(message.declarationEvidence ?? [], entry => entry)
|
|
1401
1549
|
const cellGlobals = []
|
|
1402
1550
|
const committedRedeclarations = new Set()
|
|
1403
1551
|
|
|
@@ -1543,6 +1691,13 @@ channel.on('message', (message) => {
|
|
|
1543
1691
|
channel.postMessage({ type: 'ready', id: message.id })
|
|
1544
1692
|
return
|
|
1545
1693
|
}
|
|
1694
|
+
if (message?.type === 'restore') {
|
|
1695
|
+
// Recovery installs the values a volatile cell recorded. No cell code runs
|
|
1696
|
+
// here: replaying the source would redispatch the ambient effects that made
|
|
1697
|
+
// the cell volatile, which is exactly what the record exists to avoid.
|
|
1698
|
+
channel.postMessage({ type: 'restored', id: message.id, ...restoreRecordedValues(message) })
|
|
1699
|
+
return
|
|
1700
|
+
}
|
|
1546
1701
|
if (message?.type === 'observe') {
|
|
1547
1702
|
channel.postMessage({ type: 'observation-started', id: message.id })
|
|
1548
1703
|
channel.postMessage({ type: 'observation', id: message.id,
|
|
@@ -1585,7 +1740,16 @@ try {
|
|
|
1585
1740
|
await Promise.race([
|
|
1586
1741
|
verifyEvaluation(),
|
|
1587
1742
|
new Promise((_, reject) => {
|
|
1588
|
-
|
|
1743
|
+
/* c8 ignore start -- the probe settles in microtasks, so the timer cannot win */
|
|
1744
|
+
// `verifyEvaluation` dispatches its fixed sources through this realm's own
|
|
1745
|
+
// evaluator, and every await it takes settles in a microtask (`throw`,
|
|
1746
|
+
// `await Promise.reject`, a syntax rejection). A `setTimeout` is a macrotask,
|
|
1747
|
+
// so it cannot run before those microtasks drain: the rejection below is
|
|
1748
|
+
// unreachable for any input, and a bound of 1 ms still starts the worker.
|
|
1749
|
+
startupTimer = setTimeout(() => reject(new Error(
|
|
1750
|
+
`REPL settlement probe timed out after ${startupProbeMs}ms`,
|
|
1751
|
+
)), startupProbeMs)
|
|
1752
|
+
/* c8 ignore stop */
|
|
1589
1753
|
}),
|
|
1590
1754
|
])
|
|
1591
1755
|
replModuleRealm = (await evaluate('({Promise,stringify:value=>`${value}`,importModule:(source,options)=>import(source,options)})')).value
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** Historical legacy-v1 lowering, retained from c51616c8d6c8:cell-analysis.js. */
|
|
2
|
+
import { boundedLiteralEvidence } from './bounded-literal.js'
|
|
2
3
|
import { ModuleRewriteError, rewriteModuleImportsExports } from './legacy-cell-rewriter.js'
|
|
3
4
|
import { parseExecutableCell } from './legacy-cell-parser.js'
|
|
4
5
|
import {
|
|
@@ -74,6 +75,7 @@ function topLevelDeclarations(body, variableRedeclarations = false) {
|
|
|
74
75
|
if (statement.type === 'VariableDeclaration') {
|
|
75
76
|
const definitionSpan = declarationSpan(statement)
|
|
76
77
|
for (const declaration of statement.declarations) {
|
|
78
|
+
const first = declarations.length
|
|
77
79
|
addPatternDeclarations(
|
|
78
80
|
declaration.id,
|
|
79
81
|
declarations,
|
|
@@ -81,6 +83,14 @@ function topLevelDeclarations(body, variableRedeclarations = false) {
|
|
|
81
83
|
definitionSpan,
|
|
82
84
|
statement.kind !== 'const' || variableRedeclarations,
|
|
83
85
|
)
|
|
86
|
+
// A plain `const name = { … }` initializer proves its own size from the
|
|
87
|
+
// source, so recovery may record the value without walking it. A
|
|
88
|
+
// destructuring pattern has no single initializer to prove.
|
|
89
|
+
const evidence = declaration.id.type === 'Identifier' && declaration.init !== null
|
|
90
|
+
? boundedLiteralEvidence(declaration.init) : undefined
|
|
91
|
+
if (evidence !== undefined) {
|
|
92
|
+
for (const record of declarations.slice(first)) record.boundedLiteral = evidence
|
|
93
|
+
}
|
|
84
94
|
}
|
|
85
95
|
} else if ((statement.type === 'FunctionDeclaration' || statement.type === 'ClassDeclaration')
|
|
86
96
|
&& statement.id !== null) {
|
|
@@ -408,6 +418,7 @@ function normalizePreparationOptions({
|
|
|
408
418
|
importNamespaces = new Set(),
|
|
409
419
|
writableBindings,
|
|
410
420
|
moduleSemantics = LIVE_MODULE_SEMANTICS,
|
|
421
|
+
rewriteDescriptions,
|
|
411
422
|
}) {
|
|
412
423
|
if (typeof bindingPolicy === 'boolean') {
|
|
413
424
|
bindingPolicy = {
|
|
@@ -435,7 +446,7 @@ function normalizePreparationOptions({
|
|
|
435
446
|
throw new TypeError('ptc-plus: module semantics must define supported default-export and import-expression rules')
|
|
436
447
|
}
|
|
437
448
|
return { knownBindings, bindingPolicy, reservedBindings, rewritesEnabled,
|
|
438
|
-
importBindings, importNamespaces, writableBindings, moduleSemantics }
|
|
449
|
+
importBindings, importNamespaces, writableBindings, moduleSemantics, rewriteDescriptions }
|
|
439
450
|
}
|
|
440
451
|
|
|
441
452
|
function parseMappedCell(moduleRewrite, program) {
|
|
@@ -522,7 +533,7 @@ function classifyPrepared({ code, sourceMap }, program, knownBindings, moduleRew
|
|
|
522
533
|
export function prepareLegacyProgram(program, options = {}) {
|
|
523
534
|
if (typeof program !== 'string') throw new TypeError('ptc-plus: program must be a string')
|
|
524
535
|
const { knownBindings, bindingPolicy, reservedBindings, rewritesEnabled,
|
|
525
|
-
importBindings, importNamespaces, writableBindings, moduleSemantics } = normalizePreparationOptions(options)
|
|
536
|
+
importBindings, importNamespaces, writableBindings, moduleSemantics, rewriteDescriptions } = normalizePreparationOptions(options)
|
|
526
537
|
const unavailableGeneratedNames = new Set([
|
|
527
538
|
...knownBindings, ...reservedBindings, ...importBindings.keys(), ...importNamespaces,
|
|
528
539
|
])
|
|
@@ -561,6 +572,7 @@ export function prepareLegacyProgram(program, options = {}) {
|
|
|
561
572
|
bindingPolicy,
|
|
562
573
|
autoSplitRedeclarations: rewritesEnabled.autoSplitRedeclarations,
|
|
563
574
|
commitSignal,
|
|
575
|
+
rewriteDescriptions,
|
|
564
576
|
})
|
|
565
577
|
const { executableCode, executableSourceMap, collisions, redeclared, rewrites } = convenience
|
|
566
578
|
const commitTargets = new Set(moduleRewrite.commitTargets)
|
|
@@ -9,6 +9,18 @@ import {
|
|
|
9
9
|
import { applySourceEdits, createMappedTextBuilder, identitySourceMap } from './source-position-map.js'
|
|
10
10
|
import { SKIP_AST_CHILDREN, walkAst } from './ast-traversal.js'
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Historical descriptions, used when a caller supplies no resolved document.
|
|
14
|
+
*
|
|
15
|
+
* They are presentation only: no recognizer compares a rewrite description, so a
|
|
16
|
+
* customized description changes what the model reads and nothing else.
|
|
17
|
+
*/
|
|
18
|
+
const DEFAULT_DESCRIPTIONS = Object.freeze({
|
|
19
|
+
mixed: MIXED_REDECLARATION_REWRITE,
|
|
20
|
+
function: FUNCTION_REDECLARATION_REWRITE,
|
|
21
|
+
class: CLASS_REDECLARATION_REWRITE,
|
|
22
|
+
})
|
|
23
|
+
|
|
12
24
|
/**
|
|
13
25
|
* Optional compatibility policy for a persistent REPL. The cell language does
|
|
14
26
|
* not own this policy: callers may replace it when persistence uses separate
|
|
@@ -129,6 +141,7 @@ export function rewriteReplRedeclarations({
|
|
|
129
141
|
bindingPolicy,
|
|
130
142
|
autoSplitRedeclarations,
|
|
131
143
|
commitSignal,
|
|
144
|
+
rewriteDescriptions = DEFAULT_DESCRIPTIONS,
|
|
132
145
|
}) {
|
|
133
146
|
const redeclared = []
|
|
134
147
|
const rewrites = []
|
|
@@ -184,8 +197,8 @@ export function rewriteReplRedeclarations({
|
|
|
184
197
|
rewrites.push({
|
|
185
198
|
kind: 'redeclaration',
|
|
186
199
|
description: kind === 'class'
|
|
187
|
-
?
|
|
188
|
-
:
|
|
200
|
+
? rewriteDescriptions.class
|
|
201
|
+
: rewriteDescriptions.function,
|
|
189
202
|
source: statement.id.name,
|
|
190
203
|
})
|
|
191
204
|
}
|
|
@@ -262,7 +275,7 @@ export function rewriteReplRedeclarations({
|
|
|
262
275
|
redeclared.push(...existing.map(collisionFor))
|
|
263
276
|
rewrites.push({
|
|
264
277
|
kind: 'redeclaration',
|
|
265
|
-
description:
|
|
278
|
+
description: rewriteDescriptions.mixed,
|
|
266
279
|
source: existing.map(binding => binding.name).join(', '),
|
|
267
280
|
})
|
|
268
281
|
} else if (existing.length === bindings.length && bindings.length > 0) {
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Official persistent-REPL priority.
|
|
3
|
+
*
|
|
4
|
+
* The official `persistent-in-memory` execution mode is a live provider
|
|
5
|
+
* capability, not a release fact: a provider reports `programFormat` `'repl'`
|
|
6
|
+
* exactly when it retains a REPL per state owner. While the user selects the
|
|
7
|
+
* official REPL priority and that provider is live, the plugin yields PTC
|
|
8
|
+
* session execution to the official implementation and installs no session
|
|
9
|
+
* runtime: `run_code`, its descriptors and its guidance stay official. The
|
|
10
|
+
* decision is re-evaluated on every configuration change and host source
|
|
11
|
+
* change, so either side can take over without restarting the host.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Decide whether this configuration yields PTC session execution to the
|
|
16
|
+
* official provider.
|
|
17
|
+
* @param {object} config - resolved plugin configuration.
|
|
18
|
+
* @param {object} provider - the live provider surface registered for the seam.
|
|
19
|
+
* @returns {boolean} true when the official persistent REPL owns execution.
|
|
20
|
+
*/
|
|
21
|
+
export function defersToOfficialRepl(config, provider) {
|
|
22
|
+
if (config?.officialReplPriority !== true) return false
|
|
23
|
+
try {
|
|
24
|
+
return provider?.programFormat === 'repl'
|
|
25
|
+
} catch {
|
|
26
|
+
// An unreadable capability descriptor cannot prove official ownership;
|
|
27
|
+
// the plugin keeps its execution responsibility instead of guessing.
|
|
28
|
+
return false
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Runtime record for an attachment that yields execution to the official
|
|
34
|
+
* provider. It owns no execution resources: the official provider releases
|
|
35
|
+
* anything it retained through its own lifecycle, and the plugin's settings,
|
|
36
|
+
* storage and UI owners remain available so the user can switch back.
|
|
37
|
+
*/
|
|
38
|
+
export function createDeferredRuntime() {
|
|
39
|
+
return Object.freeze({
|
|
40
|
+
cancelCordisActivation() {
|
|
41
|
+
// No Cordis companion is installed while execution is deferred.
|
|
42
|
+
},
|
|
43
|
+
contextsForRequest: () => [],
|
|
44
|
+
async reconfigure() {
|
|
45
|
+
// This attachment has no owners to reconfigure.
|
|
46
|
+
},
|
|
47
|
+
async dispose() {
|
|
48
|
+
// Nothing is owned here; disposal returns immediately.
|
|
49
|
+
},
|
|
50
|
+
})
|
|
51
|
+
}
|