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.
- package/README.en.md +15 -3
- package/README.md +15 -3
- package/client.js +1157 -533
- package/cordis.patch.yml +12 -2
- package/docs/README.md +2 -0
- package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +60 -4
- package/docs/adr/0017-track-the-latest-dsh-public-surface.md +5 -1
- package/docs/adr/0019-plugin-settings-and-kill-switch.md +14 -3
- package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +2 -2
- package/docs/adr/0023-global-user-bindings.md +1 -1
- package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +82 -18
- package/docs/adr/0030-serve-both-typert-codec-generations-from-one-codec.md +3 -3
- package/docs/adr/0032-own-session-log-facts-in-a-host-only-projection.md +27 -0
- package/docs/adr/0033-gate-client-visual-usability-in-a-browser.md +23 -0
- package/docs/architecture.md +6 -4
- package/docs/binding-review-placement-design.md +29 -21
- package/docs/client-ui.md +20 -8
- package/docs/client-visual-contract.md +48 -0
- package/docs/durability-design.md +12 -0
- package/docs/installation.md +48 -9
- package/docs/publishing.md +4 -4
- package/docs/release-notes.md +109 -0
- package/docs/runtime-reference.md +8 -2
- package/docs/user-bindings.en.md +2 -2
- package/docs/user-bindings.md +3 -3
- package/docs/verification-optimization-work.md +26 -0
- package/docs/verification.md +7 -1
- package/index.js +28 -7
- package/internal/compiler-platform.js +5 -3
- package/internal/compiler-service.js +2 -9
- package/internal/cordis-tools-owner.js +75 -16
- package/internal/direct-surface-owner.js +333 -7
- package/internal/edit-transport-owner.js +8 -5
- package/internal/execution-provider-owner.js +157 -0
- package/internal/execution-seam-compat.js +69 -113
- package/internal/isolated-worker.js +12 -8
- package/internal/recovery-tips.js +50 -21
- package/internal/rejected-cell-editor.js +11 -21
- package/internal/runtime-bridge-owner.js +88 -57
- package/internal/runtime-contexts.js +13 -13
- package/internal/session-events.js +40 -4
- package/internal/session-journal-recovery.js +678 -181
- package/internal/session-log-view.js +189 -43
- package/internal/session-runtime.js +58 -10
- package/internal/settings-compat.js +12 -2
- package/internal/user-bindings-owner.js +6 -5
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +18 -13
- package/src/client-authoring-view.js +222 -141
- package/src/client-binding-review.js +2 -0
- package/src/client-code-editor.js +6 -0
- package/src/client-copy.js +10 -0
- package/src/client-feature-gates.js +12 -5
- package/src/client-host-compat.js +23 -2
- package/src/client-primitives.js +249 -0
- package/src/client-repl-view.js +3 -3
- package/src/client-settings-view.js +2 -29
- package/src/client-styles.js +92 -33
- package/src/client-tool-view.js +8 -3
- package/src/client-workbench.js +9 -7
- package/src/client.js +72 -46
- package/assets/ptc-plus-bindings-en.png +0 -0
- package/assets/ptc-plus-bindings-zh.png +0 -0
- package/assets/ptc-plus-repair-en.png +0 -0
- package/assets/ptc-plus-repair-zh.png +0 -0
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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 :
|
|
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.
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
|
|
321
|
-
|
|
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:
|
|
490
|
+
executableCalls: {},
|
|
324
491
|
calls: [],
|
|
325
|
-
results:
|
|
492
|
+
results: {},
|
|
326
493
|
boundaries: [],
|
|
327
494
|
found: false,
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
-
|
|
354
|
-
|
|
847
|
+
editClaims.clear()
|
|
848
|
+
clearRunSelection(openTurn ? 'turn/start' : 'turn/end')
|
|
355
849
|
}
|
|
356
850
|
|
|
357
|
-
for (
|
|
358
|
-
|
|
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
|
-
|
|
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
|
-
|
|
398
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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
|
|
467
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
516
|
-
state.editableRun = undefined
|
|
960
|
+
clearRunSelection('duplicate-result')
|
|
517
961
|
continue
|
|
518
962
|
}
|
|
519
|
-
ordinaryResultSeqs.
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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 ??
|
|
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 (
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
meta[RECOVERY_BOUNDARY_KEY], raw.eventSeq
|
|
564
|
-
|
|
565
|
-
|
|
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
|
-
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
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
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
normalized =
|
|
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
|
-
|
|
607
|
-
if (
|
|
608
|
-
|
|
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
|
-
|
|
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
|
-
|
|
635
|
-
state.latestRun = run
|
|
636
|
-
state.editableRun = run
|
|
637
|
-
if (successfulTimelineRun(run)) state.lastSuccessfulRunIndex = eventIndex
|
|
1109
|
+
publishRun()
|
|
638
1110
|
}
|
|
639
|
-
|
|
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. */
|