dsh-ptc-plus 0.4.8 → 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 (139) hide show
  1. package/README.en.md +61 -123
  2. package/README.md +61 -123
  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 +13 -2
  11. package/docs/README.md +7 -2
  12. package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +31 -179
  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 +14 -3
  18. package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +2 -2
  19. package/docs/adr/0022-stage-npm-releases-with-oidc.md +4 -2
  20. package/docs/adr/0023-global-user-bindings.md +3 -1
  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 +101 -35
  23. package/docs/adr/0030-serve-both-typert-codec-generations-from-one-codec.md +3 -3
  24. package/docs/adr/0031-close-semantic-obligations-by-owner.md +1 -1
  25. package/docs/adr/0032-own-session-log-facts-in-a-host-only-projection.md +27 -0
  26. package/docs/adr/0033-gate-client-visual-usability-in-a-browser.md +23 -0
  27. package/docs/adr/0034-keep-program-values-separate-from-output-projections.md +29 -0
  28. package/docs/adr/0035-run-coverage-only-before-a-new-release.md +27 -0
  29. package/docs/adr/0036-separate-focused-development-from-delivery-verification.md +29 -0
  30. package/docs/adr/0037-keep-tool-hover-previews-passive.md +52 -0
  31. package/docs/adr/0038-freeze-session-capability-plans.md +71 -0
  32. package/docs/adr/0039-verify-documentation-without-runtime-suites.md +29 -0
  33. package/docs/adr/0040-separate-capability-configurations-from-session-observations.md +39 -0
  34. package/docs/adr/0041-record-cell-execution-directories.md +65 -0
  35. package/docs/adr/0042-persist-provider-availability-and-handback.md +41 -0
  36. package/docs/adr/0043-compose-capability-and-context-policies.md +37 -0
  37. package/docs/adr/README.md +6 -0
  38. package/docs/architecture.md +25 -27
  39. package/docs/binding-review-placement-design.md +29 -21
  40. package/docs/capability-planning-components.md +93 -0
  41. package/docs/capability-projection.md +7 -0
  42. package/docs/client-ui.md +88 -29
  43. package/docs/client-visual-contract.md +65 -0
  44. package/docs/context-management.md +22 -0
  45. package/docs/context-optimization-plan.md +353 -0
  46. package/docs/context-public-interface-coverage.md +25 -0
  47. package/docs/durability-design.md +31 -14
  48. package/docs/evaluation.md +41 -6
  49. package/docs/evaluations/command-goat-deepseek-v4.1-flash-20261006.md +126 -0
  50. package/docs/evaluations/command-goat-deepseek-v4.1-flash.md +281 -0
  51. package/docs/installation.md +50 -8
  52. package/docs/program-data-plane.md +5 -3
  53. package/docs/publishing.md +5 -5
  54. package/docs/release-coverage.md +73 -0
  55. package/docs/release-notes.md +1 -1
  56. package/docs/review-procedure.md +56 -0
  57. package/docs/runtime-reference.md +48 -16
  58. package/docs/semantic-validation.md +14 -2
  59. package/docs/stateful-binding-updates-proposal.md +2 -2
  60. package/docs/user-bindings.en.md +2 -2
  61. package/docs/user-bindings.md +3 -3
  62. package/docs/value-wire.md +11 -1
  63. package/docs/verification-optimization-work.md +52 -15
  64. package/docs/verification.md +30 -86
  65. package/index.js +147 -23
  66. package/internal/binding-toggle-receipt.js +24 -0
  67. package/internal/capability-command-receipt.js +29 -0
  68. package/internal/capability-environment.js +1247 -0
  69. package/internal/capability-plan.js +285 -0
  70. package/internal/capability-policy.js +96 -0
  71. package/internal/capability-statistics-store.js +180 -0
  72. package/internal/capability-statistics.js +210 -0
  73. package/internal/compiler-platform.js +5 -3
  74. package/internal/compiler-service.js +2 -9
  75. package/internal/config-spec.js +79 -9
  76. package/internal/cordis-tools-owner.js +22 -19
  77. package/internal/direct-surface-owner.js +29 -29
  78. package/internal/dynamic-environment-runtime.js +9 -9
  79. package/internal/edit-transport-owner.js +183 -139
  80. package/internal/execution-provider-owner.js +160 -0
  81. package/internal/execution-seam-compat.js +76 -120
  82. package/internal/isolated-worker.js +12 -8
  83. package/internal/kernel-worker.js +104 -32
  84. package/internal/native-root-dynamic.js +12 -13
  85. package/internal/program-bindings.js +190 -0
  86. package/internal/recovery-tips.js +53 -23
  87. package/internal/rejected-cell-editor.js +8 -8
  88. package/internal/repl-observation-interest.js +11 -3
  89. package/internal/rpc-contract.js +1 -0
  90. package/internal/runtime-bridge-owner.js +351 -82
  91. package/internal/runtime-config.js +58 -0
  92. package/internal/runtime-contexts.js +85 -16
  93. package/internal/runtime-intrinsics.js +1 -0
  94. package/internal/runtime-messages.js +41 -0
  95. package/internal/sdk-projection.js +10 -82
  96. package/internal/session-cell-executor.js +42 -19
  97. package/internal/session-events.js +74 -4
  98. package/internal/session-journal-recovery.js +904 -184
  99. package/internal/session-journal-schema.js +6 -3
  100. package/internal/session-journal.js +31 -2
  101. package/internal/session-log-view.js +189 -43
  102. package/internal/session-runtime.js +175 -23
  103. package/internal/settings-compat.js +23 -6
  104. package/internal/stateful-module-compiler.js +3 -2
  105. package/internal/stateful-module-runtime.js +7 -3
  106. package/internal/stateful-root-runtime.js +9 -4
  107. package/internal/user-binding-authoring.js +2 -7
  108. package/internal/user-bindings-owner.js +47 -7
  109. package/internal/validated-parse-repair.js +1 -1
  110. package/internal/value-wire.js +68 -21
  111. package/internal/worker-cwd-virtualization.js +20 -18
  112. package/internal/worker-realm-surfaces.js +3 -3
  113. package/locale/en.json +6 -0
  114. package/locale/zh.json +6 -0
  115. package/package.json +55 -38
  116. package/src/client-authoring-view.js +286 -168
  117. package/src/client-binding-review.js +4 -1
  118. package/src/client-binding-toggle.js +18 -0
  119. package/src/client-capabilities-view.js +1341 -0
  120. package/src/client-capability-sort.js +29 -0
  121. package/src/client-code-editor.js +6 -0
  122. package/src/client-copy.js +881 -8
  123. package/src/client-feature-gates.js +15 -5
  124. package/src/client-host-compat.js +23 -2
  125. package/src/client-indicator-view.js +4 -6
  126. package/src/client-primitives.js +280 -0
  127. package/src/client-repl-view.js +23 -16
  128. package/src/client-settings-view.js +412 -69
  129. package/src/client-styles.js +193 -41
  130. package/src/client-tool-hover.js +185 -0
  131. package/src/client-tool-view.js +45 -11
  132. package/src/client-transport.js +44 -9
  133. package/src/client-workbench.js +9 -10
  134. package/src/client.js +148 -68
  135. package/assets/ptc-plus-bindings-en.png +0 -0
  136. package/assets/ptc-plus-bindings-zh.png +0 -0
  137. package/assets/ptc-plus-repair-en.png +0 -0
  138. package/assets/ptc-plus-repair-zh.png +0 -0
  139. package/client.js +0 -31857
@@ -1,12 +1,18 @@
1
- import { isDeepStrictEqual } from 'node:util'
2
- import { isRecord } from './record-utils.js'
3
- import { sessionEvents } from './session-events.js'
1
+ import { createHash } from 'node:crypto'
2
+ import { deepEqualJson, isJsonValue } from '@deepseek-ai/dsh-util-values'
3
+ import { deepFreeze, isRecord } from './record-utils.js'
4
+ import { sessionEvents, sessionSourceMessages } from './session-events.js'
5
+ import { USER_BINDINGS_META_KEY } from './user-bindings.js'
4
6
  import {
5
7
  JOURNAL_KEY,
6
8
  RECOVERY_BOUNDARY_KEY,
7
9
  RECOVERY_BOUNDARY_EVENT,
8
10
  LEGACY_JOURNAL_VERSION,
9
11
  REPL_TOOL_NAMES,
12
+ REWRITES_KEY,
13
+ EDIT_TARGET_KEY,
14
+ DERIVED_RUN_KEY,
15
+ POST_EDIT_KEY,
10
16
  } from './session-journal-schema.js'
