dsh-ptc-plus 0.4.2 → 0.4.8

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 (89) hide show
  1. package/README.en.md +170 -76
  2. package/README.md +171 -77
  3. package/assets/dsh-ptc-plus-banner-en.webp +0 -0
  4. package/assets/dsh-ptc-plus-banner-zh.webp +0 -0
  5. package/assets/ptc-plus-icon.svg +5 -0
  6. package/client.js +599 -310
  7. package/compiler-core.cjs +162 -160
  8. package/compiler-core.cjs.map +3 -3
  9. package/docs/README.md +4 -1
  10. package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +49 -11
  11. package/docs/adr/0008-define-agent-operating-contract.md +2 -2
  12. package/docs/adr/0010-session-log-derived-recovery-tips.md +7 -0
  13. package/docs/adr/0016-archive-resolved-review-findings.md +3 -3
  14. package/docs/adr/0017-track-the-latest-dsh-public-surface.md +5 -1
  15. package/docs/adr/0019-plugin-settings-and-kill-switch.md +3 -3
  16. package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +3 -1
  17. package/docs/adr/0021-separate-host-bootstrap-from-session-runtime.md +14 -3
  18. package/docs/adr/0023-global-user-bindings.md +1 -1
  19. package/docs/adr/0026-persist-the-delivery-verdict-against-the-verified-candidate.md +23 -7
  20. package/docs/adr/0027-run-the-session-kernel-in-a-killable-helper-process.md +4 -0
  21. package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +26 -6
  22. package/docs/adr/0030-serve-both-typert-codec-generations-from-one-codec.md +2 -2
  23. package/docs/adr/0031-close-semantic-obligations-by-owner.md +29 -0
  24. package/docs/adr/README.md +4 -4
  25. package/docs/architecture.md +8 -5
  26. package/docs/binding-review-placement-design.md +1 -1
  27. package/docs/client-ui.md +8 -4
  28. package/docs/durability-design.md +7 -7
  29. package/docs/installation.md +48 -12
  30. package/docs/publishing.md +4 -4
  31. package/docs/release-notes.md +109 -0
  32. package/docs/runtime-reference.md +25 -12
  33. package/docs/semantic-validation.md +7 -3
  34. package/docs/user-bindings.en.md +7 -6
  35. package/docs/user-bindings.md +6 -5
  36. package/docs/value-wire.md +1 -1
  37. package/docs/verification.md +43 -2
  38. package/index.js +39 -34
  39. package/internal/async-local-storage-intrinsics.js +12 -7
  40. package/internal/callable-source-facts.js +37 -1
  41. package/internal/cell-analysis.js +64 -9
  42. package/internal/config-spec.js +1 -1
  43. package/internal/cordis-tools-owner.js +81 -15
  44. package/internal/direct-surface-owner.js +331 -7
  45. package/internal/edit-transport-owner.js +80 -20
  46. package/internal/execution-seam-compat.js +192 -30
  47. package/internal/kernel-worker.js +162 -72
  48. package/internal/message-sources.js +14 -0
  49. package/internal/module-compilation.js +2 -1
  50. package/internal/rejected-cell-editor.js +11 -21
  51. package/internal/repl-memory-projection.js +10 -0
  52. package/internal/repl-scope-normalizer.js +72 -35
  53. package/internal/repl-value-observer.js +141 -45
  54. package/internal/runtime-config.js +62 -2
  55. package/internal/runtime-intrinsics.js +3 -0
  56. package/internal/runtime-messages.js +6 -4
  57. package/internal/session-cell-executor.js +24 -1
  58. package/internal/session-journal-recovery.js +267 -67
  59. package/internal/session-journal.js +466 -40
  60. package/internal/session-log-view.js +5 -4
  61. package/internal/session-runtime.js +45 -21
  62. package/internal/session-state.js +7 -29
  63. package/internal/settings-compat.js +66 -1
  64. package/internal/source-position-map.js +19 -0
  65. package/internal/user-binding-console-worker.js +23 -9
  66. package/internal/user-binding-draft-projection.js +3 -2
  67. package/internal/user-bindings-owner.js +2 -1
  68. package/internal/user-bindings.js +6 -1
  69. package/internal/validated-parse-repair.js +6 -2
  70. package/internal/value-wire.js +95 -30
  71. package/internal/worker-client.js +99 -8
  72. package/internal/worker-cwd-virtualization.js +10 -9
  73. package/internal/worker-output-capture.js +187 -0
  74. package/internal/worker-output-fence.js +26 -0
  75. package/internal/worker-realm-surfaces.js +197 -0
  76. package/internal/worker-repl-realm.js +10 -4
  77. package/package.json +42 -4
  78. package/src/client-activity.js +4 -1
  79. package/src/client-authoring-view.js +121 -62
  80. package/src/client-console.js +4 -2
  81. package/src/client-copy.js +4 -2
  82. package/src/client-feature-gates.js +7 -9
  83. package/src/client-host-compat.js +104 -27
  84. package/src/client-repl-view.js +3 -1
  85. package/src/client-styles.js +12 -1
  86. package/src/client-tool-view.js +3 -1
  87. package/src/client-transport.js +2 -2
  88. package/src/client-workbench.js +10 -7
  89. package/src/client.js +56 -17
