dsh-ptc-plus 0.3.2 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. package/README.en.md +65 -78
  2. package/README.md +65 -78
  3. package/assets/ptc-plus-bindings-en.png +0 -0
  4. package/assets/ptc-plus-bindings-zh.png +0 -0
  5. package/assets/ptc-plus-repair-en.png +0 -0
  6. package/assets/ptc-plus-repair-zh.png +0 -0
  7. package/assets/ptc-plus-repl-workspace-en.png +0 -0
  8. package/assets/ptc-plus-repl-workspace-zh.png +0 -0
  9. package/assets/ptc-plus-settings-en.png +0 -0
  10. package/assets/ptc-plus-settings-zh.png +0 -0
  11. package/client.js +30596 -742
  12. package/compiler-amaro.cjs +5 -0
  13. package/compiler-amaro.cjs.map +6 -0
  14. package/compiler-asset-124f80c1bba92ff5fe79cfec6e8ad518f476de4a42514cae3919538887f510e8.txt +1 -0
  15. package/compiler-assets.json +3 -0
  16. package/compiler-core.cjs +554 -0
  17. package/compiler-core.cjs.map +6 -0
  18. package/compiler-platform.cjs +2 -0
  19. package/compiler-platform.cjs.map +6 -0
  20. package/compiler-typescript.cjs +388 -0
  21. package/compiler-typescript.cjs.map +6 -0
  22. package/docs/README.md +6 -0
  23. package/docs/adr/0004-declare-program-bindings-at-the-host.md +1 -1
  24. package/docs/adr/0006-rewrite-module-syntax-with-an-ast.md +5 -3
  25. package/docs/adr/0007-separate-worker-transport-from-session-semantics.md +1 -1
  26. package/docs/adr/0009-bound-expensive-acceptance-default.md +2 -0
  27. package/docs/adr/0010-session-log-derived-recovery-tips.md +59 -5
  28. package/docs/adr/0011-isolate-the-local-recovery-tip-provider.md +1 -1
  29. package/docs/adr/0012-preserve-mixed-destructuring-initialization.md +3 -1
  30. package/docs/adr/0013-isolate-repl-redeclaration-convenience.md +3 -1
  31. package/docs/adr/0014-persist-cell-rewrite-policy.md +6 -2
  32. package/docs/adr/0015-preserve-source-positions-through-rewrites.md +1 -1
  33. package/docs/adr/0016-archive-resolved-review-findings.md +1 -1
  34. package/docs/adr/0017-track-the-latest-dsh-public-surface.md +49 -0
  35. package/docs/adr/0018-use-versioned-fixture-for-ab-benchmark.md +3 -3
  36. package/docs/adr/0019-plugin-settings-and-kill-switch.md +5 -3
  37. package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +2 -2
  38. package/docs/adr/0021-separate-host-bootstrap-from-session-runtime.md +12 -0
  39. package/docs/adr/0023-global-user-bindings.md +104 -0
  40. package/docs/adr/0024-repl-console-observation.md +33 -0
  41. package/docs/adr/0025-use-versioned-logical-binding-identities.md +119 -0
  42. package/docs/adr/0026-persist-the-delivery-verdict-against-the-verified-candidate.md +21 -0
  43. package/docs/adr/0027-run-the-session-kernel-in-a-killable-helper-process.md +37 -0
  44. package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +103 -0
  45. package/docs/adr/0029-keep-plugin-unavailability-out-of-the-host-agent-lifecycle.md +27 -0
  46. package/docs/adr/0030-serve-both-typert-codec-generations-from-one-codec.md +27 -0
  47. package/docs/architecture.md +73 -21
  48. package/docs/binding-review-placement-design.md +200 -0
  49. package/docs/capability-projection.md +1 -1
  50. package/docs/client-ui.md +95 -19
  51. package/docs/durability-design.md +25 -10
  52. package/docs/evaluation.md +78 -6
  53. package/docs/installation.md +41 -6
  54. package/docs/publishing.md +5 -3
  55. package/docs/runtime-reference.md +72 -25
  56. package/docs/semantic-validation.md +29 -0
  57. package/docs/stateful-binding-updates-proposal.md +755 -0
  58. package/docs/user-bindings.en.md +64 -0
  59. package/docs/user-bindings.md +64 -0
  60. package/docs/value-wire.md +3 -3
  61. package/docs/verification.md +57 -0
  62. package/index.js +369 -92
  63. package/internal/ast-traversal.js +5 -2
  64. package/internal/binding-descriptors.js +1 -0
  65. package/internal/binding-pattern-traversal.js +25 -0
  66. package/internal/binding-pattern.js +26 -29
  67. package/internal/binding-source-analysis.js +391 -0
  68. package/internal/binding-update-policy.js +66 -0
  69. package/internal/bounded-worker-round.js +56 -0
  70. package/internal/callable-definition-environment.js +42 -0
  71. package/internal/callable-source-catalog.js +129 -0
  72. package/internal/callable-source-encoding.js +47 -0
  73. package/internal/callable-source-facts.js +459 -0
  74. package/internal/callable-source-format.js +2 -0
  75. package/internal/callable-source-lowering.js +123 -0
  76. package/internal/cell-analysis-contract.js +25 -0
  77. package/internal/cell-analysis.js +240 -290
  78. package/internal/cell-completion.js +15 -0
  79. package/internal/cell-error.js +7 -0
  80. package/internal/cell-parser.js +66 -0
  81. package/internal/cell-rewriter.js +3 -677
  82. package/internal/commonjs-export-evidence.js +63 -0
  83. package/internal/commonjs-source-evidence.js +72 -0
  84. package/internal/compiler-amaro.cjs +2 -0
  85. package/internal/compiler-compression.js +12 -0
  86. package/internal/compiler-data.js +58 -0
  87. package/internal/compiler-descriptors.js +54 -0
  88. package/internal/compiler-dynamic-cache.js +48 -0
  89. package/internal/compiler-entry.js +35 -0
  90. package/internal/compiler-generated-names.js +17 -0
  91. package/internal/compiler-intrinsics.js +84 -0
  92. package/internal/compiler-module-links.js +34 -0
  93. package/internal/compiler-operations.js +61 -0
  94. package/internal/compiler-platform-factory.js +84 -0
  95. package/internal/compiler-platform.js +136 -0
  96. package/internal/compiler-record-roles.js +53 -0
  97. package/internal/compiler-region-output.js +259 -0
  98. package/internal/compiler-resource-lowering.js +103 -0
  99. package/internal/compiler-scope-facts.js +115 -0
  100. package/internal/compiler-service.js +109 -0
  101. package/internal/compiler-source-cache.js +32 -0
  102. package/internal/compiler-source-pieces.js +93 -0
  103. package/internal/compiler-source-regions.js +132 -0
  104. package/internal/compiler-storage-source.js +54 -0
  105. package/internal/compiler-text.js +15 -0
  106. package/internal/compiler-typescript.cjs +2 -0
  107. package/internal/config-spec.js +83 -27
  108. package/internal/cordis-tools-owner.js +130 -21
  109. package/internal/diagnostic.js +27 -1
  110. package/internal/direct-surface-owner.js +76 -23
  111. package/internal/dynamic-binding-evidence.js +16 -0
  112. package/internal/dynamic-environment-compiler.js +779 -0
  113. package/internal/dynamic-environment-integration.js +87 -0
  114. package/internal/dynamic-environment-runtime.js +362 -0
  115. package/internal/dynamic-native-calls.js +150 -0
  116. package/internal/dynamic-native-runtime.js +248 -0
  117. package/internal/dynamic-scope-analysis.js +90 -0
  118. package/internal/edit-transport-owner.js +27 -2
  119. package/internal/execution-seam-compat.js +193 -0
  120. package/internal/failure-reporting.js +118 -18
  121. package/internal/host-rpc.js +66 -0
  122. package/internal/isolated-worker.js +548 -0
  123. package/internal/kernel-child.js +139 -0
  124. package/internal/kernel-worker.js +904 -120
  125. package/internal/language-semantics.js +14 -0
  126. package/internal/legacy-binding-catalog.js +48 -0
  127. package/internal/legacy-cell-analysis.js +600 -0
  128. package/internal/legacy-cell-parser.js +50 -0
  129. package/internal/legacy-cell-rewriter.js +631 -0
  130. package/internal/legacy-repl-convenience.js +291 -0
  131. package/internal/managed-module-operations.js +170 -0
  132. package/internal/module-compilation.js +321 -0
  133. package/internal/module-runtime-platform.js +19 -0
  134. package/internal/module-transform-contract.js +12 -0
  135. package/internal/native-await-declarations.js +68 -0
  136. package/internal/native-root-compilation.js +54 -0
  137. package/internal/native-root-dynamic.js +67 -0
  138. package/internal/recovery-tips.js +27 -15
  139. package/internal/rejected-cell-editor.js +13 -8
  140. package/internal/repl-convenience.js +2 -289
  141. package/internal/repl-memory-projection.js +81 -12
  142. package/internal/repl-observation-interest.js +70 -0
  143. package/internal/repl-rewrite-contract.js +6 -0
  144. package/internal/repl-scope-normalizer.js +2091 -0
  145. package/internal/repl-value-observer.js +114 -0
  146. package/internal/rpc-contract.js +52 -0
  147. package/internal/runtime-bridge-owner.js +172 -28
  148. package/internal/runtime-config.js +48 -52
  149. package/internal/runtime-contexts.js +82 -10
  150. package/internal/runtime-messages.js +111 -0
  151. package/internal/session-cell-executor.js +401 -87
  152. package/internal/session-journal-recovery.js +684 -0
  153. package/internal/session-journal-schema.js +106 -0
  154. package/internal/session-journal.js +184 -654
  155. package/internal/session-log-view.js +42 -9
  156. package/internal/session-runtime.js +256 -95
  157. package/internal/session-state.js +247 -71
  158. package/internal/source-position-map.js +211 -71
  159. package/internal/stateful-module-compiler.js +154 -0
  160. package/internal/stateful-module-runtime.js +303 -0
  161. package/internal/stateful-root-compiler.js +598 -0
  162. package/internal/stateful-root-runtime.js +340 -0
  163. package/internal/typert-codec-compat.js +28 -0
  164. package/internal/typescript-transform.js +29 -0
  165. package/internal/user-binding-authoring.js +70 -0
  166. package/internal/user-binding-console-worker.js +184 -0
  167. package/internal/user-binding-console.js +148 -0
  168. package/internal/user-binding-draft-projection.js +335 -0
  169. package/internal/user-binding-model-context.js +31 -0
  170. package/internal/user-binding-runner.js +56 -0
  171. package/internal/user-bindings-owner.js +1065 -0
  172. package/internal/user-bindings-store.js +293 -0
  173. package/internal/user-bindings.js +353 -0
  174. package/internal/value-wire-schema.js +28 -0
  175. package/internal/value-wire.js +29 -20
  176. package/internal/worker-client.js +57 -67
  177. package/internal/worker-environment.js +45 -0
  178. package/internal/worker-output-relay.js +29 -0
  179. package/internal/worker-shutdown.js +14 -0
  180. package/package.json +75 -7
  181. package/src/client-activity.js +88 -53
  182. package/src/client-authoring-view.js +580 -0
  183. package/src/client-binding-review.js +230 -0
  184. package/src/client-bindings-data.js +29 -0
  185. package/src/client-catalog.js +127 -0
  186. package/src/client-code-editor.js +84 -0
  187. package/src/client-console.js +112 -0
  188. package/src/client-copy.js +370 -0
  189. package/src/client-feature-gates.js +62 -0
  190. package/src/client-host-compat.js +150 -0
  191. package/src/client-indicator-view.js +267 -0
  192. package/src/client-repl-view.js +292 -0
  193. package/src/client-rpc.js +20 -0
  194. package/src/client-settings-view.js +221 -0
  195. package/src/client-styles.js +106 -0
  196. package/src/client-tool-view.js +108 -0
  197. package/src/client-transport.js +159 -0
  198. package/src/client-workbench.js +521 -0
  199. package/src/client.js +233 -615