11
17
  import {
12
18
  isCanonicalSequence,
@@ -17,8 +23,14 @@ import {
17
23
  userBindingsForJournal,
18
24
  validatedRewrites,
19
25
  } from './session-journal.js'
26
+ import { decodeValue } from './value-wire.js'
27
+ import { editRejectedCell } from './rejected-cell-editor.js'
20
28
 
21
29
  const RECOVERY_BOUNDARY_EVIDENCE = Symbol('recoveryBoundaryEvidence')
30
+ const RUN_SELECTION_CLEAR_REASONS = new Set([
31
+ 'turn/start', 'turn/end', 'duplicate-call-sequence', 'invalid-source-relation',
32
+ 'ambiguous-call-id', 'ambiguous-call-sequence', 'duplicate-result', 'result-identity-mismatch',
33
+ ])
22
34
  const MODEL_VISIBLE_SURFACE_EVENTS = new Set([
23
35
  'system/message',
24
36
  'developer/message',
@@ -147,6 +159,12 @@ export function visibleExecutableCallSeqs(session) {
147
159
  return new Set()
148
160
  }
149
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
+ }
150
168
  const eventBySeq = new Map()
151
169
  const duplicateEventSeqs = new Set()
152
170
  for (const event of events) {
@@ -171,7 +189,7 @@ export function visibleExecutableCallSeqs(session) {
171
189
  }
172
190
  if (event?.type === 'assistant/message') {
173
191
  pendingAssistantCalls.clear()
174
- const content = event.data?.message?.content
192
+ const content = sourceMessages.get(event.seq)?.content
175
193
  if (Array.isArray(content)) {
176
194
  for (const part of content) {
177
195
  if (part?.type === 'tool-call' && typeof part.id === 'string') {
@@ -205,6 +223,13 @@ export function visibleExecutableCallSeqs(session) {
205
223
  if (event?.type !== 'tool/call' || !REPL_TOOL_NAMES.has(event.data?.name)) continue
206
224
  if (visibleAssistantCallSeqs.has(event.seq)) calls.add(event.seq)
207
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
+ }
208
233
  return calls
209
234
  }
210
235
 
@@ -241,6 +266,33 @@ function successfulTimelineRun(run) {
241
266
  return run.journal?.completion?.kind === 'return' && run.journal.status !== 'noop'
242
267
  }
243
268
 
269
+ function settlementRun(settlement) {
270
+ const publication = settlement.publication
271
+ if (publication === undefined) return undefined
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
+ }
277
+ return timelineJson(publication.derived === undefined
278
+ ? timelineRun(settlement.call, result, publication.index, publication.journal)
279
+ : timelineDerivedRun(settlement.call, result, publication.index, publication.derived))
280
+ }
281
+
282
+ function selectSettlementRun(selected, settlement, scope, frontier) {
283
+ const publication = settlement.publication
284
+ if (publication === undefined || settlement.entry.scope !== scope
285
+ || (frontier !== null && publication.index <= frontier.index)) return selected
286
+ return selected === undefined || publication.index >= selected.index ? settlementRun(settlement) : selected
287
+ }
288
+
289
+ function editTargetForSelection(run, claimedTargets) {
290
+ const callSeq = run?.callSeq
291
+ return callSeq !== undefined && run.source !== undefined && !claimedTargets.has(callSeq)
292
+ ? Object.freeze({ source: run.source, callSeq })
293
+ : undefined
294
+ }
295
+
244
296
  function pruneWindowForEvent(event, eventIndex) {
245
297
  const shadowedSeqs = event.data.shadowedSeqs
246
298
  const seqs = new Set(shadowedSeqs)
@@ -254,7 +306,7 @@ function pruneWindowForEvent(event, eventIndex) {
254
306
  && shadowedSeqs.every(isCanonicalSequence)
255
307
  && shadowedSeqs.every((seq, index) => index === 0 || seq > shadowedSeqs[index - 1])
256
308
  && rangeValid
257
- return { seqs: valid ? seqs : new Set(), lastEventIndex: eventIndex }
309
+ return { seqs: valid ? [...seqs] : [], lastEventIndex: eventIndex, lastEventSeq: event.seq }
258
310
  }
259
311
 
260
312
  function continuesPruneWindow(window, event, eventIndex) {
@@ -264,7 +316,7 @@ function continuesPruneWindow(window, event, eventIndex) {
264
316
  function identifiesPrunedResult(window, event, eventIndex) {
265
317
  const sourceSeq = event.sourceEventSeqs?.[0]
266
318
  return isCanonicalSequence(sourceSeq)
267
- && window?.seqs.has(sourceSeq)
319
+ && window?.seqs.includes(sourceSeq)
268
320
  && eventIndex === window.lastEventIndex + 1
269
321
  && Array.isArray(event.sourceEventSeqs) && event.sourceEventSeqs.length === 1
270
322
  && typeof event.data?.message?.source?.callId === 'string'
@@ -305,44 +357,652 @@ function exactResultReplacement(event, sourceSeq) {
305
357
  || (surfaceOp.startSeq === sourceSeq && surfaceOp.endSeq === sourceSeq)
306
358
  }
307
359
 
308
- /** A replacement representation may differ from its shadowed result only in message content. */
309
- function sameResultExceptContent(previous, next) {
310
- const withoutContent = data => isRecord(data) && isRecord(data.message)
311
- ? { ...data, message: { ...data.message, content: undefined } }
312
- : data
313
- return isDeepStrictEqual(withoutContent(previous), withoutContent(next))
360
+ function resolveTimelineResult(event, eventIndex, executableCalls, pendingByCallId,
361
+ settledResultsByEventSeq, pruneReplacementWindow, identity) {
362
+ const hasSourceRelation = Object.hasOwn(event, 'sourceEventSeqs')
363
+ const sourceRelation = event.sourceEventSeqs
364
+ const sourceSeq = sourceRelation?.[0]
365
+ const canonicalSourceRelation = Array.isArray(sourceRelation)
366
+ && sourceRelation.length === 1
367
+ && isCanonicalSequence(sourceSeq)
368
+ const callId = event.data?.message?.source?.callId
369
+ let entry = canonicalSourceRelation ? executableCalls.get(sourceSeq) : undefined
370
+ let prunedReplacement = false
371
+ let republishedSettlement
372
+ if (entry === undefined && identifiesPrunedResult(pruneReplacementWindow, event, eventIndex)) {
373
+ const candidate = pendingByCallId.get(callId)
374
+ const pruneSeq = pruneReplacementWindow.lastEventSeq
375
+ if (replacesPendingCall(candidate, event, pruneSeq)) {
376
+ entry = candidate
377
+ prunedReplacement = true
378
+ pruneReplacementWindow = { ...pruneReplacementWindow, lastEventIndex: eventIndex, lastEventSeq: event.seq }
379
+ }
380
+ }
381
+ if (entry === undefined && !hasSourceRelation && typeof callId === 'string') {
382
+ entry = pendingByCallId.get(callId)
383
+ }
384
+ if (entry === undefined && canonicalSourceRelation) {
385
+ // The Host pruner may replace an already-settled result with a
386
+ // content-only clone that cites the shadowed result event. It is the same
387
+ // call's settlement representation, not a new unknown boundary.
388
+ const settled = settledResultsByEventSeq.get(sourceSeq)
389
+ if (settled !== undefined && typeof callId === 'string'
390
+ && settled.call?.data?.callId === callId
391
+ && exactResultReplacement(event, sourceSeq)
392
+ && settled.identity === identity) {
393
+ entry = executableCalls.get(settled.call?.seq) ?? settled.entry
394
+ prunedReplacement = true
395
+ republishedSettlement = settled
396
+ }
397
+ }
398
+ return { entry, prunedReplacement, republishedSettlement, pruneReplacementWindow }
399
+ }
400
+
401
+ function invalidSourceCallSeq(event, executableCalls, pendingByCallId) {
402
+ const callId = event.data?.message?.source?.callId
403
+ const identityEntry = typeof callId === 'string' ? pendingByCallId.get(callId) : undefined
404
+ const callSeqs = new Set(Array.isArray(event.sourceEventSeqs)
405
+ ? event.sourceEventSeqs.filter(isCanonicalSequence)
406
+ .map(seq => executableCalls.get(seq)?.event?.seq).filter(isCanonicalSequence) : [])
407
+ if (isCanonicalSequence(identityEntry?.event?.seq)) callSeqs.add(identityEntry.event.seq)
408
+ return callSeqs.size === 0 ? undefined : Math.min(...callSeqs)
409
+ }
410
+
411
+ function timelineResultError(event, eventIndex, callSeq, error) {
412
+ return { eventSeq: isCanonicalSequence(event.seq) ? event.seq : callSeq, eventIndex, error }
413
+ }
414
+
415
+ function republishedTimelineResult(result, event) {
416
+ return { ...result, eventSeq: event.seq, positionSeq: result.positionSeq ?? result.eventSeq }
417
+ }
418
+
419
+ function correlationEvent(event) {
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
+ } } : {}),
433
+ ...(event.type === 'tool/call' && typeof event.data?.callId === 'string'
434
+ ? { data: timelineCallEvent(event).data } : {}),
435
+ ...(event.type === 'tool/result' ? { data: { message: { source: {
436
+ ...(typeof event.data?.message?.source?.callId === 'string'
437
+ ? { callId: event.data.message.source.callId } : {}),
438
+ } } } } : {}),
439
+ ...(event.type === 'compaction/prune' ? { data: {
440
+ ...(event.data?.shadowedSeqs === undefined ? {} : { shadowedSeqs: event.data.shadowedSeqs }),
441
+ ...(event.data?.shadowedRange === undefined ? {} : { shadowedRange: event.data.shadowedRange }),
442
+ } } : {}),
443
+ ...(Object.hasOwn(event, 'sourceEventSeqs') ? { sourceEventSeqs: event.sourceEventSeqs } : {}),
444
+ ...(event.surfaceOp === undefined ? {} : { surfaceOp: event.surfaceOp }),
445
+ })
446
+ }
447
+
448
+ function recordPendingCall(pending, seen, entry, duplicateSequence) {
449
+ const id = entry.event.data.callId
450
+ if (duplicateSequence !== undefined) {
451
+ pending.set(duplicateSequence.event.data.callId, null)
452
+ pending.set(id, null)
453
+ seen.add(id)
454
+ } else if (seen.has(id)) pending.set(id, null)
455
+ else {
456
+ seen.add(id)
457
+ pending.set(id, entry)
458
+ }
459
+ }
460
+
461
+ function nextPruneWindow(window, event, eventIndex) {
462
+ if (window !== undefined && !continuesPruneWindow(window, event, eventIndex)) return undefined
463
+ return window
314
464
  }
315
465
 
316
466
  /**
317
467
  * Fold persisted tool events once into the call/result and edit-target timeline
318
468
  * shared by prompt projection and cold journal recovery.
319
469
  */
320
- export function foldSessionTimeline(events) {
321
- const empty = {
470
+ class TimelineTable {
471
+ constructor(table = {}, onSet) {
472
+ this.table = table
473
+ this.copied = false
474
+ this.onSet = onSet
475
+ }
476
+
477
+ get(key) { return this.table[`k${key}`]?.[1] }
478
+ has(key) { return Object.hasOwn(this.table, `k${key}`) }
479
+ values() { return Object.values(this.table).map(entry => entry[1]) }
480
+ copy() {
481
+ if (!this.copied) {
482
+ this.table = { ...this.table }
483
+ this.copied = true
484
+ }
485
+ }
486
+ set(key, value) {
487
+ this.onSet?.(this.get(key), value)
488
+ this.copy()
489
+ this.table[`k${key}`] = [key, timelineJson(value ?? null)]
490
+ }
491
+ add(key) { this.set(key, true) }
492
+ delete(key) {
493
+ if (!this.has(key)) return
494
+ this.copy()
495
+ delete this.table[`k${key}`]
496
+ }
497
+ clear() { this.table = {}; this.copied = true }
498
+ }
499
+
500
+ function timelineJson(value) {
501
+ return deepFreeze(JSON.parse(JSON.stringify(value ?? null)))
502
+ }
503
+
504
+ function timelineCallEvent(event) {
505
+ return timelineJson({ seq: event.seq, type: event.type, data: {
506
+ callId: event.data.callId, name: event.data.name,
507
+ ...(REPL_TOOL_NAMES.has(event.data.name) ? { arguments: event.data.arguments } : {}),
508
+ } })
509
+ }
510
+
511
+ function resultIdentity(data) {
512
+ const identity = timelineJson(isRecord(data) && isRecord(data.message)
513
+ ? { ...data, message: { ...data.message, content: undefined } } : data)
514
+ return createHash('sha256').update(JSON.stringify(identity, (_key, value) => (
515
+ isRecord(value) ? Object.fromEntries(Object.keys(value).sort().map(key => [key, value[key]])) : value
516
+ ))).digest('hex')
517
+ }
518
+
519
+ export function createSessionTimelineState() {
520
+ return {
322
521
  openTurn: false,
323
- executableCalls: new Map(),
522
+ executableCalls: {},
324
523
  calls: [],
325
- results: new Map(),
524
+ results: {},
326
525
  boundaries: [],
327
526
  found: false,
328
- unavailableResultSeq: undefined,
329
- unavailableResultReason: undefined,
330
- lastSuccessfulRunIndex: undefined,
331
- latestRun: undefined,
332
- editableRun: undefined,
333
- editTargets: new Map(),
334
- }
335
- if (!Array.isArray(events)) return empty
336
-
337
- const state = { ...empty }
338
- const pendingByCallId = new Map()
339
- const seenCallIds = new Set()
340
- const claimedEditTargets = new Set()
341
- const editClaims = new Map()
342
- const ordinaryResultSeqs = new Set()
343
- const settledResultsByEventSeq = new Map()
344
- let pruneReplacementWindow
345
- let scope = 0
527
+ cordisTranscript: { calls: 0, inspections: 0 },
528
+ editTargets: {},
529
+ pendingByCallId: {},
530
+ seenCallIds: {},
531
+ claimedEditTargets: {},
532
+ editClaims: {},
533
+ ordinaryResultSeqs: {},
534
+ settledResultsByEventSeq: {},
535
+ runSelectionFrontier: null,
536
+ runSelectionHistory: [],
537
+ lastEventIndex: null,
538
+ eventFacts: [],
539
+ scope: 0,
540
+ }
541
+ }
542
+
543
+ export function validateSessionTimelineState(value) {
544
+ const requireFact = condition => {
545
+ if (!condition) throw new TypeError('invalid PTC session-log projection timeline checkpoint')
546
+ }
547
+ const optional = (field, predicate) => field === undefined || predicate(field)
548
+ const fields = (record, names) => isRecord(record) && Object.keys(record).every(name => names.includes(name))
549
+ const target = field => fields(field, ['callSeq', 'source']) && isCanonicalSequence(field.callSeq) && typeof field.source === 'string'
550
+ const callEvent = event => fields(event, ['seq', 'type', 'data']) && isCanonicalSequence(event.seq) && event.type === 'tool/call'
551
+ && isRecord(event.data) && typeof event.data.callId === 'string'
552
+ && fields(event.data, ['callId', 'name', 'arguments'])
553
+ && optional(event.data.name, name => typeof name === 'string')
554
+ && optional(event.data.arguments, args => typeof args === 'string')
555
+ const callEntry = entry => fields(entry, ['event', 'eventIndex', 'scope', 'ambiguous', 'editTarget']) && callEvent(entry.event)
556
+ && isCanonicalSequence(entry.eventIndex) && isCanonicalSequence(entry.scope) && entry.scope <= value.scope
557
+ && optional(entry.ambiguous, field => field === true) && optional(entry.editTarget, target)
558
+ const table = (name, keyValid, payloadValid) => {
559
+ requireFact(isRecord(value[name]))
560
+ for (const [key, pair] of Object.entries(value[name])) {
561
+ requireFact(Array.isArray(pair) && pair.length === 2 && keyValid(pair[0]) && key === `k${pair[0]}`)
562
+ requireFact(payloadValid(pair[1], pair[0]))
563
+ }
564
+ return new Map(Object.values(value[name]))
565
+ }
566
+ requireFact(isJsonValue(value) && fields(value, ['openTurn', 'executableCalls', 'calls', 'results', 'boundaries', 'found',
567
+ 'cordisTranscript', 'editTargets', 'pendingByCallId', 'seenCallIds', 'claimedEditTargets', 'editClaims',
568
+ 'ordinaryResultSeqs', 'settledResultsByEventSeq', 'scope', 'latestRun', 'editableRun', 'lastSuccessfulRunIndex',
569
+ 'unavailableResultSeq', 'unavailableResultReason', 'pruneReplacementWindow', 'runSelectionFrontier',
570
+ 'runSelectionHistory', 'lastEventIndex', 'eventFacts']) && typeof value.openTurn === 'boolean'
571
+ && typeof value.found === 'boolean' && isCanonicalSequence(value.scope)
572
+ && Array.isArray(value.calls) && value.calls.every(callEvent) && Array.isArray(value.boundaries))
573
+ requireFact(value.lastEventIndex === null || isCanonicalSequence(value.lastEventIndex))
574
+ requireFact(Array.isArray(value.runSelectionHistory))
575
+ let boundaryScope = 0
576
+ let boundaryIndex = -1
577
+ let openTurn = false
578
+ for (const frontier of value.runSelectionHistory) {
579
+ requireFact(fields(frontier, ['index', 'scope', 'reason']) && isCanonicalSequence(frontier.index)
580
+ && value.lastEventIndex !== null && frontier.index <= value.lastEventIndex && frontier.index > boundaryIndex
581
+ && RUN_SELECTION_CLEAR_REASONS.has(frontier.reason))
582
+ if (frontier.reason === 'turn/start' || frontier.reason === 'turn/end') {
583
+ boundaryScope += 1
584
+ openTurn = frontier.reason === 'turn/start'
585
+ }
586
+ requireFact(frontier.scope === boundaryScope)
587
+ boundaryIndex = frontier.index
588
+ }
589
+ requireFact(boundaryScope === value.scope && openTurn === value.openTurn
590
+ && deepEqualJson(value.runSelectionFrontier, value.runSelectionHistory.at(-1) ?? null))
591
+ const executable = table('executableCalls', isCanonicalSequence, (entry, seq) => callEntry(entry)
592
+ && value.lastEventIndex !== null && entry.eventIndex <= value.lastEventIndex
593
+ && entry.event.seq === seq && REPL_TOOL_NAMES.has(entry.event.data.name))
594
+ requireFact(value.calls.length === executable.size)
595
+ const callSeqs = new Set()
596
+ for (const call of value.calls) {
597
+ requireFact(!callSeqs.has(call.seq) && deepEqualJson(executable.get(call.seq)?.event, call))
598
+ callSeqs.add(call.seq)
599
+ }
600
+ const journalFacts = result => {
601
+ if (!isRecord(result.journal)) return false
602
+ const journal = normalizeJournal(result.journal)
603
+ if (!deepEqualJson(journal, result.journal)) return false
604
+ if (!deepEqualJson(userBindingsForJournal(result.userBindings === undefined ? {} : { [USER_BINDINGS_META_KEY]: result.userBindings }, journal) ?? null,
605
+ result.userBindings ?? null)) return false
606
+ if (result.derived !== undefined) {
607
+ const derived = result.derived
608
+ if (!isRecord(derived)) return false
609
+ const normalized = normalizeDerivedEditResult({
610
+ [JOURNAL_KEY]: result.journal,
611
+ [EDIT_TARGET_KEY]: { targetCallSeq: derived.targetCallSeq },
612
+ [DERIVED_RUN_KEY]: { code: derived.code, description: derived.description },
613
+ ...(derived.recoveryBoundaries === undefined ? {} : { [RECOVERY_BOUNDARY_KEY]: derived.recoveryBoundaries }),
614
+ ...(derived.userBindings === undefined ? {} : { [USER_BINDINGS_META_KEY]: derived.userBindings }),
615
+ }, derived.targetCallSeq)
616
+ if (!deepEqualJson(normalized, derived) || !executable.has(derived.targetCallSeq)) return false
617
+ }
618
+ return true
619
+ }
620
+ const normalizedResult = result => {
621
+ if (!fields(result, ['eventSeq', 'eventIndex', 'positionSeq', 'error', 'journal', 'derived', 'userBindings',
622
+ 'postEdit', 'postEditError'])
623
+ || !isCanonicalSequence(result.eventSeq) || !isCanonicalSequence(result.eventIndex)
624
+ || !optional(result.positionSeq, isCanonicalSequence)) return false
625
+ if (result.error !== undefined) return typeof result.error === 'string' && result.journal === undefined && result.derived === undefined
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))
641
+ }
642
+ const results = table('results', isCanonicalSequence, normalizedResult)
643
+ const targets = table('editTargets', isCanonicalSequence, (field, seq) => (
644
+ executable.get(seq)?.event.data.name === 'edit_run_code'
645
+ && (field === null || (target(field) && executable.has(field.callSeq)))
646
+ && deepEqualJson(field, executable.get(seq).editTarget ?? null)
647
+ ))
648
+ for (const [seq, entry] of executable) {
649
+ requireFact(entry.event.data.name !== 'edit_run_code' || targets.has(seq))
650
+ }
651
+ const stringKey = key => typeof key === 'string'
652
+ const trueValue = field => field === true
653
+ const seen = table('seenCallIds', stringKey, trueValue)
654
+ table('pendingByCallId', stringKey, (entry, id) => seen.has(id) && (entry === null
655
+ || (callEntry(entry) && entry.event.data.callId === id && entry.scope === value.scope
656
+ && (!REPL_TOOL_NAMES.has(entry.event.data.name) || deepEqualJson(executable.get(entry.event.seq), entry)))))
657
+ const claimed = table('claimedEditTargets', isCanonicalSequence, (field, seq) => trueValue(field) && executable.has(seq))
658
+ const edits = table('editClaims', isCanonicalSequence, (seq, editSeq) => isCanonicalSequence(seq)
659
+ && claimed.has(seq) && targets.get(editSeq)?.callSeq === seq)
660
+ const ordinaryObservations = table('ordinaryResultSeqs', isCanonicalSequence, (observation, seq) => fields(observation,
661
+ ['call', 'eventSeq', 'eventIndex']) && callEvent(observation.call) && observation.call.seq === seq
662
+ && isCanonicalSequence(observation.eventSeq) && isCanonicalSequence(observation.eventIndex)
663
+ && value.lastEventIndex !== null && observation.eventIndex <= value.lastEventIndex
664
+ && (!REPL_TOOL_NAMES.has(observation.call.data.name)
665
+ || (deepEqualJson(executable.get(seq)?.event, observation.call)
666
+ && observation.eventIndex >= executable.get(seq).eventIndex)))
667
+ const settlements = table('settledResultsByEventSeq', isCanonicalSequence, settlement => fields(settlement,
668
+ ['call', 'entry', 'identity', 'publication', 'observedIndex', 'ordinary', 'normalized'])
669
+ && callEvent(settlement.call) && callEntry(settlement.entry)
670
+ && optional(settlement.normalized, normalizedResult)
671
+ && typeof settlement.ordinary === 'boolean'
672
+ && isCanonicalSequence(settlement.observedIndex) && value.lastEventIndex !== null
673
+ && settlement.observedIndex >= settlement.entry.eventIndex && settlement.observedIndex <= value.lastEventIndex
674
+ && deepEqualJson(settlement.call, settlement.entry.event)
675
+ && typeof settlement.identity === 'string' && /^[a-f0-9]{64}$/u.test(settlement.identity)
676
+ && (!REPL_TOOL_NAMES.has(settlement.call.data.name)
677
+ || deepEqualJson(executable.get(settlement.call.seq)?.event, settlement.call))
678
+ && optional(settlement.publication, publication => fields(publication,
679
+ ['index', 'journal', 'derived', 'userBindings', 'rewrites', 'postEdit', 'postEditRewrites'])
680
+ && deepEqualJson(publication.journal ?? null, settlement.normalized?.journal ?? null)
681
+ && deepEqualJson(publication.derived ?? null, settlement.normalized?.derived ?? null)
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)
685
+ && !settlement.entry.ambiguous && isCanonicalSequence(publication.index)
686
+ && publication.index === (settlement.ordinary ? settlement.observedIndex : settlement.entry.eventIndex)
687
+ && ((settlement.call.data.name === 'run_code' && publication.derived === undefined)
688
+ || (settlement.call.data.name === 'edit_run_code'
689
+ && publication.derived?.targetCallSeq === settlement.entry.editTarget?.callSeq
690
+ && publication.derived !== undefined))
691
+ && (publication.journal === undefined
692
+ ? publication.derived === undefined && publication.userBindings === undefined
693
+ : journalFacts(publication))
694
+ && deepEqualJson(validatedRewrites({ [REWRITES_KEY]: publication.rewrites }) ?? null, publication.rewrites ?? null)))
695
+ // Normalized event facts retain distinctions needed by the transition but
696
+ // omit result content. Admission uses that same transition, not a second
697
+ // partial interpretation of the materialized caches.
698
+ requireFact(Array.isArray(value.eventFacts))
699
+ let reconstructed = createSessionTimelineState()
700
+ let correlationIndex = -1
701
+ for (const observation of value.eventFacts) {
702
+ requireFact(fields(observation, ['eventIndex', 'event', 'identity', 'hasJournal', 'normalized',
703
+ 'boundaries', 'boundaryFailure', 'rewrites', 'selectedTarget'])
704
+ && isCanonicalSequence(observation.eventIndex) && observation.eventIndex > correlationIndex
705
+ && value.lastEventIndex !== null && observation.eventIndex <= value.lastEventIndex
706
+ && isRecord(observation.event) && isCanonicalSequence(observation.event.seq)
707
+ && ['turn/start', 'turn/end', RECOVERY_BOUNDARY_EVENT, 'compaction/prune', 'tool/call', 'tool/result',
708
+ 'tool/ptc-dispatch-start', 'tool/ptc-dispatch']
709
+ .includes(observation.event.type)
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)
715
+ && (observation.event.type === 'tool/result'
716
+ ? typeof observation.identity === 'string' && /^[a-f0-9]{64}$/u.test(observation.identity)
717
+ && typeof observation.hasJournal === 'boolean'
718
+ : observation.identity === undefined && observation.hasJournal === undefined)
719
+ && optional(observation.normalized, normalizedResult)
720
+ && optional(observation.boundaryFailure, failed => failed === true)
721
+ && optional(observation.boundaries, boundaries => Array.isArray(boundaries)
722
+ && boundaries.every(boundary => {
723
+ if (!isRecord(boundary)) return false
724
+ const { eventSeq, ...evidence } = boundary
725
+ return eventSeq === observation.event.seq
726
+ && deepEqualJson(normalizeRecoveryBoundaries([evidence], eventSeq), [boundary])
727
+ }))
728
+ && deepEqualJson(validatedRewrites({ [REWRITES_KEY]: observation.rewrites }) ?? null,
729
+ observation.rewrites ?? null))
730
+ correlationIndex = observation.eventIndex
731
+ reconstructed = replaySessionTimelineFact(reconstructed, observation)
732
+ }
733
+ requireFact(deepEqualJson(reconstructed, value))
734
+ for (const observation of ordinaryObservations.values()) {
735
+ const settlement = settlements.get(observation.eventSeq)
736
+ requireFact(settlement !== undefined
737
+ ? settlement.ordinary && settlement.observedIndex === observation.eventIndex
738
+ && deepEqualJson(settlement.call, observation.call)
739
+ : value.runSelectionHistory.some(frontier => frontier.index === observation.eventIndex
740
+ && frontier.reason === 'result-identity-mismatch'))
741
+ }
742
+ for (const settlement of settlements.values()) {
743
+ if (!settlement.ordinary) continue
744
+ const observation = ordinaryObservations.get(settlement.call.seq)
745
+ requireFact(observation !== undefined && observation.eventIndex === settlement.observedIndex
746
+ && deepEqualJson(observation.call, settlement.call))
747
+ }
748
+ const publications = [...settlements.values()].filter(settlement => settlement.publication !== undefined)
749
+ // Dispatch targets are derived from first-observed settlements and retained
750
+ // clears. A late prune reconstruction cannot establish an earlier target.
751
+ const positions = new Map()
752
+ for (const settlement of settlements.values()) {
753
+ const previous = positions.get(settlement.observedIndex)
754
+ requireFact(previous === undefined || deepEqualJson(previous, settlement))
755
+ positions.set(settlement.observedIndex, settlement)
756
+ }
757
+ const chronology = [
758
+ ...value.runSelectionHistory.map(frontier => ({ index: frontier.index, frontier })),
759
+ ...[...executable.values()].map(entry => ({ index: entry.eventIndex, entry })),
760
+ ...[...positions.values()].map(settlement => ({ index: settlement.observedIndex, settlement })),
761
+ ].sort((left, right) => left.index - right.index)
762
+ let dispatchScope = 0
763
+ let dispatchFrontier = null
764
+ let dispatchRun
765
+ const dispatchClaims = new Set()
766
+ const dispatchEdits = new Map()
767
+ for (const item of chronology) {
768
+ if (item.frontier !== undefined) {
769
+ dispatchFrontier = item.frontier
770
+ dispatchRun = undefined
771
+ if (item.frontier.reason === 'turn/start' || item.frontier.reason === 'turn/end') {
772
+ dispatchScope = item.frontier.scope
773
+ dispatchClaims.clear()
774
+ dispatchEdits.clear()
775
+ }
776
+ } else if (item.entry !== undefined) {
777
+ requireFact(item.entry.scope === dispatchScope)
778
+ if (item.entry.event.data.name !== 'edit_run_code') continue
779
+ const expected = editTargetForSelection(dispatchRun, dispatchClaims)
780
+ requireFact(deepEqualJson(targets.get(item.entry.event.seq), expected ?? null))
781
+ if (expected !== undefined) {
782
+ dispatchClaims.add(expected.callSeq)
783
+ dispatchEdits.set(item.entry.event.seq, expected.callSeq)
784
+ }
785
+ } else {
786
+ const settlement = item.settlement
787
+ const claim = dispatchEdits.get(settlement.call.seq)
788
+ if (claim !== undefined) {
789
+ dispatchEdits.delete(settlement.call.seq)
790
+ if (settlement.publication?.derived === undefined) dispatchClaims.delete(claim)
791
+ }
792
+ dispatchRun = selectSettlementRun(dispatchRun, settlement, dispatchScope, dispatchFrontier)
793
+ }
794
+ }
795
+ requireFact(claimed.size === dispatchClaims.size && [...dispatchClaims].every(seq => claimed.has(seq)))
796
+ requireFact(edits.size === dispatchEdits.size && [...dispatchEdits].every(([seq, targetSeq]) => edits.get(seq) === targetSeq))
797
+ let selectedRun
798
+ let lastSuccessfulRunIndex
799
+ for (const settlement of publications) {
800
+ selectedRun = selectSettlementRun(selectedRun, settlement, value.scope, value.runSelectionFrontier)
801
+ if (successfulTimelineRun(settlementRun(settlement))) {
802
+ lastSuccessfulRunIndex = Math.max(lastSuccessfulRunIndex ?? -1, settlement.publication.index)
803
+ }
804
+ }
805
+ requireFact(selectedRun === undefined || publications.some(settlement =>
806
+ settlement.entry.scope === value.scope && !settlement.entry.ambiguous
807
+ && settlementRun(settlement).callSeq === selectedRun.callSeq))
808
+ requireFact(selectedRun === undefined ? value.latestRun === undefined && value.editableRun === undefined
809
+ : deepEqualJson(value.latestRun ?? null, selectedRun) && deepEqualJson(value.editableRun ?? null, selectedRun))
810
+ requireFact(value.lastSuccessfulRunIndex === lastSuccessfulRunIndex
811
+ && optional(value.unavailableResultSeq, isCanonicalSequence))
812
+ requireFact(optional(value.unavailableResultReason, reason => isRecord(reason)
813
+ && isCanonicalSequence(reason.seq) && typeof reason.reason === 'string'))
814
+ requireFact(optional(value.pruneReplacementWindow, window => isRecord(window)
815
+ && Array.isArray(window.seqs) && window.seqs.every(isCanonicalSequence)
816
+ && new Set(window.seqs).size === window.seqs.length
817
+ && isCanonicalSequence(window.lastEventIndex) && isCanonicalSequence(window.lastEventSeq)))
818
+ for (const boundary of value.boundaries) {
819
+ requireFact(isRecord(boundary) && isCanonicalSequence(boundary.carrierCallSeq))
820
+ const { carrierCallSeq: _carrier, eventSeq, ...evidence } = boundary
821
+ requireFact(isCanonicalSequence(eventSeq)
822
+ && deepEqualJson(normalizeRecoveryBoundaries([evidence], eventSeq), [{ ...evidence, eventSeq }]))
823
+ }
824
+ const transcript = { calls: 0, inspections: 0 }
825
+ for (const result of results.values()) {
826
+ for (const call of result.journal?.calls ?? []) {
827
+ if (call.global !== 'tools' || typeof call.member !== 'string' || !call.member.startsWith('cordis_')) continue
828
+ transcript.calls += 1
829
+ if (call.ok === true && call.member.startsWith('cordis_inspect')) transcript.inspections += 1
830
+ }
831
+ }
832
+ requireFact(deepEqualJson(transcript, value.cordisTranscript))
833
+ return value
834
+ }
835
+
836
+ export function advanceSessionTimeline(previous, inputEvent, eventIndex = inputEvent?.seq) {
837
+ return advanceTimelineEvent(previous, inputEvent, eventIndex)
838
+ }
839
+
840
+ /** Reconstruct an already validated normalized fact at its recorded cut. */
841
+ export function replaySessionTimelineFact(previous, fact) {
842
+ return advanceTimelineEvent(previous, fact.event, fact.eventIndex, fact)
843
+ }
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
+
949
+ function advanceTimelineEvent(previous, inputEvent, eventIndex, replayFact) {
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)) {
958
+ return previous
959
+ }
960
+ const observation = { eventIndex, event: correlationEvent(inputEvent),
961
+ ...(inputEvent.type === 'tool/result' ? {
962
+ identity: replayFact === undefined ? resultIdentity(inputEvent.data) : replayFact.identity,
963
+ hasJournal: replayFact === undefined
964
+ ? isRecord(inputEvent.data?.meta) && Object.hasOwn(inputEvent.data.meta, JOURNAL_KEY) : replayFact.hasJournal,
965
+ } : {}),
966
+ }
967
+ const state = { ...previous,
968
+ lastEventIndex: eventIndex,
969
+ eventFacts: [...previous.eventFacts, observation],
970
+ executableCalls: new TimelineTable(previous.executableCalls),
971
+ editTargets: new TimelineTable(previous.editTargets),
972
+ }
973
+ state.results = new TimelineTable(previous.results, (oldResult, newResult) => {
974
+ const count = result => {
975
+ let calls = 0
976
+ let inspections = 0
977
+ for (const call of result?.journal?.calls ?? []) {
978
+ if (call.global !== 'tools' || typeof call.member !== 'string' || !call.member.startsWith('cordis_')) continue
979
+ calls += 1
980
+ if (call.ok === true && call.member.startsWith('cordis_inspect')) inspections += 1
981
+ }
982
+ return { calls, inspections }
983
+ }
984
+ const before = count(oldResult)
985
+ const after = count(newResult)
986
+ state.cordisTranscript = {
987
+ calls: state.cordisTranscript.calls + after.calls - before.calls,
988
+ inspections: state.cordisTranscript.inspections + after.inspections - before.inspections,
989
+ }
990
+ })
991
+ const pendingByCallId = new TimelineTable(previous.pendingByCallId)
992
+ const seenCallIds = new TimelineTable(previous.seenCallIds)
993
+ const claimedEditTargets = new TimelineTable(previous.claimedEditTargets)
994
+ const editClaims = new TimelineTable(previous.editClaims)
995
+ const ordinaryResultSeqs = new TimelineTable(previous.ordinaryResultSeqs)
996
+ const settledResultsByEventSeq = new TimelineTable(previous.settledResultsByEventSeq)
997
+ let pruneReplacementWindow = previous.pruneReplacementWindow
998
+ let scope = previous.scope
999
+
1000
+ const clearRunSelection = reason => {
1001
+ state.runSelectionFrontier = timelineJson({ index: eventIndex, scope, reason })
1002
+ state.runSelectionHistory = [...state.runSelectionHistory, state.runSelectionFrontier]
1003
+ state.latestRun = undefined
1004
+ state.editableRun = undefined
1005
+ }
346
1006
 
347
1007
  const resetTurn = (openTurn) => {
348
1008
  state.openTurn = openTurn
@@ -350,16 +1010,12 @@ export function foldSessionTimeline(events) {
350
1010
  pendingByCallId.clear()
351
1011
  seenCallIds.clear()
352
1012
  claimedEditTargets.clear()
353
- state.latestRun = undefined
354
- state.editableRun = undefined
1013
+ editClaims.clear()
1014
+ clearRunSelection(openTurn ? 'turn/start' : 'turn/end')
355
1015
  }
356
1016
 
357
- for (let eventIndex = 0; eventIndex < events.length; eventIndex += 1) {
358
- const event = events[eventIndex]
359
- if (pruneReplacementWindow !== undefined
360
- && !continuesPruneWindow(pruneReplacementWindow, event, eventIndex)) {
361
- pruneReplacementWindow = undefined
362
- }
1017
+ for (const event of [inputEvent]) {
1018
+ pruneReplacementWindow = nextPruneWindow(pruneReplacementWindow, event, eventIndex)
363
1019
  if (event?.type === 'turn/start') {
364
1020
  resetTurn(true)
365
1021
  continue
@@ -376,15 +1032,25 @@ export function foldSessionTimeline(events) {
376
1032
  pruneReplacementWindow = pruneWindowForEvent(event, eventIndex)
377
1033
  continue
378
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
379
1045
  if (event?.type === 'tool/call' && typeof event.data?.callId === 'string') {
380
1046
  const executable = REPL_TOOL_NAMES.has(event.data.name)
381
- let entry = { event, eventIndex, scope }
1047
+ let entry = { event: timelineCallEvent(event), eventIndex, scope }
382
1048
  if (executable) {
383
1049
  if (isCanonicalSequence(event.seq) && state.executableCalls.has(event.seq)) {
384
1050
  // A sequence collision disproves both sources, but not the earlier
385
1051
  // verified frontier. Keep its position for persistent contraction.
386
- const previous = state.executableCalls.get(event.seq)
387
- previous.ambiguous = true
1052
+ const previous = { ...state.executableCalls.get(event.seq), ambiguous: true }
1053
+ state.executableCalls.set(event.seq, previous)
388
1054
  state.unavailableResultSeq = Math.min(state.unavailableResultSeq ?? event.seq, event.seq)
389
1055
  if (state.unavailableResultReason === undefined
390
1056
  || event.seq < state.unavailableResultReason.seq) {
@@ -394,36 +1060,25 @@ export function foldSessionTimeline(events) {
394
1060
  }
395
1061
  }
396
1062
  state.found = true
397
- state.latestRun = undefined
398
- state.editableRun = undefined
399
- pendingByCallId.set(previous.event.data.callId, null)
400
- pendingByCallId.set(event.data.callId, null)
401
- seenCallIds.add(event.data.callId)
1063
+ clearRunSelection('duplicate-call-sequence')
1064
+ recordPendingCall(pendingByCallId, seenCallIds, entry, previous)
402
1065
  continue
403
1066
  }
404
1067
  if (event.data.name === 'edit_run_code') {
405
- const targetCallSeq = state.editableRun?.callSeq
406
- const target = targetCallSeq !== undefined && state.editableRun.source !== undefined
407
- && !claimedEditTargets.has(targetCallSeq)
408
- ? Object.freeze({ source: state.editableRun.source, callSeq: targetCallSeq })
409
- : undefined
1068
+ const target = editTargetForSelection(state.editableRun, claimedEditTargets)
410
1069
  entry = { ...entry, editTarget: target }
411
- state.editTargets.set(event.seq, target)
1070
+ state.editTargets.set(event.seq, target ?? null)
412
1071
  if (target !== undefined) {
413
1072
  claimedEditTargets.add(target.callSeq)
414
1073
  editClaims.set(event.seq, target.callSeq)
415
1074
  }
416
1075
  }
417
- state.calls.push(event)
1076
+ state.calls = [...state.calls, entry.event]
418
1077
  if (isCanonicalSequence(event.seq)) {
419
1078
  state.executableCalls.set(event.seq, entry)
420
1079
  }
421
1080
  }
422
- if (seenCallIds.has(event.data.callId)) pendingByCallId.set(event.data.callId, null)
423
- else {
424
- seenCallIds.add(event.data.callId)
425
- pendingByCallId.set(event.data.callId, entry)
426
- }
1081
+ recordPendingCall(pendingByCallId, seenCallIds, entry)
427
1082
  continue
428
1083
  }
429
1084
  if (event?.type !== 'tool/result') continue
@@ -432,73 +1087,42 @@ export function foldSessionTimeline(events) {
432
1087
  const sourceRelation = event.sourceEventSeqs
433
1088
  const sourceSeq = sourceRelation?.[0]
434
1089
  const canonicalSourceRelation = Array.isArray(sourceRelation)
435
- && sourceRelation.length === 1
436
- && isCanonicalSequence(sourceSeq)
1090
+ && sourceRelation.length === 1 && isCanonicalSequence(sourceSeq)
437
1091
  const callId = event.data?.message?.source?.callId
438
- let entry = canonicalSourceRelation ? state.executableCalls.get(sourceSeq) : undefined
439
- let prunedReplacement = false
440
- if (entry === undefined && identifiesPrunedResult(pruneReplacementWindow, event, eventIndex)) {
441
- const candidate = pendingByCallId.get(callId)
442
- const pruneSeq = events[pruneReplacementWindow.lastEventIndex]?.seq
443
- if (replacesPendingCall(candidate, event, pruneSeq)) {
444
- entry = candidate
445
- prunedReplacement = true
446
- pruneReplacementWindow.lastEventIndex = eventIndex
447
- }
448
- }
449
- if (entry === undefined && !hasSourceRelation && typeof callId === 'string') {
450
- entry = pendingByCallId.get(callId)
451
- }
452
- if (entry === undefined && canonicalSourceRelation) {
453
- // The Host pruner may replace an already-settled result with a
454
- // content-only clone that cites the shadowed result event. It is the same
455
- // call's settlement representation, not a new unknown boundary.
456
- const settled = settledResultsByEventSeq.get(sourceSeq)
457
- if (settled !== undefined && typeof callId === 'string'
458
- && settled.call?.data?.callId === callId
459
- && exactResultReplacement(event, sourceSeq)
460
- && sameResultExceptContent(settled.data, event.data)) {
461
- entry = settled.entry
462
- prunedReplacement = true
1092
+ const resolution = resolveTimelineResult(event, eventIndex, state.executableCalls, pendingByCallId,
1093
+ settledResultsByEventSeq, pruneReplacementWindow, observation.identity)
1094
+ const { entry, prunedReplacement, republishedSettlement } = resolution
1095
+ pruneReplacementWindow = resolution.pruneReplacementWindow
1096
+ if (republishedSettlement !== undefined) {
1097
+ const resultSeq = republishedSettlement.call.seq
1098
+ const settledResult = state.results.get(resultSeq)
1099
+ if (settledResult !== undefined) {
1100
+ state.results.set(resultSeq, republishedTimelineResult(settledResult, event))
463
1101
  }
1102
+ settledResultsByEventSeq.set(event.seq, republishedSettlement)
1103
+ continue
464
1104
  }
465
1105
  if (hasSourceRelation && !canonicalSourceRelation) {
466
- const identityEntry = typeof callId === 'string' ? pendingByCallId.get(callId) : undefined
467
- const callSeqs = new Set(Array.isArray(sourceRelation)
468
- ? sourceRelation
469
- .filter(isCanonicalSequence)
470
- .map(candidateSeq => state.executableCalls.get(candidateSeq)?.event?.seq)
471
- .filter(isCanonicalSequence)
472
- : [])
473
- if (isCanonicalSequence(identityEntry?.event?.seq)) {
474
- callSeqs.add(identityEntry.event.seq)
475
- }
476
- if (callSeqs.size > 0) {
477
- const callSeq = Math.min(...callSeqs)
1106
+ const callSeq = invalidSourceCallSeq(event, state.executableCalls, pendingByCallId)
1107
+ if (callSeq !== undefined) {
478
1108
  state.unavailableResultSeq = Math.min(state.unavailableResultSeq ?? callSeq, callSeq)
479
- state.results.set(callSeq, {
480
- eventSeq: isCanonicalSequence(event.seq) ? event.seq : callSeq,
481
- eventIndex,
482
- error: `tool result has an invalid source relation for call seq ${callSeq}`,
483
- })
1109
+ state.results.set(callSeq, timelineResultError(event, eventIndex, callSeq,
1110
+ `tool result has an invalid source relation for call seq ${callSeq}`))
484
1111
  state.found = true
485
1112
  }
486
1113
  if (typeof callId === 'string') pendingByCallId.delete(callId)
487
- state.latestRun = undefined
488
- state.editableRun = undefined
1114
+ clearRunSelection('invalid-source-relation')
489
1115
  continue
490
1116
  }
491
1117
  if (typeof callId === 'string') pendingByCallId.delete(callId)
492
1118
  if (entry === null) {
493
- state.latestRun = undefined
494
- state.editableRun = undefined
1119
+ clearRunSelection('ambiguous-call-id')
495
1120
  continue
496
1121
  }
497
1122
 
498
1123
  const call = entry?.event
499
1124
  if (entry?.ambiguous) {
500
- state.latestRun = undefined
501
- state.editableRun = undefined
1125
+ clearRunSelection('ambiguous-call-sequence')
502
1126
  continue
503
1127
  }
504
1128
  const callSeq = call?.seq
@@ -506,34 +1130,27 @@ export function foldSessionTimeline(events) {
506
1130
  if (!prunedReplacement && isCanonicalSequence(callSeq)) {
507
1131
  if (ordinaryResultSeqs.has(callSeq)) {
508
1132
  state.unavailableResultSeq ??= callSeq
509
- state.results.set(callSeq, {
510
- eventSeq: isCanonicalSequence(event.seq) ? event.seq : callSeq,
511
- eventIndex,
512
- error: `session log contains duplicate ordinary tool results for call seq ${callSeq}`,
513
- })
1133
+ state.results.set(callSeq, timelineResultError(event, eventIndex, callSeq,
1134
+ `session log contains duplicate ordinary tool results for call seq ${callSeq}`))
514
1135
  state.found = true
515
- state.latestRun = undefined
516
- state.editableRun = undefined
1136
+ clearRunSelection('duplicate-result')
517
1137
  continue
518
1138
  }
519
- ordinaryResultSeqs.add(callSeq)
1139
+ ordinaryResultSeqs.set(callSeq, timelineJson({ call,
1140
+ eventSeq: isCanonicalSequence(event.seq) ? event.seq : callSeq, eventIndex }))
520
1141
  }
521
1142
  if (call !== undefined && typeof callId === 'string' && call.data.callId !== callId) {
522
- state.latestRun = undefined
523
- state.editableRun = undefined
1143
+ clearRunSelection('result-identity-mismatch')
524
1144
  if (isCanonicalSequence(callSeq)) {
525
1145
  state.unavailableResultSeq ??= callSeq
526
- state.results.set(callSeq, {
527
- eventSeq: isCanonicalSequence(event.seq) ? event.seq : callSeq,
528
- eventIndex,
529
- error: `tool result identities disagree for call seq ${callSeq}`,
530
- })
1146
+ state.results.set(callSeq, timelineResultError(event, eventIndex, callSeq,
1147
+ `tool result identities disagree for call seq ${callSeq}`))
531
1148
  state.found = true
532
1149
  }
533
1150
  continue
534
1151
  }
535
1152
  let normalized
536
- if (isRecord(meta) && Object.hasOwn(meta, JOURNAL_KEY)) {
1153
+ if (observation.hasJournal) {
537
1154
  if (entry === undefined) {
538
1155
  if (isCanonicalSequence(sourceSeq)) {
539
1156
  state.unavailableResultSeq ??= sourceSeq
@@ -549,66 +1166,113 @@ export function foldSessionTimeline(events) {
549
1166
  // clone by its own row would apply the boundary before that call exists
550
1167
  // in the verified frontier.
551
1168
  const settlementPosition = prunedReplacement
552
- ? state.results.get(resultSeq)?.eventSeq ?? resultSeq
1169
+ ? state.results.get(resultSeq)?.positionSeq ?? state.results.get(resultSeq)?.eventSeq ?? sourceSeq
553
1170
  : undefined
554
1171
  const raw = {
555
- meta,
556
1172
  eventSeq: isCanonicalSequence(event.seq) ? event.seq : resultSeq,
557
1173
  ...(settlementPosition === undefined ? {} : { positionSeq: settlementPosition }),
558
1174
  eventIndex,
559
1175
  }
560
- if (Object.hasOwn(meta, RECOVERY_BOUNDARY_KEY)) {
561
- try {
562
- state.boundaries.push(...normalizeRecoveryBoundaries(
563
- meta[RECOVERY_BOUNDARY_KEY], raw.eventSeq,
564
- ).map(boundary => ({ ...boundary, carrierCallSeq: resultSeq })))
565
- } catch {
566
- state.unavailableResultSeq ??= callSeq
567
- }
568
- }
569
- try {
570
- if (call.data.name === 'edit_run_code') {
571
- const derived = normalizeDerivedEditResult(meta, entry.editTarget?.callSeq)
572
- normalized = {
573
- ...raw,
574
- journal: derived.journal,
575
- derived,
576
- ...(derived.userBindings === undefined ? {} : { userBindings: derived.userBindings }),
1176
+ if (replayFact === undefined) {
1177
+ if (Object.hasOwn(meta, RECOVERY_BOUNDARY_KEY)) {
1178
+ try {
1179
+ observation.boundaries = normalizeRecoveryBoundaries(meta[RECOVERY_BOUNDARY_KEY], raw.eventSeq)
1180
+ } catch {
1181
+ observation.boundaryFailure = true
577
1182
  }
578
- } else if (call.data.name === 'run_code') {
579
- const rawJournal = meta[JOURNAL_KEY]
580
- const resolveLegacyConfirm = rawJournal?.version === LEGACY_JOURNAL_VERSION
581
- ? legacyCallId => {
582
- const candidates = [...state.executableCalls.values()]
583
- .filter(candidate => candidate.eventIndex < eventIndex
584
- && candidate.event.data?.name === 'run_code'
585
- && candidate.event.data.callId === legacyCallId
586
- && !state.results.has(candidate.event.seq))
587
- return candidates.length === 1 ? candidates[0].event.seq : undefined
1183
+ }
1184
+ try {
1185
+ if (call.data.name === 'edit_run_code') {
1186
+ const derived = normalizeDerivedEditResult(meta, entry.editTarget?.callSeq)
1187
+ normalized = {
1188
+ ...raw,
1189
+ journal: derived.journal,
1190
+ derived,
1191
+ ...(derived.userBindings === undefined ? {} : { userBindings: derived.userBindings }),
1192
+ }
1193
+ } else if (call.data.name === 'run_code') {
1194
+ const rawJournal = meta[JOURNAL_KEY]
1195
+ const resolveLegacyConfirm = rawJournal?.version === LEGACY_JOURNAL_VERSION
1196
+ ? legacyCallId => {
1197
+ const candidates = [...state.executableCalls.values()]
1198
+ .filter(candidate => candidate.eventIndex < eventIndex
1199
+ && candidate.event.data?.name === 'run_code'
1200
+ && candidate.event.data.callId === legacyCallId
1201
+ && !state.results.has(candidate.event.seq))
1202
+ return candidates.length === 1 ? candidates[0].event.seq : undefined
1203
+ }
1204
+ : undefined
1205
+ const journal = normalizeJournal(rawJournal, { resolveLegacyConfirm })
1206
+ const userBindings = userBindingsForJournal(meta, journal)
1207
+ normalized = {
1208
+ ...raw,
1209
+ journal,
1210
+ ...(userBindings === undefined ? {} : { userBindings }),
588
1211
  }
589
- : undefined
590
- const journal = normalizeJournal(rawJournal, { resolveLegacyConfirm })
591
- const userBindings = userBindingsForJournal(meta, journal)
592
- normalized = {
593
- ...raw,
594
- journal,
595
- ...(userBindings === undefined ? {} : { userBindings }),
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
1220
+ }
1221
+ } catch (error) {
1222
+ normalized = { ...raw, error: error.message }
1223
+ }
1224
+ if (normalized === undefined) {
1225
+ // PTC metadata on a settlement whose call is not a REPL tool is
1226
+ // unproved evidence: it must contract the frontier, not escape the fold.
1227
+ normalized = { ...raw, error: 'PTC journal metadata appeared on a non-REPL tool result' }
1228
+ }
1229
+ } else {
1230
+ normalized = replayFact.normalized
1231
+ if (normalized === undefined || !deepEqualJson(raw, {
1232
+ eventSeq: normalized.eventSeq, eventIndex: normalized.eventIndex,
1233
+ ...(normalized.positionSeq === undefined ? {} : { positionSeq: normalized.positionSeq }),
1234
+ })) throw new TypeError('invalid PTC session-log projection timeline checkpoint result facts')
1235
+ if (replayFact.boundaries !== undefined) observation.boundaries = replayFact.boundaries
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'}`)
596
1241
  }
597
1242
  }
598
- } catch (error) {
599
- normalized = { ...raw, error: error.message }
600
- }
601
- if (normalized === undefined) {
602
- // PTC metadata on a settlement whose call is not a REPL tool is
603
- // unproved evidence: it must contract the frontier, not escape the fold.
604
- normalized = { ...raw, error: 'PTC journal metadata appeared on a non-REPL tool result' }
605
1243
  }
606
- if (isCanonicalSequence(resultSeq)) state.results.set(resultSeq, normalized)
607
- if (!prunedReplacement && isCanonicalSequence(event.seq)) {
608
- settledResultsByEventSeq.set(event.seq, { call, entry, data: event.data })
1244
+ observation.normalized = normalized
1245
+ if (observation.boundaries !== undefined) {
1246
+ state.boundaries = [...state.boundaries, ...observation.boundaries.map(boundary => ({
1247
+ ...boundary, carrierCallSeq: resultSeq,
1248
+ }))]
609
1249
  }
1250
+ if (observation.boundaryFailure) state.unavailableResultSeq ??= callSeq
1251
+ if (isCanonicalSequence(resultSeq)) state.results.set(resultSeq, timelineJson(normalized))
610
1252
  state.found = true
611
1253
  }
1254
+ const settlement = call === undefined ? undefined : { call, entry, identity: observation.identity,
1255
+ observedIndex: eventIndex, ordinary: !prunedReplacement,
1256
+ ...(normalized === undefined ? {} : { normalized }) }
1257
+ if (settlement !== undefined && entry.scope === scope
1258
+ && (call.data.name === 'run_code' || normalized?.derived !== undefined)) {
1259
+ const rewrites = replayFact === undefined ? validatedRewrites(meta) : replayFact.rewrites
1260
+ if (rewrites !== undefined) observation.rewrites = rewrites
1261
+ settlement.publication = {
1262
+ index: prunedReplacement ? entry.eventIndex : eventIndex,
1263
+ journal: normalized?.journal,
1264
+ derived: normalized?.derived,
1265
+ userBindings: normalized?.userBindings,
1266
+ rewrites,
1267
+ ...(normalized?.postEdit === undefined ? {} : {
1268
+ postEdit: normalized.postEdit,
1269
+ postEditRewrites: normalized.postEdit.rewrites,
1270
+ }),
1271
+ }
1272
+ }
1273
+ if (settlement !== undefined && isCanonicalSequence(event.seq)) {
1274
+ settledResultsByEventSeq.set(event.seq, settlement)
1275
+ }
612
1276
 
613
1277
  const claimedTarget = editClaims.get(callSeq)
614
1278
  if (claimedTarget !== undefined) {
@@ -616,12 +1280,17 @@ export function foldSessionTimeline(events) {
616
1280
  if (normalized?.derived === undefined) claimedEditTargets.delete(claimedTarget)
617
1281
  }
618
1282
  if (entry === undefined || entry.scope !== scope) continue
1283
+ const publishRun = () => {
1284
+ const run = settlementRun(settlement)
1285
+ state.latestRun = selectSettlementRun(state.latestRun, settlement, scope, state.runSelectionFrontier)
1286
+ state.editableRun = state.latestRun
1287
+ if (successfulTimelineRun(run)) {
1288
+ state.lastSuccessfulRunIndex = Math.max(state.lastSuccessfulRunIndex ?? -1, run.index)
1289
+ }
1290
+ }
619
1291
  if (call.data.name === 'edit_run_code') {
620
1292
  if (normalized?.derived !== undefined) {
621
- const run = timelineDerivedRun(call, event, eventIndex, normalized.derived)
622
- state.latestRun = run
623
- state.editableRun = run
624
- if (successfulTimelineRun(run)) state.lastSuccessfulRunIndex = eventIndex
1293
+ publishRun()
625
1294
  }
626
1295
  continue
627
1296
  }
@@ -631,12 +1300,34 @@ export function foldSessionTimeline(events) {
631
1300
  // new executable cell or turn boundary supersedes it.
632
1301
  continue
633
1302
  }
634
- const run = timelineRun(call, event, eventIndex, normalized?.journal)
635
- state.latestRun = run
636
- state.editableRun = run
637
- if (successfulTimelineRun(run)) state.lastSuccessfulRunIndex = eventIndex
1303
+ publishRun()
1304
+ }
1305
+ state.eventFacts[state.eventFacts.length - 1] = timelineJson(observation)
1306
+ return Object.fromEntries(Object.entries({ ...state,
1307
+ executableCalls: state.executableCalls.table, results: state.results.table, editTargets: state.editTargets.table,
1308
+ pendingByCallId: pendingByCallId.table, seenCallIds: seenCallIds.table,
1309
+ claimedEditTargets: claimedEditTargets.table, editClaims: editClaims.table,
1310
+ ordinaryResultSeqs: ordinaryResultSeqs.table, settledResultsByEventSeq: settledResultsByEventSeq.table,
1311
+ pruneReplacementWindow, scope,
1312
+ }).filter(([_key, value]) => value !== undefined))
1313
+ }
1314
+
1315
+ export function sessionTimelineValue(checkpoint) {
1316
+ return { ...checkpoint,
1317
+ executableCalls: new Map(Object.values(checkpoint.executableCalls)),
1318
+ results: new Map(Object.values(checkpoint.results)),
1319
+ editTargets: new Map(Object.values(checkpoint.editTargets).map(([seq, target]) => [seq, target ?? undefined])),
638
1320
  }
639
- return state
1321
+ }
1322
+
1323
+ export function foldSessionTimeline(events) {
1324
+ let checkpoint = createSessionTimelineState()
1325
+ if (Array.isArray(events)) {
1326
+ for (let index = 0; index < events.length; index += 1) {
1327
+ checkpoint = advanceSessionTimeline(checkpoint, events[index], index)
1328
+ }
1329
+ }
1330
+ return sessionTimelineValue(checkpoint)
640
1331
  }
641
1332
 
642
1333
  /** Fold the session log into the last exactly replayable frontier. */
@@ -746,7 +1437,20 @@ export function recoverJournal(session, currentCallSeq, options = {}) {
746
1437
  const boundary = boundaries[boundaryIndex++]
747
1438
  const carrierCallSeq = carrierCallSeqForBoundary(boundary)
748
1439
  const failedCall = executableCalls.get(boundary.failedCallSeq)
749
- 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) {
750
1454
  rejectBoundary(boundary, 'recovery boundary does not identify an earlier executable call')
751
1455
  continue
752
1456
  }
@@ -802,6 +1506,22 @@ export function recoverJournal(session, currentCallSeq, options = {}) {
802
1506
  if (call.data?.name === 'edit_run_code' && record.code === undefined && result === undefined) continue
803
1507
  if (result?.journal === undefined && confirmedNoops.has(call.seq)) continue
804
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
+ }
805
1525
  }
806
1526
  orderedRecords.sort((left, right) => (
807
1527
  recordEventSeq(left) - recordEventSeq(right) || left.call.seq - right.call.seq