@@ -1,4 +1,7 @@
1
1
  /** Own PTC direct presentation, prompt projection, and stream normalization. */
2
+ import { boundContextSummary, createUserMessage } from '@deepseek-ai/dsh-llm'
3
+ import { validateJsonSchemaValue } from '@deepseek-ai/dsh-tools'
4
+ import { PTC_MESSAGE_SOURCE_KIND } from './message-sources.js'
2
5
  import { canonicalizeToolCallStream } from './tool-call-canonicalizer.js'
3
6
  import { editRunCodeSchema } from './rejected-cell-editor.js'
4
7
  import { sessionRuntimeContexts } from './runtime-contexts.js'
@@ -19,6 +22,12 @@ const RUN_CODE_CODE_DESCRIPTION = 'Code for the next REPL cell, parsed as the bo
19
22
  const RUN_CODE_DESCRIPTION_DESCRIPTION = 'Short active-voice summary of what this cell does, 5-10 words (shown in the UI).'
20
23
  const CODE_TRANSPORT_INSTRUCTION = '`run_code` and `edit_run_code` are the only tools callable directly. Call every native tool declared by the SDK from inside a program.'
21
24
  const PTC_COLLAPSE_SECTION_NAMES = Object.freeze(['tools:ptc-only', 'tools:code-only'])
25
+ const INVALID_TOOL_JSON_FAILURE = Object.freeze({
26
+ code: 'MALFORMED_RESPONSE',
27
+ message: 'DeepSeek Messages stream: tool input is invalid JSON',
28
+ })
29
+ const MALFORMED_TOOL_REJECTION_ID = 'dsh-ptc-plus-malformed-tool-json'
30
+ const MALFORMED_TOOL_REJECTION_ARGUMENTS = '{"dsh_ptc_plus_malformed_tool_json":'
22
31
 
23
32
  function adaptRunCodeSchema(tool) {
24
33
  const parameters = tool.parameters
@@ -139,6 +148,314 @@ async function* bindCallPolicies(source, policy, calls) {
139
148
  }
140
149
  }
141
150
 
