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,80 @@
1
+ import {
2
+ foldSessionTimeline,
3
+ } from './session-journal.js'
4
+
5
+ const SYSTEM_PROMPT_PLUGIN = '@deepseek-ai/dsh-system-prompt'
6
+
7
+ function systemPromptSnapshot(event, index, contextStep) {
8
+ const source = event?.data?.source
9
+ if (event?.type !== 'user/message' || source?.kind !== 'plugin'
10
+ || source.plugin !== SYSTEM_PROMPT_PLUGIN || source.form !== 'snapshot'
11
+ || !Array.isArray(source.sections)) return undefined
12
+ const names = new Set()
13
+ const sections = []
14
+ for (const section of source.sections) {
15
+ if (section === null || typeof section !== 'object' || Array.isArray(section)
16
+ || typeof section.name !== 'string' || section.name.length === 0
17
+ || typeof section.text !== 'string' || names.has(section.name)) return undefined
18
+ names.add(section.name)
19
+ sections.push(Object.freeze({ name: section.name, text: section.text }))
20
+ }
21
+ return Object.freeze({ index, contextStep, sections: Object.freeze(sections) })
22
+ }
23
+
24
+ function isContextStep(event) {
25
+ return event?.type === 'request/header'
26
+ || event?.type === 'assistant/message'
27
+ || (event?.type === 'user/message' && event.data?.source?.kind === 'user')
28
+ }
29
+
30
+ /** Project session events into immutable facts consumed by prompt presentation. */
31
+ export function projectSessionLog(agent, requestedEdit) {
32
+ const events = agent?.session?.events
33
+ if (!Array.isArray(events)) {
34
+ return Object.freeze({
35
+ openTurn: false,
36
+ contextStep: 0,
37
+ systemPromptSnapshots: Object.freeze([]),
38
+ lastSuccessfulRunIndex: undefined,
39
+ latestRun: undefined,
40
+ editableRun: undefined,
41
+ repairSource: undefined,
42
+ })
43
+ }
44
+ let contextStep = 0
45
+ const systemPromptSnapshots = []
46
+ for (let index = 0; index < events.length; index += 1) {
47
+ const event = events[index]
48
+ const snapshot = systemPromptSnapshot(event, index, contextStep)
49
+ if (snapshot !== undefined) systemPromptSnapshots.push(snapshot)
50
+ if (isContextStep(event)) contextStep += 1
51
+ }
52
+ const timeline = foldSessionTimeline(events)
53
+ const requestedEntry = requestedEdit === undefined
54
+ ? undefined
55
+ : timeline.executableCalls.get(requestedEdit.callSeq)
56
+ const requestedEditTarget = requestedEdit !== undefined
57
+ && requestedEntry?.event.data?.callId === requestedEdit.callId
58
+ && requestedEntry.event.data.name === 'edit_run_code'
59
+ ? timeline.editTargets.get(requestedEdit.callSeq)
60
+ : undefined
61
+ const latestRun = timeline.openTurn ? timeline.latestRun : undefined
62
+ const editableRun = timeline.openTurn ? timeline.editableRun : undefined
63
+ return Object.freeze({
64
+ openTurn: timeline.openTurn,
65
+ contextStep,
66
+ systemPromptSnapshots: Object.freeze(systemPromptSnapshots),
67
+ lastSuccessfulRunIndex: timeline.lastSuccessfulRunIndex,
68
+ latestRun,
69
+ editableRun,
70
+ repairSource: editableRun?.source,
71
+ ...(requestedEdit === undefined ? {} : { requestedEditTarget }),
72
+ })
73
+ }
74
+
75
+ /** Return the target snapshot captured at one persisted edit call event. */
76
+ export function editTargetForCall(agent, callId, callSeq) {
77
+ if (typeof callId !== 'string' || callId.length === 0
78
+ || !Number.isSafeInteger(callSeq) || callSeq < 0) return undefined
79
+ return projectSessionLog(agent, { callId, callSeq }).requestedEditTarget
80
+ }
@@ -0,0 +1,536 @@
1
+ import { diagnostic, renderDiagnostic } from './diagnostic.js'
2
+ import { createFailureTracker, messageOf } from './failure-reporting.js'
3
+ import {
4
+ createJournal,
5
+ liveToolCallSeq,
6
+ normalizeJournal,
7
+ pathToHead,
8
+ reduceStateOperations,
9
+ recoverJournal,
10
+ } from './session-journal.js'
11
+ import { normalizeBindingDescriptors } from './binding-descriptors.js'
12
+ import { resolveConfig } from './runtime-config.js'
13
+ import { WorkerClient } from './worker-client.js'
14
+ import { BindingCatalog, durabilityState, transitionDurability } from './session-state.js'
15
+ import { SessionCellExecutor } from './session-cell-executor.js'
16
+
17
+ const WORKER_URL = new URL('./kernel-worker.js', import.meta.url)
18
+ function recoveryDiagnostic(count) {
19
+ return diagnostic({
20
+ code: 'PTC-R002',
21
+ severity: 'warning',
22
+ phase: 'recover',
23
+ message: `Restored the durable head and skipped ${count} unreconstructable historical cell(s); their source remains in the session log.`,
24
+ stateEffect: 'rolled-back',
25
+ help: [
26
+ 'continue from the restored bindings',
27
+ 'do not reference values created only in the skipped suffix',
28
+ ],
29
+ })
30
+ }
31
+
32
+ function rewritePolicy(config) {
33
+ return {
34
+ autoRewriteImports: config.autoRewriteImports,
35
+ autoStripExports: config.autoStripExports,
36
+ autoSplitRedeclarations: config.autoSplitRedeclarations,
37
+ }
38
+ }
39
+
40
+ function resolvedRuntimeConfig(config) {
41
+ return Object.freeze(resolveConfig(config))
42
+ }
43
+
44
+ function emptyHistory() {
45
+ return { nodes: [], head: undefined, checkpoints: new Map(), volatileSuffix: [], available: true }
46
+ }
47
+
48
+ class ReplayFailure extends Error {
49
+ constructor(node, cause) {
50
+ super(messageOf(cause))
51
+ this.node = node
52
+ }
53
+ }
54
+
55
+ class ReplayCancelled extends Error {
56
+ constructor(result) {
57
+ super(result.error?.message ?? 'session replay cancelled')
58
+ this.result = result
59
+ }
60
+ }
61
+
62
+ class SessionKernel {
63
+ constructor({ config, history, cwd, session, withInitiator }) {
64
+ this.config = config
65
+ this.history = history
66
+ this.cwd = cwd
67
+ this.session = session
68
+ this.withInitiator = withInitiator
69
+ this.durability = durabilityState()
70
+ this.bindingCatalog = new BindingCatalog()
71
+ this.replayed = false
72
+ this.recoveryNotice = history.volatileSuffix.length === 0
73
+ ? undefined
74
+ : recoveryDiagnostic(history.volatileSuffix.length)
75
+ this.active = undefined
76
+ this.sequence = 0
77
+ this.tail = Promise.resolve()
78
+ this.tentatives = new WeakMap()
79
+ this.workerReservations = new Set()
80
+ this.cellExecutor = new SessionCellExecutor(this)
81
+ this.client = new WorkerClient({
82
+ workerUrl: WORKER_URL,
83
+ cwd,
84
+ onMessage: message => this.cellExecutor.onMessage(message),
85
+ onFailure: message => this.active?.resolve({
86
+ logs: [], error: { kind: 'worker-exit', message },
87
+ }, true),
88
+ })
89
+ this.failures = createFailureTracker()
90
+ this.disposed = false
91
+ }
92
+
93
+ valueLimits(config = this.config) {
94
+ return {
95
+ maxNodes: config.maxValueNodes,
96
+ maxEdges: config.maxValueEdges,
97
+ maxArrayLength: config.maxValueArrayLength,
98
+ maxBigIntDigits: config.maxValueBigIntDigits,
99
+ maxStringBytes: config.maxOutputBytes,
100
+ }
101
+ }
102
+
103
+ assertReconfigurationAllowed(config) {
104
+ if ((this.client.workerLimit !== undefined || this.workerReservations.size > 0)
105
+ && config.maxOldGenerationSizeMb !== this.config.maxOldGenerationSizeMb) {
106
+ throw new Error(
107
+ 'ptc-plus: maxOldGenerationSizeMb cannot change while a session worker is active; retry after the session is disposed',
108
+ )
109
+ }
110
+ }
111
+
112
+ reconfigure(config) {
113
+ this.assertReconfigurationAllowed(config)
114
+ this.config = config
115
+ }
116
+
117
+ reserveWorkerConfiguration(config) {
118
+ const reservation = Object.freeze({ maxOldGenerationSizeMb: config.maxOldGenerationSizeMb })
119
+ this.workerReservations.add(reservation)
120
+ return reservation
121
+ }
122
+
123
+ releaseWorkerConfiguration(reservation) {
124
+ this.workerReservations.delete(reservation)
125
+ }
126
+
127
+ run(request, config = this.config) {
128
+ const execute = () => this.execute(request, config)
129
+ const result = this.tail.then(execute, execute)
130
+ this.tail = result.then(() => undefined, () => undefined)
131
+ return result
132
+ }
133
+
134
+ async execute(request, config) {
135
+ const recoveryBoundaries = []
136
+ const finishResult = result => recoveryBoundaries.length === 0
137
+ ? result
138
+ : { ...result, recoveryBoundaries: recoveryBoundaries.map(boundary => ({ ...boundary })) }
139
+ if (!this.replayed && config.durableReplay) {
140
+ let skipped = 0
141
+ while (!this.replayed) {
142
+ try {
143
+ await this.replayHistory(request, config)
144
+ this.replayed = true
145
+ } catch (error) {
146
+ const worker = this.client.worker
147
+ if (worker !== undefined) await this.client.reset(worker)
148
+ if (error instanceof ReplayCancelled) {
149
+ this.completeJournal(request.journal, 'noop', error.result)
150
+ return finishResult(error.result)
151
+ }
152
+ if (!(error instanceof ReplayFailure)) {
153
+ const result = {
154
+ logs: [],
155
+ error: {
156
+ kind: 'recovery',
157
+ message: `cannot reconstruct REPL from session log: ${messageOf(error)}`,
158
+ },
159
+ }
160
+ this.completeJournal(request.journal, 'noop', result)
161
+ return finishResult(result)
162
+ }
163
+ const frontier = error.node.parent === undefined
164
+ ? undefined
165
+ : this.history.nodes[error.node.parent]
166
+ const previousPathLength = pathToHead(this.history).length
167
+ try {
168
+ const boundary = {
169
+ failedCallSeq: error.node.callSeq,
170
+ frontierCallSeq: frontier?.callSeq ?? null,
171
+ }
172
+ const recovered = recoverJournal(this.session, request.callSeq, {
173
+ extraBoundaries: [boundary],
174
+ })
175
+ recoveryBoundaries.push(boundary)
176
+ this.history = recovered
177
+ } catch (boundaryError) {
178
+ const result = {
179
+ logs: [],
180
+ error: {
181
+ kind: 'recovery',
182
+ message: `cannot apply REPL recovery boundary: ${messageOf(boundaryError)}`,
183
+ },
184
+ }
185
+ this.completeJournal(request.journal, 'noop', result)
186
+ return finishResult(result)
187
+ }
188
+ const nextPathLength = pathToHead(this.history).length
189
+ skipped += Math.max(1, previousPathLength - nextPathLength)
190
+ this.durability = durabilityState()
191
+ }
192
+ }
193
+ if (skipped > 0) this.recoveryNotice = recoveryDiagnostic(skipped)
194
+ }
195
+ const leadingDiagnostics = this.recoveryNotice === undefined ? [] : [this.recoveryNotice]
196
+ this.recoveryNotice = undefined
197
+ if (request.journal !== undefined) request.journal.diagnostics.push(...leadingDiagnostics)
198
+ const result = await this.cellExecutor.executeCell(request, undefined, config)
199
+ if (leadingDiagnostics.length > 0) {
200
+ const rendered = leadingDiagnostics.map(item => renderDiagnostic(item, request.program))
201
+ result.logs = [...rendered, ...result.logs]
202
+ }
203
+ if (result.error === undefined) {
204
+ this.failures.reset()
205
+ } else {
206
+ const hint = this.failures.hint(result.error)
207
+ if (hint !== undefined) {
208
+ result.logs = [...result.logs, renderDiagnostic(hint, request.program)]
209
+ if (request.journal !== undefined) request.journal.diagnostics.push(hint)
210
+ }
211
+ }
212
+ return finishResult(result)
213
+ }
214
+
215
+ async replayHistory(request, config) {
216
+ this.bindingCatalog = new BindingCatalog()
217
+ const path = pathToHead(this.history)
218
+ for (const node of path) {
219
+ try {
220
+ const result = await this.cellExecutor.executeCell(
221
+ { ...request, program: node.code, journal: undefined },
222
+ node.journal,
223
+ config,
224
+ )
225
+ const completion = node.journal.completion
226
+ if (result.error !== undefined && !['exception', 'invalid-output'].includes(result.error.kind)) {
227
+ if (result.error.kind === 'abort') throw new ReplayCancelled(result)
228
+ throw new Error(`cell replay infrastructure failed (${result.error.kind}): ${result.error.message}`)
229
+ }
230
+ if (completion.kind === 'return' && result.error !== undefined) {
231
+ throw new Error(`cell replay failed: ${result.error.message}`)
232
+ }
233
+ if (completion.kind === 'throw') {
234
+ if (result.error === undefined) throw new Error('cell replay succeeded where the recorded cell failed')
235
+ if (result.error.kind !== completion.error.kind || result.error.message !== completion.error.message) {
236
+ throw new Error('cell replay produced a different semantic failure')
237
+ }
238
+ }
239
+ } catch (error) {
240
+ if (error instanceof ReplayCancelled) throw error
241
+ throw new ReplayFailure(node, error)
242
+ }
243
+ }
244
+ }
245
+
246
+ completeJournal(journal, status, result, volatileReason, diagnostics = [], completion = undefined) {
247
+ if (journal === undefined) return
248
+ journal.status = status
249
+ journal.completion = result.error === undefined
250
+ ? {
251
+ kind: 'return',
252
+ hasValue: completion?.hasValue === true,
253
+ ...(completion?.hasValue === true ? { value: completion.value } : {}),
254
+ }
255
+ : { kind: 'throw', error: { kind: result.error.kind, message: result.error.message } }
256
+ if (volatileReason !== undefined) journal.volatileReason = volatileReason
257
+ if (diagnostics.length > 0) journal.diagnostics.push(...diagnostics)
258
+ if (status === 'volatile') {
259
+ journal.operations = journal.operations.filter(operation => operation.action !== 'save')
260
+ }
261
+ if (status === 'discarded' || status === 'noop') {
262
+ journal.calls.length = 0
263
+ journal.operations.length = 0
264
+ }
265
+ }
266
+
267
+ rollbackToDurable() {
268
+ this.durability = durabilityState()
269
+ this.replayed = false
270
+ this.bindingCatalog = new BindingCatalog()
271
+ }
272
+
273
+ settleCell(active, result, terminate = false) {
274
+ /* c8 ignore next */
275
+ if (this.active !== active) return
276
+ const { request, journal, replay, prepared, worker } = active
277
+ clearInterval(active.computeTimer)
278
+ clearTimeout(active.wallTimer)
279
+ request.signal?.removeEventListener('abort', active.onAbort)
280
+ if (active.rewrites !== undefined && active.rewrites.length > 0) {
281
+ result = { ...result, rewrites: active.rewrites }
282
+ }
283
+ if (journal !== undefined && replay === undefined) {
284
+ if (terminate) {
285
+ const volatileReason = active.pendingBindings.values().next().value ?? active.durability.reason
286
+ this.completeJournal(journal, 'discarded', result, volatileReason, active.diagnostics)
287
+ this.rollbackToDurable()
288
+ if (volatileReason !== undefined) {
289
+ this.durability = transitionDurability(this.durability, {
290
+ type: 'volatile',
291
+ reason: volatileReason,
292
+ })
293
+ }
294
+ } else {
295
+ const status = active.durability.status
296
+ this.completeJournal(
297
+ journal, status, result, active.durability.reason, active.diagnostics, active.completion,
298
+ )
299
+ this.tentatives.set(journal, {
300
+ callSeq: request.callSeq,
301
+ program: request.program,
302
+ bindingCatalog: active.appliedBindingCatalog
303
+ ?? active.priorBindingCatalog.advance(prepared),
304
+ worker,
305
+ })
306
+ }
307
+ }
308
+ if (replay !== undefined && !terminate) {
309
+ this.bindingCatalog = active.appliedBindingCatalog
310
+ ?? active.priorBindingCatalog.advance(prepared)
311
+ }
312
+ this.active = undefined
313
+ if (terminate) void this.client.reset(worker)
314
+ active.finish(result)
315
+ }
316
+
317
+ finalizeJournal(journal, confirmed) {
318
+ const tentative = this.tentatives.get(journal)
319
+ if (tentative === undefined) return
320
+ this.tentatives.delete(journal)
321
+ if (!confirmed) {
322
+ if (journal.status === 'durable' || journal.status === 'volatile') {
323
+ const reason = journal.volatileReason ?? 'run_code journal was not preserved in the final tool result'
324
+ this.durability = transitionDurability(this.durability, {
325
+ type: 'volatile',
326
+ reason,
327
+ })
328
+ this.bindingCatalog = tentative.bindingCatalog
329
+ }
330
+ return
331
+ }
332
+ if (journal.status === 'durable') {
333
+ const normalized = normalizeJournal(journal)
334
+ const node = Object.freeze({
335
+ code: tentative.program,
336
+ journal: normalized,
337
+ ...(tentative.callSeq === undefined ? {} : { callSeq: tentative.callSeq }),
338
+ parent: this.history.head,
339
+ })
340
+ const index = this.history.nodes.push(node) - 1
341
+ this.history.head = index
342
+ this.bindingCatalog = tentative.bindingCatalog
343
+ this.finishStateOperations(journal.operations, index, tentative.worker)
344
+ return
345
+ }
346
+ if (journal.status === 'volatile') {
347
+ this.durability = transitionDurability(this.durability, {
348
+ type: 'volatile',
349
+ reason: journal.volatileReason,
350
+ })
351
+ this.bindingCatalog = tentative.bindingCatalog
352
+ this.finishStateOperations(journal.operations, undefined, tentative.worker)
353
+ }
354
+ }
355
+
356
+ finishStateOperations(operations, index, worker) {
357
+ const transition = reduceStateOperations(this.history, operations, index)
358
+ this.history.head = transition.head
359
+ this.history.checkpoints = transition.checkpoints
360
+ if (transition.restored) {
361
+ this.rollbackToDurable()
362
+ void this.client.reset(worker)
363
+ }
364
+ }
365
+
366
+ async dispose() {
367
+ this.disposed = true
368
+ const worker = this.client.worker
369
+ if (worker !== undefined) {
370
+ /* c8 ignore next */
371
+ this.active?.resolve({ logs: [], error: { kind: 'abort', message: 'session kernel disposed' } }, true)
372
+ }
373
+ await this.client.dispose()
374
+ await this.tail
375
+ }
376
+ }
377
+
378
+ function sessionOf(sessionContext) {
379
+ if (typeof sessionContext !== 'object' || sessionContext === null) {
380
+ return {
381
+ id: String(sessionContext),
382
+ session: undefined,
383
+ callId: undefined,
384
+ persistedCallSeq: undefined,
385
+ cwd: undefined,
386
+ }
387
+ }
388
+ const session = sessionContext.session
389
+ return {
390
+ id: String(sessionContext.id),
391
+ session,
392
+ callId: sessionContext.callId,
393
+ persistedCallSeq: sessionContext.persistedCallSeq,
394
+ cwd: typeof session?.header?.cwd === 'string' ? session.header.cwd : undefined,
395
+ }
396
+ }
397
+
398
+ export class SessionRuntime {
399
+ constructor(config = {}, options = {}) {
400
+ this.config = resolvedRuntimeConfig(config)
401
+ this.kernels = new Map()
402
+ this.pendingNoops = new Map()
403
+ this.settlements = new WeakSet()
404
+ this.disposed = false
405
+ this.withInitiator = typeof options.withInitiator === 'function' ? options.withInitiator : undefined
406
+ }
407
+
408
+ async run(sessionContext, request) {
409
+ const execution = await this.runTentative(sessionContext, request)
410
+ if (execution.settlement !== undefined) this.finalize(execution.settlement, true)
411
+ return execution.result
412
+ }
413
+
414
+ reconfigure(config) {
415
+ const resolved = resolvedRuntimeConfig(config)
416
+ const kernels = [...this.kernels.values()]
417
+ for (const kernel of kernels) kernel.assertReconfigurationAllowed(resolved)
418
+ for (const kernel of kernels) kernel.reconfigure(resolved)
419
+ this.config = resolved
420
+ }
421
+
422
+ async runTentative(sessionContext, request) {
423
+ const completed = result => Object.freeze({ result, settlement: undefined })
424
+ if (this.disposed) return completed({ logs: [], error: { kind: 'abort', message: 'PTC runtime disposed' } })
425
+ const cellConfig = this.config
426
+ let bindingDescriptors
427
+ try {
428
+ bindingDescriptors = normalizeBindingDescriptors(request?.bindings)
429
+ } catch (error) {
430
+ return completed({ logs: [], error: { kind: 'exception', message: messageOf(error) } })
431
+ }
432
+ request = { ...request, bindings: bindingDescriptors.namespaces, bindingDescriptors }
433
+ const { id: sessionId, session, callId, persistedCallSeq, cwd } = sessionOf(sessionContext)
434
+ let callSeq
435
+ try {
436
+ if (persistedCallSeq !== undefined
437
+ && (!Number.isSafeInteger(persistedCallSeq) || persistedCallSeq < 0)) {
438
+ throw new Error('persisted tool call sequence must be a non-negative safe integer')
439
+ }
440
+ callSeq = cellConfig.durableReplay
441
+ ? persistedCallSeq ?? liveToolCallSeq(session, callId, 'run_code')
442
+ : undefined
443
+ } catch (error) {
444
+ return completed({ logs: [], error: { kind: 'recovery', message: `cannot identify current run_code call in session log: ${messageOf(error)}` } })
445
+ }
446
+ let kernel = this.kernels.get(sessionId)
447
+ if (kernel === undefined) {
448
+ let history
449
+ try {
450
+ history = cellConfig.durableReplay
451
+ ? recoverJournal(session, callSeq)
452
+ : emptyHistory()
453
+ } catch (error) {
454
+ return completed({ logs: [], error: { kind: 'recovery', message: `cannot reconstruct REPL from session log: ${messageOf(error)}` } })
455
+ }
456
+ kernel = new SessionKernel({
457
+ config: cellConfig,
458
+ history,
459
+ cwd,
460
+ session,
461
+ withInitiator: this.withInitiator,
462
+ })
463
+ this.kernels.set(sessionId, kernel)
464
+ }
465
+ const journal = createJournal(
466
+ /* c8 ignore next */
467
+ this.pendingNoops.get(sessionId) ?? [],
468
+ cellConfig.looseTopLevelRedeclarations ? 'loose' : 'strict',
469
+ rewritePolicy(cellConfig),
470
+ )
471
+ const workerReservation = kernel.reserveWorkerConfiguration(cellConfig)
472
+ let result
473
+ try {
474
+ result = await kernel.run({ ...request, journal, callSeq }, cellConfig)
475
+ } finally {
476
+ kernel.releaseWorkerConfiguration(workerReservation)
477
+ }
478
+ const settlement = Object.freeze({
479
+ journal,
480
+ kernel,
481
+ sessionId,
482
+ ...(result.recoveryBoundaries === undefined
483
+ ? {}
484
+ : { recoveryBoundaries: result.recoveryBoundaries }),
485
+ ...(result.rewrites === undefined ? {} : { rewrites: result.rewrites }),
486
+ })
487
+ this.settlements.add(settlement)
488
+ const {
489
+ journal: _ignored,
490
+ recoveryBoundaries: _recoveryBoundaries,
491
+ ...publicResult
492
+ } = result
493
+ return Object.freeze({ result: publicResult, settlement })
494
+ }
495
+
496
+ noteNoop(sessionId, session, callId) {
497
+ const callSeq = liveToolCallSeq(session, String(callId), 'run_code')
498
+ if (callSeq === undefined) return
499
+ const id = String(sessionId)
500
+ let calls = this.pendingNoops.get(id)
501
+ if (calls === undefined) {
502
+ calls = new Set()
503
+ this.pendingNoops.set(id, calls)
504
+ }
505
+ calls.add(callSeq)
506
+ }
507
+
508
+ finalize(settlement, confirmed) {
509
+ if (settlement === null || typeof settlement !== 'object' || !this.settlements.delete(settlement)) {
510
+ throw new TypeError('ptc-plus: finalize requires one unsettled SessionRuntime settlement handle')
511
+ }
512
+ const { journal, kernel, sessionId } = settlement
513
+ kernel.finalizeJournal(journal, confirmed)
514
+ if (!confirmed) return
515
+ const noops = this.pendingNoops.get(sessionId)
516
+ if (noops === undefined) return
517
+ for (const callSeq of journal.confirms ?? []) noops.delete(callSeq)
518
+ if (noops.size === 0) this.pendingNoops.delete(sessionId)
519
+ }
520
+
521
+ async disposeSession(sessionId) {
522
+ const id = String(sessionId)
523
+ const kernel = this.kernels.get(id)
524
+ this.pendingNoops.delete(id)
525
+ if (kernel === undefined) return
526
+ this.kernels.delete(id)
527
+ await kernel.dispose()
528
+ }
529
+
530
+ async dispose() {
531
+ this.disposed = true
532
+ const kernels = [...this.kernels.values()]
533
+ this.kernels.clear()
534
+ await Promise.all(kernels.map(kernel => kernel.dispose()))
535
+ }
536
+ }
@@ -0,0 +1,45 @@
1
+ // Runtime state transitions shared by the session coordinator and cell executor.
2
+
3
+ export function durabilityState(overrides = {}) {
4
+ return Object.freeze({
5
+ status: 'durable',
6
+ reason: undefined,
7
+ ...overrides,
8
+ })
9
+ }
10
+
11
+ export function transitionDurability(state, transition) {
12
+ if (transition.type !== 'volatile') return state
13
+ return durabilityState({
14
+ ...state,
15
+ status: 'volatile',
16
+ reason: state.reason ?? transition.reason,
17
+ })
18
+ }
19
+
20
+ export class BindingCatalog {
21
+ #known
22
+ #imports
23
+ #namespaces
24
+
25
+ constructor(known = new Set(), imports = new Map(), namespaces = new Set()) {
26
+ this.#known = new Set(known)
27
+ this.#imports = new Map(imports)
28
+ this.#namespaces = new Set(namespaces)
29
+ Object.freeze(this)
30
+ }
31
+
32
+ inputs() {
33
+ return {
34
+ knownBindings: new Set(this.#known),
35
+ importBindings: new Map(this.#imports),
36
+ importNamespaces: new Set(this.#namespaces),
37
+ }
38
+ }
39
+
40
+ advance(prepared) {
41
+ const known = new Set(this.#known)
42
+ for (const name of prepared.declared) known.add(name)
43
+ return new BindingCatalog(known, prepared.imports, prepared.importNamespaces)
44
+ }
45
+ }