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