dsh-ptc-plus 0.3.1 → 0.3.2
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 +6 -0
- package/README.md +6 -0
- package/assets/ptc-plus-bindings-en.png +0 -0
- package/assets/ptc-plus-bindings-zh.png +0 -0
- package/client.js +43 -9
- package/docs/adr/0006-rewrite-module-syntax-with-an-ast.md +1 -1
- package/docs/adr/0007-separate-worker-transport-from-session-semantics.md +15 -3
- package/docs/adr/0013-isolate-repl-redeclaration-convenience.md +7 -3
- package/docs/adr/0014-persist-cell-rewrite-policy.md +7 -5
- package/docs/adr/0019-plugin-settings-and-kill-switch.md +3 -3
- package/docs/adr/0021-separate-host-bootstrap-from-session-runtime.md +5 -3
- package/docs/architecture.md +14 -6
- package/docs/client-ui.md +5 -5
- package/docs/durability-design.md +42 -20
- package/docs/program-data-plane.md +6 -0
- package/docs/runtime-reference.md +10 -3
- package/index.js +8 -3
- package/internal/cell-analysis.js +72 -12
- package/internal/cell-rewriter.js +128 -4
- package/internal/config-spec.js +10 -0
- package/internal/edit-transport-owner.js +1 -1
- package/internal/kernel-worker.js +11 -0
- package/internal/repl-convenience.js +94 -6
- package/internal/repl-rewrite-contract.js +15 -0
- package/internal/runtime-config.js +1 -0
- package/internal/session-cell-executor.js +65 -15
- package/internal/session-journal.js +328 -61
- package/internal/session-runtime.js +91 -7
- package/internal/session-state.js +36 -3
- package/package.json +7 -5
- package/src/client-activity.js +38 -9
|
@@ -2,22 +2,34 @@ import { decodeValue, encodeValue, normalizeValueWire } from './value-wire.js'
|
|
|
2
2
|
import { normalizeDiagnostic } from './diagnostic.js'
|
|
3
3
|
import { assertOwnFields, isRecord } from './record-utils.js'
|
|
4
4
|
import { sessionEvents } from './session-events.js'
|
|
5
|
+
import {
|
|
6
|
+
LEGACY_DEFAULT_EXPORT_BINDING,
|
|
7
|
+
LIVE_DEFAULT_EXPORT_BINDING,
|
|
8
|
+
} from './repl-rewrite-contract.js'
|
|
5
9
|
|
|
6
10
|
export const JOURNAL_KEY = 'dshPtcPlus'
|
|
7
11
|
export const EDIT_TARGET_KEY = 'dshPtcPlusEdit'
|
|
8
12
|
export const DERIVED_RUN_KEY = 'dshPtcPlusDerivedRun'
|
|
9
13
|
export const REWRITES_KEY = 'dshPtcPlusRewrites'
|
|
10
14
|
export const RECOVERY_BOUNDARY_KEY = 'dshPtcPlusRecoveryBoundaries'
|
|
11
|
-
export const JOURNAL_VERSION =
|
|
15
|
+
export const JOURNAL_VERSION = 4
|
|
12
16
|
const LEGACY_JOURNAL_VERSION = 1
|
|
13
17
|
const INTERMEDIATE_JOURNAL_VERSION = 2
|
|
18
|
+
const PREVIOUS_JOURNAL_VERSION = 3
|
|
14
19
|
export const RECOVERY_BOUNDARY_EVENT = 'ptc-plus/recovery-boundary'
|
|
15
20
|
|
|
16
21
|
const STATUSES = new Set(['durable', 'volatile', 'discarded', 'noop'])
|
|
17
22
|
const BINDING_MODES = new Set(['loose', 'strict'])
|
|
18
|
-
const JOURNAL_FIELDS = new Set(['version', '
|
|
19
|
-
const
|
|
23
|
+
const JOURNAL_FIELDS = new Set(['version', 'bindingPolicy', 'rewritePolicy', 'moduleSemantics', 'status', 'calls', 'operations', 'confirms', 'diagnostics', 'completion', 'volatileReason'])
|
|
24
|
+
const PREDECESSOR_JOURNAL_FIELDS = new Set(['version', 'bindingMode', 'rewritePolicy', 'status', 'calls', 'operations', 'confirms', 'diagnostics', 'completion', 'volatileReason'])
|
|
25
|
+
const LEGACY_JOURNAL_FIELDS = new Set([...PREDECESSOR_JOURNAL_FIELDS].filter(field => field !== 'rewritePolicy'))
|
|
26
|
+
const BINDING_POLICY_FIELDS = new Set(['variableRedeclarations', 'functionClassRedeclarations'])
|
|
20
27
|
const REWRITE_POLICY_FIELDS = new Set(['autoRewriteImports', 'autoStripExports', 'autoSplitRedeclarations'])
|
|
28
|
+
const MODULE_SEMANTICS_FIELDS = new Set(['defaultExportBinding'])
|
|
29
|
+
const DEFAULT_EXPORT_BINDINGS = new Set([
|
|
30
|
+
LEGACY_DEFAULT_EXPORT_BINDING,
|
|
31
|
+
LIVE_DEFAULT_EXPORT_BINDING,
|
|
32
|
+
])
|
|
21
33
|
const CALL_SUCCESS_FIELDS = new Set(['global', 'member', 'args', 'ok', 'value', 'settle'])
|
|
22
34
|
const CALL_ERROR_FIELDS = new Set(['global', 'member', 'args', 'ok', 'error', 'settle'])
|
|
23
35
|
const OPERATION_FIELDS = new Set(['action', 'name'])
|
|
@@ -167,28 +179,64 @@ function normalizeRewritePolicy(value) {
|
|
|
167
179
|
})
|
|
168
180
|
}
|
|
169
181
|
|
|
182
|
+
function normalizeBindingPolicy(value) {
|
|
183
|
+
if (!isRecord(value)) throw new Error('invalid dsh-ptc-plus journal binding policy')
|
|
184
|
+
assertOwnFields(value, BINDING_POLICY_FIELDS, 'journal binding policy')
|
|
185
|
+
for (const key of BINDING_POLICY_FIELDS) {
|
|
186
|
+
if (typeof value[key] !== 'boolean') throw new Error(`invalid dsh-ptc-plus journal binding policy ${key}`)
|
|
187
|
+
}
|
|
188
|
+
return Object.freeze({
|
|
189
|
+
variableRedeclarations: value.variableRedeclarations,
|
|
190
|
+
functionClassRedeclarations: value.functionClassRedeclarations,
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function normalizeModuleSemantics(value) {
|
|
195
|
+
if (!isRecord(value)) throw new Error('invalid dsh-ptc-plus journal module semantics')
|
|
196
|
+
assertOwnFields(value, MODULE_SEMANTICS_FIELDS, 'journal module semantics')
|
|
197
|
+
if (!DEFAULT_EXPORT_BINDINGS.has(value.defaultExportBinding)) {
|
|
198
|
+
throw new Error('invalid dsh-ptc-plus journal default export binding semantics')
|
|
199
|
+
}
|
|
200
|
+
return Object.freeze({ defaultExportBinding: value.defaultExportBinding })
|
|
201
|
+
}
|
|
202
|
+
|
|
170
203
|
function migrateJournal(value, resolveLegacyConfirm) {
|
|
171
|
-
if (value.version
|
|
172
|
-
|
|
204
|
+
if (value.version === JOURNAL_VERSION) return value
|
|
205
|
+
const legacy = value.version === LEGACY_JOURNAL_VERSION
|
|
206
|
+
assertOwnFields(
|
|
207
|
+
value,
|
|
208
|
+
legacy ? LEGACY_JOURNAL_FIELDS : PREDECESSOR_JOURNAL_FIELDS,
|
|
209
|
+
'dsh-ptc-plus journal',
|
|
210
|
+
)
|
|
211
|
+
if (!BINDING_MODES.has(value.bindingMode)) throw new Error('invalid dsh-ptc-plus journal binding mode')
|
|
212
|
+
const { bindingMode, ...rest } = value
|
|
173
213
|
return {
|
|
174
|
-
...
|
|
214
|
+
...rest,
|
|
175
215
|
version: JOURNAL_VERSION,
|
|
176
|
-
|
|
177
|
-
|
|
216
|
+
bindingPolicy: {
|
|
217
|
+
variableRedeclarations: bindingMode === 'loose',
|
|
218
|
+
functionClassRedeclarations: false,
|
|
219
|
+
},
|
|
220
|
+
rewritePolicy: legacy ? LEGACY_REWRITE_POLICY : value.rewritePolicy,
|
|
221
|
+
moduleSemantics: { defaultExportBinding: LEGACY_DEFAULT_EXPORT_BINDING },
|
|
222
|
+
confirms: legacy
|
|
223
|
+
? normalizeLegacyConfirms(value.confirms, resolveLegacyConfirm)
|
|
224
|
+
: value.confirms,
|
|
178
225
|
}
|
|
179
226
|
}
|
|
180
227
|
|
|
181
228
|
/** Validate and detach one journal emitted by the runtime. */
|
|
182
229
|
export function normalizeJournal(value, options = {}) {
|
|
183
230
|
if (!isRecord(value)) throw new Error('invalid dsh-ptc-plus journal')
|
|
184
|
-
if (![LEGACY_JOURNAL_VERSION, INTERMEDIATE_JOURNAL_VERSION, JOURNAL_VERSION].includes(value.version)
|
|
231
|
+
if (![LEGACY_JOURNAL_VERSION, INTERMEDIATE_JOURNAL_VERSION, PREVIOUS_JOURNAL_VERSION, JOURNAL_VERSION].includes(value.version)
|
|
185
232
|
|| !STATUSES.has(value.status)) {
|
|
186
233
|
throw new Error('invalid dsh-ptc-plus journal')
|
|
187
234
|
}
|
|
188
235
|
const migrated = migrateJournal(value, options.resolveLegacyConfirm)
|
|
189
236
|
assertOwnFields(migrated, JOURNAL_FIELDS, 'dsh-ptc-plus journal')
|
|
190
|
-
|
|
237
|
+
const bindingPolicy = normalizeBindingPolicy(migrated.bindingPolicy)
|
|
191
238
|
const rewritePolicy = normalizeRewritePolicy(migrated.rewritePolicy)
|
|
239
|
+
const moduleSemantics = normalizeModuleSemantics(migrated.moduleSemantics)
|
|
192
240
|
const calls = normalizeCalls(migrated.calls)
|
|
193
241
|
const operations = normalizeOperations(migrated.operations)
|
|
194
242
|
const confirms = normalizeConfirms(migrated.confirms)
|
|
@@ -209,8 +257,9 @@ export function normalizeJournal(value, options = {}) {
|
|
|
209
257
|
}
|
|
210
258
|
return Object.freeze({
|
|
211
259
|
version: JOURNAL_VERSION,
|
|
212
|
-
|
|
260
|
+
bindingPolicy,
|
|
213
261
|
rewritePolicy,
|
|
262
|
+
moduleSemantics,
|
|
214
263
|
status: migrated.status,
|
|
215
264
|
calls: Object.freeze(calls),
|
|
216
265
|
operations: Object.freeze(operations),
|
|
@@ -394,12 +443,21 @@ export function liveToolCallSeq(session, callId, toolName) {
|
|
|
394
443
|
}
|
|
395
444
|
|
|
396
445
|
/** Start a mutable journal for one live cell. */
|
|
397
|
-
export function createJournal(confirms = [],
|
|
398
|
-
if (
|
|
446
|
+
export function createJournal(confirms = [], bindingPolicy, rewritePolicy) {
|
|
447
|
+
if (typeof bindingPolicy === 'string') {
|
|
448
|
+
if (!BINDING_MODES.has(bindingPolicy)) throw new TypeError('invalid dsh-ptc-plus journal binding mode')
|
|
449
|
+
bindingPolicy = {
|
|
450
|
+
variableRedeclarations: bindingPolicy === 'loose',
|
|
451
|
+
functionClassRedeclarations: false,
|
|
452
|
+
}
|
|
453
|
+
}
|
|
399
454
|
return {
|
|
400
455
|
version: JOURNAL_VERSION,
|
|
401
|
-
|
|
456
|
+
bindingPolicy: normalizeBindingPolicy(bindingPolicy),
|
|
402
457
|
rewritePolicy: normalizeRewritePolicy(rewritePolicy),
|
|
458
|
+
moduleSemantics: normalizeModuleSemantics({
|
|
459
|
+
defaultExportBinding: LIVE_DEFAULT_EXPORT_BINDING,
|
|
460
|
+
}),
|
|
403
461
|
calls: [],
|
|
404
462
|
operations: [],
|
|
405
463
|
confirms: [...confirms],
|
|
@@ -456,11 +514,6 @@ function applyOperations(state, operations, nodeIndex) {
|
|
|
456
514
|
function applyRecord(state, record, invalidCallSeqs) {
|
|
457
515
|
const { call, code, result } = record
|
|
458
516
|
if (invalidCallSeqs.has(call.seq)) return
|
|
459
|
-
if (code === undefined || result?.journal === undefined) {
|
|
460
|
-
state.trusted = false
|
|
461
|
-
state.volatileSuffix.push({ seq: call.seq, code, reason: result?.error ?? 'missing dsh-ptc-plus journal result' })
|
|
462
|
-
return
|
|
463
|
-
}
|
|
464
517
|
const journal = result.journal
|
|
465
518
|
if (journal.status === 'noop') return
|
|
466
519
|
if (journal.status === 'discarded') {
|
|
@@ -476,7 +529,7 @@ function applyRecord(state, record, invalidCallSeqs) {
|
|
|
476
529
|
applyOperations(state, journal.operations, undefined)
|
|
477
530
|
return
|
|
478
531
|
}
|
|
479
|
-
if (!state.trusted) {
|
|
532
|
+
if (!state.trusted && !state.surfaceContracted) {
|
|
480
533
|
state.trusted = true
|
|
481
534
|
state.volatileSuffix.length = 0
|
|
482
535
|
}
|
|
@@ -488,38 +541,31 @@ function applyRecord(state, record, invalidCallSeqs) {
|
|
|
488
541
|
})
|
|
489
542
|
const index = state.nodes.push(node) - 1
|
|
490
543
|
state.head = index
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
if (state.head === -1) throw new Error('recovery boundary frontier is not reconstructable')
|
|
499
|
-
state.trusted = true
|
|
500
|
-
state.volatileSuffix = []
|
|
544
|
+
try {
|
|
545
|
+
applyOperations(state, journal.operations, index)
|
|
546
|
+
} catch (error) {
|
|
547
|
+
state.nodes.pop()
|
|
548
|
+
state.head = node.parent
|
|
549
|
+
throw error
|
|
550
|
+
}
|
|
501
551
|
}
|
|
502
552
|
|
|
503
553
|
function recordEventSeq(record) {
|
|
504
554
|
return record.result?.eventSeq ?? record.call.seq
|
|
505
555
|
}
|
|
506
556
|
|
|
507
|
-
function foldRecords(records, invalidCallSeqs,
|
|
557
|
+
function foldRecords(records, invalidCallSeqs, options = {}) {
|
|
508
558
|
const state = {
|
|
509
559
|
nodes: [],
|
|
510
560
|
head: undefined,
|
|
511
561
|
checkpoints: new Map(),
|
|
512
562
|
volatileSuffix: [],
|
|
513
563
|
trusted: true,
|
|
564
|
+
surfaceContracted: options.surfaceContracted === true,
|
|
514
565
|
}
|
|
515
|
-
let boundaryIndex = 0
|
|
516
566
|
for (const record of records) {
|
|
517
|
-
while (boundaryIndex < boundaries.length && boundaries[boundaryIndex].eventSeq < recordEventSeq(record)) {
|
|
518
|
-
forceRecoveryHead(state, boundaries[boundaryIndex++])
|
|
519
|
-
}
|
|
520
567
|
applyRecord(state, record, invalidCallSeqs)
|
|
521
568
|
}
|
|
522
|
-
while (boundaryIndex < boundaries.length) forceRecoveryHead(state, boundaries[boundaryIndex++])
|
|
523
569
|
return state
|
|
524
570
|
}
|
|
525
571
|
|
|
@@ -530,6 +576,77 @@ function dependsOn(nodes, index, ancestor) {
|
|
|
530
576
|
return false
|
|
531
577
|
}
|
|
532
578
|
|
|
579
|
+
/** Return executable call sequences whose provenance remains model-visible. */
|
|
580
|
+
export function visibleExecutableCallSeqs(session) {
|
|
581
|
+
const events = sessionEvents(session)
|
|
582
|
+
let nodes
|
|
583
|
+
try {
|
|
584
|
+
nodes = session?.surface?.nodes
|
|
585
|
+
} catch {
|
|
586
|
+
return undefined
|
|
587
|
+
}
|
|
588
|
+
if (!Array.isArray(events) || !Array.isArray(nodes)) return undefined
|
|
589
|
+
const eventBySeq = new Map()
|
|
590
|
+
for (const event of events) eventBySeq.set(event?.seq, event)
|
|
591
|
+
for (const seq of nodes) {
|
|
592
|
+
if (!Number.isSafeInteger(seq) || seq < 0 || eventBySeq.get(seq) === undefined) return undefined
|
|
593
|
+
}
|
|
594
|
+
const visible = new Set(nodes)
|
|
595
|
+
const resultSources = new Set()
|
|
596
|
+
const visibleAssistantCallSeqs = new Set()
|
|
597
|
+
const pendingAssistantCalls = new Map()
|
|
598
|
+
let assistantCallEvidence = false
|
|
599
|
+
for (const event of events) {
|
|
600
|
+
if (event?.type === 'turn/start' || event?.type === 'turn/end') {
|
|
601
|
+
pendingAssistantCalls.clear()
|
|
602
|
+
continue
|
|
603
|
+
}
|
|
604
|
+
if (event?.type === 'assistant/message') {
|
|
605
|
+
pendingAssistantCalls.clear()
|
|
606
|
+
const content = event.data?.message?.content
|
|
607
|
+
if (Array.isArray(content)) {
|
|
608
|
+
for (const part of content) {
|
|
609
|
+
if (part?.type === 'tool-call' && typeof part.id === 'string') {
|
|
610
|
+
const isVisible = visible.has(event.seq)
|
|
611
|
+
if (isVisible) assistantCallEvidence = true
|
|
612
|
+
let occurrences = pendingAssistantCalls.get(part.id)
|
|
613
|
+
if (occurrences === undefined) {
|
|
614
|
+
occurrences = []
|
|
615
|
+
pendingAssistantCalls.set(part.id, occurrences)
|
|
616
|
+
}
|
|
617
|
+
occurrences.push(isVisible)
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
continue
|
|
622
|
+
}
|
|
623
|
+
if (event?.type === 'tool/call' && typeof event.data?.callId === 'string') {
|
|
624
|
+
const occurrences = pendingAssistantCalls.get(event.data.callId)
|
|
625
|
+
if (occurrences !== undefined && occurrences.length > 0) {
|
|
626
|
+
const isVisible = occurrences.shift()
|
|
627
|
+
if (occurrences.length === 0) pendingAssistantCalls.delete(event.data.callId)
|
|
628
|
+
if (isVisible && ['run_code', 'edit_run_code'].includes(event.data.name)) {
|
|
629
|
+
visibleAssistantCallSeqs.add(event.seq)
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
if (visible.has(event?.seq)
|
|
634
|
+
&& event?.type === 'tool/result' && Array.isArray(event.sourceEventSeqs)) {
|
|
635
|
+
for (const sourceSeq of event.sourceEventSeqs) {
|
|
636
|
+
if (Number.isSafeInteger(sourceSeq) && sourceSeq >= 0) resultSources.add(sourceSeq)
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
const calls = new Set()
|
|
641
|
+
for (const event of events) {
|
|
642
|
+
if (event?.type !== 'tool/call' || !['run_code', 'edit_run_code'].includes(event.data?.name)) continue
|
|
643
|
+
if (assistantCallEvidence
|
|
644
|
+
? visibleAssistantCallSeqs.has(event.seq)
|
|
645
|
+
: (visible.has(event.seq) || resultSources.has(event.seq))) calls.add(event.seq)
|
|
646
|
+
}
|
|
647
|
+
return calls
|
|
648
|
+
}
|
|
649
|
+
|
|
533
650
|
function timelineRun(call, result, eventIndex, journal) {
|
|
534
651
|
let args
|
|
535
652
|
try {
|
|
@@ -589,6 +706,7 @@ export function foldSessionTimeline(events) {
|
|
|
589
706
|
const claimedEditTargets = new Set()
|
|
590
707
|
const editClaims = new Map()
|
|
591
708
|
const journalResultSeqs = new Set()
|
|
709
|
+
let pruneReplacementWindow
|
|
592
710
|
let scope = 0
|
|
593
711
|
|
|
594
712
|
const resetTurn = (openTurn) => {
|
|
@@ -603,6 +721,11 @@ export function foldSessionTimeline(events) {
|
|
|
603
721
|
|
|
604
722
|
for (let eventIndex = 0; eventIndex < events.length; eventIndex += 1) {
|
|
605
723
|
const event = events[eventIndex]
|
|
724
|
+
if (pruneReplacementWindow !== undefined
|
|
725
|
+
&& event?.type !== 'compaction/prune'
|
|
726
|
+
&& eventIndex !== pruneReplacementWindow.lastEventIndex + 1) {
|
|
727
|
+
pruneReplacementWindow = undefined
|
|
728
|
+
}
|
|
606
729
|
if (event?.type === 'turn/start') {
|
|
607
730
|
resetTurn(true)
|
|
608
731
|
continue
|
|
@@ -612,7 +735,23 @@ export function foldSessionTimeline(events) {
|
|
|
612
735
|
continue
|
|
613
736
|
}
|
|
614
737
|
if (event?.type === RECOVERY_BOUNDARY_EVENT) {
|
|
615
|
-
|
|
738
|
+
state.unavailableResultSeq ??= Number.isSafeInteger(event.seq) ? event.seq : eventIndex
|
|
739
|
+
continue
|
|
740
|
+
}
|
|
741
|
+
if (event?.type === 'compaction/prune' && Array.isArray(event.data?.shadowedSeqs)) {
|
|
742
|
+
const shadowedSeqs = event.data.shadowedSeqs
|
|
743
|
+
const seqs = new Set(shadowedSeqs)
|
|
744
|
+
const shadowedRange = event.data.shadowedRange
|
|
745
|
+
const rangeValid = shadowedRange === undefined
|
|
746
|
+
|| (isRecord(shadowedRange)
|
|
747
|
+
&& shadowedRange.start === shadowedSeqs[0]
|
|
748
|
+
&& shadowedRange.end === shadowedSeqs[shadowedSeqs.length - 1])
|
|
749
|
+
const valid = shadowedSeqs.length > 0
|
|
750
|
+
&& seqs.size === shadowedSeqs.length
|
|
751
|
+
&& shadowedSeqs.every(seq => Number.isSafeInteger(seq) && seq >= 0)
|
|
752
|
+
&& shadowedSeqs.every((seq, index) => index === 0 || seq > shadowedSeqs[index - 1])
|
|
753
|
+
&& rangeValid
|
|
754
|
+
pruneReplacementWindow = { seqs: valid ? seqs : new Set(), lastEventIndex: eventIndex }
|
|
616
755
|
continue
|
|
617
756
|
}
|
|
618
757
|
if (event?.type === 'tool/call' && typeof event.data?.callId === 'string') {
|
|
@@ -652,6 +791,31 @@ export function foldSessionTimeline(events) {
|
|
|
652
791
|
const sourceSeq = event.sourceEventSeqs?.[0]
|
|
653
792
|
const callId = event.data?.message?.source?.callId
|
|
654
793
|
let entry = Number.isSafeInteger(sourceSeq) ? state.executableCalls.get(sourceSeq) : undefined
|
|
794
|
+
let prunedReplacement = false
|
|
795
|
+
if (entry === undefined && Number.isSafeInteger(sourceSeq)
|
|
796
|
+
&& pruneReplacementWindow?.seqs.has(sourceSeq)
|
|
797
|
+
&& eventIndex === pruneReplacementWindow.lastEventIndex + 1
|
|
798
|
+
&& Array.isArray(event.sourceEventSeqs) && event.sourceEventSeqs.length === 1
|
|
799
|
+
&& typeof callId === 'string') {
|
|
800
|
+
const candidate = pendingByCallId.get(callId)
|
|
801
|
+
const candidateSeq = candidate?.event?.seq
|
|
802
|
+
const pruneSeq = events[pruneReplacementWindow.lastEventIndex]?.seq
|
|
803
|
+
const resultEventSeq = event.seq
|
|
804
|
+
const surfaceOp = event.surfaceOp
|
|
805
|
+
if (candidate !== undefined && candidate !== null
|
|
806
|
+
&& (candidate.event.data.name === 'run_code' || candidate.event.data.name === 'edit_run_code')
|
|
807
|
+
&& Number.isSafeInteger(candidateSeq) && candidateSeq >= 0
|
|
808
|
+
&& sourceSeq > candidateSeq
|
|
809
|
+
&& Number.isSafeInteger(pruneSeq) && pruneSeq >= 0
|
|
810
|
+
&& Number.isSafeInteger(resultEventSeq) && resultEventSeq - pruneSeq === 1
|
|
811
|
+
&& sourceSeq < pruneSeq
|
|
812
|
+
&& (surfaceOp === undefined
|
|
813
|
+
|| (surfaceOp?.op === 'replace' && surfaceOp.start === sourceSeq && surfaceOp.end === sourceSeq))) {
|
|
814
|
+
entry = candidate
|
|
815
|
+
prunedReplacement = true
|
|
816
|
+
pruneReplacementWindow.lastEventIndex = eventIndex
|
|
817
|
+
}
|
|
818
|
+
}
|
|
655
819
|
if (entry === undefined && !Number.isSafeInteger(sourceSeq) && typeof callId === 'string') {
|
|
656
820
|
entry = pendingByCallId.get(callId)
|
|
657
821
|
}
|
|
@@ -668,23 +832,38 @@ export function foldSessionTimeline(events) {
|
|
|
668
832
|
let normalized
|
|
669
833
|
if (isRecord(meta) && Object.hasOwn(meta, JOURNAL_KEY)) {
|
|
670
834
|
if (Number.isSafeInteger(sourceSeq) && journalResultSeqs.has(sourceSeq)) {
|
|
671
|
-
|
|
835
|
+
state.unavailableResultSeq ??= sourceSeq
|
|
836
|
+
state.results.set(sourceSeq, {
|
|
837
|
+
eventSeq: Number.isSafeInteger(event.seq) && event.seq >= 0 ? event.seq : sourceSeq,
|
|
838
|
+
eventIndex,
|
|
839
|
+
error: `session log contains duplicate PTC journal results for call seq ${sourceSeq}`,
|
|
840
|
+
})
|
|
841
|
+
state.found = true
|
|
842
|
+
continue
|
|
672
843
|
}
|
|
673
844
|
if (Number.isSafeInteger(sourceSeq)) journalResultSeqs.add(sourceSeq)
|
|
674
845
|
if (entry === undefined) {
|
|
675
|
-
if (Number.isSafeInteger(sourceSeq))
|
|
846
|
+
if (Number.isSafeInteger(sourceSeq)) {
|
|
847
|
+
state.unavailableResultSeq ??= sourceSeq
|
|
848
|
+
}
|
|
676
849
|
continue
|
|
677
850
|
}
|
|
678
|
-
const resultSeq =
|
|
851
|
+
const resultSeq = prunedReplacement
|
|
852
|
+
? callSeq
|
|
853
|
+
: Number.isSafeInteger(sourceSeq) ? sourceSeq : callSeq
|
|
679
854
|
const raw = {
|
|
680
855
|
meta,
|
|
681
856
|
eventSeq: Number.isSafeInteger(event.seq) && event.seq >= 0 ? event.seq : resultSeq,
|
|
682
857
|
eventIndex,
|
|
683
858
|
}
|
|
684
859
|
if (Object.hasOwn(meta, RECOVERY_BOUNDARY_KEY)) {
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
860
|
+
try {
|
|
861
|
+
state.boundaries.push(...normalizeRecoveryBoundaries(
|
|
862
|
+
meta[RECOVERY_BOUNDARY_KEY], raw.eventSeq,
|
|
863
|
+
))
|
|
864
|
+
} catch {
|
|
865
|
+
state.unavailableResultSeq ??= callSeq
|
|
866
|
+
}
|
|
688
867
|
}
|
|
689
868
|
try {
|
|
690
869
|
if (call.data.name === 'edit_run_code') {
|
|
@@ -749,57 +928,98 @@ export function recoverJournal(session, currentCallSeq, options = {}) {
|
|
|
749
928
|
const timeline = foldSessionTimeline(events)
|
|
750
929
|
const calls = timeline.calls.filter(call => call.seq !== currentCallSeq)
|
|
751
930
|
const { executableCalls, results, found } = timeline
|
|
931
|
+
const visibleCalls = options?.visibleCallSeqs
|
|
932
|
+
let unavailableSeq = timeline.unavailableResultSeq
|
|
933
|
+
let unavailableReason = 'unavailable or malformed dsh-ptc-plus journal result'
|
|
934
|
+
for (const [resultSeq, result] of results.entries()) {
|
|
935
|
+
if (result?.error !== undefined && Number.isSafeInteger(resultSeq)) {
|
|
936
|
+
unavailableSeq ??= resultSeq
|
|
937
|
+
unavailableReason = result.error
|
|
938
|
+
}
|
|
939
|
+
}
|
|
752
940
|
const extraBoundaries = options?.extraBoundaries ?? []
|
|
941
|
+
let normalizedExtraBoundaries = []
|
|
942
|
+
try {
|
|
943
|
+
normalizedExtraBoundaries = normalizeRecoveryBoundaries(extraBoundaries, undefined)
|
|
944
|
+
} catch {
|
|
945
|
+
normalizedExtraBoundaries = []
|
|
946
|
+
}
|
|
753
947
|
const boundaries = [
|
|
754
948
|
...timeline.boundaries,
|
|
755
|
-
...
|
|
949
|
+
...normalizedExtraBoundaries.map(boundary => ({
|
|
756
950
|
...boundary,
|
|
757
951
|
eventSeq: Number.POSITIVE_INFINITY,
|
|
758
952
|
})),
|
|
759
953
|
].sort((left, right) => left.eventSeq - right.eventSeq)
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
}
|
|
954
|
+
// An unavailable historical result contracts the replay frontier. It must
|
|
955
|
+
// not prevent the current request from executing on the verified prefix.
|
|
763
956
|
|
|
764
957
|
const confirmedNoops = new Set()
|
|
765
|
-
|
|
958
|
+
const invalidResultReasons = new Map()
|
|
959
|
+
for (const [resultSeq, { journal, eventIndex }] of results.entries()) {
|
|
960
|
+
const candidateConfirms = []
|
|
766
961
|
for (const callSeq of journal?.confirms ?? []) {
|
|
767
962
|
const confirmed = executableCalls.get(callSeq)
|
|
768
963
|
if (confirmed === undefined || confirmed.eventIndex >= eventIndex || results.has(callSeq)) {
|
|
769
|
-
|
|
964
|
+
const reason = `confirmed no-op does not identify an earlier unjournaled run_code call seq ${callSeq}`
|
|
965
|
+
invalidResultReasons.set(resultSeq, reason)
|
|
966
|
+
unavailableSeq ??= resultSeq
|
|
967
|
+
unavailableReason = reason
|
|
968
|
+
break
|
|
770
969
|
}
|
|
771
|
-
|
|
970
|
+
candidateConfirms.push(callSeq)
|
|
971
|
+
}
|
|
972
|
+
if (!invalidResultReasons.has(resultSeq)) {
|
|
973
|
+
for (const callSeq of candidateConfirms) confirmedNoops.add(callSeq)
|
|
772
974
|
}
|
|
773
975
|
}
|
|
774
976
|
const records = []
|
|
775
977
|
const orderedRecords = []
|
|
776
978
|
const invalidCallSeqs = new Set()
|
|
777
|
-
const appliedBoundaries = []
|
|
778
979
|
let state = foldRecords(records, invalidCallSeqs)
|
|
980
|
+
let blockedUnavailableSeq = unavailableSeq
|
|
779
981
|
let boundaryIndex = 0
|
|
780
982
|
const applyBoundariesBefore = (seq) => {
|
|
781
983
|
while (boundaryIndex < boundaries.length && boundaries[boundaryIndex].eventSeq <= seq) {
|
|
782
984
|
const boundary = boundaries[boundaryIndex++]
|
|
985
|
+
const failedCall = executableCalls.get(boundary.failedCallSeq)
|
|
986
|
+
if (failedCall === undefined || failedCall.event.seq >= boundary.eventSeq) continue
|
|
783
987
|
const failedIndex = state.nodes.findIndex(node => node.callSeq === boundary.failedCallSeq)
|
|
784
|
-
|
|
785
|
-
const expectedFrontier = state.nodes[failedIndex].parent
|
|
988
|
+
const expectedFrontier = failedIndex < 0 ? state.head : state.nodes[failedIndex].parent
|
|
786
989
|
const frontierIndex = boundary.frontierCallSeq === null
|
|
787
990
|
? undefined
|
|
788
991
|
: state.nodes.findIndex(node => node.callSeq === boundary.frontierCallSeq)
|
|
789
|
-
if (frontierIndex !== expectedFrontier)
|
|
790
|
-
|
|
791
|
-
|
|
992
|
+
if (frontierIndex !== expectedFrontier) continue
|
|
993
|
+
const resolvesUnavailable = blockedUnavailableSeq === boundary.failedCallSeq
|
|
994
|
+
if (failedIndex < 0 && !resolvesUnavailable) continue
|
|
792
995
|
for (let index = 0; index < state.nodes.length; index += 1) {
|
|
793
996
|
if (dependsOn(state.nodes, index, failedIndex)) invalidCallSeqs.add(state.nodes[index].callSeq)
|
|
794
997
|
}
|
|
795
|
-
|
|
796
|
-
|
|
998
|
+
if (failedIndex < 0) {
|
|
999
|
+
for (const record of records) {
|
|
1000
|
+
if (record.call.seq >= boundary.failedCallSeq) invalidCallSeqs.add(record.call.seq)
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
state = foldRecords(records, invalidCallSeqs, {
|
|
1004
|
+
surfaceContracted: resolvesUnavailable ? false : state.surfaceContracted,
|
|
1005
|
+
})
|
|
1006
|
+
if (resolvesUnavailable) {
|
|
1007
|
+
blockedUnavailableSeq = undefined
|
|
1008
|
+
if (unavailableSeq === boundary.failedCallSeq) unavailableSeq = undefined
|
|
1009
|
+
}
|
|
797
1010
|
}
|
|
798
1011
|
}
|
|
799
1012
|
for (const call of calls) {
|
|
800
1013
|
const result = results.get(call.seq)
|
|
801
1014
|
const code = call.data?.name === 'edit_run_code' ? result?.derived?.code : sourceForRunCall(call)
|
|
802
|
-
const record = {
|
|
1015
|
+
const record = {
|
|
1016
|
+
call,
|
|
1017
|
+
code,
|
|
1018
|
+
result: invalidResultReasons.has(call.seq)
|
|
1019
|
+
? { ...result, journal: undefined, error: invalidResultReasons.get(call.seq) }
|
|
1020
|
+
: result,
|
|
1021
|
+
hidden: visibleCalls instanceof Set && !visibleCalls.has(call.seq),
|
|
1022
|
+
}
|
|
803
1023
|
if (call.data?.name === 'edit_run_code' && record.code === undefined && result === undefined) continue
|
|
804
1024
|
if (result?.journal === undefined && confirmedNoops.has(call.seq)) continue
|
|
805
1025
|
orderedRecords.push(record)
|
|
@@ -810,15 +1030,62 @@ export function recoverJournal(session, currentCallSeq, options = {}) {
|
|
|
810
1030
|
for (const record of orderedRecords) {
|
|
811
1031
|
applyBoundariesBefore(recordEventSeq(record))
|
|
812
1032
|
records.push(record)
|
|
813
|
-
|
|
1033
|
+
if (record.hidden) {
|
|
1034
|
+
blockedUnavailableSeq ??= record.call.seq
|
|
1035
|
+
unavailableSeq ??= record.call.seq
|
|
1036
|
+
unavailableReason = 'model-visible provenance was shadowed'
|
|
1037
|
+
invalidCallSeqs.add(record.call.seq)
|
|
1038
|
+
state.surfaceContracted = true
|
|
1039
|
+
state.trusted = false
|
|
1040
|
+
state.volatileSuffix.push({ seq: record.call.seq, code: undefined, reason: unavailableReason })
|
|
1041
|
+
continue
|
|
1042
|
+
}
|
|
1043
|
+
if (blockedUnavailableSeq !== undefined && record.call.seq >= blockedUnavailableSeq) {
|
|
1044
|
+
invalidCallSeqs.add(record.call.seq)
|
|
1045
|
+
if (!state.volatileSuffix.some(item => item.seq === record.call.seq)) {
|
|
1046
|
+
state.trusted = false
|
|
1047
|
+
state.volatileSuffix.push({
|
|
1048
|
+
seq: record.call.seq,
|
|
1049
|
+
code: record.code,
|
|
1050
|
+
reason: unavailableReason,
|
|
1051
|
+
})
|
|
1052
|
+
}
|
|
1053
|
+
continue
|
|
1054
|
+
}
|
|
1055
|
+
if (record.result?.journal === undefined) {
|
|
1056
|
+
blockedUnavailableSeq ??= record.call.seq
|
|
1057
|
+
unavailableSeq ??= record.call.seq
|
|
1058
|
+
unavailableReason = record.result?.error ?? 'missing dsh-ptc-plus journal result'
|
|
1059
|
+
invalidCallSeqs.add(record.call.seq)
|
|
1060
|
+
state.trusted = false
|
|
1061
|
+
state.volatileSuffix.push({ seq: record.call.seq, code: record.code, reason: unavailableReason })
|
|
1062
|
+
continue
|
|
1063
|
+
}
|
|
1064
|
+
try {
|
|
1065
|
+
applyRecord(state, record, invalidCallSeqs)
|
|
1066
|
+
} catch (error) {
|
|
1067
|
+
unavailableSeq ??= record.call.seq
|
|
1068
|
+
blockedUnavailableSeq ??= record.call.seq
|
|
1069
|
+
unavailableReason = error.message
|
|
1070
|
+
invalidCallSeqs.add(record.call.seq)
|
|
1071
|
+
}
|
|
814
1072
|
}
|
|
815
1073
|
applyBoundariesBefore(Number.POSITIVE_INFINITY)
|
|
1074
|
+
if (unavailableSeq !== undefined
|
|
1075
|
+
&& !state.volatileSuffix.some(item => item.seq === unavailableSeq)) {
|
|
1076
|
+
state.trusted = false
|
|
1077
|
+
state.volatileSuffix.push({
|
|
1078
|
+
seq: unavailableSeq,
|
|
1079
|
+
code: undefined,
|
|
1080
|
+
reason: unavailableReason,
|
|
1081
|
+
})
|
|
1082
|
+
}
|
|
816
1083
|
return {
|
|
817
1084
|
nodes: state.nodes,
|
|
818
1085
|
head: state.head,
|
|
819
1086
|
checkpoints: state.checkpoints,
|
|
820
1087
|
volatileSuffix: state.volatileSuffix,
|
|
821
|
-
available: found,
|
|
1088
|
+
available: found && !state.surfaceContracted && blockedUnavailableSeq === undefined,
|
|
822
1089
|
}
|
|
823
1090
|
}
|
|
824
1091
|
|