dsh-ptc-plus 0.4.3 → 0.4.10

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 (66) hide show
  1. package/README.en.md +15 -3
  2. package/README.md +15 -3
  3. package/client.js +1157 -533
  4. package/cordis.patch.yml +12 -2
  5. package/docs/README.md +2 -0
  6. package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +60 -4
  7. package/docs/adr/0017-track-the-latest-dsh-public-surface.md +5 -1
  8. package/docs/adr/0019-plugin-settings-and-kill-switch.md +14 -3
  9. package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +2 -2
  10. package/docs/adr/0023-global-user-bindings.md +1 -1
  11. package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +82 -18
  12. package/docs/adr/0030-serve-both-typert-codec-generations-from-one-codec.md +3 -3
  13. package/docs/adr/0032-own-session-log-facts-in-a-host-only-projection.md +27 -0
  14. package/docs/adr/0033-gate-client-visual-usability-in-a-browser.md +23 -0
  15. package/docs/architecture.md +6 -4
  16. package/docs/binding-review-placement-design.md +29 -21
  17. package/docs/client-ui.md +20 -8
  18. package/docs/client-visual-contract.md +48 -0
  19. package/docs/durability-design.md +12 -0
  20. package/docs/installation.md +48 -9
  21. package/docs/publishing.md +4 -4
  22. package/docs/release-notes.md +109 -0
  23. package/docs/runtime-reference.md +8 -2
  24. package/docs/user-bindings.en.md +2 -2
  25. package/docs/user-bindings.md +3 -3
  26. package/docs/verification-optimization-work.md +26 -0
  27. package/docs/verification.md +7 -1
  28. package/index.js +28 -7
  29. package/internal/compiler-platform.js +5 -3
  30. package/internal/compiler-service.js +2 -9
  31. package/internal/cordis-tools-owner.js +75 -16
  32. package/internal/direct-surface-owner.js +333 -7
  33. package/internal/edit-transport-owner.js +8 -5
  34. package/internal/execution-provider-owner.js +157 -0
  35. package/internal/execution-seam-compat.js +69 -113
  36. package/internal/isolated-worker.js +12 -8
  37. package/internal/recovery-tips.js +50 -21
  38. package/internal/rejected-cell-editor.js +11 -21
  39. package/internal/runtime-bridge-owner.js +88 -57
  40. package/internal/runtime-contexts.js +13 -13
  41. package/internal/session-events.js +40 -4
  42. package/internal/session-journal-recovery.js +678 -181
  43. package/internal/session-log-view.js +189 -43
  44. package/internal/session-runtime.js +58 -10
  45. package/internal/settings-compat.js +12 -2
  46. package/internal/user-bindings-owner.js +6 -5
  47. package/locale/en.json +6 -0
  48. package/locale/zh.json +6 -0
  49. package/package.json +18 -13
  50. package/src/client-authoring-view.js +222 -141
  51. package/src/client-binding-review.js +2 -0
  52. package/src/client-code-editor.js +6 -0
  53. package/src/client-copy.js +10 -0
  54. package/src/client-feature-gates.js +12 -5
  55. package/src/client-host-compat.js +23 -2
  56. package/src/client-primitives.js +249 -0
  57. package/src/client-repl-view.js +3 -3
  58. package/src/client-settings-view.js +2 -29
  59. package/src/client-styles.js +92 -33
  60. package/src/client-tool-view.js +8 -3
  61. package/src/client-workbench.js +9 -7
  62. package/src/client.js +72 -46
  63. package/assets/ptc-plus-bindings-en.png +0 -0
  64. package/assets/ptc-plus-bindings-zh.png +0 -0
  65. package/assets/ptc-plus-repair-en.png +0 -0
  66. package/assets/ptc-plus-repair-zh.png +0 -0
@@ -1,12 +1,17 @@
1
- import { isDeepStrictEqual } from 'node:util'
2
- import { isRecord } from './record-utils.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'
3
4
  import { sessionEvents } 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,
10
15
  } from './session-journal-schema.js'
