dsh-ptc-plus 0.2.1
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/LICENSE +21 -0
- package/README.md +137 -0
- package/README.zh.md +137 -0
- package/assets/dsh-ptc-plus-banner.webp +0 -0
- package/assets/ptc-plus-repair.png +0 -0
- package/assets/ptc-plus-settings.png +0 -0
- package/client.js +366 -0
- package/cordis.patch.yml +5 -0
- package/docs/README.md +21 -0
- package/docs/adr/0001-delegate-governance-to-dsh.md +3 -0
- package/docs/adr/0002-limit-work-map-scope.md +3 -0
- package/docs/adr/0003-prefer-native-program-surfaces.md +3 -0
- package/docs/adr/0004-declare-program-bindings-at-the-host.md +5 -0
- package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +129 -0
- package/docs/adr/0006-rewrite-module-syntax-with-an-ast.md +45 -0
- package/docs/adr/0007-separate-worker-transport-from-session-semantics.md +39 -0
- package/docs/adr/0008-define-agent-operating-contract.md +25 -0
- package/docs/adr/0009-bound-expensive-acceptance-default.md +39 -0
- package/docs/adr/0010-session-log-derived-recovery-tips.md +26 -0
- package/docs/adr/0011-isolate-the-local-recovery-tip-provider.md +20 -0
- package/docs/adr/0012-preserve-mixed-destructuring-initialization.md +23 -0
- package/docs/adr/0013-isolate-repl-redeclaration-convenience.md +21 -0
- package/docs/adr/0014-persist-cell-rewrite-policy.md +25 -0
- package/docs/adr/0015-preserve-source-positions-through-rewrites.md +25 -0
- package/docs/adr/0016-archive-resolved-review-findings.md +43 -0
- package/docs/adr/0017-track-the-latest-dsh-public-surface.md +23 -0
- package/docs/adr/0018-use-versioned-fixture-for-ab-benchmark.md +25 -0
- package/docs/adr/0019-plugin-settings-and-kill-switch.md +32 -0
- package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +36 -0
- package/docs/adr/0021-separate-host-bootstrap-from-session-runtime.md +68 -0
- package/docs/adr/README.md +18 -0
- package/docs/architecture.md +134 -0
- package/docs/capability-projection.md +48 -0
- package/docs/client-ui.md +47 -0
- package/docs/durability-design.md +265 -0
- package/docs/evaluation.md +62 -0
- package/docs/installation.md +82 -0
- package/docs/program-data-plane.md +61 -0
- package/docs/publishing.md +37 -0
- package/docs/runtime-reference.md +119 -0
- package/docs/value-wire.md +61 -0
- package/index.js +532 -0
- package/internal/ast-traversal.js +20 -0
- package/internal/binding-descriptors.js +95 -0
- package/internal/binding-pattern.js +54 -0
- package/internal/builtin-esm-sync.js +9 -0
- package/internal/cell-analysis.js +467 -0
- package/internal/cell-rewriter.js +520 -0
- package/internal/config-spec.js +179 -0
- package/internal/cordis-tools-owner.js +363 -0
- package/internal/diagnostic.js +133 -0
- package/internal/direct-surface-owner.js +326 -0
- package/internal/edit-transport-owner.js +287 -0
- package/internal/failure-reporting.js +139 -0
- package/internal/kernel-worker.js +484 -0
- package/internal/module-policy.js +46 -0
- package/internal/program-bindings.js +187 -0
- package/internal/record-utils.js +53 -0
- package/internal/recovery-tips.js +81 -0
- package/internal/rejected-cell-editor.js +328 -0
- package/internal/repl-convenience.js +201 -0
- package/internal/runtime-bridge-owner.js +417 -0
- package/internal/runtime-config.js +77 -0
- package/internal/runtime-contexts.js +31 -0
- package/internal/sdk-projection.js +88 -0
- package/internal/session-cell-executor.js +612 -0
- package/internal/session-journal.js +889 -0
- package/internal/session-log-view.js +80 -0
- package/internal/session-runtime.js +536 -0
- package/internal/session-state.js +45 -0
- package/internal/source-position-map.js +224 -0
- package/internal/tool-call-canonicalizer.js +339 -0
- package/internal/value-wire.js +421 -0
- package/internal/worker-client.js +218 -0
- package/internal/worker-cwd-virtualization.js +155 -0
- package/package.json +100 -0
- package/src/client.js +165 -0
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
import {
|
|
2
|
+
decodeValue,
|
|
3
|
+
encodeValue,
|
|
4
|
+
normalizeValueWire,
|
|
5
|
+
projectValueWire,
|
|
6
|
+
valueWiresEqual,
|
|
7
|
+
} from './value-wire.js'
|
|
8
|
+
import { diagnostic, renderDiagnostic } from './diagnostic.js'
|
|
9
|
+
import {
|
|
10
|
+
firstLine,
|
|
11
|
+
limitLogs,
|
|
12
|
+
markBindingFailure,
|
|
13
|
+
messageOf,
|
|
14
|
+
oneLineMessage,
|
|
15
|
+
safeProperty,
|
|
16
|
+
} from './failure-reporting.js'
|
|
17
|
+
import { assertStateName } from './session-journal.js'
|
|
18
|
+
import { PreflightError, prepareProgram } from './cell-analysis.js'
|
|
19
|
+
import { ModuleRewriteError } from './cell-rewriter.js'
|
|
20
|
+
import { mapSourcePosition } from './source-position-map.js'
|
|
21
|
+
import { durabilityState, transitionDurability } from './session-state.js'
|
|
22
|
+
import { LONG_FAILED_CELL_TIP_CODE_UNITS } from './recovery-tips.js'
|
|
23
|
+
|
|
24
|
+
const OUTPUT_LIMIT_MESSAGE = bytes => `output exceeded ${bytes} bytes; reduce the returned value or keep it in a REPL binding`
|
|
25
|
+
|
|
26
|
+
function earlyResult(kind, message) {
|
|
27
|
+
return { logs: [], error: { kind, message } }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function desiredDurability(kernel, replayRecord, prepared, config) {
|
|
31
|
+
if (replayRecord !== undefined) return 'durable'
|
|
32
|
+
if (!config.durableReplay || kernel.durability.status === 'volatile') return 'volatile'
|
|
33
|
+
return prepared.durability
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function hostCause(error) {
|
|
37
|
+
const candidate = safeProperty(error, 'diagnostic') ?? safeProperty(error, 'cause') ?? error
|
|
38
|
+
const candidateMessage = safeProperty(candidate, 'message')
|
|
39
|
+
const message = firstLine(candidateMessage, oneLineMessage(error))
|
|
40
|
+
const candidateCode = safeProperty(candidate, 'code')
|
|
41
|
+
const errorCode = safeProperty(error, 'code')
|
|
42
|
+
const code = firstLine(candidateCode, firstLine(errorCode, undefined))
|
|
43
|
+
return { ...(code === undefined ? {} : { code }), message }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isBindingReferenceError(error) {
|
|
47
|
+
return error.name === 'ReferenceError'
|
|
48
|
+
&& (/\bis not defined\b/.test(error.message)
|
|
49
|
+
|| /Cannot access ['"][^'"]+['"] before initialization/.test(error.message))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function parseDiagnostic(error, source) {
|
|
53
|
+
const cellPosition = error instanceof ModuleRewriteError ? error.cellPosition : undefined
|
|
54
|
+
const line = Number.isSafeInteger(cellPosition?.line)
|
|
55
|
+
? cellPosition.line
|
|
56
|
+
: Number.isSafeInteger(error?.loc?.line) ? error.loc.line - 1 : undefined
|
|
57
|
+
const column = Number.isSafeInteger(cellPosition?.column)
|
|
58
|
+
? cellPosition.column
|
|
59
|
+
: Number.isSafeInteger(error?.loc?.column) ? error.loc.column + 1 : undefined
|
|
60
|
+
return diagnostic({
|
|
61
|
+
code: 'PTC-C001',
|
|
62
|
+
severity: 'error',
|
|
63
|
+
phase: 'parse',
|
|
64
|
+
message: `cell could not be parsed: ${oneLineMessage(error)}`,
|
|
65
|
+
stateEffect: 'unchanged',
|
|
66
|
+
...(line !== undefined && line >= 1 && column !== undefined ? {
|
|
67
|
+
source: { cell: 'current', start: { line, column } },
|
|
68
|
+
} : {}),
|
|
69
|
+
help: ['repair the reported syntax and retry only this cell'],
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function preflightDiagnostic(error) {
|
|
74
|
+
return diagnostic({
|
|
75
|
+
code: 'PTC-C002',
|
|
76
|
+
severity: 'error',
|
|
77
|
+
phase: 'preflight',
|
|
78
|
+
message: oneLineMessage(error),
|
|
79
|
+
stateEffect: 'unchanged',
|
|
80
|
+
/* c8 ignore next */
|
|
81
|
+
...(error.span === undefined ? {} : {
|
|
82
|
+
source: {
|
|
83
|
+
cell: 'current',
|
|
84
|
+
start: { line: error.span.line, column: error.span.column },
|
|
85
|
+
/* c8 ignore next */
|
|
86
|
+
...(error.span.end === undefined ? {} : { end: error.span.end }),
|
|
87
|
+
},
|
|
88
|
+
}),
|
|
89
|
+
help: ['remove the kernel-control import and use the provided REPL or tools bindings'],
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function collisionDiagnostic(collisions) {
|
|
94
|
+
const names = [...new Set(collisions.map(item => item.name))]
|
|
95
|
+
const first = collisions[0]
|
|
96
|
+
return diagnostic({
|
|
97
|
+
code: 'PTC-N001',
|
|
98
|
+
severity: 'error',
|
|
99
|
+
phase: 'preflight',
|
|
100
|
+
message: `top-level bindings already exist: ${names.join(', ')}. This cell was not executed; the REPL state is unchanged.`,
|
|
101
|
+
stateEffect: 'unchanged',
|
|
102
|
+
source: { cell: 'current', start: first.start, end: first.end },
|
|
103
|
+
help: [
|
|
104
|
+
'reuse the existing bindings',
|
|
105
|
+
'place one-off declarations inside a block',
|
|
106
|
+
],
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function exceptionDiagnostic({
|
|
111
|
+
error,
|
|
112
|
+
cause,
|
|
113
|
+
position,
|
|
114
|
+
declared,
|
|
115
|
+
longCellFailure = false,
|
|
116
|
+
}) {
|
|
117
|
+
const message = firstLine(error.message, 'Unknown exception')
|
|
118
|
+
const rawName = typeof error.name === 'string' && error.name.length > 0
|
|
119
|
+
? error.name
|
|
120
|
+
/* c8 ignore next */
|
|
121
|
+
: typeof error.kind === 'string' && error.kind.length > 0 ? error.kind : 'Error'
|
|
122
|
+
const name = firstLine(rawName, 'Error')
|
|
123
|
+
const source = position === undefined || !Number.isSafeInteger(position.line) || position.line < 1
|
|
124
|
+
|| !Number.isSafeInteger(position.column) || position.column < 1
|
|
125
|
+
? undefined
|
|
126
|
+
: { cell: 'current', start: { line: position.line, column: position.column } }
|
|
127
|
+
return diagnostic({
|
|
128
|
+
code: 'PTC-X001',
|
|
129
|
+
severity: 'error',
|
|
130
|
+
phase: 'execute',
|
|
131
|
+
message: `uncaught ${name}: ${message}`,
|
|
132
|
+
stateEffect: 'partially-applied',
|
|
133
|
+
...(cause === undefined ? {} : { cause }),
|
|
134
|
+
...(source === undefined ? {} : { source }),
|
|
135
|
+
help: [
|
|
136
|
+
'inspect existing bindings and retry only the failing expression',
|
|
137
|
+
...(error.name === 'ToolCallError'
|
|
138
|
+
&& typeof error.toolName === 'string'
|
|
139
|
+
&& error.toolName.startsWith('cordis_')
|
|
140
|
+
? ['bindings assigned before this Cordis failure remain live; reuse them instead of resending large source']
|
|
141
|
+
: []),
|
|
142
|
+
...(longCellFailure
|
|
143
|
+
? ['execution may have occurred; inspect live state in a new short `run_code` cell before deciding whether a correction is safe']
|
|
144
|
+
: []),
|
|
145
|
+
...(declared.size === 0 || longCellFailure
|
|
146
|
+
? []
|
|
147
|
+
: ['use fresh names for one-off top-level bindings after partial execution; later declarations may be uninitialized']),
|
|
148
|
+
],
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function invalidOutputDiagnostic(detail) {
|
|
153
|
+
return diagnostic({
|
|
154
|
+
code: 'PTC-O001',
|
|
155
|
+
severity: 'error',
|
|
156
|
+
phase: 'execute',
|
|
157
|
+
message: `cell result could not cross the PTC Value V1 boundary: ${firstLine(detail, 'unknown output encoding failure')}`,
|
|
158
|
+
stateEffect: 'partially-applied',
|
|
159
|
+
help: [
|
|
160
|
+
'return a PTC Value V1 value or keep the live value in a REPL binding',
|
|
161
|
+
'reduce the returned graph when it exceeds the configured value budget',
|
|
162
|
+
],
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function stateArguments(value) {
|
|
167
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
168
|
+
throw new TypeError('repl.state expects an object')
|
|
169
|
+
}
|
|
170
|
+
const action = value.action
|
|
171
|
+
if (!['list', 'save', 'restore', 'delete'].includes(action)) {
|
|
172
|
+
throw new TypeError('repl.state action must be list, save, restore, or delete')
|
|
173
|
+
}
|
|
174
|
+
if (action === 'save' || action === 'delete' || (action === 'restore' && value.name !== undefined)) {
|
|
175
|
+
assertStateName(value.name)
|
|
176
|
+
}
|
|
177
|
+
return { action, ...(value.name === undefined ? {} : { name: value.name }) }
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Owns one cell's evaluator lifecycle and the worker-to-host binding bridge. */
|
|
181
|
+
export class SessionCellExecutor {
|
|
182
|
+
constructor(kernel) {
|
|
183
|
+
this.kernel = kernel
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async executeCell(request, replayRecord = undefined, config = this.kernel.config) {
|
|
187
|
+
const kernel = this.kernel
|
|
188
|
+
if (kernel.disposed) {
|
|
189
|
+
const result = earlyResult('abort', 'session kernel disposed')
|
|
190
|
+
kernel.completeJournal(request.journal, 'noop', result)
|
|
191
|
+
return result
|
|
192
|
+
}
|
|
193
|
+
if (request.signal?.aborted) {
|
|
194
|
+
const result = earlyResult('abort', String(request.signal.reason))
|
|
195
|
+
kernel.completeJournal(request.journal, 'noop', result)
|
|
196
|
+
return result
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
let prepared
|
|
200
|
+
const catalog = kernel.bindingCatalog.inputs()
|
|
201
|
+
let looseTopLevelRedeclarations
|
|
202
|
+
try {
|
|
203
|
+
looseTopLevelRedeclarations = replayRecord === undefined
|
|
204
|
+
? config.looseTopLevelRedeclarations
|
|
205
|
+
: replayRecord.bindingMode === 'loose'
|
|
206
|
+
prepared = prepareProgram(
|
|
207
|
+
request.program,
|
|
208
|
+
catalog.knownBindings,
|
|
209
|
+
looseTopLevelRedeclarations,
|
|
210
|
+
request.bindingDescriptors.reservedNames,
|
|
211
|
+
replayRecord === undefined ? {
|
|
212
|
+
autoRewriteImports: config.autoRewriteImports,
|
|
213
|
+
autoStripExports: config.autoStripExports,
|
|
214
|
+
autoSplitRedeclarations: config.autoSplitRedeclarations,
|
|
215
|
+
} : replayRecord.rewritePolicy,
|
|
216
|
+
catalog.importBindings,
|
|
217
|
+
catalog.importNamespaces,
|
|
218
|
+
)
|
|
219
|
+
} catch (error) {
|
|
220
|
+
const result = earlyResult('exception', messageOf(error))
|
|
221
|
+
const failure = error instanceof PreflightError ? preflightDiagnostic(error) : parseDiagnostic(error, request.program)
|
|
222
|
+
result.error.message = renderDiagnostic(failure, request.program)
|
|
223
|
+
kernel.completeJournal(request.journal, 'noop', result, undefined, [failure])
|
|
224
|
+
return result
|
|
225
|
+
}
|
|
226
|
+
if (prepared.collisions.length > 0) {
|
|
227
|
+
const result = {
|
|
228
|
+
logs: [],
|
|
229
|
+
error: markBindingFailure({ kind: 'exception', message: 'top-level binding collision' }),
|
|
230
|
+
}
|
|
231
|
+
const failure = collisionDiagnostic(prepared.collisions)
|
|
232
|
+
result.error.message = renderDiagnostic(failure, request.program)
|
|
233
|
+
kernel.completeJournal(request.journal, 'noop', result, undefined, [failure])
|
|
234
|
+
return result
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
let worker
|
|
238
|
+
try {
|
|
239
|
+
worker = await kernel.client.ensure(config.maxOldGenerationSizeMb)
|
|
240
|
+
} catch (error) {
|
|
241
|
+
const result = earlyResult('worker-exit', messageOf(error))
|
|
242
|
+
kernel.completeJournal(request.journal, 'discarded', result)
|
|
243
|
+
kernel.rollbackToDurable()
|
|
244
|
+
return result
|
|
245
|
+
}
|
|
246
|
+
if (request.signal?.aborted) {
|
|
247
|
+
const result = earlyResult('abort', String(request.signal.reason))
|
|
248
|
+
kernel.completeJournal(request.journal, 'discarded', result)
|
|
249
|
+
kernel.rollbackToDurable()
|
|
250
|
+
void kernel.client.reset(worker)
|
|
251
|
+
return result
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const journal = request.journal
|
|
255
|
+
const durability = desiredDurability(kernel, replayRecord, prepared, config)
|
|
256
|
+
const valueLimits = kernel.valueLimits(config)
|
|
257
|
+
const bindings = this.withControlBinding(request.bindingDescriptors, journal, replayRecord)
|
|
258
|
+
const id = ++kernel.sequence
|
|
259
|
+
return new Promise((resolve) => {
|
|
260
|
+
const started = worker.performance.eventLoopUtilization()
|
|
261
|
+
const active = {
|
|
262
|
+
id,
|
|
263
|
+
request: { ...request, bindings: bindings.namespaces },
|
|
264
|
+
finish: resolve,
|
|
265
|
+
computeTimer: undefined,
|
|
266
|
+
wallTimer: undefined,
|
|
267
|
+
onAbort: undefined,
|
|
268
|
+
journal,
|
|
269
|
+
replay: replayRecord,
|
|
270
|
+
replayIndex: 0,
|
|
271
|
+
replayNextSettle: 0,
|
|
272
|
+
replayPending: new Map(),
|
|
273
|
+
pendingBindings: new Map(),
|
|
274
|
+
settlementSequence: 0,
|
|
275
|
+
diagnostics: [],
|
|
276
|
+
appliedBindingCatalog: undefined,
|
|
277
|
+
completion: undefined,
|
|
278
|
+
config,
|
|
279
|
+
valueLimits,
|
|
280
|
+
durability: durabilityState({
|
|
281
|
+
status: durability,
|
|
282
|
+
reason: kernel.durability.status === 'volatile'
|
|
283
|
+
? kernel.durability.reason
|
|
284
|
+
: !config.durableReplay
|
|
285
|
+
? 'durable replay disabled by configuration'
|
|
286
|
+
: prepared.reason || undefined,
|
|
287
|
+
}),
|
|
288
|
+
control: { names: new Set(kernel.history.checkpoints.keys()) },
|
|
289
|
+
rewrites: prepared.rewrites,
|
|
290
|
+
prepared,
|
|
291
|
+
priorBindingCatalog: kernel.bindingCatalog,
|
|
292
|
+
worker,
|
|
293
|
+
}
|
|
294
|
+
active.resolve = (result, terminate = false) => kernel.settleCell(active, result, terminate)
|
|
295
|
+
active.onAbort = () => active.resolve(
|
|
296
|
+
earlyResult('abort', String(request.signal?.reason)),
|
|
297
|
+
true,
|
|
298
|
+
)
|
|
299
|
+
active.computeTimer = setInterval(() => {
|
|
300
|
+
if (worker.performance.eventLoopUtilization(started).active > config.computeMs) {
|
|
301
|
+
active.resolve(earlyResult('timeout', `compute budget exhausted (${config.computeMs}ms busy); split the work into smaller cells`), true)
|
|
302
|
+
}
|
|
303
|
+
}, Math.min(100, config.computeMs))
|
|
304
|
+
active.wallTimer = setTimeout(() => {
|
|
305
|
+
active.resolve(earlyResult('timeout', `wall-clock ceiling reached (${config.maxWallMs}ms); split long-running work into smaller cells`), true)
|
|
306
|
+
}, config.maxWallMs)
|
|
307
|
+
kernel.active = active
|
|
308
|
+
request.signal?.addEventListener('abort', active.onAbort, { once: true })
|
|
309
|
+
if (request.signal?.aborted) {
|
|
310
|
+
active.onAbort()
|
|
311
|
+
return
|
|
312
|
+
}
|
|
313
|
+
try {
|
|
314
|
+
kernel.client.post({
|
|
315
|
+
type: 'run', id, program: prepared.code, namespaces: bindings.workerDescriptors,
|
|
316
|
+
moduleLoads: prepared.moduleLoads,
|
|
317
|
+
returnSignal: prepared.returnSignal,
|
|
318
|
+
maxOutputBytes: config.maxOutputBytes,
|
|
319
|
+
valueLimits,
|
|
320
|
+
durability,
|
|
321
|
+
})
|
|
322
|
+
} catch (error) {
|
|
323
|
+
active.resolve(earlyResult('worker-exit', messageOf(error)), true)
|
|
324
|
+
}
|
|
325
|
+
})
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
withControlBinding(bindingDescriptors, journal, replayRecord) {
|
|
329
|
+
if (replayRecord === undefined && journal === undefined) return bindingDescriptors
|
|
330
|
+
const control = async args => {
|
|
331
|
+
const parsed = stateArguments(args)
|
|
332
|
+
if (replayRecord !== undefined) return { action: parsed.action, ...(parsed.name === undefined ? {} : { name: parsed.name }) }
|
|
333
|
+
return this.controlState(parsed)
|
|
334
|
+
}
|
|
335
|
+
const namespace = Object.freeze({
|
|
336
|
+
global: 'repl',
|
|
337
|
+
functions: Object.freeze({ state: control }),
|
|
338
|
+
members: Object.freeze(['state']),
|
|
339
|
+
})
|
|
340
|
+
return Object.freeze({
|
|
341
|
+
namespaces: Object.freeze([...bindingDescriptors.namespaces, namespace]),
|
|
342
|
+
reservedNames: bindingDescriptors.reservedNames,
|
|
343
|
+
workerDescriptors: Object.freeze([
|
|
344
|
+
...bindingDescriptors.workerDescriptors,
|
|
345
|
+
Object.freeze({ global: 'repl', members: namespace.members }),
|
|
346
|
+
]),
|
|
347
|
+
})
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
controlState(parsed) {
|
|
351
|
+
const kernel = this.kernel
|
|
352
|
+
const active = kernel.active
|
|
353
|
+
if (active?.control === undefined || active.journal === undefined) {
|
|
354
|
+
throw new Error('REPL state control is unavailable outside a cell')
|
|
355
|
+
}
|
|
356
|
+
const { action, name } = parsed
|
|
357
|
+
if (action === 'list') {
|
|
358
|
+
return {
|
|
359
|
+
names: [...active.control.names].sort(),
|
|
360
|
+
mode: active.durability.status,
|
|
361
|
+
...(active.durability.reason === undefined ? {} : { volatileReason: active.durability.reason }),
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
if (action === 'save') {
|
|
365
|
+
if (active.durability.status === 'volatile') {
|
|
366
|
+
throw new Error('cannot save a durable REPL state from a volatile segment; restore a durable state first')
|
|
367
|
+
}
|
|
368
|
+
active.control.names.add(name)
|
|
369
|
+
active.journal.operations.push({ action, name })
|
|
370
|
+
return { action, name, saved: true }
|
|
371
|
+
}
|
|
372
|
+
if (action === 'delete') {
|
|
373
|
+
active.control.names.delete(name)
|
|
374
|
+
active.journal.operations.push({ action, name })
|
|
375
|
+
return { action, name, deleted: true }
|
|
376
|
+
}
|
|
377
|
+
if (name !== undefined && !active.control.names.has(name)) throw new Error(`REPL state "${name}" does not exist`)
|
|
378
|
+
active.journal.operations.push({ action, ...(name === undefined ? {} : { name }) })
|
|
379
|
+
return { action, ...(name === undefined ? {} : { name }), restored: true }
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
onMessage(message) {
|
|
383
|
+
if (message === null || typeof message !== 'object') return
|
|
384
|
+
const handler = {
|
|
385
|
+
volatile: this.handleVolatile,
|
|
386
|
+
call: this.handleCall,
|
|
387
|
+
'output-limit': this.handleOutputLimit,
|
|
388
|
+
done: this.handleDone,
|
|
389
|
+
}[message.type]
|
|
390
|
+
handler?.call(this, message)
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
handleVolatile(message) {
|
|
394
|
+
const active = this.kernel.active
|
|
395
|
+
if (active?.id !== message.id) return
|
|
396
|
+
active.durability = transitionDurability(active.durability, {
|
|
397
|
+
type: 'volatile',
|
|
398
|
+
reason: typeof message.reason === 'string' ? message.reason : undefined,
|
|
399
|
+
})
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
handleCall(message) {
|
|
403
|
+
void this.invokeBinding(message)
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
handleOutputLimit(message) {
|
|
407
|
+
const kernel = this.kernel
|
|
408
|
+
const active = kernel.active
|
|
409
|
+
if (active?.id !== message.id) return
|
|
410
|
+
/* c8 ignore next */
|
|
411
|
+
const logs = Array.isArray(message.logs) && message.logs.every(log => typeof log === 'string') ? message.logs : []
|
|
412
|
+
active.resolve({
|
|
413
|
+
logs: limitLogs(logs),
|
|
414
|
+
error: { kind: 'output-limit', message: OUTPUT_LIMIT_MESSAGE(active.config.maxOutputBytes) },
|
|
415
|
+
}, true)
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
handleDone(message) {
|
|
419
|
+
const kernel = this.kernel
|
|
420
|
+
if (kernel.active?.id !== message.id) return
|
|
421
|
+
const active = kernel.active
|
|
422
|
+
const logs = Array.isArray(message.logs) && message.logs.every(log => typeof log === 'string') ? message.logs : []
|
|
423
|
+
if (!['durable', 'volatile'].includes(message.durability)) {
|
|
424
|
+
active.resolve({ logs, error: { kind: 'worker-exit', message: 'kernel returned an invalid durability state' } }, true)
|
|
425
|
+
return
|
|
426
|
+
}
|
|
427
|
+
if (message.durability === 'volatile') {
|
|
428
|
+
active.durability = transitionDurability(active.durability, {
|
|
429
|
+
type: 'volatile',
|
|
430
|
+
reason: typeof message.volatileReason === 'string' ? message.volatileReason : undefined,
|
|
431
|
+
})
|
|
432
|
+
}
|
|
433
|
+
if (active.replay !== undefined && active.durability.status !== 'durable') {
|
|
434
|
+
active.resolve({ logs, error: { kind: 'recovery', message: 'durable history requested a volatile capability during replay' } }, true)
|
|
435
|
+
return
|
|
436
|
+
}
|
|
437
|
+
if (active.replay !== undefined
|
|
438
|
+
&& (active.replayIndex !== active.replay.calls.length || active.replayPending.size !== 0)) {
|
|
439
|
+
active.resolve({ logs, error: { kind: 'recovery', message: 'session log replay consumed a different host-call transcript' } }, true)
|
|
440
|
+
return
|
|
441
|
+
}
|
|
442
|
+
const bytes = Buffer.byteLength(JSON.stringify({ logs, value: message.value }), 'utf8')
|
|
443
|
+
if (bytes > active.config.maxOutputBytes) {
|
|
444
|
+
active.resolve({
|
|
445
|
+
logs: limitLogs(logs),
|
|
446
|
+
error: { kind: 'output-limit', message: OUTPUT_LIMIT_MESSAGE(active.config.maxOutputBytes) },
|
|
447
|
+
}, true)
|
|
448
|
+
return
|
|
449
|
+
}
|
|
450
|
+
if (typeof message.error === 'string') {
|
|
451
|
+
const rawError = {
|
|
452
|
+
kind: 'exception',
|
|
453
|
+
name: typeof message.errorName === 'string' ? message.errorName : 'Error',
|
|
454
|
+
message: message.error,
|
|
455
|
+
...(typeof message.toolName === 'string' ? { toolName: message.toolName } : {}),
|
|
456
|
+
}
|
|
457
|
+
const actualFailure = exceptionDiagnostic({
|
|
458
|
+
error: rawError,
|
|
459
|
+
cause: message.cause,
|
|
460
|
+
position: message.moduleLoadFailed === true
|
|
461
|
+
? message.position
|
|
462
|
+
: mapSourcePosition(
|
|
463
|
+
message.position,
|
|
464
|
+
active.prepared.code,
|
|
465
|
+
active.request.program,
|
|
466
|
+
active.prepared.sourceMap,
|
|
467
|
+
),
|
|
468
|
+
declared: message.moduleLoadFailed === true ? new Set() : active.prepared.declared,
|
|
469
|
+
longCellFailure: active.request.program.length >= LONG_FAILED_CELL_TIP_CODE_UNITS,
|
|
470
|
+
})
|
|
471
|
+
active.appliedBindingCatalog = message.moduleLoadFailed === true
|
|
472
|
+
? active.priorBindingCatalog
|
|
473
|
+
: active.priorBindingCatalog.advance(active.prepared)
|
|
474
|
+
const recordedFailure = active.replay?.diagnostics?.find(item => item.code === 'PTC-X001')
|
|
475
|
+
const failure = recordedFailure?.message === actualFailure.message ? recordedFailure : actualFailure
|
|
476
|
+
const error = {
|
|
477
|
+
kind: 'exception',
|
|
478
|
+
message: renderDiagnostic(failure, active.request.program),
|
|
479
|
+
}
|
|
480
|
+
if (isBindingReferenceError(rawError)) markBindingFailure(error)
|
|
481
|
+
active.diagnostics.push(failure)
|
|
482
|
+
active.resolve({ logs, error })
|
|
483
|
+
return
|
|
484
|
+
}
|
|
485
|
+
if (typeof message.invalidOutput === 'string') {
|
|
486
|
+
const failure = invalidOutputDiagnostic(message.invalidOutput)
|
|
487
|
+
const error = { kind: 'invalid-output', message: renderDiagnostic(failure, active.request.program) }
|
|
488
|
+
active.diagnostics.push(failure)
|
|
489
|
+
active.resolve({ logs, error })
|
|
490
|
+
return
|
|
491
|
+
}
|
|
492
|
+
try {
|
|
493
|
+
if (typeof message.hasValue !== 'boolean'
|
|
494
|
+
|| (message.hasValue ? message.value === undefined : message.value !== undefined)) {
|
|
495
|
+
throw new TypeError('invalid PTC completion envelope')
|
|
496
|
+
}
|
|
497
|
+
const value = message.hasValue ? normalizeValueWire(message.value, active.valueLimits) : undefined
|
|
498
|
+
active.completion = {
|
|
499
|
+
hasValue: message.hasValue,
|
|
500
|
+
...(message.hasValue ? { value } : {}),
|
|
501
|
+
}
|
|
502
|
+
active.appliedBindingCatalog = active.priorBindingCatalog.advance(active.prepared)
|
|
503
|
+
if (active.replay?.completion?.kind === 'return'
|
|
504
|
+
&& (active.replay.completion.hasValue !== message.hasValue
|
|
505
|
+
|| (message.hasValue && !valueWiresEqual(active.replay.completion.value, value, active.valueLimits)))) {
|
|
506
|
+
active.resolve({ logs, error: { kind: 'recovery', message: 'cell replay produced a different completion value' } }, true)
|
|
507
|
+
return
|
|
508
|
+
}
|
|
509
|
+
active.resolve({
|
|
510
|
+
logs,
|
|
511
|
+
...(message.hasValue ? { value: projectValueWire(value, active.valueLimits) } : {}),
|
|
512
|
+
})
|
|
513
|
+
} catch (error) {
|
|
514
|
+
const failure = invalidOutputDiagnostic(messageOf(error))
|
|
515
|
+
const invalid = { kind: 'invalid-output', message: renderDiagnostic(failure, active.request.program) }
|
|
516
|
+
active.diagnostics.push(failure)
|
|
517
|
+
active.resolve({ logs, error: invalid })
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
async invokeBinding(message) {
|
|
522
|
+
const kernel = this.kernel
|
|
523
|
+
const active = kernel.active
|
|
524
|
+
if (active?.worker !== kernel.client.worker || active?.id !== message.runId) {
|
|
525
|
+
kernel.client.postIfAlive({ type: 'reply', runId: message.runId, id: message.id, ok: false, error: 'PTC execution lease expired' })
|
|
526
|
+
return
|
|
527
|
+
}
|
|
528
|
+
const namespace = active.request.bindings.find(binding => binding.global === message.global)
|
|
529
|
+
const binding = namespace?.functions?.[message.member]
|
|
530
|
+
if (typeof binding !== 'function') {
|
|
531
|
+
kernel.client.post({ type: 'reply', runId: message.runId, id: message.id, ok: false, error: `unknown binding ${message.global}.${message.member}` })
|
|
532
|
+
return
|
|
533
|
+
}
|
|
534
|
+
let argsWire
|
|
535
|
+
let args
|
|
536
|
+
try {
|
|
537
|
+
argsWire = normalizeValueWire(message.args, active.valueLimits)
|
|
538
|
+
args = decodeValue(argsWire, active.valueLimits)
|
|
539
|
+
} catch (error) {
|
|
540
|
+
kernel.client.post({ type: 'reply', runId: message.runId, id: message.id, ok: false, error: messageOf(error) })
|
|
541
|
+
return
|
|
542
|
+
}
|
|
543
|
+
const recorded = active.replay?.calls?.[active.replayIndex]
|
|
544
|
+
if (active.replay !== undefined) {
|
|
545
|
+
active.replayIndex += 1
|
|
546
|
+
if (recorded === undefined || recorded.global !== message.global || recorded.member !== message.member
|
|
547
|
+
|| !valueWiresEqual(recorded.args, argsWire, active.valueLimits)) {
|
|
548
|
+
kernel.client.post({ type: 'reply', runId: message.runId, id: message.id, ok: false, error: 'session log replay diverged at a host binding call' })
|
|
549
|
+
return
|
|
550
|
+
}
|
|
551
|
+
const pending = { message, recorded }
|
|
552
|
+
active.replayPending.set(recorded.settle, pending)
|
|
553
|
+
this.flushReplayReplies(active)
|
|
554
|
+
return
|
|
555
|
+
}
|
|
556
|
+
/* c8 ignore next */
|
|
557
|
+
const call = active.journal === undefined
|
|
558
|
+
? undefined
|
|
559
|
+
: { global: message.global, member: message.member, args: argsWire }
|
|
560
|
+
if (call !== undefined) active.journal.calls.push(call)
|
|
561
|
+
active.pendingBindings.set(message.id, `${message.global}.${message.member}`)
|
|
562
|
+
try {
|
|
563
|
+
// Restore the initiator boundary lost across the worker callback.
|
|
564
|
+
const agent = active.request.executionToken?.agent
|
|
565
|
+
const invoke = () => binding(args)
|
|
566
|
+
const value = kernel.withInitiator === undefined || agent === undefined
|
|
567
|
+
? await invoke()
|
|
568
|
+
: await kernel.withInitiator(agent, invoke)
|
|
569
|
+
const valueWire = encodeValue(value, active.valueLimits)
|
|
570
|
+
if (call !== undefined) {
|
|
571
|
+
call.ok = true
|
|
572
|
+
call.value = valueWire
|
|
573
|
+
call.settle = active.settlementSequence++
|
|
574
|
+
}
|
|
575
|
+
if (active.worker === kernel.client.worker) {
|
|
576
|
+
kernel.client.post({ type: 'reply', runId: message.runId, id: message.id, ok: true, value: valueWire })
|
|
577
|
+
}
|
|
578
|
+
} catch (error) {
|
|
579
|
+
const cause = hostCause(error)
|
|
580
|
+
if (call !== undefined) {
|
|
581
|
+
call.ok = false
|
|
582
|
+
call.error = messageOf(error)
|
|
583
|
+
call.settle = active.settlementSequence++
|
|
584
|
+
}
|
|
585
|
+
if (active.worker === kernel.client.worker) {
|
|
586
|
+
kernel.client.post({
|
|
587
|
+
type: 'reply', runId: message.runId, id: message.id, ok: false,
|
|
588
|
+
error: messageOf(error), cause,
|
|
589
|
+
})
|
|
590
|
+
}
|
|
591
|
+
} finally {
|
|
592
|
+
active.pendingBindings.delete(message.id)
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
flushReplayReplies(active) {
|
|
597
|
+
const kernel = this.kernel
|
|
598
|
+
while (active.worker === kernel.client.worker) {
|
|
599
|
+
const pending = active.replayPending.get(active.replayNextSettle)
|
|
600
|
+
if (pending === undefined) return
|
|
601
|
+
if (pending.waiting === true && pending.response === undefined) return
|
|
602
|
+
active.replayPending.delete(active.replayNextSettle)
|
|
603
|
+
active.replayNextSettle += 1
|
|
604
|
+
const { message, recorded } = pending
|
|
605
|
+
const response = pending.response
|
|
606
|
+
const selected = response ?? recorded
|
|
607
|
+
kernel.client.post(selected.ok
|
|
608
|
+
? { type: 'reply', runId: message.runId, id: message.id, ok: true, value: selected.value }
|
|
609
|
+
: { type: 'reply', runId: message.runId, id: message.id, ok: false, error: selected.error })
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|