dsh-ptc-plus 0.4.10 → 0.5.0

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 (122) hide show
  1. package/README.en.md +60 -130
  2. package/README.md +60 -130
  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-repl-workspace-en.png +0 -0
  6. package/assets/ptc-plus-repl-workspace-zh.png +0 -0
  7. package/assets/ptc-plus-settings-en.png +0 -0
  8. package/assets/ptc-plus-settings-zh.png +0 -0
  9. package/client.cjs +42336 -0
  10. package/cordis.patch.yml +1 -0
  11. package/docs/README.md +6 -2
  12. package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +31 -198
  13. package/docs/adr/0008-define-agent-operating-contract.md +5 -1
  14. package/docs/adr/0009-bound-expensive-acceptance-default.md +2 -2
  15. package/docs/adr/0016-archive-resolved-review-findings.md +1 -1
  16. package/docs/adr/0017-track-the-latest-dsh-public-surface.md +2 -2
  17. package/docs/adr/0019-plugin-settings-and-kill-switch.md +1 -1
  18. package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +1 -1
  19. package/docs/adr/0022-stage-npm-releases-with-oidc.md +4 -2
  20. package/docs/adr/0023-global-user-bindings.md +2 -0
  21. package/docs/adr/0026-persist-the-delivery-verdict-against-the-verified-candidate.md +2 -0
  22. package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +25 -23
  23. package/docs/adr/0031-close-semantic-obligations-by-owner.md +1 -1
  24. package/docs/adr/0034-keep-program-values-separate-from-output-projections.md +29 -0
  25. package/docs/adr/0035-run-coverage-only-before-a-new-release.md +27 -0
  26. package/docs/adr/0036-separate-focused-development-from-delivery-verification.md +29 -0
  27. package/docs/adr/0037-keep-tool-hover-previews-passive.md +52 -0
  28. package/docs/adr/0038-freeze-session-capability-plans.md +71 -0
  29. package/docs/adr/0039-verify-documentation-without-runtime-suites.md +29 -0
  30. package/docs/adr/0040-separate-capability-configurations-from-session-observations.md +39 -0
  31. package/docs/adr/0041-record-cell-execution-directories.md +65 -0
  32. package/docs/adr/0042-persist-provider-availability-and-handback.md +41 -0
  33. package/docs/adr/0043-compose-capability-and-context-policies.md +37 -0
  34. package/docs/adr/README.md +6 -0
  35. package/docs/architecture.md +23 -25
  36. package/docs/capability-planning-components.md +93 -0
  37. package/docs/capability-projection.md +7 -0
  38. package/docs/client-ui.md +70 -23
  39. package/docs/client-visual-contract.md +26 -9
  40. package/docs/context-management.md +22 -0
  41. package/docs/context-optimization-plan.md +353 -0
  42. package/docs/context-public-interface-coverage.md +25 -0
  43. package/docs/durability-design.md +20 -15
  44. package/docs/evaluation.md +41 -6
  45. package/docs/evaluations/command-goat-deepseek-v4.1-flash-20261006.md +126 -0
  46. package/docs/evaluations/command-goat-deepseek-v4.1-flash.md +281 -0
  47. package/docs/installation.md +31 -10
  48. package/docs/program-data-plane.md +5 -3
  49. package/docs/publishing.md +5 -5
  50. package/docs/release-coverage.md +73 -0
  51. package/docs/release-notes.md +1 -1
  52. package/docs/review-procedure.md +56 -0
  53. package/docs/runtime-reference.md +48 -16
  54. package/docs/semantic-validation.md +14 -2
  55. package/docs/stateful-binding-updates-proposal.md +2 -2
  56. package/docs/user-bindings.md +1 -1
  57. package/docs/value-wire.md +11 -1
  58. package/docs/verification-optimization-work.md +26 -15
  59. package/docs/verification.md +30 -88
  60. package/index.js +123 -20
  61. package/internal/binding-toggle-receipt.js +24 -0
  62. package/internal/capability-command-receipt.js +29 -0
  63. package/internal/capability-environment.js +1247 -0
  64. package/internal/capability-plan.js +285 -0
  65. package/internal/capability-policy.js +96 -0
  66. package/internal/capability-statistics-store.js +180 -0
  67. package/internal/capability-statistics.js +210 -0
  68. package/internal/config-spec.js +79 -9
  69. package/internal/direct-surface-owner.js +27 -29
  70. package/internal/dynamic-environment-runtime.js +9 -9
  71. package/internal/edit-transport-owner.js +180 -139
  72. package/internal/execution-provider-owner.js +15 -12
  73. package/internal/execution-seam-compat.js +8 -8
  74. package/internal/kernel-worker.js +104 -32
  75. package/internal/native-root-dynamic.js +12 -13
  76. package/internal/program-bindings.js +190 -0
  77. package/internal/recovery-tips.js +3 -2
  78. package/internal/rejected-cell-editor.js +8 -8
  79. package/internal/repl-observation-interest.js +11 -3
  80. package/internal/rpc-contract.js +1 -0
  81. package/internal/runtime-bridge-owner.js +272 -34
  82. package/internal/runtime-config.js +58 -0
  83. package/internal/runtime-contexts.js +74 -5
  84. package/internal/runtime-intrinsics.js +1 -0
  85. package/internal/runtime-messages.js +41 -0
  86. package/internal/sdk-projection.js +10 -82
  87. package/internal/session-cell-executor.js +42 -19
  88. package/internal/session-events.js +34 -0
  89. package/internal/session-journal-recovery.js +234 -11
  90. package/internal/session-journal-schema.js +6 -3
  91. package/internal/session-journal.js +31 -2
  92. package/internal/session-log-view.js +1 -1
  93. package/internal/session-runtime.js +119 -15
  94. package/internal/settings-compat.js +12 -5
  95. package/internal/stateful-module-compiler.js +3 -2
  96. package/internal/stateful-module-runtime.js +7 -3
  97. package/internal/stateful-root-runtime.js +9 -4
  98. package/internal/user-binding-authoring.js +2 -7
  99. package/internal/user-bindings-owner.js +41 -2
  100. package/internal/validated-parse-repair.js +1 -1
  101. package/internal/value-wire.js +68 -21
  102. package/internal/worker-cwd-virtualization.js +20 -18
  103. package/internal/worker-realm-surfaces.js +3 -3
  104. package/package.json +47 -29
  105. package/src/client-authoring-view.js +67 -30
  106. package/src/client-binding-review.js +2 -1
  107. package/src/client-binding-toggle.js +18 -0
  108. package/src/client-capabilities-view.js +1341 -0
  109. package/src/client-capability-sort.js +29 -0
  110. package/src/client-copy.js +871 -8
  111. package/src/client-feature-gates.js +3 -0
  112. package/src/client-indicator-view.js +4 -6
  113. package/src/client-primitives.js +33 -2
  114. package/src/client-repl-view.js +20 -13
  115. package/src/client-settings-view.js +411 -41
  116. package/src/client-styles.js +108 -15
  117. package/src/client-tool-hover.js +185 -0
  118. package/src/client-tool-view.js +37 -8
  119. package/src/client-transport.js +44 -9
  120. package/src/client-workbench.js +6 -7
  121. package/src/client.js +76 -22
  122. package/client.js +0 -32479
