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.
Files changed (77) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +137 -0
  3. package/README.zh.md +137 -0
  4. package/assets/dsh-ptc-plus-banner.webp +0 -0
  5. package/assets/ptc-plus-repair.png +0 -0
  6. package/assets/ptc-plus-settings.png +0 -0
  7. package/client.js +366 -0
  8. package/cordis.patch.yml +5 -0
  9. package/docs/README.md +21 -0
  10. package/docs/adr/0001-delegate-governance-to-dsh.md +3 -0
  11. package/docs/adr/0002-limit-work-map-scope.md +3 -0
  12. package/docs/adr/0003-prefer-native-program-surfaces.md +3 -0
  13. package/docs/adr/0004-declare-program-bindings-at-the-host.md +5 -0
  14. package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +129 -0
  15. package/docs/adr/0006-rewrite-module-syntax-with-an-ast.md +45 -0
  16. package/docs/adr/0007-separate-worker-transport-from-session-semantics.md +39 -0
  17. package/docs/adr/0008-define-agent-operating-contract.md +25 -0
  18. package/docs/adr/0009-bound-expensive-acceptance-default.md +39 -0
  19. package/docs/adr/0010-session-log-derived-recovery-tips.md +26 -0
  20. package/docs/adr/0011-isolate-the-local-recovery-tip-provider.md +20 -0
  21. package/docs/adr/0012-preserve-mixed-destructuring-initialization.md +23 -0
  22. package/docs/adr/0013-isolate-repl-redeclaration-convenience.md +21 -0
  23. package/docs/adr/0014-persist-cell-rewrite-policy.md +25 -0
  24. package/docs/adr/0015-preserve-source-positions-through-rewrites.md +25 -0
  25. package/docs/adr/0016-archive-resolved-review-findings.md +43 -0
  26. package/docs/adr/0017-track-the-latest-dsh-public-surface.md +23 -0
  27. package/docs/adr/0018-use-versioned-fixture-for-ab-benchmark.md +25 -0
  28. package/docs/adr/0019-plugin-settings-and-kill-switch.md +32 -0
  29. package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +36 -0
  30. package/docs/adr/0021-separate-host-bootstrap-from-session-runtime.md +68 -0
  31. package/docs/adr/README.md +18 -0
  32. package/docs/architecture.md +134 -0
  33. package/docs/capability-projection.md +48 -0
  34. package/docs/client-ui.md +47 -0
  35. package/docs/durability-design.md +265 -0
  36. package/docs/evaluation.md +62 -0
  37. package/docs/installation.md +82 -0
  38. package/docs/program-data-plane.md +61 -0
  39. package/docs/publishing.md +37 -0
  40. package/docs/runtime-reference.md +119 -0
  41. package/docs/value-wire.md +61 -0
  42. package/index.js +532 -0
  43. package/internal/ast-traversal.js +20 -0
  44. package/internal/binding-descriptors.js +95 -0
  45. package/internal/binding-pattern.js +54 -0
  46. package/internal/builtin-esm-sync.js +9 -0
  47. package/internal/cell-analysis.js +467 -0
  48. package/internal/cell-rewriter.js +520 -0
  49. package/internal/config-spec.js +179 -0
  50. package/internal/cordis-tools-owner.js +363 -0
  51. package/internal/diagnostic.js +133 -0
  52. package/internal/direct-surface-owner.js +326 -0
  53. package/internal/edit-transport-owner.js +287 -0
  54. package/internal/failure-reporting.js +139 -0
  55. package/internal/kernel-worker.js +484 -0
  56. package/internal/module-policy.js +46 -0
  57. package/internal/program-bindings.js +187 -0
  58. package/internal/record-utils.js +53 -0
  59. package/internal/recovery-tips.js +81 -0
  60. package/internal/rejected-cell-editor.js +328 -0
  61. package/internal/repl-convenience.js +201 -0
  62. package/internal/runtime-bridge-owner.js +417 -0
  63. package/internal/runtime-config.js +77 -0
  64. package/internal/runtime-contexts.js +31 -0
  65. package/internal/sdk-projection.js +88 -0
  66. package/internal/session-cell-executor.js +612 -0
  67. package/internal/session-journal.js +889 -0
  68. package/internal/session-log-view.js +80 -0
  69. package/internal/session-runtime.js +536 -0
  70. package/internal/session-state.js +45 -0
  71. package/internal/source-position-map.js +224 -0
  72. package/internal/tool-call-canonicalizer.js +339 -0
  73. package/internal/value-wire.js +421 -0
  74. package/internal/worker-client.js +218 -0
  75. package/internal/worker-cwd-virtualization.js +155 -0
  76. package/package.json +100 -0
  77. 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
+ }