151
+ function invalidToolJsonFinish(chunk) {
152
+ const failure = chunk?.reason?.failure
153
+ return chunk?.type === 'finish'
154
+ && chunk.reason.kind === 'error'
155
+ && failure?.code === INVALID_TOOL_JSON_FAILURE.code
156
+ && failure?.message === INVALID_TOOL_JSON_FAILURE.message
157
+ }
158
+
159
+ function invalidToolJsonError(error) {
160
+ const failure = error?.failure ?? error
161
+ return failure?.code === INVALID_TOOL_JSON_FAILURE.code
162
+ && failure?.message === INVALID_TOOL_JSON_FAILURE.message
163
+ }
164
+
165
+ function directToolSchemas(tools) {
166
+ const schemas = new Map()
167
+ if (!Array.isArray(tools)) return schemas
168
+ for (const tool of tools) {
169
+ if (tool?.name !== RUN_CODE && tool?.name !== EDIT_RUN_CODE) continue
170
+ if (schemas.has(tool.name) || !isRecord(tool.parameters)) return new Map()
171
+ schemas.set(tool.name, tool.parameters)
172
+ }
173
+ return schemas
174
+ }
175
+
176
+ function repairableDirectCall(chunks, tools) {
177
+ const finish = chunks.at(-1)
178
+ if (!invalidToolJsonFinish(finish)) return undefined
179
+ const completed = completedToolBlocks(chunks)
180
+ if (completed?.length !== 1) return undefined
181
+ const body = chunks.slice(0, -1)
182
+ const starts = body.filter(chunk => chunk?.type === 'block-start' && chunk.blockType === 'tool-call')
183
+ const deltas = body.filter(chunk => chunk?.type === 'tool-call-delta')
184
+ const ends = body.filter(chunk => chunk?.type === 'block-end' && chunk.block?.type === 'tool-call')
185
+ const usages = body.filter(chunk => chunk?.type === 'usage')
186
+ if (starts.length !== 1 || deltas.length === 0 || ends.length !== 1
187
+ || usages.length > 1
188
+ || body.some(chunk => chunk?.type !== 'usage' && !toolStreamChunk(chunk))) {
189
+ return undefined
190
+ }
191
+ const index = starts[0].index
192
+ const end = ends[0]
193
+ const id = end.block.id
194
+ const name = end.block.name
195
+ if (!Number.isSafeInteger(index) || index < 0 || typeof id !== 'string' || id.length === 0
196
+ || (name !== RUN_CODE && name !== EDIT_RUN_CODE)
197
+ || end.index !== index
198
+ || typeof end.block.arguments !== 'string'
199
+ || deltas.some(chunk => chunk.index !== index || chunk.id !== id
200
+ || (chunk.name !== undefined && chunk.name !== name)
201
+ || typeof chunk.argumentsDelta !== 'string')) {
202
+ return undefined
203
+ }
204
+ const raw = deltas.map(chunk => chunk.argumentsDelta).join('')
205
+ if (raw !== end.block.arguments || !raw.endsWith('}')) return undefined
206
+ const candidate = raw.slice(0, -1)
207
+ let value
208
+ try {
209
+ value = JSON.parse(candidate)
210
+ } catch {
211
+ return undefined
212
+ }
213
+ const schema = directToolSchemas(tools).get(name)
214
+ if (!isRecord(value) || schema === undefined
215
+ || validateJsonSchemaValue(schema, value).length !== 0) return undefined
216
+ return { index, candidate }
217
+ }
218
+
219
+ function canonicalToolBlock(index, block) {
220
+ return [
221
+ { type: 'block-start', index, blockType: 'tool-call' },
222
+ {
223
+ type: 'tool-call-delta', index, id: block.id, name: block.name,
224
+ argumentsDelta: block.arguments,
225
+ },
226
+ { type: 'block-end', index, block },
227
+ ]
228
+ }
229
+
230
+ function repairedDirectChunks(chunks, repair, usageAlreadyEmitted) {
231
+ const end = chunks.find(chunk => chunk?.type === 'block-end'
232
+ && chunk.index === repair.index && chunk.block?.type === 'tool-call')
233
+ const block = { ...end.block, arguments: repair.candidate }
234
+ const usage = usageAlreadyEmitted ? undefined : firstUsageChunk(chunks)
235
+ return [
236
+ ...canonicalToolBlock(repair.index, block),
237
+ ...usage === undefined ? [] : [usage],
238
+ { type: 'finish', reason: { kind: 'tool-calls' } },
239
+ ]
240
+ }
241
+
242
+ function completedToolBlocks(chunks) {
243
+ const body = chunks.slice(0, -1)
244
+ if (body.filter(chunk => chunk?.type === 'usage').length > 1
245
+ || body.some(chunk => chunk?.type !== 'usage' && !toolStreamChunk(chunk))) {
246
+ return undefined
247
+ }
248
+ const calls = new Map()
249
+ for (const chunk of chunks) {
250
+ if (chunk?.type === 'block-start' && chunk.blockType === 'tool-call') {
251
+ if (!Number.isSafeInteger(chunk.index) || chunk.index < 0) return undefined
252
+ const call = calls.get(chunk.index)
253
+ if (call?.started === true || call?.block !== undefined) return undefined
254
+ if (call === undefined) calls.set(chunk.index, {
255
+ arguments: '', hasDelta: false, started: true,
256
+ })
257
+ else call.started = true
258
+ } else if (chunk?.type === 'tool-call-delta') {
259
+ if (!Number.isSafeInteger(chunk.index) || chunk.index < 0) return undefined
260
+ let call = calls.get(chunk.index)
261
+ if (call === undefined) {
262
+ call = { arguments: '', hasDelta: false, started: false }
263
+ calls.set(chunk.index, call)
264
+ }
265
+ if (call.block !== undefined
266
+ || typeof chunk.id !== 'string' || chunk.id.length === 0
267
+ || (call.id !== undefined && call.id !== chunk.id)
268
+ || (chunk.name !== undefined && (typeof chunk.name !== 'string' || chunk.name.length === 0
269
+ || (call.name !== undefined && call.name !== chunk.name)))
270
+ || typeof chunk.argumentsDelta !== 'string') return undefined
271
+ call.id ??= chunk.id
272
+ if (chunk.name !== undefined) call.name ??= chunk.name
273
+ call.hasDelta = true
274
+ call.arguments += chunk.argumentsDelta
275
+ } else if (chunk?.type === 'block-end' && chunk.block?.type === 'tool-call') {
276
+ if (!Number.isSafeInteger(chunk.index) || chunk.index < 0) return undefined
277
+ let call = calls.get(chunk.index)
278
+ if (call === undefined) {
279
+ call = { arguments: '', hasDelta: false, started: false }
280
+ calls.set(chunk.index, call)
281
+ }
282
+ if (call.block !== undefined
283
+ || typeof chunk.block.id !== 'string' || chunk.block.id.length === 0
284
+ || typeof chunk.block.name !== 'string' || chunk.block.name.length === 0
285
+ || typeof chunk.block.arguments !== 'string'
286
+ || (call.id !== undefined && call.id !== chunk.block.id)
287
+ || (call.name !== undefined && call.name !== chunk.block.name)
288
+ || (call.hasDelta && call.arguments !== chunk.block.arguments)) return undefined
289
+ call.block = chunk.block
290
+ }
291
+ }
292
+ if (calls.size === 0 || [...calls.values()].some(call => call.block === undefined)) {
293
+ return undefined
294
+ }
295
+ const completed = [...calls].map(([index, call]) => ({ index, block: call.block }))
296
+ if (new Set(completed.map(call => call.block.id)).size !== completed.length) return undefined
297
+ return completed
298
+ }
299
+
300
+ function firstUsageChunk(chunks) {
301
+ return chunks.find(chunk => chunk?.type === 'usage')
302
+ }
303
+
304
+ function settleMalformedToolCalls(chunks, priorIndexes, usageAlreadyEmitted) {
305
+ if (!invalidToolJsonFinish(chunks.at(-1))) return undefined
306
+ const calls = completedToolBlocks(chunks)
307
+ if (calls === undefined || calls.some(call => priorIndexes.has(call.index))) return undefined
308
+ const usage = usageAlreadyEmitted ? undefined : firstUsageChunk(chunks)
309
+ return [
310
+ ...calls.flatMap(call => canonicalToolBlock(call.index, call.block)),
311
+ ...usage === undefined ? [] : [usage],
312
+ { type: 'finish', reason: { kind: 'tool-calls' } },
313
+ ]
314
+ }
315
+
316
+ function toolStreamChunk(chunk) {
317
+ return chunk?.type === 'tool-call-delta'
318
+ || (chunk?.type === 'block-start' && chunk.blockType === 'tool-call')
319
+ || (chunk?.type === 'block-end' && chunk.block?.type === 'tool-call')
320
+ }
321
+
322
+ function unusedString(base, used) {
323
+ let value = base
324
+ for (let suffix = 2; used.has(value); suffix++) value = `${base}-${suffix}`
325
+ return value
326
+ }
327
+
328
+ function visibleBlockClosures(openBlocks, chunks) {
329
+ const closing = new Map([...openBlocks].map(([index, block]) => [index, { ...block }]))
330
+ for (const chunk of chunks) {
331
+ const block = closing.get(chunk?.index)
332
+ if (block === undefined) continue
333
+ if (chunk.type === `${block.type}-delta` && typeof chunk.text === 'string') {
334
+ block.text += chunk.text
335
+ } else if (chunk.type === 'block-end' && chunk.block?.type === block.type) {
336
+ block.final = chunk.block
337
+ }
338
+ }
339
+ return [...closing].map(([index, block]) => ({
340
+ type: 'block-end',
341
+ index,
342
+ block: block.final ?? { type: block.type, text: block.text },
343
+ }))
344
+ }
345
+
346
+ function rejectedMalformedToolStream(
347
+ chunks,
348
+ priorIndexes = new Set(),
349
+ openBlocks = new Map(),
350
+ usageAlreadyEmitted = false,
351
+ ) {
352
+ const usedIndexes = new Set(priorIndexes)
353
+ const usedIds = new Set()
354
+ for (const chunk of chunks) {
355
+ if (Number.isSafeInteger(chunk?.index) && chunk.index >= 0) usedIndexes.add(chunk.index)
356
+ if (typeof chunk?.id === 'string') usedIds.add(chunk.id)
357
+ if (typeof chunk?.block?.id === 'string') usedIds.add(chunk.block.id)
358
+ }
359
+ let index = 0
360
+ while (usedIndexes.has(index)) index++
361
+ const id = unusedString(MALFORMED_TOOL_REJECTION_ID, usedIds)
362
+ const usage = usageAlreadyEmitted ? undefined : firstUsageChunk(chunks)
363
+ return [
364
+ ...visibleBlockClosures(openBlocks, chunks),
365
+ { type: 'block-start', index, blockType: 'tool-call' },
366
+ {
367
+ type: 'tool-call-delta', index, id, name: RUN_CODE,
368
+ argumentsDelta: MALFORMED_TOOL_REJECTION_ARGUMENTS,
369
+ },
370
+ {
371
+ type: 'block-end', index,
372
+ block: {
373
+ type: 'tool-call', id, name: RUN_CODE,
374
+ arguments: MALFORMED_TOOL_REJECTION_ARGUMENTS,
375
+ },
376
+ },
377
+ ...usage === undefined ? [] : [usage],
378
+ { type: 'finish', reason: { kind: 'tool-calls' } },
379
+ ]
380
+ }
381
+
382
+ function recoveredMalformedChunks(chunks, tools, priorIndexes, openBlocks, usageAlreadyEmitted) {
383
+ if (usageAlreadyEmitted && firstUsageChunk(chunks) !== undefined) {
384
+ return rejectedMalformedToolStream(chunks, priorIndexes, openBlocks, true)
385
+ }
386
+ const repair = repairableDirectCall(chunks, tools)
387
+ const settled = repair !== undefined && !priorIndexes.has(repair.index)
388
+ ? repairedDirectChunks(chunks, repair, usageAlreadyEmitted)
389
+ : settleMalformedToolCalls(chunks, priorIndexes, usageAlreadyEmitted)
390
+ return settled === undefined
391
+ ? rejectedMalformedToolStream(chunks, priorIndexes, openBlocks, usageAlreadyEmitted)
392
+ : [...visibleBlockClosures(openBlocks, chunks), ...settled]
393
+ }
394
+
395
+ function observeVisibleBlock(openBlocks, chunk) {
396
+ if (chunk?.type === 'block-start'
397
+ && (chunk.blockType === 'text' || chunk.blockType === 'reasoning')) {
398
+ openBlocks.set(chunk.index, { type: chunk.blockType, text: '' })
399
+ } else if (chunk?.type === 'text-delta' || chunk?.type === 'reasoning-delta') {
400
+ const block = openBlocks.get(chunk.index)
401
+ const type = chunk.type === 'text-delta' ? 'text' : 'reasoning'
402
+ if (block?.type === type) block.text += chunk.text
403
+ } else if (chunk?.type === 'block-end') {
404
+ openBlocks.delete(chunk.index)
405
+ }
406
+ }
407
+
408
+ async function* recoverMalformedToolCallStream(source, tools) {
409
+ let pending = []
410
+ const priorIndexes = new Set()
411
+ const openBlocks = new Map()
412
+ let usageSeen = false
413
+ try {
414
+ for await (const chunk of source) {
415
+ if (pending.length === 0) {
416
+ if (toolStreamChunk(chunk)) pending.push(chunk)
417
+ else if (chunk?.type === 'usage' && usageSeen) pending.push(chunk)
418
+ else if (invalidToolJsonFinish(chunk)) {
419
+ yield* rejectedMalformedToolStream([chunk], priorIndexes, openBlocks, usageSeen)
420
+ openBlocks.clear()
421
+ } else {
422
+ if (Number.isSafeInteger(chunk?.index) && chunk.index >= 0) priorIndexes.add(chunk.index)
423
+ observeVisibleBlock(openBlocks, chunk)
424
+ if (chunk?.type === 'usage') usageSeen = true
425
+ yield chunk
426
+ }
427
+ continue
428
+ }
429
+ pending.push(chunk)
430
+ if (chunk?.type !== 'finish') continue
431
+ const output = invalidToolJsonFinish(chunk)
432
+ ? recoveredMalformedChunks(pending, tools, priorIndexes, openBlocks, usageSeen)
433
+ : pending
434
+ for (const outputChunk of output) {
435
+ if (Number.isSafeInteger(outputChunk?.index) && outputChunk.index >= 0) {
436
+ priorIndexes.add(outputChunk.index)
437
+ }
438
+ if (outputChunk?.type === 'usage') usageSeen = true
439
+ yield outputChunk
440
+ }
441
+ if (invalidToolJsonFinish(chunk)) openBlocks.clear()
442
+ pending = []
443
+ }
444
+ } catch (error) {
445
+ if (invalidToolJsonError(error)) {
446
+ yield* recoveredMalformedChunks([
447
+ ...pending,
448
+ { type: 'finish', reason: { kind: 'error', failure: INVALID_TOOL_JSON_FAILURE } },
449
+ ], tools, priorIndexes, openBlocks, usageSeen)
450
+ return
451
+ } else {
452
+ yield* pending
453
+ throw error
454
+ }
455
+ }
456
+ yield* pending
457
+ }
458
+
142
459
  export function createDirectSurfaceOwner({
143
460
  editTransport,
144
461
  runtimeConfig,
@@ -383,7 +700,8 @@ export function createDirectSurfaceOwner({
383
700
  editToolName: EDIT_RUN_CODE,
384
701
  })
385
702
  : next()
386
- return bindCallPolicies(source, policy, policy.owner.calls)
703
+ const recovered = recoverMalformedToolCallStream(source, options.tools)
704
+ return bindCallPolicies(recovered, policy, policy.owner.calls)
387
705
  },
388
706
  executionRejection(exec) {
389
707
  if (exec.name === EDIT_RUN_CODE && exec.parent !== undefined) {
@@ -430,12 +748,18 @@ export function createDirectSurfaceOwner({
430
748
  const outer = missingPath === 'description'
431
749
  && policy?.autoDescribeRunCode !== true
432
750
  && !(isRecord(exec.arguments) && typeof exec.arguments.description === 'string')
433
- const context = {
434
- name: 'tools:ptc-plus-run-code-arguments',
435
- text: outer
436
- ? 'The run_code outer transport arguments are invalid at JSON path $.description. Add a sibling string `description` field to run_code; a description nested inside a native-tool argument does not satisfy this outer requirement.'
437
- : `A nested native-tool argument is missing required property "${missingPath}". Add a string description at that path in the native-tool arguments; the outer run_code description does not satisfy this nested requirement.`,
438
- }
751
+ const text = outer
752
+ ? 'The run_code outer transport arguments are invalid at JSON path $.description. Add a sibling string `description` field to run_code; a description nested inside a native-tool argument does not satisfy this outer requirement.'
753
+ : `A nested native-tool argument is missing required property "${missingPath}". Add a string description at that path in the native-tool arguments; the outer run_code description does not satisfy this nested requirement.`
754
+ // additionalContexts enter the durable inbox as messages, not prompt sections.
755
+ const context = createUserMessage({
756
+ source: {
757
+ kind: PTC_MESSAGE_SOURCE_KIND,
758
+ form: 'notice',
759
+ summary: boundContextSummary('tools:ptc-plus-run-code-arguments'),
760
+ },
761
+ content: [{ type: 'text', text }],
762
+ })
439
763
  return {
440
764
  ...diagnosed,
441
765
  additionalContexts: [...diagnosed.additionalContexts ?? [], context],
@@ -238,11 +238,34 @@ export function createEditTransportOwner(ctx, {
238
238
  }
239
239
  }
240
240
 
241
+ const disposeInstalled = (installed) => {
242
+ const failures = []
243
+ for (const key of ['disposePresentation', 'disposeRegistration']) {
244
+ const dispose = installed[key]
245
+ if (dispose === undefined) continue
246
+ try {
247
+ dispose()
248
+ installed[key] = undefined
249
+ } catch (error) {
250
+ failures.push(error)
251
+ }
252
+ }
253
+ if (failures.length > 0) {
254
+ throw new AggregateError(failures, 'ptc-plus: edit_run_code scope disposal failed')
255
+ }
256
+ }
257
+
241
258
  const uninstall = (agent) => {
242
259
  const installed = installedScopes.get(agent)
243
260
  if (installed === undefined) return
244
- installedScopes.delete(agent)
245
- installed.dispose()
261
+ try {
262
+ disposeInstalled(installed)
263
+ } finally {
264
+ if (installed.disposePresentation === undefined
265
+ && installed.disposeRegistration === undefined) {
266
+ installedScopes.delete(agent)
267
+ }
268
+ }
246
269
  }
247
270
 
248
271
  return Object.freeze({
@@ -251,11 +274,19 @@ export function createEditTransportOwner(ctx, {
251
274
  currentDurableReplay = nextConfig.durableReplay
252
275
  },
253
276
  isInstalled(agent) {
254
- return installedScopes.has(agent)
277
+ const installed = installedScopes.get(agent)
278
+ return installed?.disposePresentation !== undefined
279
+ && installed.disposeRegistration !== undefined
255
280
  },
256
281
  ensureInstalled(agent) {
257
282
  const installed = installedScopes.get(agent)
258
- if (installed !== undefined) return installed.definition
283
+ if (installed !== undefined) {
284
+ if (installed.disposePresentation !== undefined
285
+ && installed.disposeRegistration !== undefined) {
286
+ return installed.definition
287
+ }
288
+ uninstall(agent)
289
+ }
259
290
  const liveSchemas = typeof toolSchemasForAgent === 'function'
260
291
  ? toolSchemasForAgent(agent)
261
292
  : []
@@ -271,24 +302,31 @@ export function createEditTransportOwner(ctx, {
271
302
  if (typeof disposeRegistration !== 'function') {
272
303
  throw new Error('ptc-plus: DSH tools.register did not return a disposer')
273
304
  }
274
- let disposePresentation
305
+ const nextInstalled = {
306
+ definition,
307
+ disposePresentation: undefined,
308
+ disposeRegistration,
309
+ sessionId: sessionId(agent),
310
+ }
311
+ installedScopes.set(agent, nextInstalled)
275
312
  try {
276
- disposePresentation = scopedTools.presentAs('both')
313
+ const disposePresentation = scopedTools.presentAs('both')
277
314
  if (typeof disposePresentation !== 'function') {
278
315
  throw new Error('ptc-plus: DSH tools.presentAs did not return a disposer')
279
316
  }
317
+ nextInstalled.disposePresentation = disposePresentation
280
318
  } catch (error) {
281
- disposeRegistration()
319
+ try {
320
+ uninstall(agent)
321
+ } catch (cleanupError) {
322
+ throw new AggregateError(
323
+ [error, cleanupError],
324
+ 'ptc-plus: edit_run_code installation and rollback failed',
325
+ { cause: error },
326
+ )
327
+ }
282
328
  throw error
283
329
  }
284
- let active = true
285
- const dispose = () => {
286
- if (!active) return
287
- active = false
288
- disposePresentation()
289
- disposeRegistration()
290
- }
291
- installedScopes.set(agent, { definition, dispose, sessionId: sessionId(agent) })
292
330
  return definition
293
331
  },
294
332
  handleResult(exec, result) {
@@ -305,28 +343,50 @@ export function createEditTransportOwner(ctx, {
305
343
  releaseClaim(pending.id, pending.targetCallSeq)
306
344
  },
307
345
  disposeAgent(agent) {
308
- uninstall(agent)
346
+ let failure
347
+ try {
348
+ uninstall(agent)
349
+ } catch (error) {
350
+ failure = error
351
+ }
309
352
  const id = sessionId(agent)
310
353
  if (id !== undefined) {
311
354
  discardSettlements(id)
312
355
  editClaims.delete(id)
313
356
  }
357
+ if (failure !== undefined) throw failure
314
358
  },
315
359
  disposeSession(session) {
316
360
  const id = String(session.id)
361
+ const failures = []
317
362
  for (const [agent, installed] of installedScopes) {
318
363
  if (installed.sessionId !== id) continue
319
- installedScopes.delete(agent)
320
- installed.dispose()
364
+ try {
365
+ uninstall(agent)
366
+ } catch (error) {
367
+ failures.push(error)
368
+ }
321
369
  }
322
370
  discardSettlements(id)
323
371
  editClaims.delete(id)
372
+ if (failures.length > 0) {
373
+ throw new AggregateError(failures, 'ptc-plus: edit_run_code session disposal failed')
374
+ }
324
375
  },
325
376
  dispose() {
326
377
  discardSettlements()
327
378
  editClaims.clear()
328
- for (const installed of installedScopes.values()) installed.dispose()
329
- installedScopes.clear()
379
+ const failures = []
380
+ for (const agent of [...installedScopes.keys()]) {
381
+ try {
382
+ uninstall(agent)
383
+ } catch (error) {
384
+ failures.push(error)
385
+ }
386
+ }
387
+ if (failures.length > 0) {
388
+ throw new AggregateError(failures, 'ptc-plus: edit_run_code owner disposal failed')
389
+ }
330
390
  },
331
391
  })
332
392
  }