@@ -1,7 +1,7 @@
1
1
  import { createHash } from 'node:crypto'
2
2
  import { deepEqualJson, isJsonValue } from '@deepseek-ai/dsh-util-values'
3
3
  import { deepFreeze, isRecord } from './record-utils.js'
4
- import { sessionEvents } from './session-events.js'
4
+ import { sessionEvents, sessionSourceMessages } from './session-events.js'
5
5
  import { USER_BINDINGS_META_KEY } from './user-bindings.js'
6
6
  import {
7
7
  JOURNAL_KEY,
@@ -12,6 +12,7 @@ import {
12
12
  REWRITES_KEY,
13
13
  EDIT_TARGET_KEY,
14
14
  DERIVED_RUN_KEY,
15
+ POST_EDIT_KEY,
15
16
  } from './session-journal-schema.js'
16
17
  import {
17
18
  isCanonicalSequence,
@@ -22,6 +23,8 @@ import {
22
23
  userBindingsForJournal,
23
24
  validatedRewrites,
24
25
  } from './session-journal.js'
26
+ import { decodeValue } from './value-wire.js'
27
+ import { editRejectedCell } from './rejected-cell-editor.js'
25
28
 
26
29
  const RECOVERY_BOUNDARY_EVIDENCE = Symbol('recoveryBoundaryEvidence')
27
30
  const RUN_SELECTION_CLEAR_REASONS = new Set([
@@ -156,6 +159,12 @@ export function visibleExecutableCallSeqs(session) {
156
159
  return new Set()
157
160
  }
158
161
  if (!Array.isArray(events) || !Array.isArray(nodes)) return new Set()
162
+ let sourceMessages
163
+ try {
164
+ sourceMessages = sessionSourceMessages(session, events, nodes)
165
+ } catch {
166
+ return new Set()
167
+ }
159
168
  const eventBySeq = new Map()
160
169
  const duplicateEventSeqs = new Set()
161
170
  for (const event of events) {
@@ -180,7 +189,7 @@ export function visibleExecutableCallSeqs(session) {
180
189
  }
181
190
  if (event?.type === 'assistant/message') {
182
191
  pendingAssistantCalls.clear()
183
- const content = event.data?.message?.content
192
+ const content = sourceMessages.get(event.seq)?.content
184
193
  if (Array.isArray(content)) {
185
194
  for (const part of content) {
186
195
  if (part?.type === 'tool-call' && typeof part.id === 'string') {
@@ -214,6 +223,13 @@ export function visibleExecutableCallSeqs(session) {
214
223
  if (event?.type !== 'tool/call' || !REPL_TOOL_NAMES.has(event.data?.name)) continue
215
224
  if (visibleAssistantCallSeqs.has(event.seq)) calls.add(event.seq)
216
225
  }
226
+ const timeline = foldSessionTimeline(events)
227
+ for (const root of timeline.calls) {
228
+ const post = timeline.results.get(root.seq)?.postEdit
229
+ if (post !== undefined && calls.has(root.seq) && calls.has(post.targetCallSeq)) {
230
+ calls.add(post.executionStartSeq)
231
+ }
232
+ }
217
233
  return calls
218
234
  }
219
235
 
@@ -253,7 +269,11 @@ function successfulTimelineRun(run) {
253
269
  function settlementRun(settlement) {
254
270
  const publication = settlement.publication
255
271
  if (publication === undefined) return undefined
256
- const result = { data: { meta: { [REWRITES_KEY]: publication.rewrites } } }
272
+ const result = { data: { meta: { [REWRITES_KEY]: publication.postEditRewrites ?? publication.rewrites } } }
273
+ if (publication.postEdit !== undefined) {
274
+ return timelineJson(timelineDerivedRun({ seq: publication.postEdit.executionStartSeq }, result,
275
+ publication.index, publication.postEdit.derived))
276
+ }
257
277
  return timelineJson(publication.derived === undefined
258
278
  ? timelineRun(settlement.call, result, publication.index, publication.journal)
259
279
  : timelineDerivedRun(settlement.call, result, publication.index, publication.derived))
@@ -398,6 +418,18 @@ function republishedTimelineResult(result, event) {
398
418
 
399
419
  function correlationEvent(event) {
400
420
  return timelineJson({ seq: event.seq, type: event.type,
421
+ ...(['tool/ptc-dispatch-start', 'tool/ptc-dispatch'].includes(event.type) ? { data: {
422
+ rootCallId: event.data?.rootCallId,
423
+ parentCallId: event.data?.parentCallId,
424
+ subCallId: event.data?.subCallId,
425
+ name: event.data?.name,
426
+ arguments: event.data?.arguments,
427
+ ...(event.type === 'tool/ptc-dispatch' ? {
428
+ isError: event.data?.isError,
429
+ ...(event.data?.name === 'run_code' && event.data.meta?.[JOURNAL_KEY] !== undefined
430
+ ? { meta: { [JOURNAL_KEY]: event.data.meta[JOURNAL_KEY] } } : {}),
431
+ } : {}),
432
+ } } : {}),
401
433
  ...(event.type === 'tool/call' && typeof event.data?.callId === 'string'
402
434
  ? { data: timelineCallEvent(event).data } : {}),
403
435
  ...(event.type === 'tool/result' ? { data: { message: { source: {
@@ -586,11 +618,26 @@ export function validateSessionTimelineState(value) {
586
618
  return true
587
619
  }
588
620
  const normalizedResult = result => {
589
- if (!fields(result, ['eventSeq', 'eventIndex', 'positionSeq', 'error', 'journal', 'derived', 'userBindings'])
621
+ if (!fields(result, ['eventSeq', 'eventIndex', 'positionSeq', 'error', 'journal', 'derived', 'userBindings',
622
+ 'postEdit', 'postEditError'])
590
623
  || !isCanonicalSequence(result.eventSeq) || !isCanonicalSequence(result.eventIndex)
591
624
  || !optional(result.positionSeq, isCanonicalSequence)) return false
592
625
  if (result.error !== undefined) return typeof result.error === 'string' && result.journal === undefined && result.derived === undefined
593
626
  return journalFacts(result)
627
+ && optional(result.postEditError, error => typeof error === 'string')
628
+ && optional(result.postEdit, post => fields(post, ['startSeq', 'executionStartSeq', 'targetCallSeq', 'derived', 'rewrites'])
629
+ && isCanonicalSequence(post.startSeq) && isCanonicalSequence(post.executionStartSeq)
630
+ && isCanonicalSequence(post.targetCallSeq)
631
+ && deepEqualJson(normalizeDerivedEditResult({
632
+ [JOURNAL_KEY]: post.derived?.journal,
633
+ [EDIT_TARGET_KEY]: { targetCallSeq: post.targetCallSeq },
634
+ [DERIVED_RUN_KEY]: { code: post.derived?.code, description: post.derived?.description },
635
+ ...(post.derived?.recoveryBoundaries === undefined ? {} : {
636
+ [RECOVERY_BOUNDARY_KEY]: post.derived.recoveryBoundaries }),
637
+ ...(post.derived?.userBindings === undefined ? {} : {
638
+ [USER_BINDINGS_META_KEY]: post.derived.userBindings }),
639
+ }, post.targetCallSeq), post.derived)
640
+ && deepEqualJson(validatedRewrites({ [REWRITES_KEY]: post.rewrites }) ?? null, post.rewrites ?? null))
594
641
  }
595
642
  const results = table('results', isCanonicalSequence, normalizedResult)
596
643
  const targets = table('editTargets', isCanonicalSequence, (field, seq) => (
@@ -629,10 +676,12 @@ export function validateSessionTimelineState(value) {
629
676
  && (!REPL_TOOL_NAMES.has(settlement.call.data.name)
630
677
  || deepEqualJson(executable.get(settlement.call.seq)?.event, settlement.call))
631
678
  && optional(settlement.publication, publication => fields(publication,
632
- ['index', 'journal', 'derived', 'userBindings', 'rewrites'])
679
+ ['index', 'journal', 'derived', 'userBindings', 'rewrites', 'postEdit', 'postEditRewrites'])
633
680
  && deepEqualJson(publication.journal ?? null, settlement.normalized?.journal ?? null)
634
681
  && deepEqualJson(publication.derived ?? null, settlement.normalized?.derived ?? null)
635
682
  && deepEqualJson(publication.userBindings ?? null, settlement.normalized?.userBindings ?? null)
683
+ && deepEqualJson(publication.postEdit ?? null, settlement.normalized?.postEdit ?? null)
684
+ && deepEqualJson(publication.postEditRewrites ?? null, settlement.normalized?.postEdit?.rewrites ?? null)
636
685
  && !settlement.entry.ambiguous && isCanonicalSequence(publication.index)
637
686
  && publication.index === (settlement.ordinary ? settlement.observedIndex : settlement.entry.eventIndex)
638
687
  && ((settlement.call.data.name === 'run_code' && publication.derived === undefined)
@@ -651,13 +700,18 @@ export function validateSessionTimelineState(value) {
651
700
  let correlationIndex = -1
652
701
  for (const observation of value.eventFacts) {
653
702
  requireFact(fields(observation, ['eventIndex', 'event', 'identity', 'hasJournal', 'normalized',
654
- 'boundaries', 'boundaryFailure', 'rewrites'])
703
+ 'boundaries', 'boundaryFailure', 'rewrites', 'selectedTarget'])
655
704
  && isCanonicalSequence(observation.eventIndex) && observation.eventIndex > correlationIndex
656
705
  && value.lastEventIndex !== null && observation.eventIndex <= value.lastEventIndex
657
706
  && isRecord(observation.event) && isCanonicalSequence(observation.event.seq)
658
- && ['turn/start', 'turn/end', RECOVERY_BOUNDARY_EVENT, 'compaction/prune', 'tool/call', 'tool/result']
707
+ && ['turn/start', 'turn/end', RECOVERY_BOUNDARY_EVENT, 'compaction/prune', 'tool/call', 'tool/result',
708
+ 'tool/ptc-dispatch-start', 'tool/ptc-dispatch']
659
709
  .includes(observation.event.type)
660
710
  && deepEqualJson(correlationEvent(observation.event), observation.event)
711
+ && (observation.event.type === 'tool/ptc-dispatch-start'
712
+ && observation.event.data?.name === 'edit_run_code'
713
+ ? observation.selectedTarget === null || target(observation.selectedTarget)
714
+ : observation.selectedTarget === undefined)
661
715
  && (observation.event.type === 'tool/result'
662
716
  ? typeof observation.identity === 'string' && /^[a-f0-9]{64}$/u.test(observation.identity)
663
717
  && typeof observation.hasJournal === 'boolean'
@@ -748,8 +802,9 @@ export function validateSessionTimelineState(value) {
748
802
  lastSuccessfulRunIndex = Math.max(lastSuccessfulRunIndex ?? -1, settlement.publication.index)
749
803
  }
750
804
  }
751
- requireFact(selectedRun === undefined || (executable.get(selectedRun.callSeq)?.scope === value.scope
752
- && !executable.get(selectedRun.callSeq).ambiguous))
805
+ requireFact(selectedRun === undefined || publications.some(settlement =>
806
+ settlement.entry.scope === value.scope && !settlement.entry.ambiguous
807
+ && settlementRun(settlement).callSeq === selectedRun.callSeq))
753
808
  requireFact(selectedRun === undefined ? value.latestRun === undefined && value.editableRun === undefined
754
809
  : deepEqualJson(value.latestRun ?? null, selectedRun) && deepEqualJson(value.editableRun ?? null, selectedRun))
755
810
  requireFact(value.lastSuccessfulRunIndex === lastSuccessfulRunIndex
@@ -787,8 +842,119 @@ export function replaySessionTimelineFact(previous, fact) {
787
842
  return advanceTimelineEvent(previous, fact.event, fact.eventIndex, fact)
788
843
  }
789
844
 
845
+ function dispatchPairWithinRoot(facts, call, resultEvent, startSeq) {
846
+ const selectedStarts = facts.filter(fact => fact.event.type === 'tool/ptc-dispatch-start'
847
+ && fact.event.seq === startSeq)
848
+ if (selectedStarts.length !== 1) return undefined
849
+ const subCallId = selectedStarts[0].event.data?.subCallId
850
+ if (typeof subCallId !== 'string') return undefined
851
+ // A sequence locates a start; the bounded root window must also prove that
852
+ // its dispatch identity names exactly one start and settlement. Settled ID
853
+ // reuse outside this root/result window does not change this execution.
854
+ const identityFacts = facts.filter(fact => fact.event.seq > call.seq
855
+ && fact.event.seq < resultEvent.seq && fact.event.data?.subCallId === subCallId)
856
+ const starts = identityFacts.filter(fact => fact.event.type === 'tool/ptc-dispatch-start')
857
+ const settlements = identityFacts.filter(fact => fact.event.type === 'tool/ptc-dispatch')
858
+ if (starts.length !== 1 || settlements.length !== 1
859
+ || starts[0].event.seq !== startSeq || startSeq >= settlements[0].event.seq) return undefined
860
+ return { start: starts[0], settled: settlements[0] }
861
+ }
862
+
863
+ function normalizePostEditFact(facts, call, resultEvent, result, replayPostEdit) {
864
+ const post = replayPostEdit === undefined ? resultEvent.data?.meta?.[POST_EDIT_KEY] : {
865
+ startSeq: replayPostEdit.startSeq,
866
+ executionStartSeq: replayPostEdit.executionStartSeq,
867
+ rootCallSeq: call.seq,
868
+ subCallId: facts.find(fact => fact.event.seq === replayPostEdit.startSeq)?.event.data?.subCallId,
869
+ meta: {
870
+ [JOURNAL_KEY]: replayPostEdit.derived.journal,
871
+ [EDIT_TARGET_KEY]: { targetCallSeq: replayPostEdit.targetCallSeq },
872
+ [DERIVED_RUN_KEY]: { code: replayPostEdit.derived.code, description: replayPostEdit.derived.description },
873
+ ...(replayPostEdit.derived.recoveryBoundaries === undefined ? {} : {
874
+ [RECOVERY_BOUNDARY_KEY]: replayPostEdit.derived.recoveryBoundaries }),
875
+ ...(replayPostEdit.derived.userBindings === undefined ? {} : {
876
+ [USER_BINDINGS_META_KEY]: replayPostEdit.derived.userBindings }),
877
+ ...(replayPostEdit.rewrites === undefined ? {} : { [REWRITES_KEY]: replayPostEdit.rewrites }),
878
+ },
879
+ }
880
+ const acceptedCalls = result.journal?.calls?.filter(entry => (
881
+ entry.global === 'tools' && entry.member === 'edit_run_code' && entry.ok === true
882
+ && decodeValue(entry.value)?.accepted === true
883
+ )) ?? []
884
+ if (post === undefined && acceptedCalls.length === 0) return undefined
885
+ const fail = reason => ({ error: `unproved deferred edit: ${reason}` })
886
+ if (!isRecord(post) || !isCanonicalSequence(post.startSeq)
887
+ || !isCanonicalSequence(post.executionStartSeq)
888
+ || post.rootCallSeq !== call.seq || typeof post.subCallId !== 'string'
889
+ || !isRecord(post.meta) || acceptedCalls.length !== 1) return fail('root metadata or receipt is missing')
890
+ const editorPair = dispatchPairWithinRoot(facts, call, resultEvent, post.startSeq)
891
+ if (editorPair === undefined || editorPair.start.event.data.subCallId !== post.subCallId) {
892
+ return fail('nested dispatch pair is incomplete or ambiguous')
893
+ }
894
+ const start = editorPair.start.event
895
+ const settled = editorPair.settled.event
896
+ const target = editorPair.start.selectedTarget
897
+ if (start.seq <= call.seq || start.seq >= settled.seq || settled.seq >= resultEvent.seq
898
+ || start.data.rootCallId !== call.data.callId || start.data.parentCallId !== call.data.callId
899
+ || settled.data.rootCallId !== start.data.rootCallId
900
+ || settled.data.parentCallId !== start.data.parentCallId
901
+ || settled.data.name !== 'edit_run_code' || start.data.name !== 'edit_run_code'
902
+ || !deepEqualJson(start.data.arguments, settled.data.arguments)
903
+ || settled.data.isError !== false) return fail('nested dispatch relation changed')
904
+ const receipt = decodeValue(acceptedCalls[0].value)
905
+ if (!deepEqualJson(receipt, { accepted: true, scheduled: true,
906
+ target_call_seq: post.meta?.[EDIT_TARGET_KEY]?.targetCallSeq })
907
+ || !deepEqualJson(decodeValue(acceptedCalls[0].args), start.data.arguments)) {
908
+ return fail('nested receipt does not match root journal')
909
+ }
910
+ let derived
911
+ try {
912
+ derived = normalizeDerivedEditResult(post.meta, target?.callSeq)
913
+ } catch (error) {
914
+ return fail(error.message)
915
+ }
916
+ if (target?.source === undefined || sourceForRunCall(call) === undefined) {
917
+ return fail('nested edit has no selected source')
918
+ }
919
+ const edit = editRejectedCell(start.data.arguments, target.source)
920
+ if (!edit.edited || edit.code !== derived.code || edit.description !== derived.description
921
+ || (start.data.arguments.expected_target_call_seq !== undefined
922
+ && start.data.arguments.expected_target_call_seq !== target.callSeq)) {
923
+ return fail('derived source does not match selected edit')
924
+ }
925
+ const executionPair = dispatchPairWithinRoot(facts, call, resultEvent, post.executionStartSeq)
926
+ if (executionPair === undefined) return fail('derived execution dispatch pair is incomplete or ambiguous')
927
+ const executionStart = executionPair.start.event
928
+ const executionSettled = executionPair.settled.event
929
+ const argumentsValue = { code: derived.code, description: derived.description }
930
+ if (executionStart.seq <= settled.seq || executionStart.data.rootCallId !== call.data.callId
931
+ || executionStart.data.parentCallId !== call.data.callId
932
+ || executionStart.data.name !== 'run_code' || executionSettled.data.name !== 'run_code'
933
+ || executionSettled.data.rootCallId !== executionStart.data.rootCallId
934
+ || executionSettled.data.parentCallId !== executionStart.data.parentCallId
935
+ || !deepEqualJson(executionStart.data.arguments, argumentsValue)
936
+ || !deepEqualJson(executionSettled.data.arguments, argumentsValue)
937
+ || !deepEqualJson(executionSettled.data.meta?.[JOURNAL_KEY], derived.journal)
938
+ || typeof executionSettled.data.isError !== 'boolean'
939
+ || executionSettled.data.isError !== (derived.journal.completion?.kind === 'throw'
940
+ || derived.journal.status === 'discarded')) {
941
+ return fail('derived execution relation changed')
942
+ }
943
+ const rewrites = validatedRewrites(post.meta)
944
+ return { startSeq: start.seq, executionStartSeq: executionStart.seq,
945
+ targetCallSeq: target.callSeq, derived,
946
+ ...(rewrites === undefined ? {} : { rewrites }) }
947
+ }
948
+
790
949
  function advanceTimelineEvent(previous, inputEvent, eventIndex, replayFact) {
791
- if (!['turn/start', 'turn/end', RECOVERY_BOUNDARY_EVENT, 'compaction/prune', 'tool/call', 'tool/result'].includes(inputEvent?.type)) {
950
+ const nested = inputEvent?.type === 'tool/ptc-dispatch-start'
951
+ || inputEvent?.type === 'tool/ptc-dispatch'
952
+ if (nested && inputEvent.data?.name !== 'edit_run_code'
953
+ && inputEvent.data?.name !== 'run_code'
954
+ && !previous.eventFacts.some(fact => fact.event.type === 'tool/ptc-dispatch-start'
955
+ && fact.event.data?.subCallId === inputEvent.data?.subCallId)) return previous
956
+ if (!['turn/start', 'turn/end', RECOVERY_BOUNDARY_EVENT, 'compaction/prune', 'tool/call', 'tool/result',
957
+ 'tool/ptc-dispatch-start', 'tool/ptc-dispatch'].includes(inputEvent?.type)) {
792
958
  return previous
793
959
  }
794
960
  const observation = { eventIndex, event: correlationEvent(inputEvent),
@@ -866,6 +1032,16 @@ function advanceTimelineEvent(previous, inputEvent, eventIndex, replayFact) {
866
1032
  pruneReplacementWindow = pruneWindowForEvent(event, eventIndex)
867
1033
  continue
868
1034
  }
1035
+ if (event?.type === 'tool/ptc-dispatch-start') {
1036
+ if (event.data?.name !== 'edit_run_code') continue
1037
+ observation.selectedTarget = state.editableRun?.source === undefined ? null
1038
+ : timelineJson({ callSeq: state.editableRun.callSeq, source: state.editableRun.source })
1039
+ if (replayFact !== undefined && !deepEqualJson(observation.selectedTarget, replayFact.selectedTarget)) {
1040
+ throw new TypeError('invalid PTC session-log projection nested edit target fact')
1041
+ }
1042
+ continue
1043
+ }
1044
+ if (event?.type === 'tool/ptc-dispatch') continue
869
1045
  if (event?.type === 'tool/call' && typeof event.data?.callId === 'string') {
870
1046
  const executable = REPL_TOOL_NAMES.has(event.data.name)
871
1047
  let entry = { event: timelineCallEvent(event), eventIndex, scope }
@@ -1033,6 +1209,14 @@ function advanceTimelineEvent(previous, inputEvent, eventIndex, replayFact) {
1033
1209
  journal,
1034
1210
  ...(userBindings === undefined ? {} : { userBindings }),
1035
1211
  }
1212
+ let postEdit
1213
+ try {
1214
+ postEdit = normalizePostEditFact(state.eventFacts, call, event, normalized)
1215
+ } catch (error) {
1216
+ postEdit = { error: `unproved deferred edit: ${error.message}` }
1217
+ }
1218
+ if (postEdit?.error !== undefined) normalized.postEditError = postEdit.error
1219
+ else if (postEdit !== undefined) normalized.postEdit = postEdit
1036
1220
  }
1037
1221
  } catch (error) {
1038
1222
  normalized = { ...raw, error: error.message }
@@ -1050,6 +1234,12 @@ function advanceTimelineEvent(previous, inputEvent, eventIndex, replayFact) {
1050
1234
  })) throw new TypeError('invalid PTC session-log projection timeline checkpoint result facts')
1051
1235
  if (replayFact.boundaries !== undefined) observation.boundaries = replayFact.boundaries
1052
1236
  if (replayFact.boundaryFailure !== undefined) observation.boundaryFailure = replayFact.boundaryFailure
1237
+ if (normalized.postEdit !== undefined) {
1238
+ const checked = normalizePostEditFact(state.eventFacts, call, event, normalized, normalized.postEdit)
1239
+ if (checked?.error !== undefined || !deepEqualJson(checked, normalized.postEdit)) {
1240
+ throw new TypeError(`invalid PTC session-log projection deferred edit fact: ${checked?.error ?? 'derived fact changed'}`)
1241
+ }
1242
+ }
1053
1243
  }
1054
1244
  observation.normalized = normalized
1055
1245
  if (observation.boundaries !== undefined) {
@@ -1074,6 +1264,10 @@ function advanceTimelineEvent(previous, inputEvent, eventIndex, replayFact) {
1074
1264
  derived: normalized?.derived,
1075
1265
  userBindings: normalized?.userBindings,
1076
1266
  rewrites,
1267
+ ...(normalized?.postEdit === undefined ? {} : {
1268
+ postEdit: normalized.postEdit,
1269
+ postEditRewrites: normalized.postEdit.rewrites,
1270
+ }),
1077
1271
  }
1078
1272
  }
1079
1273
  if (settlement !== undefined && isCanonicalSequence(event.seq)) {
@@ -1243,7 +1437,20 @@ export function recoverJournal(session, currentCallSeq, options = {}) {
1243
1437
  const boundary = boundaries[boundaryIndex++]
1244
1438
  const carrierCallSeq = carrierCallSeqForBoundary(boundary)
1245
1439
  const failedCall = executableCalls.get(boundary.failedCallSeq)
1246
- if (failedCall === undefined || failedCall.event.seq >= boundary.eventSeq) {
1440
+ // Derived records exist only after the shared timeline has proved both
1441
+ // dispatch pairs and the final root publication. A raw nested start does
1442
+ // not establish a program identity or a completed recovery boundary.
1443
+ const derivedRecords = failedCall === undefined ? orderedRecords.filter(record => (
1444
+ record.call.seq === boundary.failedCallSeq
1445
+ && record.call.type === 'tool/ptc-dispatch-start'
1446
+ && record.call.data?.name === 'run_code' && record.result?.derived !== undefined
1447
+ )) : []
1448
+ const failedExecution = failedCall === undefined && derivedRecords.length === 1
1449
+ ? derivedRecords[0] : undefined
1450
+ const isEarlierExecution = failedCall === undefined
1451
+ ? failedExecution !== undefined && recordEventSeq(failedExecution) < boundary.eventSeq
1452
+ : failedCall.event.seq < boundary.eventSeq
1453
+ if (!isEarlierExecution) {
1247
1454
  rejectBoundary(boundary, 'recovery boundary does not identify an earlier executable call')
1248
1455
  continue
1249
1456
  }
@@ -1299,6 +1506,22 @@ export function recoverJournal(session, currentCallSeq, options = {}) {
1299
1506
  if (call.data?.name === 'edit_run_code' && record.code === undefined && result === undefined) continue
1300
1507
  if (result?.journal === undefined && confirmedNoops.has(call.seq)) continue
1301
1508
  orderedRecords.push(record)
1509
+ if (call.data?.name === 'run_code' && result?.journal !== undefined) {
1510
+ if (result.postEditError !== undefined) {
1511
+ invalidResultReasons.set(call.seq, result.postEditError)
1512
+ registerUnavailable({ seq: call.seq, reason: result.postEditError })
1513
+ } else if (result.postEdit !== undefined) {
1514
+ const postEdit = result.postEdit
1515
+ orderedRecords.push({
1516
+ call: timeline.eventFacts.find(fact => fact.event.seq === postEdit.executionStartSeq).event,
1517
+ code: postEdit.derived.code,
1518
+ result: { eventSeq: result.eventSeq, journal: postEdit.derived.journal,
1519
+ derived: postEdit.derived, userBindings: postEdit.derived.userBindings },
1520
+ hidden: visibleCalls instanceof Set && (!visibleCalls.has(call.seq)
1521
+ || !visibleCalls.has(postEdit.targetCallSeq)),
1522
+ })
1523
+ }
1524
+ }
1302
1525
  }
1303
1526
  orderedRecords.sort((left, right) => (
1304
1527
  recordEventSeq(left) - recordEventSeq(right) || left.call.seq - right.call.seq
@@ -8,13 +8,15 @@ import {
8
8
  export const JOURNAL_KEY = 'dshPtcPlus'
9
9
  export const EDIT_TARGET_KEY = 'dshPtcPlusEdit'
10
10
  export const DERIVED_RUN_KEY = 'dshPtcPlusDerivedRun'
11
+ export const POST_EDIT_KEY = 'dshPtcPlusPostEdit'
11
12
  export const REWRITES_KEY = 'dshPtcPlusRewrites'
12
13
  export const RECOVERY_BOUNDARY_KEY = 'dshPtcPlusRecoveryBoundaries'
13
14
  export const IMPORT_BOUNDARY_JOURNAL_VERSION = 7
14
15
  export const PER_NAME_USER_BINDINGS_JOURNAL_VERSION = 8
15
16
  export const LANGUAGE_SEMANTICS_JOURNAL_VERSION = 9
16
17
  export const MODULE_TRANSFORM_JOURNAL_VERSION = 10
17
- export const JOURNAL_VERSION = MODULE_TRANSFORM_JOURNAL_VERSION
18
+ export const EXECUTION_DIRECTORY_JOURNAL_VERSION = 11
19
+ export const JOURNAL_VERSION = EXECUTION_DIRECTORY_JOURNAL_VERSION
18
20
  export const LIVE_USER_BINDINGS_SHADOW_POLICY = 'per-name'
19
21
  export const LEGACY_USER_BINDINGS_SHADOW_POLICY = 'whole-entry'
20
22
  export const LIVE_USER_BINDINGS_REUSE_POLICY = 'implementation-v1'
@@ -32,7 +34,8 @@ export const BINDING_MODES = new Set(['loose', 'strict'])
32
34
  export const WHOLE_ENTRY_JOURNAL_FIELDS = new Set(['version', 'bindingPolicy', 'rewritePolicy', 'moduleSemantics', 'userBindingsFingerprint', 'userBindingsReusePolicy', 'status', 'calls', 'operations', 'confirms', 'diagnostics', 'completion', 'volatileReason'])
33
35
  export const PER_NAME_JOURNAL_FIELDS = new Set([...WHOLE_ENTRY_JOURNAL_FIELDS, 'userBindingsShadowPolicy', 'userBindingNames'])
34
36
  export const LANGUAGE_SEMANTICS_JOURNAL_FIELDS = new Set([...PER_NAME_JOURNAL_FIELDS, 'languageSemantics'])
35
- export const JOURNAL_FIELDS = new Set([...LANGUAGE_SEMANTICS_JOURNAL_FIELDS, 'moduleTransform'])
37
+ export const MODULE_TRANSFORM_JOURNAL_FIELDS = new Set([...LANGUAGE_SEMANTICS_JOURNAL_FIELDS, 'moduleTransform'])
38
+ export const JOURNAL_FIELDS = new Set([...MODULE_TRANSFORM_JOURNAL_FIELDS, 'executionCwd'])
36
39
  export const FINGERPRINT_REUSE_JOURNAL_FIELDS = new Set([...WHOLE_ENTRY_JOURNAL_FIELDS].filter(field => field !== 'userBindingsReusePolicy'))
37
40
  export const RELATIONLESS_JOURNAL_FIELDS = new Set([...FINGERPRINT_REUSE_JOURNAL_FIELDS].filter(field => field !== 'userBindingsFingerprint'))
38
41
  export const PREDECESSOR_JOURNAL_FIELDS = new Set(['version', 'bindingMode', 'rewritePolicy', 'status', 'calls', 'operations', 'confirms', 'diagnostics', 'completion', 'volatileReason'])
@@ -58,7 +61,7 @@ export const ERROR_FIELDS = new Set(['kind', 'message'])
58
61
  export const EDIT_TARGET_FIELDS = new Set(['targetCallSeq'])
59
62
  export const DERIVED_RUN_FIELDS = new Set(['code', 'description'])
60
63
 
61
- export const JOURNAL_VERSIONS = new Set([LEGACY_JOURNAL_VERSION, INTERMEDIATE_JOURNAL_VERSION, PREVIOUS_JOURNAL_VERSION, USER_BINDING_RELATIONLESS_JOURNAL_VERSION, FINGERPRINT_REUSE_JOURNAL_VERSION, VERSIONED_BINDING_REUSE_JOURNAL_VERSION, IMPORT_BOUNDARY_JOURNAL_VERSION, PER_NAME_USER_BINDINGS_JOURNAL_VERSION, LANGUAGE_SEMANTICS_JOURNAL_VERSION, JOURNAL_VERSION])
64
+ export const JOURNAL_VERSIONS = new Set([LEGACY_JOURNAL_VERSION, INTERMEDIATE_JOURNAL_VERSION, PREVIOUS_JOURNAL_VERSION, USER_BINDING_RELATIONLESS_JOURNAL_VERSION, FINGERPRINT_REUSE_JOURNAL_VERSION, VERSIONED_BINDING_REUSE_JOURNAL_VERSION, IMPORT_BOUNDARY_JOURNAL_VERSION, PER_NAME_USER_BINDINGS_JOURNAL_VERSION, LANGUAGE_SEMANTICS_JOURNAL_VERSION, MODULE_TRANSFORM_JOURNAL_VERSION, JOURNAL_VERSION])
62
65
  export const USER_BINDINGS_REUSE_POLICIES = new Set([LEGACY_USER_BINDINGS_REUSE_POLICY, LIVE_USER_BINDINGS_REUSE_POLICY])
63
66
  export const USER_BINDINGS_SHADOW_POLICIES = new Set([LEGACY_USER_BINDINGS_SHADOW_POLICY, LIVE_USER_BINDINGS_SHADOW_POLICY])
64
67
  const USER_BINDING_NAME_STATES = new Set(['provider', 'local', 'absent', 'unknown'])
@@ -1,4 +1,5 @@
1
1
  import { isDeepStrictEqual } from 'node:util'
2
+ import { posix, win32 } from 'node:path'
2
3
  import { decodeValue, encodeValue, normalizeValueWire } from './value-wire.js'
3
4
  import { normalizeDiagnostic } from './diagnostic.js'
4
5
  import { assertOwnFields, isRecord } from './record-utils.js'
@@ -19,6 +20,8 @@ import {
19
20
  REWRITES_KEY,
20
21
  RECOVERY_BOUNDARY_KEY,
21
22
  JOURNAL_VERSION,
23
+ MODULE_TRANSFORM_JOURNAL_VERSION,
24
+ MODULE_TRANSFORM_JOURNAL_FIELDS,
22
25
  LANGUAGE_SEMANTICS_JOURNAL_VERSION,
23
26
  LANGUAGE_SEMANTICS_JOURNAL_FIELDS,
24
27
  IMPORT_BOUNDARY_JOURNAL_VERSION,
@@ -262,6 +265,10 @@ function normalizeUserBindingsFingerprint(value) {
262
265
 
263
266
  function migrateJournal(value, resolveLegacyConfirm) {
264
267
  if (value.version === JOURNAL_VERSION) return value
268
+ if (value.version === MODULE_TRANSFORM_JOURNAL_VERSION) {
269
+ assertOwnFields(value, MODULE_TRANSFORM_JOURNAL_FIELDS, 'dsh-ptc-plus journal')
270
+ return { ...value, version: JOURNAL_VERSION, executionCwd: null }
271
+ }
265
272
  if (value.version === LANGUAGE_SEMANTICS_JOURNAL_VERSION) {
266
273
  assertOwnFields(value, LANGUAGE_SEMANTICS_JOURNAL_FIELDS, 'dsh-ptc-plus journal')
267
274
  const languageSemantics = normalizeLanguageSemantics(value.languageSemantics)
@@ -332,6 +339,24 @@ function migrateJournal(value, resolveLegacyConfirm) {
332
339
  }
333
340
  }
334
341
 
342
+ /**
343
+ * A recorded execution directory belongs to the execution world that produced
344
+ * the journal, not to the platform reading it. Judge both absolute spellings so
345
+ * a Windows Host journal stays valid evidence inside a POSIX audit checkout
346
+ * (and the reverse), and never rewrite the recorded bytes.
347
+ */
348
+ function isRecordedExecutionDirectory(value) {
349
+ return typeof value === 'string' && value.length > 0
350
+ && (posix.isAbsolute(value) || win32.isAbsolute(value))
351
+ }
352
+
353
+ function normalizeExecutionCwd(value) {
354
+ if (value !== null && !isRecordedExecutionDirectory(value)) {
355
+ throw new Error('invalid dsh-ptc-plus journal execution directory')
356
+ }
357
+ return value
358
+ }
359
+
335
360
  /** Validate and detach one journal emitted by the runtime. */
336
361
  export function normalizeJournal(value, options = {}) {
337
362
  if (!isRecord(value)) throw new Error('invalid dsh-ptc-plus journal')
@@ -339,10 +364,12 @@ export function normalizeJournal(value, options = {}) {
339
364
  || !STATUSES.has(value.status)) {
340
365
  throw new Error('invalid dsh-ptc-plus journal')
341
366
  }
342
- const migrated = migrateJournal(value, options.resolveLegacyConfirm)
367
+ const migrated = { ...migrateJournal(value, options.resolveLegacyConfirm),
368
+ ...(value.version < JOURNAL_VERSION ? { executionCwd: null } : {}) }
343
369
  assertOwnFields(migrated, JOURNAL_FIELDS, 'dsh-ptc-plus journal')
344
370
  const languageSemantics = normalizeLanguageSemantics(migrated.languageSemantics)
345
371
  const moduleTransform = normalizeModuleTransform(migrated.moduleTransform, languageSemantics)
372
+ const executionCwd = normalizeExecutionCwd(migrated.executionCwd)
346
373
  const bindingPolicy = normalizeBindingPolicy(migrated.bindingPolicy)
347
374
  const rewritePolicy = normalizeRewritePolicy(migrated.rewritePolicy)
348
375
  const moduleSemantics = normalizeModuleSemantics(migrated.moduleSemantics)
@@ -374,6 +401,7 @@ export function normalizeJournal(value, options = {}) {
374
401
  version: JOURNAL_VERSION,
375
402
  languageSemantics,
376
403
  moduleTransform,
404
+ executionCwd,
377
405
  bindingPolicy,
378
406
  rewritePolicy,
379
407
  moduleSemantics,
@@ -1022,7 +1050,7 @@ export function liveToolCallSeq(session, callId, toolName) {
1022
1050
 
1023
1051
  /** Start a mutable journal for one live cell. */
1024
1052
  export function createJournal(confirms = [], bindingPolicy, rewritePolicy, languageSemantics = LEGACY_LANGUAGE_SEMANTICS,
1025
- moduleTransform = moduleTransformForLanguage(languageSemantics)) {
1053
+ moduleTransform = moduleTransformForLanguage(languageSemantics), executionCwd = null) {
1026
1054
  if (typeof bindingPolicy === 'string') {
1027
1055
  if (!BINDING_MODES.has(bindingPolicy)) throw new TypeError('invalid dsh-ptc-plus journal binding mode')
1028
1056
  bindingPolicy = {
@@ -1035,6 +1063,7 @@ export function createJournal(confirms = [], bindingPolicy, rewritePolicy, langu
1035
1063
  version: JOURNAL_VERSION,
1036
1064
  languageSemantics,
1037
1065
  moduleTransform: normalizeModuleTransform(moduleTransform, languageSemantics),
1066
+ executionCwd: normalizeExecutionCwd(executionCwd),
1038
1067
  bindingPolicy: normalizeBindingPolicy(bindingPolicy),
1039
1068
  rewritePolicy: normalizeRewritePolicy(rewritePolicy),
1040
1069
  moduleSemantics: normalizeModuleSemantics(LIVE_MODULE_SEMANTICS),
@@ -118,7 +118,7 @@ function applyLogEvent(state, event, index = event?.seq,
118
118
  export function createSessionLogProjection() {
119
119
  return Object.freeze({
120
120
  key: SESSION_LOG_PROJECTION_KEY,
121
- stateVersion: 8,
121
+ stateVersion: 9,
122
122
  stateSchema: Object.freeze({ parse(value) {
123
123
  if (!isJsonValue(value) || !isRecord(value) || !isRecord(value.timeline)
124
124
  || typeof value.timeline.openTurn !== 'boolean' || typeof value.timeline.found !== 'boolean'