dsh-ptc-plus 0.4.1 → 0.4.3

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 (103) hide show
  1. package/README.en.md +164 -78
  2. package/README.md +165 -79
  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 +854 -336
  7. package/compiler-core.cjs +161 -157
  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 +8 -7
  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/0019-plugin-settings-and-kill-switch.md +3 -3
  15. package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +2 -0
  16. package/docs/adr/0021-separate-host-bootstrap-from-session-runtime.md +14 -3
  17. package/docs/adr/0023-global-user-bindings.md +7 -7
  18. package/docs/adr/0025-use-versioned-logical-binding-identities.md +3 -3
  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 +9 -5
  21. package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +26 -6
  22. package/docs/adr/0031-close-semantic-obligations-by-owner.md +29 -0
  23. package/docs/adr/README.md +4 -4
  24. package/docs/architecture.md +15 -10
  25. package/docs/binding-review-placement-design.md +1 -1
  26. package/docs/client-ui.md +9 -5
  27. package/docs/durability-design.md +12 -9
  28. package/docs/installation.md +27 -9
  29. package/docs/runtime-reference.md +23 -12
  30. package/docs/semantic-validation.md +7 -3
  31. package/docs/stateful-binding-updates-proposal.md +8 -4
  32. package/docs/user-bindings.en.md +10 -7
  33. package/docs/user-bindings.md +9 -6
  34. package/docs/value-wire.md +1 -1
  35. package/docs/verification-optimization-work.md +41 -0
  36. package/docs/verification.md +53 -2
  37. package/index.js +39 -34
  38. package/internal/async-local-storage-intrinsics.js +43 -0
  39. package/internal/binding-declaration-contract.js +8 -0
  40. package/internal/binding-source-analysis.js +381 -64
  41. package/internal/binding-update-policy.js +7 -1
  42. package/internal/callable-source-facts.js +37 -1
  43. package/internal/cell-analysis.js +85 -17
  44. package/internal/compiler-intrinsics.js +9 -43
  45. package/internal/compiler-module-links.js +7 -4
  46. package/internal/config-spec.js +1 -1
  47. package/internal/cordis-tools-owner.js +20 -10
  48. package/internal/dynamic-environment-integration.js +8 -4
  49. package/internal/dynamic-environment-runtime.js +24 -3
  50. package/internal/dynamic-native-calls.js +4 -1
  51. package/internal/dynamic-native-runtime.js +15 -1
  52. package/internal/edit-transport-owner.js +80 -20
  53. package/internal/execution-seam-compat.js +192 -30
  54. package/internal/failure-reporting.js +38 -30
  55. package/internal/isolated-worker.js +17 -10
  56. package/internal/kernel-worker.js +645 -393
  57. package/internal/message-sources.js +14 -0
  58. package/internal/module-compilation.js +7 -5
  59. package/internal/module-transform-contract.js +32 -2
  60. package/internal/record-utils.js +24 -15
  61. package/internal/repl-memory-projection.js +10 -0
  62. package/internal/repl-scope-normalizer.js +72 -35
  63. package/internal/repl-value-observer.js +145 -46
  64. package/internal/runtime-config.js +62 -2
  65. package/internal/runtime-intrinsics.js +104 -0
  66. package/internal/runtime-messages.js +6 -4
  67. package/internal/session-cell-executor.js +26 -2
  68. package/internal/session-journal-recovery.js +267 -67
  69. package/internal/session-journal-schema.js +30 -13
  70. package/internal/session-journal.js +485 -44
  71. package/internal/session-log-view.js +5 -4
  72. package/internal/session-runtime.js +46 -21
  73. package/internal/session-state.js +7 -29
  74. package/internal/settings-compat.js +66 -1
  75. package/internal/source-position-map.js +38 -0
  76. package/internal/stateful-root-compiler.js +3 -1
  77. package/internal/typescript-transform.js +4 -2
  78. package/internal/user-binding-console-worker.js +115 -41
  79. package/internal/user-binding-draft-projection.js +3 -2
  80. package/internal/user-bindings-owner.js +2 -1
  81. package/internal/user-bindings.js +16 -3
  82. package/internal/validated-parse-repair.js +6 -2
  83. package/internal/value-wire.js +189 -94
  84. package/internal/worker-client.js +99 -8
  85. package/internal/worker-cwd-virtualization.js +82 -54
  86. package/internal/worker-environment.js +20 -0
  87. package/internal/worker-output-capture.js +187 -0
  88. package/internal/worker-output-fence.js +26 -0
  89. package/internal/worker-realm-surfaces.js +197 -0
  90. package/internal/worker-repl-realm.js +140 -0
  91. package/package.json +37 -4
  92. package/src/client-activity.js +12 -2
  93. package/src/client-authoring-view.js +121 -62
  94. package/src/client-console.js +4 -2
  95. package/src/client-copy.js +4 -2
  96. package/src/client-feature-gates.js +7 -9
  97. package/src/client-host-compat.js +104 -27
  98. package/src/client-repl-view.js +3 -1
  99. package/src/client-styles.js +12 -1
  100. package/src/client-tool-view.js +3 -1
  101. package/src/client-transport.js +2 -2
  102. package/src/client-workbench.js +4 -3
  103. package/src/client.js +56 -17
@@ -1,8 +1,11 @@
1
+ import { isDeepStrictEqual } from 'node:util'
1
2
  import { decodeValue, encodeValue, normalizeValueWire } from './value-wire.js'
2
3
  import { normalizeDiagnostic } from './diagnostic.js'
3
4
  import { assertOwnFields, isRecord } from './record-utils.js'
4
5
  import { sessionEvents } from './session-events.js'