@@ -0,0 +1,684 @@
1
+ import { isRecord } from './record-utils.js'
2
+ import { sessionEvents } from './session-events.js'
3
+ import {
4
+ JOURNAL_KEY,
5
+ RECOVERY_BOUNDARY_KEY,
6
+ RECOVERY_BOUNDARY_EVENT,
7
+ LEGACY_JOURNAL_VERSION,
8
+ REPL_TOOL_NAMES,
9
+ } from './session-journal-schema.js'
10
+ import {
11
+ normalizeDerivedEditResult,
12
+ normalizeJournal,
13
+ normalizeRecoveryBoundaries,
14
+ reduceStateOperations,
15
+ userBindingsForJournal,
16
+ validatedRewrites,
17
+ } from './session-journal.js'
18
+
19
+ function sourceForRunCall(call) {
20
+ try {
21
+ const args = JSON.parse(call.data.arguments)
22
+ return isRecord(args) && typeof args.code === 'string' ? args.code : undefined
23
+ } catch {
24
+ return undefined
25
+ }
26
+ }
27
+
28
+ /** Select the boundary of a failed replay node or an unavailable recovered suffix. */
29
+ export function recoveryBoundaryForHistory(history, failedNode, { reset = false } = {}) {
30
+ if (reset) failedNode = history.nodes[0]
31
+ const failedCallSeq = failedNode === undefined
32
+ ? history.available === false ? history.volatileSuffix[0]?.seq : undefined
33
+ : failedNode.callSeq
34
+ if (failedCallSeq === undefined) return undefined
35
+ const frontier = reset ? undefined : failedNode === undefined ? history.head : failedNode.parent
36
+ return {
37
+ failedCallSeq,
38
+ frontierCallSeq: frontier === undefined ? null : history.nodes[frontier]?.callSeq ?? null,
39
+ }
40
+ }
41
+
42
+ /** A confirmation only covers an earlier executable call with no persisted result. */
43
+ export function isConfirmableNoop(timeline, callSeq, resultEventIndex) {
44
+ const call = timeline.executableCalls.get(callSeq)
45
+ return call !== undefined && call.eventIndex < resultEventIndex && !timeline.results.has(callSeq)
46
+ }
47
+
48
+ function applyOperations(state, operations, nodeIndex) {
49
+ const transition = reduceStateOperations(state, operations, nodeIndex)
50
+ state.head = transition.head
51
+ state.checkpoints = transition.checkpoints
52
+ if (transition.restored) {
53
+ state.trusted = true
54
+ state.volatileSuffix.length = 0
55
+ }
56
+ }
57
+
58
+ function applyRecord(state, record, invalidCallSeqs) {
59
+ const { call, code, result } = record
60
+ if (invalidCallSeqs.has(call.seq)) return
61
+ const journal = result.journal
62
+ if (journal.status === 'noop') return
63
+ if (journal.status === 'discarded') {
64
+ if (journal.volatileReason !== undefined) {
65
+ state.trusted = false
66
+ state.volatileSuffix.push({ seq: call.seq, code, reason: journal.volatileReason })
67
+ }
68
+ return
69
+ }
70
+ if (journal.status === 'volatile') {
71
+ state.trusted = false
72
+ state.volatileSuffix.push({ seq: call.seq, code, reason: journal.volatileReason ?? 'volatile cell' })
73
+ applyOperations(state, journal.operations, undefined)
74
+ return
75
+ }
76
+ if (!state.trusted && !state.surfaceContracted) {
77
+ state.trusted = true
78
+ state.volatileSuffix.length = 0
79
+ }
80
+ const node = Object.freeze({
81
+ code,
82
+ journal,
83
+ callSeq: call.seq,
84
+ parent: state.head,
85
+ ...(result.userBindings === undefined ? {} : { userBindings: result.userBindings }),
86
+ })
87
+ const index = state.nodes.push(node) - 1
88
+ state.head = index
89
+ try {
90
+ applyOperations(state, journal.operations, index)
91
+ } catch (error) {
92
+ state.nodes.pop()
93
+ state.head = node.parent
94
+ throw error
95
+ }
96
+ }
97
+
98
+ function recordEventSeq(record) {
99
+ return record.result?.eventSeq ?? record.call.seq
100
+ }
101
+
102
+ function foldRecords(records, invalidCallSeqs, options = {}) {
103
+ const state = {
104
+ nodes: [],
105
+ head: undefined,
106
+ checkpoints: new Map(),
107
+ volatileSuffix: [],
108
+ trusted: true,
109
+ surfaceContracted: options.surfaceContracted === true,
110
+ }
111
+ for (const record of records) {
112
+ applyRecord(state, record, invalidCallSeqs)
113
+ }
114
+ return state
115
+ }
116
+
117
+ function dependsOn(nodes, index, ancestor) {
118
+ for (let cursor = index; cursor !== undefined; cursor = nodes[cursor]?.parent) {
119
+ if (cursor === ancestor) return true
120
+ }
121
+ return false
122
+ }
123
+
124
+ /** Return executable call sequences whose provenance remains model-visible. */
125
+ export function visibleExecutableCallSeqs(session) {
126
+ const events = sessionEvents(session)
127
+ let nodes
128
+ try {
129
+ nodes = session?.surface?.nodes
130
+ } catch {
131
+ return undefined
132
+ }
133
+ if (!Array.isArray(events) || !Array.isArray(nodes)) return undefined
134
+ const eventBySeq = new Map()
135
+ for (const event of events) eventBySeq.set(event?.seq, event)
136
+ for (const seq of nodes) {
137
+ if (!Number.isSafeInteger(seq) || seq < 0 || eventBySeq.get(seq) === undefined) return undefined
138
+ }
139
+ const visible = new Set(nodes)
140
+ const resultSources = new Set()
141
+ const visibleAssistantCallSeqs = new Set()
142
+ const pendingAssistantCalls = new Map()
143
+ let assistantCallEvidence = false
144
+ for (const event of events) {
145
+ if (event?.type === 'turn/start' || event?.type === 'turn/end') {
146
+ pendingAssistantCalls.clear()
147
+ continue
148
+ }
149
+ if (event?.type === 'assistant/message') {
150
+ pendingAssistantCalls.clear()
151
+ const content = event.data?.message?.content
152
+ if (Array.isArray(content)) {
153
+ for (const part of content) {
154
+ if (part?.type === 'tool-call' && typeof part.id === 'string') {
155
+ const isVisible = visible.has(event.seq)
156
+ if (isVisible) assistantCallEvidence = true
157
+ let occurrences = pendingAssistantCalls.get(part.id)
158
+ if (occurrences === undefined) {
159
+ occurrences = []
160
+ pendingAssistantCalls.set(part.id, occurrences)
161
+ }
162
+ occurrences.push(isVisible)
163
+ }
164
+ }
165
+ }
166
+ continue
167
+ }
168
+ if (event?.type === 'tool/call' && typeof event.data?.callId === 'string') {
169
+ const occurrences = pendingAssistantCalls.get(event.data.callId)
170
+ if (occurrences !== undefined && occurrences.length > 0) {
171
+ const isVisible = occurrences.shift()
172
+ if (occurrences.length === 0) pendingAssistantCalls.delete(event.data.callId)
173
+ if (isVisible && REPL_TOOL_NAMES.has(event.data.name)) {
174
+ visibleAssistantCallSeqs.add(event.seq)
175
+ }
176
+ }
177
+ }
178
+ if (visible.has(event?.seq)
179
+ && event?.type === 'tool/result' && Array.isArray(event.sourceEventSeqs)) {
180
+ for (const sourceSeq of event.sourceEventSeqs) {
181
+ if (Number.isSafeInteger(sourceSeq) && sourceSeq >= 0) resultSources.add(sourceSeq)
182
+ }
183
+ }
184
+ }
185
+ const calls = new Set()
186
+ for (const event of events) {
187
+ if (event?.type !== 'tool/call' || !REPL_TOOL_NAMES.has(event.data?.name)) continue
188
+ if (assistantCallEvidence
189
+ ? visibleAssistantCallSeqs.has(event.seq)
190
+ : (visible.has(event.seq) || resultSources.has(event.seq))) calls.add(event.seq)
191
+ }
192
+ return calls
193
+ }
194
+
195
+ function timelineRun(call, result, eventIndex, journal) {
196
+ let args
197
+ try {
198
+ const parsed = JSON.parse(call.data.arguments)
199
+ if (isRecord(parsed)) args = Object.freeze(parsed)
200
+ } catch {
201
+ // Invalid persisted arguments cannot support derived source facts.
202
+ }
203
+ return Object.freeze({
204
+ index: eventIndex,
205
+ callSeq: Number.isSafeInteger(call.seq) ? call.seq : undefined,
206
+ args,
207
+ source: typeof args?.code === 'string' ? args.code : undefined,
208
+ journal,
209
+ rewrites: validatedRewrites(result.data?.meta),
210
+ })
211
+ }
212
+
213
+ function timelineDerivedRun(call, result, eventIndex, derived) {
214
+ return Object.freeze({
215
+ index: eventIndex,
216
+ callSeq: Number.isSafeInteger(call.seq) ? call.seq : undefined,
217
+ args: Object.freeze({ description: derived.description }),
218
+ source: derived.code,
219
+ journal: derived.journal,
220
+ rewrites: validatedRewrites(result.data?.meta),
221
+ })
222
+ }
223
+
224
+ function successfulTimelineRun(run) {
225
+ return run.journal?.completion?.kind === 'return' && run.journal.status !== 'noop'
226
+ }
227
+
228
+ function pruneWindowForEvent(event, eventIndex) {
229
+ const shadowedSeqs = event.data.shadowedSeqs
230
+ const seqs = new Set(shadowedSeqs)
231
+ const shadowedRange = event.data.shadowedRange
232
+ const rangeValid = shadowedRange === undefined
233
+ || (isRecord(shadowedRange)
234
+ && shadowedRange.start === shadowedSeqs[0]
235
+ && shadowedRange.end === shadowedSeqs[shadowedSeqs.length - 1])
236
+ const valid = shadowedSeqs.length > 0
237
+ && seqs.size === shadowedSeqs.length
238
+ && shadowedSeqs.every(seq => Number.isSafeInteger(seq) && seq >= 0)
239
+ && shadowedSeqs.every((seq, index) => index === 0 || seq > shadowedSeqs[index - 1])
240
+ && rangeValid
241
+ return { seqs: valid ? seqs : new Set(), lastEventIndex: eventIndex }
242
+ }
243
+
244
+ function continuesPruneWindow(window, event, eventIndex) {
245
+ return event?.type === 'compaction/prune' || eventIndex === window.lastEventIndex + 1
246
+ }
247
+
248
+ function identifiesPrunedResult(window, event, eventIndex) {
249
+ const sourceSeq = event.sourceEventSeqs?.[0]
250
+ return Number.isSafeInteger(sourceSeq)
251
+ && window?.seqs.has(sourceSeq)
252
+ && eventIndex === window.lastEventIndex + 1
253
+ && Array.isArray(event.sourceEventSeqs) && event.sourceEventSeqs.length === 1
254
+ && typeof event.data?.message?.source?.callId === 'string'
255
+ }
256
+
257
+ function replacesPendingCall(candidate, event, pruneSeq) {
258
+ if (candidate === undefined || candidate === null) return false
259
+ const candidateSeq = candidate.event.seq
260
+ const sourceSeq = event.sourceEventSeqs[0]
261
+ const surfaceOp = event.surfaceOp
262
+ const orderedIdentity = REPL_TOOL_NAMES.has(candidate.event.data.name)
263
+ && Number.isSafeInteger(candidateSeq) && candidateSeq >= 0
264
+ && sourceSeq > candidateSeq
265
+ && Number.isSafeInteger(pruneSeq) && pruneSeq >= 0
266
+ && Number.isSafeInteger(event.seq) && event.seq - pruneSeq === 1
267
+ && sourceSeq < pruneSeq
268
+ const exactReplacement = surfaceOp === undefined
269
+ || (surfaceOp?.op === 'replace' && surfaceOp.start === sourceSeq && surfaceOp.end === sourceSeq)
270
+ return orderedIdentity && exactReplacement
271
+ }
272
+
273
+ /**
274
+ * Fold persisted tool events once into the call/result and edit-target timeline
275
+ * shared by prompt projection and cold journal recovery.
276
+ */
277
+ export function foldSessionTimeline(events) {
278
+ const empty = {
279
+ openTurn: false,
280
+ executableCalls: new Map(),
281
+ calls: [],
282
+ results: new Map(),
283
+ boundaries: [],
284
+ found: false,
285
+ unavailableResultSeq: undefined,
286
+ lastSuccessfulRunIndex: undefined,
287
+ latestRun: undefined,
288
+ editableRun: undefined,
289
+ editTargets: new Map(),
290
+ }
291
+ if (!Array.isArray(events)) return empty
292
+
293
+ const state = { ...empty }
294
+ const pendingByCallId = new Map()
295
+ const seenCallIds = new Set()
296
+ const claimedEditTargets = new Set()
297
+ const editClaims = new Map()
298
+ const journalResultSeqs = new Set()
299
+ let pruneReplacementWindow
300
+ let scope = 0
301
+
302
+ const resetTurn = (openTurn) => {
303
+ state.openTurn = openTurn
304
+ scope += 1
305
+ pendingByCallId.clear()
306
+ seenCallIds.clear()
307
+ claimedEditTargets.clear()
308
+ state.latestRun = undefined
309
+ state.editableRun = undefined
310
+ }
311
+
312
+ for (let eventIndex = 0; eventIndex < events.length; eventIndex += 1) {
313
+ const event = events[eventIndex]
314
+ if (pruneReplacementWindow !== undefined
315
+ && !continuesPruneWindow(pruneReplacementWindow, event, eventIndex)) {
316
+ pruneReplacementWindow = undefined
317
+ }
318
+ if (event?.type === 'turn/start') {
319
+ resetTurn(true)
320
+ continue
321
+ }
322
+ if (event?.type === 'turn/end') {
323
+ resetTurn(false)
324
+ continue
325
+ }
326
+ if (event?.type === RECOVERY_BOUNDARY_EVENT) {
327
+ state.unavailableResultSeq ??= Number.isSafeInteger(event.seq) ? event.seq : eventIndex
328
+ continue
329
+ }
330
+ if (event?.type === 'compaction/prune' && Array.isArray(event.data?.shadowedSeqs)) {
331
+ pruneReplacementWindow = pruneWindowForEvent(event, eventIndex)
332
+ continue
333
+ }
334
+ if (event?.type === 'tool/call' && typeof event.data?.callId === 'string') {
335
+ const executable = REPL_TOOL_NAMES.has(event.data.name)
336
+ let entry = { event, eventIndex, scope }
337
+ if (executable) {
338
+ if (Number.isSafeInteger(event.seq) && event.seq >= 0 && state.executableCalls.has(event.seq)) {
339
+ // A sequence collision disproves both sources, but not the earlier
340
+ // verified frontier. Keep its position for persistent contraction.
341
+ const previous = state.executableCalls.get(event.seq)
342
+ previous.ambiguous = true
343
+ state.unavailableResultSeq = Math.min(state.unavailableResultSeq ?? event.seq, event.seq)
344
+ state.found = true
345
+ state.latestRun = undefined
346
+ state.editableRun = undefined
347
+ pendingByCallId.set(previous.event.data.callId, null)
348
+ pendingByCallId.set(event.data.callId, null)
349
+ seenCallIds.add(event.data.callId)
350
+ continue
351
+ }
352
+ if (event.data.name === 'edit_run_code') {
353
+ const targetCallSeq = state.editableRun?.callSeq
354
+ const target = targetCallSeq !== undefined && state.editableRun.source !== undefined
355
+ && !claimedEditTargets.has(targetCallSeq)
356
+ ? Object.freeze({ source: state.editableRun.source, callSeq: targetCallSeq })
357
+ : undefined
358
+ entry = { ...entry, editTarget: target }
359
+ state.editTargets.set(event.seq, target)
360
+ if (target !== undefined) {
361
+ claimedEditTargets.add(target.callSeq)
362
+ editClaims.set(event.seq, target.callSeq)
363
+ }
364
+ }
365
+ state.calls.push(event)
366
+ if (Number.isSafeInteger(event.seq) && event.seq >= 0) {
367
+ state.executableCalls.set(event.seq, entry)
368
+ }
369
+ }
370
+ if (seenCallIds.has(event.data.callId)) pendingByCallId.set(event.data.callId, null)
371
+ else {
372
+ seenCallIds.add(event.data.callId)
373
+ pendingByCallId.set(event.data.callId, entry)
374
+ }
375
+ continue
376
+ }
377
+ if (event?.type !== 'tool/result') continue
378
+
379
+ const sourceSeq = event.sourceEventSeqs?.[0]
380
+ const callId = event.data?.message?.source?.callId
381
+ let entry = Number.isSafeInteger(sourceSeq) ? state.executableCalls.get(sourceSeq) : undefined
382
+ let prunedReplacement = false
383
+ if (entry === undefined && identifiesPrunedResult(pruneReplacementWindow, event, eventIndex)) {
384
+ const candidate = pendingByCallId.get(callId)
385
+ const pruneSeq = events[pruneReplacementWindow.lastEventIndex]?.seq
386
+ if (replacesPendingCall(candidate, event, pruneSeq)) {
387
+ entry = candidate
388
+ prunedReplacement = true
389
+ pruneReplacementWindow.lastEventIndex = eventIndex
390
+ }
391
+ }
392
+ if (entry === undefined && !Number.isSafeInteger(sourceSeq) && typeof callId === 'string') {
393
+ entry = pendingByCallId.get(callId)
394
+ }
395
+ if (typeof callId === 'string') pendingByCallId.delete(callId)
396
+ if (entry === null) {
397
+ state.latestRun = undefined
398
+ state.editableRun = undefined
399
+ continue
400
+ }
401
+
402
+ const call = entry?.event
403
+ if (entry?.ambiguous) {
404
+ state.latestRun = undefined
405
+ state.editableRun = undefined
406
+ continue
407
+ }
408
+ const callSeq = call?.seq
409
+ const meta = event.data?.meta
410
+ let normalized
411
+ if (isRecord(meta) && Object.hasOwn(meta, JOURNAL_KEY)) {
412
+ if (Number.isSafeInteger(sourceSeq) && journalResultSeqs.has(sourceSeq)) {
413
+ state.unavailableResultSeq ??= sourceSeq
414
+ state.results.set(sourceSeq, {
415
+ eventSeq: Number.isSafeInteger(event.seq) && event.seq >= 0 ? event.seq : sourceSeq,
416
+ eventIndex,
417
+ error: `session log contains duplicate PTC journal results for call seq ${sourceSeq}`,
418
+ })
419
+ state.found = true
420
+ continue
421
+ }
422
+ if (Number.isSafeInteger(sourceSeq)) journalResultSeqs.add(sourceSeq)
423
+ if (entry === undefined) {
424
+ if (Number.isSafeInteger(sourceSeq)) {
425
+ state.unavailableResultSeq ??= sourceSeq
426
+ }
427
+ continue
428
+ }
429
+ const resultSeq = prunedReplacement
430
+ ? callSeq
431
+ : Number.isSafeInteger(sourceSeq) ? sourceSeq : callSeq
432
+ const raw = {
433
+ meta,
434
+ eventSeq: Number.isSafeInteger(event.seq) && event.seq >= 0 ? event.seq : resultSeq,
435
+ eventIndex,
436
+ }
437
+ if (Object.hasOwn(meta, RECOVERY_BOUNDARY_KEY)) {
438
+ try {
439
+ state.boundaries.push(...normalizeRecoveryBoundaries(
440
+ meta[RECOVERY_BOUNDARY_KEY], raw.eventSeq,
441
+ ))
442
+ } catch {
443
+ state.unavailableResultSeq ??= callSeq
444
+ }
445
+ }
446
+ try {
447
+ if (call.data.name === 'edit_run_code') {
448
+ const derived = normalizeDerivedEditResult(meta, entry.editTarget?.callSeq)
449
+ normalized = {
450
+ ...raw,
451
+ journal: derived.journal,
452
+ derived,
453
+ ...(derived.userBindings === undefined ? {} : { userBindings: derived.userBindings }),
454
+ }
455
+ } else if (call.data.name === 'run_code') {
456
+ const rawJournal = meta[JOURNAL_KEY]
457
+ const resolveLegacyConfirm = rawJournal?.version === LEGACY_JOURNAL_VERSION
458
+ ? legacyCallId => {
459
+ const candidates = [...state.executableCalls.values()]
460
+ .filter(candidate => candidate.eventIndex < eventIndex
461
+ && candidate.event.data?.name === 'run_code'
462
+ && candidate.event.data.callId === legacyCallId
463
+ && !state.results.has(candidate.event.seq))
464
+ return candidates.length === 1 ? candidates[0].event.seq : undefined
465
+ }
466
+ : undefined
467
+ const journal = normalizeJournal(rawJournal, { resolveLegacyConfirm })
468
+ const userBindings = userBindingsForJournal(meta, journal)
469
+ normalized = {
470
+ ...raw,
471
+ journal,
472
+ ...(userBindings === undefined ? {} : { userBindings }),
473
+ }
474
+ }
475
+ } catch (error) {
476
+ normalized = { ...raw, error: error.message }
477
+ }
478
+ if (Number.isSafeInteger(resultSeq)) state.results.set(resultSeq, normalized)
479
+ state.found = true
480
+ }
481
+
482
+ const claimedTarget = editClaims.get(callSeq)
483
+ if (claimedTarget !== undefined) {
484
+ editClaims.delete(callSeq)
485
+ if (normalized?.derived === undefined) claimedEditTargets.delete(claimedTarget)
486
+ }
487
+ if (entry === undefined || entry.scope !== scope) continue
488
+ if (call.data.name === 'edit_run_code') {
489
+ if (normalized?.derived !== undefined) {
490
+ const run = timelineDerivedRun(call, event, eventIndex, normalized.derived)
491
+ state.latestRun = run
492
+ state.editableRun = run
493
+ if (successfulTimelineRun(run)) state.lastSuccessfulRunIndex = eventIndex
494
+ }
495
+ continue
496
+ }
497
+ if (call.data.name !== 'run_code') {
498
+ // Unrelated native settlements do not change the current editable cell.
499
+ // The edit target is captured at dispatch time and remains valid until a
500
+ // new executable cell or turn boundary supersedes it.
501
+ continue
502
+ }
503
+ const run = timelineRun(call, event, eventIndex, normalized?.journal)
504
+ state.latestRun = run
505
+ state.editableRun = run
506
+ if (successfulTimelineRun(run)) state.lastSuccessfulRunIndex = eventIndex
507
+ }
508
+ return state
509
+ }
510
+
511
+ /** Fold the session log into the last exactly replayable frontier. */
512
+ export function recoverJournal(session, currentCallSeq, options = {}) {
513
+ const events = sessionEvents(session)
514
+ if (!Array.isArray(events)) {
515
+ return { nodes: [], head: undefined, checkpoints: new Map(), volatileSuffix: [], available: true }
516
+ }
517
+ const timeline = foldSessionTimeline(events)
518
+ const calls = timeline.calls.filter(call => call.seq !== currentCallSeq)
519
+ const { executableCalls, results, found } = timeline
520
+ const visibleCalls = options?.visibleCallSeqs
521
+ const invalidCallSeqs = new Set()
522
+ let unavailableBoundary
523
+ const registerUnavailable = ({ seq, reason, code }, recoveryState) => {
524
+ if (unavailableBoundary === undefined || seq <= unavailableBoundary.seq) {
525
+ unavailableBoundary = { seq, reason }
526
+ }
527
+ if (recoveryState === undefined) return
528
+ invalidCallSeqs.add(seq)
529
+ recoveryState.trusted = false
530
+ if (!recoveryState.volatileSuffix.some(item => item.seq === seq)) {
531
+ recoveryState.volatileSuffix.push({ seq, code, reason })
532
+ }
533
+ }
534
+ if (timeline.unavailableResultSeq !== undefined) {
535
+ registerUnavailable({
536
+ seq: timeline.unavailableResultSeq,
537
+ reason: 'unavailable or malformed dsh-ptc-plus journal result',
538
+ })
539
+ }
540
+ for (const [resultSeq, result] of results.entries()) {
541
+ if (result?.error !== undefined && Number.isSafeInteger(resultSeq)) {
542
+ registerUnavailable({ seq: resultSeq, reason: result.error })
543
+ }
544
+ }
545
+ const extraBoundaries = options?.extraBoundaries ?? []
546
+ let normalizedExtraBoundaries = []
547
+ try {
548
+ normalizedExtraBoundaries = normalizeRecoveryBoundaries(extraBoundaries, undefined)
549
+ } catch {
550
+ normalizedExtraBoundaries = []
551
+ }
552
+ const boundaries = [
553
+ ...timeline.boundaries,
554
+ ...normalizedExtraBoundaries.map(boundary => ({
555
+ ...boundary,
556
+ eventSeq: Number.POSITIVE_INFINITY,
557
+ })),
558
+ ].sort((left, right) => left.eventSeq - right.eventSeq)
559
+ // An unavailable historical result contracts the replay frontier. It must
560
+ // not prevent the current request from executing on the verified prefix.
561
+
562
+ const confirmedNoops = new Set()
563
+ const invalidResultReasons = new Map()
564
+ for (const [resultSeq, { journal, eventIndex }] of results.entries()) {
565
+ const candidateConfirms = []
566
+ for (const callSeq of journal?.confirms ?? []) {
567
+ if (!isConfirmableNoop(timeline, callSeq, eventIndex)) {
568
+ const reason = `confirmed no-op does not identify an earlier unjournaled run_code call seq ${callSeq}`
569
+ invalidResultReasons.set(resultSeq, reason)
570
+ registerUnavailable({ seq: resultSeq, reason })
571
+ break
572
+ }
573
+ candidateConfirms.push(callSeq)
574
+ }
575
+ if (!invalidResultReasons.has(resultSeq)) {
576
+ for (const callSeq of candidateConfirms) confirmedNoops.add(callSeq)
577
+ }
578
+ }
579
+ const records = []
580
+ const orderedRecords = []
581
+ let state = foldRecords(records, invalidCallSeqs)
582
+ let boundaryIndex = 0
583
+ const applyBoundariesBefore = (seq) => {
584
+ while (boundaryIndex < boundaries.length && boundaries[boundaryIndex].eventSeq <= seq) {
585
+ const boundary = boundaries[boundaryIndex++]
586
+ const failedCall = executableCalls.get(boundary.failedCallSeq)
587
+ if (failedCall === undefined || failedCall.event.seq >= boundary.eventSeq) continue
588
+ const failedIndex = state.nodes.findIndex(node => node.callSeq === boundary.failedCallSeq)
589
+ const expectedFrontier = failedIndex < 0 ? state.head : state.nodes[failedIndex].parent
590
+ const frontierIndex = boundary.frontierCallSeq === null
591
+ ? undefined
592
+ : state.nodes.findIndex(node => node.callSeq === boundary.frontierCallSeq)
593
+ if (frontierIndex !== expectedFrontier) continue
594
+ const resolvesUnavailable = unavailableBoundary?.seq === boundary.failedCallSeq
595
+ if (failedIndex < 0 && !resolvesUnavailable) continue
596
+ for (let index = 0; index < state.nodes.length; index += 1) {
597
+ if (dependsOn(state.nodes, index, failedIndex)) invalidCallSeqs.add(state.nodes[index].callSeq)
598
+ }
599
+ if (failedIndex < 0) {
600
+ for (const record of records) {
601
+ if (record.call.seq >= boundary.failedCallSeq) invalidCallSeqs.add(record.call.seq)
602
+ }
603
+ }
604
+ state = foldRecords(records, invalidCallSeqs, {
605
+ surfaceContracted: resolvesUnavailable ? false : state.surfaceContracted,
606
+ })
607
+ if (resolvesUnavailable) {
608
+ unavailableBoundary = undefined
609
+ }
610
+ }
611
+ }
612
+ for (const call of calls) {
613
+ const result = results.get(call.seq)
614
+ const code = call.data?.name === 'edit_run_code' ? result?.derived?.code : sourceForRunCall(call)
615
+ const record = {
616
+ call,
617
+ code,
618
+ result,
619
+ hidden: visibleCalls instanceof Set && !visibleCalls.has(call.seq),
620
+ }
621
+ if (call.data?.name === 'edit_run_code' && record.code === undefined && result === undefined) continue
622
+ if (result?.journal === undefined && confirmedNoops.has(call.seq)) continue
623
+ orderedRecords.push(record)
624
+ }
625
+ orderedRecords.sort((left, right) => (
626
+ recordEventSeq(left) - recordEventSeq(right) || left.call.seq - right.call.seq
627
+ ))
628
+ for (const record of orderedRecords) {
629
+ applyBoundariesBefore(recordEventSeq(record))
630
+ records.push(record)
631
+ if (record.hidden) {
632
+ state.surfaceContracted = true
633
+ registerUnavailable({
634
+ seq: record.call.seq,
635
+ reason: 'model-visible provenance was shadowed',
636
+ }, state)
637
+ continue
638
+ }
639
+ if (unavailableBoundary !== undefined && record.call.seq >= unavailableBoundary.seq) {
640
+ registerUnavailable({
641
+ seq: record.call.seq,
642
+ code: record.code,
643
+ reason: unavailableBoundary.reason,
644
+ }, state)
645
+ continue
646
+ }
647
+ const invalidReason = invalidResultReasons.get(record.call.seq)
648
+ if (record.result?.journal === undefined || invalidReason !== undefined) {
649
+ registerUnavailable({
650
+ seq: record.call.seq,
651
+ code: record.code,
652
+ reason: invalidReason ?? record.result?.error ?? 'missing dsh-ptc-plus journal result',
653
+ }, state)
654
+ continue
655
+ }
656
+ try {
657
+ applyRecord(state, record, invalidCallSeqs)
658
+ } catch (error) {
659
+ registerUnavailable({ seq: record.call.seq, code: record.code, reason: error.message }, state)
660
+ }
661
+ }
662
+ applyBoundariesBefore(Number.POSITIVE_INFINITY)
663
+ if (unavailableBoundary !== undefined) registerUnavailable(unavailableBoundary, state)
664
+ return {
665
+ nodes: state.nodes,
666
+ head: state.head,
667
+ checkpoints: state.checkpoints,
668
+ volatileSuffix: state.volatileSuffix,
669
+ available: found && !state.surfaceContracted && unavailableBoundary === undefined,
670
+ }
671
+ }
672
+
673
+ /** Return source nodes from the empty state to a selected durable head. */
674
+ export function pathToHead(state) {
675
+ const path = []
676
+ for (let cursor = state.head; cursor !== undefined;) {
677
+ const node = state.nodes[cursor]
678
+ if (node === undefined) throw new Error('invalid dsh-ptc-plus journal head')
679
+ path.push(node)
680
+ cursor = node.parent
681
+ }
682
+ path.reverse()
683
+ return path
684
+ }