11
16
  import {
12
17
  isCanonicalSequence,
@@ -19,6 +24,10 @@ import {
19
24
  } from './session-journal.js'
20
25
 
21
26
  const RECOVERY_BOUNDARY_EVIDENCE = Symbol('recoveryBoundaryEvidence')
27
+ const RUN_SELECTION_CLEAR_REASONS = new Set([
28
+ 'turn/start', 'turn/end', 'duplicate-call-sequence', 'invalid-source-relation',
29
+ 'ambiguous-call-id', 'ambiguous-call-sequence', 'duplicate-result', 'result-identity-mismatch',
30
+ ])
22
31
  const MODEL_VISIBLE_SURFACE_EVENTS = new Set([
23
32
  'system/message',
24
33
  'developer/message',
@@ -241,6 +250,29 @@ function successfulTimelineRun(run) {
241
250
  return run.journal?.completion?.kind === 'return' && run.journal.status !== 'noop'
242
251
  }
243
252
 
253
+ function settlementRun(settlement) {
254
+ const publication = settlement.publication
255
+ if (publication === undefined) return undefined
256
+ const result = { data: { meta: { [REWRITES_KEY]: publication.rewrites } } }
257
+ return timelineJson(publication.derived === undefined
258
+ ? timelineRun(settlement.call, result, publication.index, publication.journal)
259
+ : timelineDerivedRun(settlement.call, result, publication.index, publication.derived))
260
+ }
261
+
262
+ function selectSettlementRun(selected, settlement, scope, frontier) {
263
+ const publication = settlement.publication
264
+ if (publication === undefined || settlement.entry.scope !== scope
265
+ || (frontier !== null && publication.index <= frontier.index)) return selected
266
+ return selected === undefined || publication.index >= selected.index ? settlementRun(settlement) : selected
267
+ }
268
+
269
+ function editTargetForSelection(run, claimedTargets) {
270
+ const callSeq = run?.callSeq
271
+ return callSeq !== undefined && run.source !== undefined && !claimedTargets.has(callSeq)
272
+ ? Object.freeze({ source: run.source, callSeq })
273
+ : undefined
274
+ }
275
+
244
276
  function pruneWindowForEvent(event, eventIndex) {
245
277
  const shadowedSeqs = event.data.shadowedSeqs
246
278
  const seqs = new Set(shadowedSeqs)
@@ -254,7 +286,7 @@ function pruneWindowForEvent(event, eventIndex) {
254
286
  && shadowedSeqs.every(isCanonicalSequence)
255
287
  && shadowedSeqs.every((seq, index) => index === 0 || seq > shadowedSeqs[index - 1])
256
288
  && rangeValid
257
- return { seqs: valid ? seqs : new Set(), lastEventIndex: eventIndex }
289
+ return { seqs: valid ? [...seqs] : [], lastEventIndex: eventIndex, lastEventSeq: event.seq }
258
290
  }
259
291
 
260
292
  function continuesPruneWindow(window, event, eventIndex) {
@@ -264,7 +296,7 @@ function continuesPruneWindow(window, event, eventIndex) {
264
296
  function identifiesPrunedResult(window, event, eventIndex) {
265
297
  const sourceSeq = event.sourceEventSeqs?.[0]
266
298
  return isCanonicalSequence(sourceSeq)
267
- && window?.seqs.has(sourceSeq)
299
+ && window?.seqs.includes(sourceSeq)
268
300
  && eventIndex === window.lastEventIndex + 1
269
301
  && Array.isArray(event.sourceEventSeqs) && event.sourceEventSeqs.length === 1
270
302
  && typeof event.data?.message?.source?.callId === 'string'
@@ -305,44 +337,506 @@ function exactResultReplacement(event, sourceSeq) {
305
337
  || (surfaceOp.startSeq === sourceSeq && surfaceOp.endSeq === sourceSeq)
306
338
  }
307
339
 
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))
340
+ function resolveTimelineResult(event, eventIndex, executableCalls, pendingByCallId,
341
+ settledResultsByEventSeq, pruneReplacementWindow, identity) {
342
+ const hasSourceRelation = Object.hasOwn(event, 'sourceEventSeqs')
343
+ const sourceRelation = event.sourceEventSeqs
344
+ const sourceSeq = sourceRelation?.[0]
345
+ const canonicalSourceRelation = Array.isArray(sourceRelation)
346
+ && sourceRelation.length === 1
347
+ && isCanonicalSequence(sourceSeq)
348
+ const callId = event.data?.message?.source?.callId
349
+ let entry = canonicalSourceRelation ? executableCalls.get(sourceSeq) : undefined
350
+ let prunedReplacement = false
351
+ let republishedSettlement
352
+ if (entry === undefined && identifiesPrunedResult(pruneReplacementWindow, event, eventIndex)) {
353
+ const candidate = pendingByCallId.get(callId)
354
+ const pruneSeq = pruneReplacementWindow.lastEventSeq
355
+ if (replacesPendingCall(candidate, event, pruneSeq)) {
356
+ entry = candidate
357
+ prunedReplacement = true
358
+ pruneReplacementWindow = { ...pruneReplacementWindow, lastEventIndex: eventIndex, lastEventSeq: event.seq }
359
+ }
360
+ }
361
+ if (entry === undefined && !hasSourceRelation && typeof callId === 'string') {
362
+ entry = pendingByCallId.get(callId)
363
+ }
364
+ if (entry === undefined && canonicalSourceRelation) {
365
+ // The Host pruner may replace an already-settled result with a
366
+ // content-only clone that cites the shadowed result event. It is the same
367
+ // call's settlement representation, not a new unknown boundary.
368
+ const settled = settledResultsByEventSeq.get(sourceSeq)
369
+ if (settled !== undefined && typeof callId === 'string'
370
+ && settled.call?.data?.callId === callId
371
+ && exactResultReplacement(event, sourceSeq)
372
+ && settled.identity === identity) {
373
+ entry = executableCalls.get(settled.call?.seq) ?? settled.entry
374
+ prunedReplacement = true
375
+ republishedSettlement = settled
376
+ }
377
+ }
378
+ return { entry, prunedReplacement, republishedSettlement, pruneReplacementWindow }
379
+ }
380
+
381
+ function invalidSourceCallSeq(event, executableCalls, pendingByCallId) {
382
+ const callId = event.data?.message?.source?.callId
383
+ const identityEntry = typeof callId === 'string' ? pendingByCallId.get(callId) : undefined
384
+ const callSeqs = new Set(Array.isArray(event.sourceEventSeqs)
385
+ ? event.sourceEventSeqs.filter(isCanonicalSequence)
386
+ .map(seq => executableCalls.get(seq)?.event?.seq).filter(isCanonicalSequence) : [])
387
+ if (isCanonicalSequence(identityEntry?.event?.seq)) callSeqs.add(identityEntry.event.seq)
388
+ return callSeqs.size === 0 ? undefined : Math.min(...callSeqs)
389
+ }
390
+
391
+ function timelineResultError(event, eventIndex, callSeq, error) {
392
+ return { eventSeq: isCanonicalSequence(event.seq) ? event.seq : callSeq, eventIndex, error }
393
+ }
394
+
395
+ function republishedTimelineResult(result, event) {
396
+ return { ...result, eventSeq: event.seq, positionSeq: result.positionSeq ?? result.eventSeq }
397
+ }
398
+
399
+ function correlationEvent(event) {
400
+ return timelineJson({ seq: event.seq, type: event.type,
401
+ ...(event.type === 'tool/call' && typeof event.data?.callId === 'string'
402
+ ? { data: timelineCallEvent(event).data } : {}),
403
+ ...(event.type === 'tool/result' ? { data: { message: { source: {
404
+ ...(typeof event.data?.message?.source?.callId === 'string'
405
+ ? { callId: event.data.message.source.callId } : {}),
406
+ } } } } : {}),
407
+ ...(event.type === 'compaction/prune' ? { data: {
408
+ ...(event.data?.shadowedSeqs === undefined ? {} : { shadowedSeqs: event.data.shadowedSeqs }),
409
+ ...(event.data?.shadowedRange === undefined ? {} : { shadowedRange: event.data.shadowedRange }),
410
+ } } : {}),
411
+ ...(Object.hasOwn(event, 'sourceEventSeqs') ? { sourceEventSeqs: event.sourceEventSeqs } : {}),
412
+ ...(event.surfaceOp === undefined ? {} : { surfaceOp: event.surfaceOp }),
413
+ })
414
+ }
415
+
416
+ function recordPendingCall(pending, seen, entry, duplicateSequence) {
417
+ const id = entry.event.data.callId
418
+ if (duplicateSequence !== undefined) {
419
+ pending.set(duplicateSequence.event.data.callId, null)
420
+ pending.set(id, null)
421
+ seen.add(id)
422
+ } else if (seen.has(id)) pending.set(id, null)
423
+ else {
424
+ seen.add(id)
425
+ pending.set(id, entry)
426
+ }
427
+ }
428
+
429
+ function nextPruneWindow(window, event, eventIndex) {
430
+ if (window !== undefined && !continuesPruneWindow(window, event, eventIndex)) return undefined
431
+ return window
314
432
  }
315
433
 
316
434
  /**
317
435
  * Fold persisted tool events once into the call/result and edit-target timeline
318
436
  * shared by prompt projection and cold journal recovery.
319
437
  */
320
- export function foldSessionTimeline(events) {
321
- const empty = {
438
+ class TimelineTable {
439
+ constructor(table = {}, onSet) {
440
+ this.table = table
441
+ this.copied = false
442
+ this.onSet = onSet
443
+ }
444
+
445
+ get(key) { return this.table[`k${key}`]?.[1] }
446
+ has(key) { return Object.hasOwn(this.table, `k${key}`) }
447
+ values() { return Object.values(this.table).map(entry => entry[1]) }
448
+ copy() {
449
+ if (!this.copied) {
450
+ this.table = { ...this.table }
451
+ this.copied = true
452
+ }
453
+ }
454
+ set(key, value) {
455
+ this.onSet?.(this.get(key), value)
456
+ this.copy()
457
+ this.table[`k${key}`] = [key, timelineJson(value ?? null)]
458
+ }
459
+ add(key) { this.set(key, true) }
460
+ delete(key) {
461
+ if (!this.has(key)) return
462
+ this.copy()
463
+ delete this.table[`k${key}`]
464
+ }
465
+ clear() { this.table = {}; this.copied = true }
466
+ }
467
+
468
+ function timelineJson(value) {
469
+ return deepFreeze(JSON.parse(JSON.stringify(value ?? null)))
470
+ }
471
+
472
+ function timelineCallEvent(event) {
473
+ return timelineJson({ seq: event.seq, type: event.type, data: {
474
+ callId: event.data.callId, name: event.data.name,
475
+ ...(REPL_TOOL_NAMES.has(event.data.name) ? { arguments: event.data.arguments } : {}),
476
+ } })
477
+ }
478
+
479
+ function resultIdentity(data) {
480
+ const identity = timelineJson(isRecord(data) && isRecord(data.message)
481
+ ? { ...data, message: { ...data.message, content: undefined } } : data)
482
+ return createHash('sha256').update(JSON.stringify(identity, (_key, value) => (
483
+ isRecord(value) ? Object.fromEntries(Object.keys(value).sort().map(key => [key, value[key]])) : value
484
+ ))).digest('hex')
485
+ }
486
+
487
+ export function createSessionTimelineState() {
488
+ return {
322
489
  openTurn: false,
323
- executableCalls: new Map(),
490
+ executableCalls: {},
324
491
  calls: [],
325
- results: new Map(),
492
+ results: {},
326
493
  boundaries: [],
327
494
  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
495
+ cordisTranscript: { calls: 0, inspections: 0 },
496
+ editTargets: {},
497
+ pendingByCallId: {},
498
+ seenCallIds: {},
499
+ claimedEditTargets: {},
500
+ editClaims: {},
501
+ ordinaryResultSeqs: {},
502
+ settledResultsByEventSeq: {},
503
+ runSelectionFrontier: null,
504
+ runSelectionHistory: [],
505
+ lastEventIndex: null,
506
+ eventFacts: [],
507
+ scope: 0,
508
+ }
509
+ }
510
+
511
+ export function validateSessionTimelineState(value) {
512
+ const requireFact = condition => {
513
+ if (!condition) throw new TypeError('invalid PTC session-log projection timeline checkpoint')
514
+ }
515
+ const optional = (field, predicate) => field === undefined || predicate(field)
516
+ const fields = (record, names) => isRecord(record) && Object.keys(record).every(name => names.includes(name))
517
+ const target = field => fields(field, ['callSeq', 'source']) && isCanonicalSequence(field.callSeq) && typeof field.source === 'string'
518
+ const callEvent = event => fields(event, ['seq', 'type', 'data']) && isCanonicalSequence(event.seq) && event.type === 'tool/call'
519
+ && isRecord(event.data) && typeof event.data.callId === 'string'
520
+ && fields(event.data, ['callId', 'name', 'arguments'])
521
+ && optional(event.data.name, name => typeof name === 'string')
522
+ && optional(event.data.arguments, args => typeof args === 'string')
523
+ const callEntry = entry => fields(entry, ['event', 'eventIndex', 'scope', 'ambiguous', 'editTarget']) && callEvent(entry.event)
524
+ && isCanonicalSequence(entry.eventIndex) && isCanonicalSequence(entry.scope) && entry.scope <= value.scope
525
+ && optional(entry.ambiguous, field => field === true) && optional(entry.editTarget, target)
526
+ const table = (name, keyValid, payloadValid) => {
527
+ requireFact(isRecord(value[name]))
528
+ for (const [key, pair] of Object.entries(value[name])) {
529
+ requireFact(Array.isArray(pair) && pair.length === 2 && keyValid(pair[0]) && key === `k${pair[0]}`)
530
+ requireFact(payloadValid(pair[1], pair[0]))
531
+ }
532
+ return new Map(Object.values(value[name]))
533
+ }
534
+ requireFact(isJsonValue(value) && fields(value, ['openTurn', 'executableCalls', 'calls', 'results', 'boundaries', 'found',
535
+ 'cordisTranscript', 'editTargets', 'pendingByCallId', 'seenCallIds', 'claimedEditTargets', 'editClaims',
536
+ 'ordinaryResultSeqs', 'settledResultsByEventSeq', 'scope', 'latestRun', 'editableRun', 'lastSuccessfulRunIndex',
537
+ 'unavailableResultSeq', 'unavailableResultReason', 'pruneReplacementWindow', 'runSelectionFrontier',
538
+ 'runSelectionHistory', 'lastEventIndex', 'eventFacts']) && typeof value.openTurn === 'boolean'
539
+ && typeof value.found === 'boolean' && isCanonicalSequence(value.scope)
540
+ && Array.isArray(value.calls) && value.calls.every(callEvent) && Array.isArray(value.boundaries))
541
+ requireFact(value.lastEventIndex === null || isCanonicalSequence(value.lastEventIndex))
542
+ requireFact(Array.isArray(value.runSelectionHistory))
543
+ let boundaryScope = 0
544
+ let boundaryIndex = -1
545
+ let openTurn = false
546
+ for (const frontier of value.runSelectionHistory) {
547
+ requireFact(fields(frontier, ['index', 'scope', 'reason']) && isCanonicalSequence(frontier.index)
548
+ && value.lastEventIndex !== null && frontier.index <= value.lastEventIndex && frontier.index > boundaryIndex
549
+ && RUN_SELECTION_CLEAR_REASONS.has(frontier.reason))
550
+ if (frontier.reason === 'turn/start' || frontier.reason === 'turn/end') {
551
+ boundaryScope += 1
552
+ openTurn = frontier.reason === 'turn/start'
553
+ }
554
+ requireFact(frontier.scope === boundaryScope)
555
+ boundaryIndex = frontier.index
556
+ }
557
+ requireFact(boundaryScope === value.scope && openTurn === value.openTurn
558
+ && deepEqualJson(value.runSelectionFrontier, value.runSelectionHistory.at(-1) ?? null))
559
+ const executable = table('executableCalls', isCanonicalSequence, (entry, seq) => callEntry(entry)
560
+ && value.lastEventIndex !== null && entry.eventIndex <= value.lastEventIndex
561
+ && entry.event.seq === seq && REPL_TOOL_NAMES.has(entry.event.data.name))
562
+ requireFact(value.calls.length === executable.size)
563
+ const callSeqs = new Set()
564
+ for (const call of value.calls) {
565
+ requireFact(!callSeqs.has(call.seq) && deepEqualJson(executable.get(call.seq)?.event, call))
566
+ callSeqs.add(call.seq)
567
+ }
568
+ const journalFacts = result => {
569
+ if (!isRecord(result.journal)) return false
570
+ const journal = normalizeJournal(result.journal)
571
+ if (!deepEqualJson(journal, result.journal)) return false
572
+ if (!deepEqualJson(userBindingsForJournal(result.userBindings === undefined ? {} : { [USER_BINDINGS_META_KEY]: result.userBindings }, journal) ?? null,
573
+ result.userBindings ?? null)) return false
574
+ if (result.derived !== undefined) {
575
+ const derived = result.derived
576
+ if (!isRecord(derived)) return false
577
+ const normalized = normalizeDerivedEditResult({
578
+ [JOURNAL_KEY]: result.journal,
579
+ [EDIT_TARGET_KEY]: { targetCallSeq: derived.targetCallSeq },
580
+ [DERIVED_RUN_KEY]: { code: derived.code, description: derived.description },
581
+ ...(derived.recoveryBoundaries === undefined ? {} : { [RECOVERY_BOUNDARY_KEY]: derived.recoveryBoundaries }),
582
+ ...(derived.userBindings === undefined ? {} : { [USER_BINDINGS_META_KEY]: derived.userBindings }),
583
+ }, derived.targetCallSeq)
584
+ if (!deepEqualJson(normalized, derived) || !executable.has(derived.targetCallSeq)) return false
585
+ }
586
+ return true
587
+ }
588
+ const normalizedResult = result => {
589
+ if (!fields(result, ['eventSeq', 'eventIndex', 'positionSeq', 'error', 'journal', 'derived', 'userBindings'])
590
+ || !isCanonicalSequence(result.eventSeq) || !isCanonicalSequence(result.eventIndex)
591
+ || !optional(result.positionSeq, isCanonicalSequence)) return false
592
+ if (result.error !== undefined) return typeof result.error === 'string' && result.journal === undefined && result.derived === undefined
593
+ return journalFacts(result)
594
+ }
595
+ const results = table('results', isCanonicalSequence, normalizedResult)
596
+ const targets = table('editTargets', isCanonicalSequence, (field, seq) => (
597
+ executable.get(seq)?.event.data.name === 'edit_run_code'
598
+ && (field === null || (target(field) && executable.has(field.callSeq)))
599
+ && deepEqualJson(field, executable.get(seq).editTarget ?? null)
600
+ ))
601
+ for (const [seq, entry] of executable) {
602
+ requireFact(entry.event.data.name !== 'edit_run_code' || targets.has(seq))
603
+ }
604
+ const stringKey = key => typeof key === 'string'
605
+ const trueValue = field => field === true
606
+ const seen = table('seenCallIds', stringKey, trueValue)
607
+ table('pendingByCallId', stringKey, (entry, id) => seen.has(id) && (entry === null
608
+ || (callEntry(entry) && entry.event.data.callId === id && entry.scope === value.scope
609
+ && (!REPL_TOOL_NAMES.has(entry.event.data.name) || deepEqualJson(executable.get(entry.event.seq), entry)))))
610
+ const claimed = table('claimedEditTargets', isCanonicalSequence, (field, seq) => trueValue(field) && executable.has(seq))
611
+ const edits = table('editClaims', isCanonicalSequence, (seq, editSeq) => isCanonicalSequence(seq)
612
+ && claimed.has(seq) && targets.get(editSeq)?.callSeq === seq)
613
+ const ordinaryObservations = table('ordinaryResultSeqs', isCanonicalSequence, (observation, seq) => fields(observation,
614
+ ['call', 'eventSeq', 'eventIndex']) && callEvent(observation.call) && observation.call.seq === seq
615
+ && isCanonicalSequence(observation.eventSeq) && isCanonicalSequence(observation.eventIndex)
616
+ && value.lastEventIndex !== null && observation.eventIndex <= value.lastEventIndex
617
+ && (!REPL_TOOL_NAMES.has(observation.call.data.name)
618
+ || (deepEqualJson(executable.get(seq)?.event, observation.call)
619
+ && observation.eventIndex >= executable.get(seq).eventIndex)))
620
+ const settlements = table('settledResultsByEventSeq', isCanonicalSequence, settlement => fields(settlement,
621
+ ['call', 'entry', 'identity', 'publication', 'observedIndex', 'ordinary', 'normalized'])
622
+ && callEvent(settlement.call) && callEntry(settlement.entry)
623
+ && optional(settlement.normalized, normalizedResult)
624
+ && typeof settlement.ordinary === 'boolean'
625
+ && isCanonicalSequence(settlement.observedIndex) && value.lastEventIndex !== null
626
+ && settlement.observedIndex >= settlement.entry.eventIndex && settlement.observedIndex <= value.lastEventIndex
627
+ && deepEqualJson(settlement.call, settlement.entry.event)
628
+ && typeof settlement.identity === 'string' && /^[a-f0-9]{64}$/u.test(settlement.identity)
629
+ && (!REPL_TOOL_NAMES.has(settlement.call.data.name)
630
+ || deepEqualJson(executable.get(settlement.call.seq)?.event, settlement.call))
631
+ && optional(settlement.publication, publication => fields(publication,
632
+ ['index', 'journal', 'derived', 'userBindings', 'rewrites'])
633
+ && deepEqualJson(publication.journal ?? null, settlement.normalized?.journal ?? null)
634
+ && deepEqualJson(publication.derived ?? null, settlement.normalized?.derived ?? null)
635
+ && deepEqualJson(publication.userBindings ?? null, settlement.normalized?.userBindings ?? null)
636
+ && !settlement.entry.ambiguous && isCanonicalSequence(publication.index)
637
+ && publication.index === (settlement.ordinary ? settlement.observedIndex : settlement.entry.eventIndex)
638
+ && ((settlement.call.data.name === 'run_code' && publication.derived === undefined)
639
+ || (settlement.call.data.name === 'edit_run_code'
640
+ && publication.derived?.targetCallSeq === settlement.entry.editTarget?.callSeq
641
+ && publication.derived !== undefined))
642
+ && (publication.journal === undefined
643
+ ? publication.derived === undefined && publication.userBindings === undefined
644
+ : journalFacts(publication))
645
+ && deepEqualJson(validatedRewrites({ [REWRITES_KEY]: publication.rewrites }) ?? null, publication.rewrites ?? null)))
646
+ // Normalized event facts retain distinctions needed by the transition but
647
+ // omit result content. Admission uses that same transition, not a second
648
+ // partial interpretation of the materialized caches.
649
+ requireFact(Array.isArray(value.eventFacts))
650
+ let reconstructed = createSessionTimelineState()
651
+ let correlationIndex = -1
652
+ for (const observation of value.eventFacts) {
653
+ requireFact(fields(observation, ['eventIndex', 'event', 'identity', 'hasJournal', 'normalized',
654
+ 'boundaries', 'boundaryFailure', 'rewrites'])
655
+ && isCanonicalSequence(observation.eventIndex) && observation.eventIndex > correlationIndex
656
+ && value.lastEventIndex !== null && observation.eventIndex <= value.lastEventIndex
657
+ && isRecord(observation.event) && isCanonicalSequence(observation.event.seq)
658
+ && ['turn/start', 'turn/end', RECOVERY_BOUNDARY_EVENT, 'compaction/prune', 'tool/call', 'tool/result']
659
+ .includes(observation.event.type)
660
+ && deepEqualJson(correlationEvent(observation.event), observation.event)
661
+ && (observation.event.type === 'tool/result'
662
+ ? typeof observation.identity === 'string' && /^[a-f0-9]{64}$/u.test(observation.identity)
663
+ && typeof observation.hasJournal === 'boolean'
664
+ : observation.identity === undefined && observation.hasJournal === undefined)
665
+ && optional(observation.normalized, normalizedResult)
666
+ && optional(observation.boundaryFailure, failed => failed === true)
667
+ && optional(observation.boundaries, boundaries => Array.isArray(boundaries)
668
+ && boundaries.every(boundary => {
669
+ if (!isRecord(boundary)) return false
670
+ const { eventSeq, ...evidence } = boundary
671
+ return eventSeq === observation.event.seq
672
+ && deepEqualJson(normalizeRecoveryBoundaries([evidence], eventSeq), [boundary])
673
+ }))
674
+ && deepEqualJson(validatedRewrites({ [REWRITES_KEY]: observation.rewrites }) ?? null,
675
+ observation.rewrites ?? null))
676
+ correlationIndex = observation.eventIndex
677
+ reconstructed = replaySessionTimelineFact(reconstructed, observation)
678
+ }
679
+ requireFact(deepEqualJson(reconstructed, value))
680
+ for (const observation of ordinaryObservations.values()) {
681
+ const settlement = settlements.get(observation.eventSeq)
682
+ requireFact(settlement !== undefined
683
+ ? settlement.ordinary && settlement.observedIndex === observation.eventIndex
684
+ && deepEqualJson(settlement.call, observation.call)
685
+ : value.runSelectionHistory.some(frontier => frontier.index === observation.eventIndex
686
+ && frontier.reason === 'result-identity-mismatch'))
687
+ }
688
+ for (const settlement of settlements.values()) {
689
+ if (!settlement.ordinary) continue
690
+ const observation = ordinaryObservations.get(settlement.call.seq)
691
+ requireFact(observation !== undefined && observation.eventIndex === settlement.observedIndex
692
+ && deepEqualJson(observation.call, settlement.call))
693
+ }
694
+ const publications = [...settlements.values()].filter(settlement => settlement.publication !== undefined)
695
+ // Dispatch targets are derived from first-observed settlements and retained
696
+ // clears. A late prune reconstruction cannot establish an earlier target.
697
+ const positions = new Map()
698
+ for (const settlement of settlements.values()) {
699
+ const previous = positions.get(settlement.observedIndex)
700
+ requireFact(previous === undefined || deepEqualJson(previous, settlement))
701
+ positions.set(settlement.observedIndex, settlement)
702
+ }
703
+ const chronology = [
704
+ ...value.runSelectionHistory.map(frontier => ({ index: frontier.index, frontier })),
705
+ ...[...executable.values()].map(entry => ({ index: entry.eventIndex, entry })),
706
+ ...[...positions.values()].map(settlement => ({ index: settlement.observedIndex, settlement })),
707
+ ].sort((left, right) => left.index - right.index)
708
+ let dispatchScope = 0
709
+ let dispatchFrontier = null
710
+ let dispatchRun
711
+ const dispatchClaims = new Set()
712
+ const dispatchEdits = new Map()
713
+ for (const item of chronology) {
714
+ if (item.frontier !== undefined) {
715
+ dispatchFrontier = item.frontier
716
+ dispatchRun = undefined
717
+ if (item.frontier.reason === 'turn/start' || item.frontier.reason === 'turn/end') {
718
+ dispatchScope = item.frontier.scope
719
+ dispatchClaims.clear()
720
+ dispatchEdits.clear()
721
+ }
722
+ } else if (item.entry !== undefined) {
723
+ requireFact(item.entry.scope === dispatchScope)
724
+ if (item.entry.event.data.name !== 'edit_run_code') continue
725
+ const expected = editTargetForSelection(dispatchRun, dispatchClaims)
726
+ requireFact(deepEqualJson(targets.get(item.entry.event.seq), expected ?? null))
727
+ if (expected !== undefined) {
728
+ dispatchClaims.add(expected.callSeq)
729
+ dispatchEdits.set(item.entry.event.seq, expected.callSeq)
730
+ }
731
+ } else {
732
+ const settlement = item.settlement
733
+ const claim = dispatchEdits.get(settlement.call.seq)
734
+ if (claim !== undefined) {
735
+ dispatchEdits.delete(settlement.call.seq)
736
+ if (settlement.publication?.derived === undefined) dispatchClaims.delete(claim)
737
+ }
738
+ dispatchRun = selectSettlementRun(dispatchRun, settlement, dispatchScope, dispatchFrontier)
739
+ }
740
+ }
741
+ requireFact(claimed.size === dispatchClaims.size && [...dispatchClaims].every(seq => claimed.has(seq)))
742
+ requireFact(edits.size === dispatchEdits.size && [...dispatchEdits].every(([seq, targetSeq]) => edits.get(seq) === targetSeq))
743
+ let selectedRun
744
+ let lastSuccessfulRunIndex
745
+ for (const settlement of publications) {
746
+ selectedRun = selectSettlementRun(selectedRun, settlement, value.scope, value.runSelectionFrontier)
747
+ if (successfulTimelineRun(settlementRun(settlement))) {
748
+ lastSuccessfulRunIndex = Math.max(lastSuccessfulRunIndex ?? -1, settlement.publication.index)
749
+ }
750
+ }
751
+ requireFact(selectedRun === undefined || (executable.get(selectedRun.callSeq)?.scope === value.scope
752
+ && !executable.get(selectedRun.callSeq).ambiguous))
753
+ requireFact(selectedRun === undefined ? value.latestRun === undefined && value.editableRun === undefined
754
+ : deepEqualJson(value.latestRun ?? null, selectedRun) && deepEqualJson(value.editableRun ?? null, selectedRun))
755
+ requireFact(value.lastSuccessfulRunIndex === lastSuccessfulRunIndex
756
+ && optional(value.unavailableResultSeq, isCanonicalSequence))
757
+ requireFact(optional(value.unavailableResultReason, reason => isRecord(reason)
758
+ && isCanonicalSequence(reason.seq) && typeof reason.reason === 'string'))
759
+ requireFact(optional(value.pruneReplacementWindow, window => isRecord(window)
760
+ && Array.isArray(window.seqs) && window.seqs.every(isCanonicalSequence)
761
+ && new Set(window.seqs).size === window.seqs.length
762
+ && isCanonicalSequence(window.lastEventIndex) && isCanonicalSequence(window.lastEventSeq)))
763
+ for (const boundary of value.boundaries) {
764
+ requireFact(isRecord(boundary) && isCanonicalSequence(boundary.carrierCallSeq))
765
+ const { carrierCallSeq: _carrier, eventSeq, ...evidence } = boundary
766
+ requireFact(isCanonicalSequence(eventSeq)
767
+ && deepEqualJson(normalizeRecoveryBoundaries([evidence], eventSeq), [{ ...evidence, eventSeq }]))
768
+ }
769
+ const transcript = { calls: 0, inspections: 0 }
770
+ for (const result of results.values()) {
771
+ for (const call of result.journal?.calls ?? []) {
772
+ if (call.global !== 'tools' || typeof call.member !== 'string' || !call.member.startsWith('cordis_')) continue
773
+ transcript.calls += 1
774
+ if (call.ok === true && call.member.startsWith('cordis_inspect')) transcript.inspections += 1
775
+ }
776
+ }
777
+ requireFact(deepEqualJson(transcript, value.cordisTranscript))
778
+ return value
779
+ }
780
+
781
+ export function advanceSessionTimeline(previous, inputEvent, eventIndex = inputEvent?.seq) {
782
+ return advanceTimelineEvent(previous, inputEvent, eventIndex)
783
+ }
784
+
785
+ /** Reconstruct an already validated normalized fact at its recorded cut. */
786
+ export function replaySessionTimelineFact(previous, fact) {
787
+ return advanceTimelineEvent(previous, fact.event, fact.eventIndex, fact)
788
+ }
789
+
790
+ function advanceTimelineEvent(previous, inputEvent, eventIndex, replayFact) {
791
+ if (!['turn/start', 'turn/end', RECOVERY_BOUNDARY_EVENT, 'compaction/prune', 'tool/call', 'tool/result'].includes(inputEvent?.type)) {
792
+ return previous
793
+ }
794
+ const observation = { eventIndex, event: correlationEvent(inputEvent),
795
+ ...(inputEvent.type === 'tool/result' ? {
796
+ identity: replayFact === undefined ? resultIdentity(inputEvent.data) : replayFact.identity,
797
+ hasJournal: replayFact === undefined
798
+ ? isRecord(inputEvent.data?.meta) && Object.hasOwn(inputEvent.data.meta, JOURNAL_KEY) : replayFact.hasJournal,
799
+ } : {}),
800
+ }
801
+ const state = { ...previous,
802
+ lastEventIndex: eventIndex,
803
+ eventFacts: [...previous.eventFacts, observation],
804
+ executableCalls: new TimelineTable(previous.executableCalls),
805
+ editTargets: new TimelineTable(previous.editTargets),
806
+ }
807
+ state.results = new TimelineTable(previous.results, (oldResult, newResult) => {
808
+ const count = result => {
809
+ let calls = 0
810
+ let inspections = 0
811
+ for (const call of result?.journal?.calls ?? []) {
812
+ if (call.global !== 'tools' || typeof call.member !== 'string' || !call.member.startsWith('cordis_')) continue
813
+ calls += 1
814
+ if (call.ok === true && call.member.startsWith('cordis_inspect')) inspections += 1
815
+ }
816
+ return { calls, inspections }
817
+ }
818
+ const before = count(oldResult)
819
+ const after = count(newResult)
820
+ state.cordisTranscript = {
821
+ calls: state.cordisTranscript.calls + after.calls - before.calls,
822
+ inspections: state.cordisTranscript.inspections + after.inspections - before.inspections,
823
+ }
824
+ })
825
+ const pendingByCallId = new TimelineTable(previous.pendingByCallId)
826
+ const seenCallIds = new TimelineTable(previous.seenCallIds)
827
+ const claimedEditTargets = new TimelineTable(previous.claimedEditTargets)
828
+ const editClaims = new TimelineTable(previous.editClaims)
829
+ const ordinaryResultSeqs = new TimelineTable(previous.ordinaryResultSeqs)
830
+ const settledResultsByEventSeq = new TimelineTable(previous.settledResultsByEventSeq)
831
+ let pruneReplacementWindow = previous.pruneReplacementWindow
832
+ let scope = previous.scope
833
+
834
+ const clearRunSelection = reason => {
835
+ state.runSelectionFrontier = timelineJson({ index: eventIndex, scope, reason })
836
+ state.runSelectionHistory = [...state.runSelectionHistory, state.runSelectionFrontier]
837
+ state.latestRun = undefined
838
+ state.editableRun = undefined
839
+ }
346
840
 
347
841
  const resetTurn = (openTurn) => {
348
842
  state.openTurn = openTurn
@@ -350,16 +844,12 @@ export function foldSessionTimeline(events) {
350
844
  pendingByCallId.clear()
351
845
  seenCallIds.clear()
352
846
  claimedEditTargets.clear()
353
- state.latestRun = undefined
354
- state.editableRun = undefined
847
+ editClaims.clear()
848
+ clearRunSelection(openTurn ? 'turn/start' : 'turn/end')
355
849
  }
356
850
 
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
- }
851
+ for (const event of [inputEvent]) {
852
+ pruneReplacementWindow = nextPruneWindow(pruneReplacementWindow, event, eventIndex)
363
853
  if (event?.type === 'turn/start') {
364
854
  resetTurn(true)
365
855
  continue
@@ -378,13 +868,13 @@ export function foldSessionTimeline(events) {
378
868
  }
379
869
  if (event?.type === 'tool/call' && typeof event.data?.callId === 'string') {
380
870
  const executable = REPL_TOOL_NAMES.has(event.data.name)
381
- let entry = { event, eventIndex, scope }
871
+ let entry = { event: timelineCallEvent(event), eventIndex, scope }
382
872
  if (executable) {
383
873
  if (isCanonicalSequence(event.seq) && state.executableCalls.has(event.seq)) {
384
874
  // A sequence collision disproves both sources, but not the earlier
385
875
  // verified frontier. Keep its position for persistent contraction.
386
- const previous = state.executableCalls.get(event.seq)
387
- previous.ambiguous = true
876
+ const previous = { ...state.executableCalls.get(event.seq), ambiguous: true }
877
+ state.executableCalls.set(event.seq, previous)
388
878
  state.unavailableResultSeq = Math.min(state.unavailableResultSeq ?? event.seq, event.seq)
389
879
  if (state.unavailableResultReason === undefined
390
880
  || event.seq < state.unavailableResultReason.seq) {
@@ -394,36 +884,25 @@ export function foldSessionTimeline(events) {
394
884
  }
395
885
  }
396
886
  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)
887
+ clearRunSelection('duplicate-call-sequence')
888
+ recordPendingCall(pendingByCallId, seenCallIds, entry, previous)
402
889
  continue
403
890
  }
404
891
  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
892
+ const target = editTargetForSelection(state.editableRun, claimedEditTargets)
410
893
  entry = { ...entry, editTarget: target }
411
- state.editTargets.set(event.seq, target)
894
+ state.editTargets.set(event.seq, target ?? null)
412
895
  if (target !== undefined) {
413
896
  claimedEditTargets.add(target.callSeq)
414
897
  editClaims.set(event.seq, target.callSeq)
415
898
  }
416
899
  }
417
- state.calls.push(event)
900
+ state.calls = [...state.calls, entry.event]
418
901
  if (isCanonicalSequence(event.seq)) {
419
902
  state.executableCalls.set(event.seq, entry)
420
903
  }
421
904
  }
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
- }
905
+ recordPendingCall(pendingByCallId, seenCallIds, entry)
427
906
  continue
428
907
  }
429
908
  if (event?.type !== 'tool/result') continue
@@ -432,73 +911,42 @@ export function foldSessionTimeline(events) {
432
911
  const sourceRelation = event.sourceEventSeqs
433
912
  const sourceSeq = sourceRelation?.[0]
434
913
  const canonicalSourceRelation = Array.isArray(sourceRelation)
435
- && sourceRelation.length === 1
436
- && isCanonicalSequence(sourceSeq)
914
+ && sourceRelation.length === 1 && isCanonicalSequence(sourceSeq)
437
915
  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
916
+ const resolution = resolveTimelineResult(event, eventIndex, state.executableCalls, pendingByCallId,
917
+ settledResultsByEventSeq, pruneReplacementWindow, observation.identity)
918
+ const { entry, prunedReplacement, republishedSettlement } = resolution
919
+ pruneReplacementWindow = resolution.pruneReplacementWindow
920
+ if (republishedSettlement !== undefined) {
921
+ const resultSeq = republishedSettlement.call.seq
922
+ const settledResult = state.results.get(resultSeq)
923
+ if (settledResult !== undefined) {
924
+ state.results.set(resultSeq, republishedTimelineResult(settledResult, event))
463
925
  }
926
+ settledResultsByEventSeq.set(event.seq, republishedSettlement)
927
+ continue
464
928
  }
465
929
  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)
930
+ const callSeq = invalidSourceCallSeq(event, state.executableCalls, pendingByCallId)
931
+ if (callSeq !== undefined) {
478
932
  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
- })
933
+ state.results.set(callSeq, timelineResultError(event, eventIndex, callSeq,
934
+ `tool result has an invalid source relation for call seq ${callSeq}`))
484
935
  state.found = true
485
936
  }
486
937
  if (typeof callId === 'string') pendingByCallId.delete(callId)
487
- state.latestRun = undefined
488
- state.editableRun = undefined
938
+ clearRunSelection('invalid-source-relation')
489
939
  continue
490
940
  }
491
941
  if (typeof callId === 'string') pendingByCallId.delete(callId)
492
942
  if (entry === null) {
493
- state.latestRun = undefined
494
- state.editableRun = undefined
943
+ clearRunSelection('ambiguous-call-id')
495
944
  continue
496
945
  }
497
946
 
498
947
  const call = entry?.event
499
948
  if (entry?.ambiguous) {
500
- state.latestRun = undefined
501
- state.editableRun = undefined
949
+ clearRunSelection('ambiguous-call-sequence')
502
950
  continue
503
951
  }
504
952
  const callSeq = call?.seq
@@ -506,34 +954,27 @@ export function foldSessionTimeline(events) {
506
954
  if (!prunedReplacement && isCanonicalSequence(callSeq)) {
507
955
  if (ordinaryResultSeqs.has(callSeq)) {
508
956
  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
- })
957
+ state.results.set(callSeq, timelineResultError(event, eventIndex, callSeq,
958
+ `session log contains duplicate ordinary tool results for call seq ${callSeq}`))
514
959
  state.found = true
515
- state.latestRun = undefined
516
- state.editableRun = undefined
960
+ clearRunSelection('duplicate-result')
517
961
  continue
518
962
  }
519
- ordinaryResultSeqs.add(callSeq)
963
+ ordinaryResultSeqs.set(callSeq, timelineJson({ call,
964
+ eventSeq: isCanonicalSequence(event.seq) ? event.seq : callSeq, eventIndex }))
520
965
  }
521
966
  if (call !== undefined && typeof callId === 'string' && call.data.callId !== callId) {
522
- state.latestRun = undefined
523
- state.editableRun = undefined
967
+ clearRunSelection('result-identity-mismatch')
524
968
  if (isCanonicalSequence(callSeq)) {
525
969
  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
- })
970
+ state.results.set(callSeq, timelineResultError(event, eventIndex, callSeq,
971
+ `tool result identities disagree for call seq ${callSeq}`))
531
972
  state.found = true
532
973
  }
533
974
  continue
534
975
  }
535
976
  let normalized
536
- if (isRecord(meta) && Object.hasOwn(meta, JOURNAL_KEY)) {
977
+ if (observation.hasJournal) {
537
978
  if (entry === undefined) {
538
979
  if (isCanonicalSequence(sourceSeq)) {
539
980
  state.unavailableResultSeq ??= sourceSeq
@@ -549,66 +990,95 @@ export function foldSessionTimeline(events) {
549
990
  // clone by its own row would apply the boundary before that call exists
550
991
  // in the verified frontier.
551
992
  const settlementPosition = prunedReplacement
552
- ? state.results.get(resultSeq)?.eventSeq ?? resultSeq
993
+ ? state.results.get(resultSeq)?.positionSeq ?? state.results.get(resultSeq)?.eventSeq ?? sourceSeq
553
994
  : undefined
554
995
  const raw = {
555
- meta,
556
996
  eventSeq: isCanonicalSequence(event.seq) ? event.seq : resultSeq,
557
997
  ...(settlementPosition === undefined ? {} : { positionSeq: settlementPosition }),
558
998
  eventIndex,
559
999
  }
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 }),
1000
+ if (replayFact === undefined) {
1001
+ if (Object.hasOwn(meta, RECOVERY_BOUNDARY_KEY)) {
1002
+ try {
1003
+ observation.boundaries = normalizeRecoveryBoundaries(meta[RECOVERY_BOUNDARY_KEY], raw.eventSeq)
1004
+ } catch {
1005
+ observation.boundaryFailure = true
577
1006
  }
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
1007
+ }
1008
+ try {
1009
+ if (call.data.name === 'edit_run_code') {
1010
+ const derived = normalizeDerivedEditResult(meta, entry.editTarget?.callSeq)
1011
+ normalized = {
1012
+ ...raw,
1013
+ journal: derived.journal,
1014
+ derived,
1015
+ ...(derived.userBindings === undefined ? {} : { userBindings: derived.userBindings }),
1016
+ }
1017
+ } else if (call.data.name === 'run_code') {
1018
+ const rawJournal = meta[JOURNAL_KEY]
1019
+ const resolveLegacyConfirm = rawJournal?.version === LEGACY_JOURNAL_VERSION
1020
+ ? legacyCallId => {
1021
+ const candidates = [...state.executableCalls.values()]
1022
+ .filter(candidate => candidate.eventIndex < eventIndex
1023
+ && candidate.event.data?.name === 'run_code'
1024
+ && candidate.event.data.callId === legacyCallId
1025
+ && !state.results.has(candidate.event.seq))
1026
+ return candidates.length === 1 ? candidates[0].event.seq : undefined
1027
+ }
1028
+ : undefined
1029
+ const journal = normalizeJournal(rawJournal, { resolveLegacyConfirm })
1030
+ const userBindings = userBindingsForJournal(meta, journal)
1031
+ normalized = {
1032
+ ...raw,
1033
+ journal,
1034
+ ...(userBindings === undefined ? {} : { userBindings }),
588
1035
  }
589
- : undefined
590
- const journal = normalizeJournal(rawJournal, { resolveLegacyConfirm })
591
- const userBindings = userBindingsForJournal(meta, journal)
592
- normalized = {
593
- ...raw,
594
- journal,
595
- ...(userBindings === undefined ? {} : { userBindings }),
596
1036
  }
1037
+ } catch (error) {
1038
+ normalized = { ...raw, error: error.message }
597
1039
  }
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' }
1040
+ if (normalized === undefined) {
1041
+ // PTC metadata on a settlement whose call is not a REPL tool is
1042
+ // unproved evidence: it must contract the frontier, not escape the fold.
1043
+ normalized = { ...raw, error: 'PTC journal metadata appeared on a non-REPL tool result' }
1044
+ }
1045
+ } else {
1046
+ normalized = replayFact.normalized
1047
+ if (normalized === undefined || !deepEqualJson(raw, {
1048
+ eventSeq: normalized.eventSeq, eventIndex: normalized.eventIndex,
1049
+ ...(normalized.positionSeq === undefined ? {} : { positionSeq: normalized.positionSeq }),
1050
+ })) throw new TypeError('invalid PTC session-log projection timeline checkpoint result facts')
1051
+ if (replayFact.boundaries !== undefined) observation.boundaries = replayFact.boundaries
1052
+ if (replayFact.boundaryFailure !== undefined) observation.boundaryFailure = replayFact.boundaryFailure
605
1053
  }
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 })
1054
+ observation.normalized = normalized
1055
+ if (observation.boundaries !== undefined) {
1056
+ state.boundaries = [...state.boundaries, ...observation.boundaries.map(boundary => ({
1057
+ ...boundary, carrierCallSeq: resultSeq,
1058
+ }))]
609
1059
  }
1060
+ if (observation.boundaryFailure) state.unavailableResultSeq ??= callSeq
1061
+ if (isCanonicalSequence(resultSeq)) state.results.set(resultSeq, timelineJson(normalized))
610
1062
  state.found = true
611
1063
  }
1064
+ const settlement = call === undefined ? undefined : { call, entry, identity: observation.identity,
1065
+ observedIndex: eventIndex, ordinary: !prunedReplacement,
1066
+ ...(normalized === undefined ? {} : { normalized }) }
1067
+ if (settlement !== undefined && entry.scope === scope
1068
+ && (call.data.name === 'run_code' || normalized?.derived !== undefined)) {
1069
+ const rewrites = replayFact === undefined ? validatedRewrites(meta) : replayFact.rewrites
1070
+ if (rewrites !== undefined) observation.rewrites = rewrites
1071
+ settlement.publication = {
1072
+ index: prunedReplacement ? entry.eventIndex : eventIndex,
1073
+ journal: normalized?.journal,
1074
+ derived: normalized?.derived,
1075
+ userBindings: normalized?.userBindings,
1076
+ rewrites,
1077
+ }
1078
+ }
1079
+ if (settlement !== undefined && isCanonicalSequence(event.seq)) {
1080
+ settledResultsByEventSeq.set(event.seq, settlement)
1081
+ }
612
1082
 
613
1083
  const claimedTarget = editClaims.get(callSeq)
614
1084
  if (claimedTarget !== undefined) {
@@ -616,12 +1086,17 @@ export function foldSessionTimeline(events) {
616
1086
  if (normalized?.derived === undefined) claimedEditTargets.delete(claimedTarget)
617
1087
  }
618
1088
  if (entry === undefined || entry.scope !== scope) continue
1089
+ const publishRun = () => {
1090
+ const run = settlementRun(settlement)
1091
+ state.latestRun = selectSettlementRun(state.latestRun, settlement, scope, state.runSelectionFrontier)
1092
+ state.editableRun = state.latestRun
1093
+ if (successfulTimelineRun(run)) {
1094
+ state.lastSuccessfulRunIndex = Math.max(state.lastSuccessfulRunIndex ?? -1, run.index)
1095
+ }
1096
+ }
619
1097
  if (call.data.name === 'edit_run_code') {
620
1098
  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
1099
+ publishRun()
625
1100
  }
626
1101
  continue
627
1102
  }
@@ -631,12 +1106,34 @@ export function foldSessionTimeline(events) {
631
1106
  // new executable cell or turn boundary supersedes it.
632
1107
  continue
633
1108
  }
634
- const run = timelineRun(call, event, eventIndex, normalized?.journal)
635
- state.latestRun = run
636
- state.editableRun = run
637
- if (successfulTimelineRun(run)) state.lastSuccessfulRunIndex = eventIndex
1109
+ publishRun()
638
1110
  }
639
- return state
1111
+ state.eventFacts[state.eventFacts.length - 1] = timelineJson(observation)
1112
+ return Object.fromEntries(Object.entries({ ...state,
1113
+ executableCalls: state.executableCalls.table, results: state.results.table, editTargets: state.editTargets.table,
1114
+ pendingByCallId: pendingByCallId.table, seenCallIds: seenCallIds.table,
1115
+ claimedEditTargets: claimedEditTargets.table, editClaims: editClaims.table,
1116
+ ordinaryResultSeqs: ordinaryResultSeqs.table, settledResultsByEventSeq: settledResultsByEventSeq.table,
1117
+ pruneReplacementWindow, scope,
1118
+ }).filter(([_key, value]) => value !== undefined))
1119
+ }
1120
+
1121
+ export function sessionTimelineValue(checkpoint) {
1122
+ return { ...checkpoint,
1123
+ executableCalls: new Map(Object.values(checkpoint.executableCalls)),
1124
+ results: new Map(Object.values(checkpoint.results)),
1125
+ editTargets: new Map(Object.values(checkpoint.editTargets).map(([seq, target]) => [seq, target ?? undefined])),
1126
+ }
1127
+ }
1128
+
1129
+ export function foldSessionTimeline(events) {
1130
+ let checkpoint = createSessionTimelineState()
1131
+ if (Array.isArray(events)) {
1132
+ for (let index = 0; index < events.length; index += 1) {
1133
+ checkpoint = advanceSessionTimeline(checkpoint, events[index], index)
1134
+ }
1135
+ }
1136
+ return sessionTimelineValue(checkpoint)
640
1137
  }
641
1138
 
642
1139
  /** Fold the session log into the last exactly replayable frontier. */