5
6
  import { LEGACY_LANGUAGE_SEMANTICS, normalizeLanguageSemantics } from './language-semantics.js'
7
+ import { LEGACY_USER_BINDING_TRANSFORM, historicalModuleTransformForLanguage,
8
+ moduleTransformForLanguage, normalizeModuleTransform } from './module-transform-contract.js'
6
9
  import {
7
10
  LEGACY_DEFAULT_EXPORT_BINDING,
8
11
  LEGACY_IMPORT_EXPRESSION_BOUNDARY,
@@ -16,6 +19,8 @@ import {
16
19
  REWRITES_KEY,
17
20
  RECOVERY_BOUNDARY_KEY,
18
21
  JOURNAL_VERSION,
22
+ LANGUAGE_SEMANTICS_JOURNAL_VERSION,
23
+ LANGUAGE_SEMANTICS_JOURNAL_FIELDS,
19
24
  IMPORT_BOUNDARY_JOURNAL_VERSION,
20
25
  PER_NAME_USER_BINDINGS_JOURNAL_VERSION,
21
26
  PER_NAME_JOURNAL_FIELDS,
@@ -54,8 +59,10 @@ import {
54
59
  JOURNAL_VERSIONS,
55
60
  USER_BINDINGS_REUSE_POLICIES,
56
61
  RECOVERY_BOUNDARY_FIELDS,
62
+ REPL_TOOL_NAMES,
57
63
  REWRITE_FIELDS,
58
64
  REWRITE_KINDS,
65
+ usesCallSequenceConfirms,
59
66
  } from './session-journal-schema.js'
60
67
 
61
68
  export {
@@ -78,12 +85,16 @@ function validName(name) {
78
85
  return typeof name === 'string' && /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(name)
79
86
  }
80
87
 
88
+ export function isCanonicalSequence(value) {
89
+ return Number.isSafeInteger(value) && value >= 0 && !Object.is(value, -0)
90
+ }
91
+
81
92
  function normalizeCalls(value) {
82
93
  if (!Array.isArray(value)) throw new Error('invalid dsh-ptc-plus journal calls')
83
94
  const calls = value.map((call, index) => {
84
95
  if (!isRecord(call) || typeof call.global !== 'string' || typeof call.member !== 'string'
85
96
  || !Object.hasOwn(call, 'args') || (call.ok !== true && call.ok !== false)
86
- || !Number.isSafeInteger(call.settle) || call.settle < 0) {
97
+ || !isCanonicalSequence(call.settle)) {
87
98
  throw new Error(`invalid dsh-ptc-plus journal call at index ${index}`)
88
99
  }
89
100
  assertOwnFields(call, call.ok ? CALL_SUCCESS_FIELDS : CALL_ERROR_FIELDS, `journal call at index ${index}`)
@@ -166,7 +177,7 @@ function normalizeLegacyConfirms(value, resolveLegacyConfirm) {
166
177
  throw new Error('legacy dsh-ptc-plus confirmed no-op calls require session call identity')
167
178
  }
168
179
  const confirms = value.map(callId => resolveLegacyConfirm(callId))
169
- if (confirms.some(callSeq => !Number.isSafeInteger(callSeq) || callSeq < 0)) {
180
+ if (confirms.some(callSeq => !isCanonicalSequence(callSeq))) {
170
181
  throw new Error('legacy dsh-ptc-plus confirmed no-op call is not uniquely persisted')
171
182
  }
172
183
  if (new Set(confirms).size !== confirms.length) {
@@ -177,7 +188,7 @@ function normalizeLegacyConfirms(value, resolveLegacyConfirm) {
177
188
 
178
189
  function normalizeConfirms(value) {
179
190
  if (value === undefined) return []
180
- if (!Array.isArray(value) || value.some(callSeq => !Number.isSafeInteger(callSeq) || callSeq < 0)) {
191
+ if (!Array.isArray(value) || value.some(callSeq => !isCanonicalSequence(callSeq))) {
181
192
  throw new Error('invalid dsh-ptc-plus confirmed no-op calls')
182
193
  }
183
194
  const confirms = [...new Set(value)]
@@ -251,12 +262,18 @@ function normalizeUserBindingsFingerprint(value) {
251
262
 
252
263
  function migrateJournal(value, resolveLegacyConfirm) {
253
264
  if (value.version === JOURNAL_VERSION) return value
265
+ if (value.version === LANGUAGE_SEMANTICS_JOURNAL_VERSION) {
266
+ assertOwnFields(value, LANGUAGE_SEMANTICS_JOURNAL_FIELDS, 'dsh-ptc-plus journal')
267
+ const languageSemantics = normalizeLanguageSemantics(value.languageSemantics)
268
+ return { ...value, version: JOURNAL_VERSION, moduleTransform: historicalModuleTransformForLanguage(languageSemantics) }
269
+ }
254
270
  if (value.version === PER_NAME_USER_BINDINGS_JOURNAL_VERSION) {
255
271
  assertOwnFields(value, PER_NAME_JOURNAL_FIELDS, 'dsh-ptc-plus journal')
256
- return { ...value, version: JOURNAL_VERSION, languageSemantics: LEGACY_LANGUAGE_SEMANTICS }
272
+ return { ...value, version: JOURNAL_VERSION, languageSemantics: LEGACY_LANGUAGE_SEMANTICS,
273
+ moduleTransform: LEGACY_USER_BINDING_TRANSFORM }
257
274
  }
258
275
  const shadow = { userBindingsShadowPolicy: LEGACY_USER_BINDINGS_SHADOW_POLICY, userBindingNames: null,
259
- languageSemantics: LEGACY_LANGUAGE_SEMANTICS }
276
+ languageSemantics: LEGACY_LANGUAGE_SEMANTICS, moduleTransform: LEGACY_USER_BINDING_TRANSFORM }
260
277
  if (value.version === IMPORT_BOUNDARY_JOURNAL_VERSION) {
261
278
  assertOwnFields(value, WHOLE_ENTRY_JOURNAL_FIELDS, 'dsh-ptc-plus journal')
262
279
  return { ...value, version: JOURNAL_VERSION, ...shadow }
@@ -325,6 +342,7 @@ export function normalizeJournal(value, options = {}) {
325
342
  const migrated = migrateJournal(value, options.resolveLegacyConfirm)
326
343
  assertOwnFields(migrated, JOURNAL_FIELDS, 'dsh-ptc-plus journal')
327
344
  const languageSemantics = normalizeLanguageSemantics(migrated.languageSemantics)
345
+ const moduleTransform = normalizeModuleTransform(migrated.moduleTransform, languageSemantics)
328
346
  const bindingPolicy = normalizeBindingPolicy(migrated.bindingPolicy)
329
347
  const rewritePolicy = normalizeRewritePolicy(migrated.rewritePolicy)
330
348
  const moduleSemantics = normalizeModuleSemantics(migrated.moduleSemantics)
@@ -355,6 +373,7 @@ export function normalizeJournal(value, options = {}) {
355
373
  return Object.freeze({
356
374
  version: JOURNAL_VERSION,
357
375
  languageSemantics,
376
+ moduleTransform,
358
377
  bindingPolicy,
359
378
  rewritePolicy,
360
379
  moduleSemantics,
@@ -374,7 +393,7 @@ export function normalizeJournal(value, options = {}) {
374
393
 
375
394
  /** Validate the required persisted relation that makes one edit result executable history. */
376
395
  export function normalizeDerivedEditResult(meta, expectedTargetCallSeq) {
377
- if (!Number.isSafeInteger(expectedTargetCallSeq) || expectedTargetCallSeq < 0) {
396
+ if (!isCanonicalSequence(expectedTargetCallSeq)) {
378
397
  throw new Error('derived edit does not identify an eligible target call')
379
398
  }
380
399
  if (!isRecord(meta)) throw new Error('invalid dsh-ptc-plus derived edit metadata')
@@ -382,7 +401,8 @@ export function normalizeDerivedEditResult(meta, expectedTargetCallSeq) {
382
401
  const derived = meta[DERIVED_RUN_KEY]
383
402
  if (!isRecord(target)) throw new Error('invalid dsh-ptc-plus edit target metadata')
384
403
  assertOwnFields(target, EDIT_TARGET_FIELDS, 'edit target metadata')
385
- if (target.targetCallSeq !== expectedTargetCallSeq) {
404
+ if (!isCanonicalSequence(target.targetCallSeq)
405
+ || target.targetCallSeq !== expectedTargetCallSeq) {
386
406
  throw new Error('derived edit target does not match the eligible target call')
387
407
  }
388
408
  if (!isRecord(derived) || typeof derived.code !== 'string' || typeof derived.description !== 'string') {
@@ -476,13 +496,13 @@ export function recoveryBoundariesEqual(left, right) {
476
496
 
477
497
  function normalizeRecoveryBoundaryValue(value, eventSeq = undefined) {
478
498
  if (!isRecord(value)
479
- || !Number.isSafeInteger(value.failedCallSeq) || value.failedCallSeq < 0
499
+ || !isCanonicalSequence(value.failedCallSeq)
480
500
  || (value.frontierCallSeq !== null
481
- && (!Number.isSafeInteger(value.frontierCallSeq) || value.frontierCallSeq < 0))) {
501
+ && !isCanonicalSequence(value.frontierCallSeq))) {
482
502
  throw new Error('invalid dsh-ptc-plus recovery boundary')
483
503
  }
484
504
  assertOwnFields(value, RECOVERY_BOUNDARY_FIELDS, 'recovery boundary')
485
- if (eventSeq !== undefined && (!Number.isSafeInteger(eventSeq) || eventSeq < 0)) {
505
+ if (eventSeq !== undefined && !isCanonicalSequence(eventSeq)) {
486
506
  throw new Error('invalid dsh-ptc-plus recovery boundary event sequence')
487
507
  }
488
508
  return {
@@ -507,30 +527,399 @@ export function withRecoveryBoundaries(meta, boundaries) {
507
527
  return base
508
528
  }
509
529
 
530
+ const HOST_SEQUENCE_RELATION_POLICY_V0 = new Map([
531
+ ['agent-preset/selected', null],
532
+ ['agent/inbox/spliced', null],
533
+ ['approval/asked', null],
534
+ ['approval/decided', null],
535
+ ['approval/policy', null],
536
+ ['assistant/chunk', null],
537
+ ['assistant/message', null],
538
+ ['command/done', 'command-source'],
539
+ ['command/run', null],
540
+ // Released format 0 also wrote the legacy spellings of the compaction events
541
+ // and a steering message that its own v0-to-v1 stage normalizes to
542
+ // `compaction/*` and `user/message`. They belong to the same frozen
543
+ // vocabulary, so the retired-boundary converter accepts them with their
544
+ // normalized relation semantics. `request/header-delta` and `mode/set` stay
545
+ // unsupported because that stage refuses them as well.
546
+ ['compact/end', null],
547
+ ['compact/prune', 'compaction-shadow'],
548
+ ['compact/start', null],
549
+ ['compact/summary', 'compaction-shadow'],
550
+ ['compaction/end', null],
551
+ ['compaction/prune', 'compaction-shadow'],
552
+ ['compaction/start', null],
553
+ ['compaction/summary', 'compaction-shadow'],
554
+ ['feedback/record', null],
555
+ ['goal/change', null],
556
+ ['hook/invoked', null],
557
+ ['hook/result', null],
558
+ ['llm/retry', null],
559
+ ['llm/retry-started', null],
560
+ ['model/selection', null],
561
+ ['permission/preset', null],
562
+ ['plan/mode', null],
563
+ ['request/context', null],
564
+ ['request/header', null],
565
+ ['sandbox/mode', null],
566
+ ['schedule/change', null],
567
+ ['session-log-deepseek/delivery-accepted', 'delivery-through'],
568
+ ['session/end-seed', null],
569
+ ['session/title', 'title-messages'],
570
+ ['session/title-llm-request', 'title-request-messages'],
571
+ ['step/end', null],
572
+ ['step/start', null],
573
+ ['steering/message', null],
574
+ ['subagent/descriptor', null],
575
+ ['subagent/model-selection-policy', null],
576
+ ['team/member', null],
577
+ ['team/message/delivered', null],
578
+ ['team/message/queued', null],
579
+ ['team/task', null],
580
+ ['todo/write', null],
581
+ ['tool-workflow/agent-end', null],
582
+ ['tool-workflow/agent-start', null],
583
+ ['tool-workflow/run-end', null],
584
+ ['tool-workflow/run-start', null],
585
+ ['tool/call', null],
586
+ ['tool/code-dispatch', null],
587
+ ['tool/code-dispatch-start', null],
588
+ ['tool/result', null],
589
+ ['turn/end', null],
590
+ ['turn/start', null],
591
+ ['user/message', null],
592
+ ['web/deepseek-search-llm-request', null],
593
+ ])
594
+
595
+ const V0_SURFACE_EVENT_TYPES = new Set([
596
+ 'user/message', 'steering/message', 'assistant/message', 'tool/result',
597
+ ])
598
+ const HOST_DATA_SEQUENCE_RELATIONS_V0 = new Map([
599
+ ['command/done', new Set(['sourceEventSeq'])],
600
+ ['compact/prune', new Set(['shadowedSeqs'])],
601
+ ['compact/summary', new Set(['shadowedSeqs'])],
602
+ ['compaction/prune', new Set(['shadowedSeqs'])],
603
+ ['compaction/summary', new Set(['shadowedSeqs'])],
604
+ ['session-log-deepseek/delivery-accepted', new Set(['throughSeq'])],
605
+ ['session/title', new Set(['messageSeqs'])],
606
+ ['session/title-llm-request', new Set(['messageSeqs'])],
607
+ ])
608
+ const SEQUENCE_RELATION_FIELD = /Seqs?$/u
609
+
610
+ function validateKnownDataSequenceRelations(event) {
611
+ if (!isRecord(event.data)) return
612
+ const allowed = HOST_DATA_SEQUENCE_RELATIONS_V0.get(event.type) ?? new Set()
613
+ for (const field of Reflect.ownKeys(event.data)) {
614
+ if (typeof field === 'string' && SEQUENCE_RELATION_FIELD.test(field) && !allowed.has(field)) {
615
+ throw new Error(`unsupported ${event.type} data sequence relation ${JSON.stringify(field)}`)
616
+ }
617
+ }
618
+ }
619
+
620
+ function validateEarlierEventReference(events, event, seq, label) {
621
+ if (!isCanonicalSequence(seq) || seq >= event.seq || events[seq]?.seq !== seq) {
622
+ throw new Error(`invalid ${label} event reference ${JSON.stringify(seq)} at event ${event.seq}`)
623
+ }
624
+ return events[seq]
625
+ }
626
+
627
+ function validateEarlierEventReferences(events, event, seqs, label) {
628
+ if (!Array.isArray(seqs)) throw new Error(`invalid ${label} event references`)
629
+ const seen = new Set()
630
+ return seqs.map(seq => {
631
+ if (seen.has(seq)) throw new Error(`duplicate ${label} event reference ${JSON.stringify(seq)} at event ${event.seq}`)
632
+ seen.add(seq)
633
+ return validateEarlierEventReference(events, event, seq, label)
634
+ })
635
+ }
636
+
637
+ function sameSequences(left, right) {
638
+ return left.length === right.length && left.every((seq, index) => seq === right[index])
639
+ }
640
+
641
+ function validateSurfaceRelation(events, event, surface) {
642
+ const eligible = V0_SURFACE_EVENT_TYPES.has(event.type)
643
+ if (!eligible) {
644
+ if (event.surfaceOp !== undefined || event.sourceEventSeqs !== undefined) {
645
+ throw new Error(`session event ${JSON.stringify(event.type)} is not surface-eligible`)
646
+ }
647
+ return
648
+ }
649
+ if (event.sourceEventSeqs !== undefined) {
650
+ validateEarlierEventReferences(events, event, event.sourceEventSeqs, 'source')
651
+ }
652
+ if (event.sourceEventSeqs?.length === 0 && event.type !== 'assistant/message') {
653
+ throw new Error('source event references must not be empty except on assistant/message')
654
+ }
655
+ const op = event.surfaceOp
656
+ // Historical host formats predate the mandatory append marker.
657
+ if (op === undefined || op === 'append') {
658
+ surface.push(event.seq)
659
+ return
660
+ }
661
+ if (!isRecord(op) || op.op !== 'replace'
662
+ || Reflect.ownKeys(op).length !== 3
663
+ || !isCanonicalSequence(op.start)
664
+ || !isCanonicalSequence(op.end)) {
665
+ throw new Error('invalid surface operation during recovery-boundary migration')
666
+ }
667
+ validateEarlierEventReference(events, event, op.start, 'surface replacement start')
668
+ validateEarlierEventReference(events, event, op.end, 'surface replacement end')
669
+ const start = surface.indexOf(op.start)
670
+ const end = surface.indexOf(op.end)
671
+ if (start < 0 || end < start) {
672
+ throw new Error(`surface replacement at event ${event.seq} does not identify a current ordered surface range`)
673
+ }
674
+ const shadowed = surface.slice(start, end + 1)
675
+ const provenance = new Set(event.sourceEventSeqs ?? [])
676
+ if (shadowed.some(seq => !provenance.has(seq))) {
677
+ throw new Error(`surface replacement at event ${event.seq} does not cite every replaced surface node`)
678
+ }
679
+ if (event.type === 'tool/result'
680
+ && (shadowed.length !== 1 || events[shadowed[0]]?.type !== 'tool/result')) {
681
+ throw new Error('tool/result surface replacement must target one current tool/result')
682
+ }
683
+ if (event.type === 'tool/result') {
684
+ const withoutContent = candidate => {
685
+ const data = candidate?.data
686
+ const message = data?.message
687
+ const content = message?.content
688
+ if (!isRecord(data) || !isRecord(message) || !Array.isArray(content)
689
+ || content.length !== 1 || !isRecord(content[0])) {
690
+ throw new Error('tool/result surface replacement has invalid message content')
691
+ }
692
+ return {
693
+ ...data,
694
+ message: { ...message, content: [{ ...content[0], content: null }] },
695
+ }
696
+ }
697
+ if (!isDeepStrictEqual(withoutContent(events[shadowed[0]]), withoutContent(event))) {
698
+ throw new Error('tool/result surface replacement may change only content')
699
+ }
700
+ }
701
+ surface.splice(start, shadowed.length, event.seq)
702
+ }
703
+
704
+ function validateCommandSource(events, event) {
705
+ if (!isRecord(event.data)) {
706
+ throw new Error(`command/done event ${event.seq} has invalid data`)
707
+ }
708
+ if (event.data.sourceEventSeq === undefined) return
709
+ if (!isRecord(event.data) || event.data.kind !== 'success') {
710
+ throw new Error(`command/done event ${event.seq} has a source outside a successful result`)
711
+ }
712
+ const source = validateEarlierEventReference(events, event, event.data.sourceEventSeq, 'command source')
713
+ if (source.type === 'command/run' || source.type === 'command/done') {
714
+ throw new Error(`command/done event ${event.seq} cites a command lifecycle event as its source`)
715
+ }
716
+ }
717
+
718
+ function validateTitleRequestSources(events, event) {
719
+ if (!isRecord(event.data) || !Array.isArray(event.data.messageSeqs)) {
720
+ throw new Error('invalid session/title-llm-request message references')
721
+ }
722
+ const sources = validateEarlierEventReferences(
723
+ events, event, event.data.messageSeqs, 'session title request message',
724
+ )
725
+ if (sources.some(source => source.type !== 'user/message')) {
726
+ throw new Error(`session/title-llm-request event ${event.seq} must cite earlier user/message events`)
727
+ }
728
+ }
729
+
730
+ function validateDeliveryThrough(events, event) {
731
+ if (!isRecord(event.data)) {
732
+ throw new Error(`session-log-deepseek/delivery-accepted event ${event.seq} has invalid data`)
733
+ }
734
+ validateEarlierEventReference(events, event, event.data.throughSeq, 'session delivery through')
735
+ }
736
+
737
+ function validateTitleSources(events, event) {
738
+ if (!isRecord(event.data) || !Array.isArray(event.data.messageSeqs)) {
739
+ throw new Error('invalid session/title message references')
740
+ }
741
+ if ((event.data.messageSeqs.length === 0) !== (event.data.source?.kind === 'user')) {
742
+ const requirement = event.data.source?.kind === 'user'
743
+ ? 'cite no message seqs'
744
+ : 'cite at least one message seq'
745
+ throw new Error(`session/title event ${event.seq} must ${requirement}`)
746
+ }
747
+ const sources = validateEarlierEventReferences(events, event, event.data.messageSeqs, 'session title message')
748
+ if (sources.some(source => source.type !== 'user/message' || source.data?.source?.kind !== 'user')) {
749
+ throw new Error(`session/title event ${event.seq} must cite earlier human user/message events`)
750
+ }
751
+ }
752
+
753
+ function validateCompactionSources(events, event, surface) {
754
+ if (!isRecord(event.data) || !Array.isArray(event.data.shadowedSeqs)) {
755
+ throw new Error(`invalid ${event.type} shadow references`)
756
+ }
757
+ const shadowed = event.data.shadowedSeqs
758
+ validateEarlierEventReferences(events, event, shadowed, 'compaction shadow')
759
+ if (event.data.shadowedRange === undefined) {
760
+ if (shadowed.length === 0) return
761
+ const start = surface.indexOf(shadowed[0])
762
+ if (start < 0 || !sameSequences(surface.slice(start, start + shadowed.length), shadowed)) {
763
+ throw new Error(`${event.type} event ${event.seq} shadow list does not match the current surface`)
764
+ }
765
+ return
766
+ }
767
+ const range = event.data.shadowedRange
768
+ if (!isRecord(range)) throw new Error(`invalid ${event.type} shadow range`)
769
+ validateEarlierEventReference(events, event, range.start, 'compaction range start')
770
+ validateEarlierEventReference(events, event, range.end, 'compaction range end')
771
+ const start = surface.indexOf(range.start)
772
+ const end = surface.indexOf(range.end)
773
+ if (start < 0 || end < start
774
+ || !sameSequences(surface.slice(start, end + 1), shadowed)) {
775
+ throw new Error(`${event.type} event ${event.seq} shadow range does not match the current surface`)
776
+ }
777
+ }
778
+
779
+ function validateHostSequenceRelations(events) {
780
+ const surface = []
781
+ for (const event of events) {
782
+ if (event.type === RECOVERY_BOUNDARY_EVENT) continue
783
+ if (!HOST_SEQUENCE_RELATION_POLICY_V0.has(event.type)) {
784
+ throw new Error(`unsupported session event type ${JSON.stringify(event.type)} during recovery-boundary migration`)
785
+ }
786
+ validateKnownDataSequenceRelations(event)
787
+ validateSurfaceRelation(events, event, surface)
788
+ const relation = HOST_SEQUENCE_RELATION_POLICY_V0.get(event.type)
789
+ if (relation === 'command-source') validateCommandSource(events, event)
790
+ else if (relation === 'title-messages') validateTitleSources(events, event)
791
+ else if (relation === 'title-request-messages') validateTitleRequestSources(events, event)
792
+ else if (relation === 'delivery-through') validateDeliveryThrough(events, event)
793
+ else if (relation === 'compaction-shadow') validateCompactionSources(events, event, surface)
794
+ }
795
+ }
796
+
797
+ function rewriteHostSequenceRelations(event, mapEvent) {
798
+ const relation = HOST_SEQUENCE_RELATION_POLICY_V0.get(event.type)
799
+ if (Object.hasOwn(event, 'sourceEventSeqs')) {
800
+ event.sourceEventSeqs = event.sourceEventSeqs.map(seq => mapEvent(seq, 'source'))
801
+ }
802
+ if (event.surfaceOp !== undefined && event.surfaceOp !== 'append') {
803
+ event.surfaceOp = {
804
+ ...event.surfaceOp,
805
+ start: mapEvent(event.surfaceOp.start, 'surface replacement start'),
806
+ end: mapEvent(event.surfaceOp.end, 'surface replacement end'),
807
+ }
808
+ }
809
+ if (relation === 'command-source') {
810
+ if (event.data.sourceEventSeq !== undefined) {
811
+ event.data = {
812
+ ...event.data,
813
+ sourceEventSeq: mapEvent(event.data.sourceEventSeq, 'command source'),
814
+ }
815
+ }
816
+ } else if (relation === 'title-messages') {
817
+ event.data = {
818
+ ...event.data,
819
+ messageSeqs: event.data.messageSeqs.map(seq => mapEvent(seq, 'session title message')),
820
+ }
821
+ } else if (relation === 'title-request-messages') {
822
+ const mapped = event.data.messageSeqs.map(seq => mapEvent(seq, 'session title request message'))
823
+ if (mapped.some((seq, index) => seq !== event.data.messageSeqs[index])) {
824
+ throw new Error('session/title-llm-request messageSeqs cannot be preserved across removed events')
825
+ }
826
+ } else if (relation === 'delivery-through') {
827
+ event.data = {
828
+ ...event.data,
829
+ throughSeq: mapEvent(event.data.throughSeq, 'session delivery through'),
830
+ }
831
+ } else if (relation === 'compaction-shadow') {
832
+ const data = {
833
+ ...event.data,
834
+ shadowedSeqs: event.data.shadowedSeqs.map(seq => mapEvent(seq, 'compaction shadow')),
835
+ }
836
+ if (event.data.shadowedRange !== undefined) {
837
+ data.shadowedRange = {
838
+ ...event.data.shadowedRange,
839
+ start: mapEvent(event.data.shadowedRange.start, 'compaction range start'),
840
+ end: mapEvent(event.data.shadowedRange.end, 'compaction range end'),
841
+ }
842
+ }
843
+ event.data = data
844
+ }
845
+ return event
846
+ }
847
+
510
848
  /**
511
849
  * Convert the retired custom boundary event in a raw log before DSH restores it.
512
850
  * The returned log is a detached, renumbered copy; the input is never mutated.
513
851
  */
514
852
  export function migrateRecoveryBoundaryEvents(events) {
515
853
  if (!Array.isArray(events)) throw new TypeError('recovery-boundary migration expects an event array')
854
+ const sequenceMap = new Map()
855
+ let nextSeq = 0
856
+ for (const [index, event] of events.entries()) {
857
+ if (!isCanonicalSequence(event?.seq)) {
858
+ throw new Error('invalid session event sequence during recovery-boundary migration')
859
+ }
860
+ if (sequenceMap.has(event.seq)) {
861
+ throw new Error(`duplicate session event sequence ${event.seq} during recovery-boundary migration`)
862
+ }
863
+ if (event.seq !== index) {
864
+ throw new Error(`session event sequence ${event.seq} at index ${index} is not contiguous from zero`)
865
+ }
866
+ const retainedSeq = event.type === RECOVERY_BOUNDARY_EVENT ? undefined : nextSeq++
867
+ sequenceMap.set(event.seq, { event, retainedSeq })
868
+ }
869
+ validateHostSequenceRelations(events)
870
+ const mapEvent = (seq, label) => {
871
+ if (!isCanonicalSequence(seq)) throw new Error(`invalid ${label} event reference`)
872
+ const target = sequenceMap.get(seq)
873
+ if (target === undefined || target.retainedSeq === undefined) {
874
+ throw new Error(`unmapped ${label} event reference ${seq}`)
875
+ }
876
+ return target.retainedSeq
877
+ }
878
+ const mapCall = (seq, resultSeq, label) => {
879
+ const target = sequenceMap.get(seq)
880
+ if (!isCanonicalSequence(seq) || target === undefined
881
+ || target.retainedSeq === undefined || target.event?.type !== 'tool/call'
882
+ || !REPL_TOOL_NAMES.has(target.event.data?.name) || seq >= resultSeq) {
883
+ throw new Error(`unproved ${label} call reference ${JSON.stringify(seq)} at event ${resultSeq}`)
884
+ }
885
+ return target.retainedSeq
886
+ }
887
+ const mapBoundary = (boundary, resultSeq) => ({
888
+ failedCallSeq: mapCall(boundary.failedCallSeq, resultSeq, 'recovery-boundary failure'),
889
+ frontierCallSeq: boundary.frontierCallSeq === null ? null : (() => {
890
+ if (boundary.frontierCallSeq >= boundary.failedCallSeq) {
891
+ throw new Error('recovery-boundary frontier must precede its failed call')
892
+ }
893
+ return mapCall(boundary.frontierCallSeq, resultSeq, 'recovery-boundary frontier')
894
+ })(),
895
+ })
516
896
  const migrated = []
517
897
  const pending = []
898
+ const boundaryOrigins = new Map()
518
899
  for (const event of events) {
519
900
  if (event?.type === RECOVERY_BOUNDARY_EVENT) {
520
- if (!Number.isSafeInteger(event.seq) || event.seq < 0) {
521
- throw new Error('invalid dsh-ptc-plus recovery boundary event sequence')
522
- }
523
- pending.push(normalizeRecoveryBoundaryValue(event.data))
901
+ pending.push({ boundary: normalizeRecoveryBoundaryValue(event.data), eventSeq: event.seq })
524
902
  continue
525
903
  }
526
904
  const detached = cloneJson(event)
527
- if (pending.length > 0 && detached?.type === 'tool/result') {
528
- const data = isRecord(detached.data) ? { ...detached.data } : {}
529
- const meta = isRecord(data.meta) ? { ...data.meta } : {}
905
+ const sourceSeqs = detached?.sourceEventSeqs
906
+ const source = Array.isArray(sourceSeqs) && sourceSeqs.length === 1
907
+ ? sequenceMap.get(sourceSeqs[0]) : undefined
908
+ const carriesJournal = detached?.type === 'tool/result'
909
+ && isRecord(detached.data?.meta) && Object.hasOwn(detached.data.meta, JOURNAL_KEY)
910
+ && source?.retainedSeq !== undefined && source.event?.type === 'tool/call'
911
+ && REPL_TOOL_NAMES.has(source.event.data?.name) && source.event.seq < event.seq
912
+ if (pending.length > 0 && carriesJournal) {
913
+ const data = { ...detached.data }
914
+ const meta = { ...data.meta }
530
915
  const existing = meta[RECOVERY_BOUNDARY_KEY] === undefined
531
916
  ? []
532
917
  : normalizeRecoveryBoundaries(meta[RECOVERY_BOUNDARY_KEY])
533
- meta[RECOVERY_BOUNDARY_KEY] = [...existing, ...pending]
918
+ meta[RECOVERY_BOUNDARY_KEY] = [...pending.map(item => item.boundary), ...existing]
919
+ boundaryOrigins.set(event.seq, {
920
+ pendingCount: pending.length,
921
+ pendingEventSeqs: pending.map(item => item.eventSeq),
922
+ })
534
923
  data.meta = meta
535
924
  detached.data = data
536
925
  pending.length = 0
@@ -538,19 +927,53 @@ export function migrateRecoveryBoundaryEvents(events) {
538
927
  migrated.push(detached)
539
928
  }
540
929
  if (pending.length > 0) {
541
- throw new Error('recovery boundary has no later tool/result settlement for migration')
930
+ throw new Error('recovery boundary has no later PTC journal result for migration')
542
931
  }
543
- const sequenceMap = new Map()
544
- for (const [index, event] of migrated.entries()) {
545
- if (Number.isSafeInteger(event?.seq) && event.seq >= 0) sequenceMap.set(event.seq, index)
932
+ return Object.freeze(migrated.map((event) => {
933
+ const oldSeq = event.seq
934
+ rewriteHostSequenceRelations(event, mapEvent)
935
+ if (event.type === 'tool/result' && isRecord(event.data?.meta)) {
936
+ const meta = event.data.meta
937
+ const journal = meta[JOURNAL_KEY]
938
+ if (usesCallSequenceConfirms(journal) && journal.confirms !== undefined) {
939
+ if (!Array.isArray(journal.confirms)) throw new Error('invalid dsh-ptc-plus confirmed no-op calls')
940
+ journal.confirms = journal.confirms.map(seq => mapCall(seq, oldSeq, 'journal confirmation'))
941
+ }
942
+ if (meta[EDIT_TARGET_KEY] !== undefined) {
943
+ if (!isRecord(meta[EDIT_TARGET_KEY])) throw new Error('invalid dsh-ptc-plus edit target')
944
+ meta[EDIT_TARGET_KEY].targetCallSeq = mapCall(
945
+ meta[EDIT_TARGET_KEY].targetCallSeq, oldSeq, 'edit target',
946
+ )
947
+ }
948
+ if (meta[RECOVERY_BOUNDARY_KEY] !== undefined) {
949
+ const origins = boundaryOrigins.get(oldSeq)
950
+ meta[RECOVERY_BOUNDARY_KEY] = normalizeRecoveryBoundaries(meta[RECOVERY_BOUNDARY_KEY])
951
+ .map((boundary, index) => mapBoundary(boundary,
952
+ origins !== undefined && index < origins.pendingCount
953
+ ? origins.pendingEventSeqs[index]
954
+ : oldSeq))
955
+ }
956
+ }
957
+ event.seq = sequenceMap.get(oldSeq).retainedSeq
958
+ return Object.freeze(event)
959
+ }))
960
+ }
961
+
962
+ function currentAssistantCallSeq(events, candidates, callId, toolName) {
963
+ let assistantIndex = events.length - 1
964
+ while (assistantIndex >= 0 && events[assistantIndex]?.type !== 'assistant/message') {
965
+ assistantIndex -= 1
546
966
  }
547
- return Object.freeze(migrated.map((event, index) => Object.freeze({
548
- ...event,
549
- seq: index,
550
- ...(Array.isArray(event.sourceEventSeqs)
551
- ? { sourceEventSeqs: event.sourceEventSeqs.map(seq => sequenceMap.get(seq) ?? seq) }
552
- : {}),
553
- })))
967
+ if (assistantIndex < 0) return undefined
968
+ const content = events[assistantIndex].data?.message?.content
969
+ if (!Array.isArray(content)) return undefined
970
+ const blocks = content.filter(part => part?.type === 'tool-call'
971
+ && part.id === callId && part.name === toolName)
972
+ if (blocks.length !== 1) return undefined
973
+ const eventIndexes = new Map(events.map((event, index) => [event, index]))
974
+ const current = candidates.filter(event => eventIndexes.get(event) > assistantIndex
975
+ && event.data.arguments === blocks[0].arguments)
976
+ return current.length === 1 ? current[0].seq : undefined
554
977
  }
555
978
 
556
979
  /** Resolve the persisted event identity for one named tool call being dispatched. */
@@ -559,31 +982,47 @@ export function liveToolCallSeq(session, callId, toolName) {
559
982
  if (!Array.isArray(events) || typeof callId !== 'string' || callId.length === 0
560
983
  || typeof toolName !== 'string' || toolName.length === 0) return undefined
561
984
 
562
- const pairedCallSeqs = new Set()
985
+ const pendingCalls = new Map()
563
986
  for (const event of events) {
564
- if (event?.type !== 'tool/result' || !Array.isArray(event.sourceEventSeqs)) continue
565
- for (const sourceSeq of event.sourceEventSeqs) {
566
- if (Number.isSafeInteger(sourceSeq) && sourceSeq >= 0) pairedCallSeqs.add(sourceSeq)
987
+ if (event?.type === 'tool/call' && event.data?.callId === callId) {
988
+ if (event.data.name === toolName && !isCanonicalSequence(event.seq)) {
989
+ throw new Error(`current ${toolName} call has an invalid session event sequence`)
990
+ }
991
+ if (isCanonicalSequence(event.seq)) pendingCalls.set(event.seq, event)
992
+ continue
993
+ }
994
+ if (event?.type !== 'tool/result') continue
995
+ const sourceRelation = event.sourceEventSeqs
996
+ const canonicalSourceRelation = Array.isArray(sourceRelation)
997
+ && sourceRelation.length === 1
998
+ && isCanonicalSequence(sourceRelation[0])
999
+ if (canonicalSourceRelation) {
1000
+ pendingCalls.delete(sourceRelation[0])
1001
+ continue
1002
+ }
1003
+ // A damaged result cannot recover state, but its ordered call identity can
1004
+ // still prove that the sole preceding same-id call is no longer live.
1005
+ if (event.data?.message?.source?.callId === callId
1006
+ && pendingCalls.size === 1) {
1007
+ pendingCalls.clear()
567
1008
  }
568
1009
  }
569
1010
 
570
- const candidates = []
571
- for (const event of events) {
572
- if (event?.type !== 'tool/call' || event.data?.name !== toolName
573
- || event.data.callId !== callId || pairedCallSeqs.has(event.seq)) continue
574
- if (!Number.isSafeInteger(event.seq) || event.seq < 0) {
575
- throw new Error(`current ${toolName} call has an invalid session event sequence`)
576
- }
577
- candidates.push(event.seq)
1011
+ const candidates = [...pendingCalls.values()]
1012
+ .filter(event => event.data?.name === toolName)
1013
+ if (candidates.length > 1) {
1014
+ const current = currentAssistantCallSeq(events, candidates, callId, toolName)
1015
+ if (current !== undefined) return current
578
1016
  }
579
1017
  if (candidates.length > 1) {
580
1018
  throw new Error(`session log contains multiple unpaired ${toolName} calls for callId ${JSON.stringify(callId)}`)
581
1019
  }
582
- return candidates[0]
1020
+ return candidates[0]?.seq
583
1021
  }
584
1022
 
585
1023
  /** Start a mutable journal for one live cell. */
586
- export function createJournal(confirms = [], bindingPolicy, rewritePolicy, languageSemantics = LEGACY_LANGUAGE_SEMANTICS) {
1024
+ export function createJournal(confirms = [], bindingPolicy, rewritePolicy, languageSemantics = LEGACY_LANGUAGE_SEMANTICS,
1025
+ moduleTransform = moduleTransformForLanguage(languageSemantics)) {
587
1026
  if (typeof bindingPolicy === 'string') {
588
1027
  if (!BINDING_MODES.has(bindingPolicy)) throw new TypeError('invalid dsh-ptc-plus journal binding mode')
589
1028
  bindingPolicy = {
@@ -591,9 +1030,11 @@ export function createJournal(confirms = [], bindingPolicy, rewritePolicy, langu
591
1030
  functionClassRedeclarations: false,
592
1031
  }
593
1032
  }
1033
+ languageSemantics = normalizeLanguageSemantics(languageSemantics)
594
1034
  return {
595
1035
  version: JOURNAL_VERSION,
596
- languageSemantics: normalizeLanguageSemantics(languageSemantics),
1036
+ languageSemantics,
1037
+ moduleTransform: normalizeModuleTransform(moduleTransform, languageSemantics),
597
1038
  bindingPolicy: normalizeBindingPolicy(bindingPolicy),
598
1039
  rewritePolicy: normalizeRewritePolicy(rewritePolicy),
599
1040
  moduleSemantics: normalizeModuleSemantics(LIVE_MODULE_SEMANTICS),