dsh-ptc-plus 0.2.4 → 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 +177 -0
- package/README.md +85 -69
- 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-settings-en.png +0 -0
- package/assets/ptc-plus-settings-zh.png +0 -0
- package/client.js +1168 -144
- 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 +19 -4
- package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +2 -2
- package/docs/adr/0021-separate-host-bootstrap-from-session-runtime.md +5 -3
- package/docs/adr/0022-stage-npm-releases-with-oidc.md +2 -2
- package/docs/architecture.md +23 -14
- package/docs/client-ui.md +29 -11
- package/docs/durability-design.md +42 -20
- package/docs/installation.md +11 -3
- package/docs/program-data-plane.md +6 -0
- package/docs/runtime-reference.md +27 -14
- package/docs/value-wire.md +1 -1
- package/index.js +38 -4
- package/internal/cell-analysis.js +89 -12
- package/internal/cell-rewriter.js +162 -4
- package/internal/config-spec.js +192 -117
- package/internal/direct-surface-owner.js +56 -5
- package/internal/edit-transport-owner.js +11 -2
- package/internal/failure-reporting.js +15 -0
- package/internal/kernel-worker.js +11 -0
- package/internal/rejected-cell-editor.js +21 -12
- package/internal/repl-convenience.js +94 -6
- package/internal/repl-memory-projection.js +277 -0
- package/internal/repl-rewrite-contract.js +15 -0
- package/internal/run-code-description.js +34 -0
- package/internal/runtime-bridge-owner.js +63 -15
- package/internal/runtime-config.js +3 -0
- package/internal/session-cell-executor.js +74 -14
- package/internal/session-events.js +7 -0
- package/internal/session-journal.js +331 -63
- package/internal/session-log-view.js +2 -1
- package/internal/session-runtime.js +110 -9
- package/internal/session-state.js +116 -4
- package/package.json +10 -7
- package/src/client-activity.js +501 -0
- package/src/client.js +456 -20
- package/README.zh.md +0 -161
- /package/assets/{ptc-plus-repair.png → ptc-plus-repair-zh.png} +0 -0
|
@@ -1,22 +1,35 @@
|
|
|
1
1
|
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
|
+
import { sessionEvents } from './session-events.js'
|
|
5
|
+
import {
|
|
6
|
+
LEGACY_DEFAULT_EXPORT_BINDING,
|
|
7
|
+
LIVE_DEFAULT_EXPORT_BINDING,
|
|
8
|
+
} from './repl-rewrite-contract.js'
|
|
4
9
|
|
|
5
10
|
export const JOURNAL_KEY = 'dshPtcPlus'
|
|
6
11
|
export const EDIT_TARGET_KEY = 'dshPtcPlusEdit'
|
|
7
12
|
export const DERIVED_RUN_KEY = 'dshPtcPlusDerivedRun'
|
|
8
13
|
export const REWRITES_KEY = 'dshPtcPlusRewrites'
|
|
9
14
|
export const RECOVERY_BOUNDARY_KEY = 'dshPtcPlusRecoveryBoundaries'
|
|
10
|
-
export const JOURNAL_VERSION =
|
|
15
|
+
export const JOURNAL_VERSION = 4
|
|
11
16
|
const LEGACY_JOURNAL_VERSION = 1
|
|
12
17
|
const INTERMEDIATE_JOURNAL_VERSION = 2
|
|
18
|
+
const PREVIOUS_JOURNAL_VERSION = 3
|
|
13
19
|
export const RECOVERY_BOUNDARY_EVENT = 'ptc-plus/recovery-boundary'
|
|
14
20
|
|
|
15
21
|
const STATUSES = new Set(['durable', 'volatile', 'discarded', 'noop'])
|
|
16
22
|
const BINDING_MODES = new Set(['loose', 'strict'])
|
|
17
|
-
const JOURNAL_FIELDS = new Set(['version', '
|
|
18
|
-
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'])
|
|
19
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
|
+
])
|
|
20
33
|
const CALL_SUCCESS_FIELDS = new Set(['global', 'member', 'args', 'ok', 'value', 'settle'])
|
|
21
34
|
const CALL_ERROR_FIELDS = new Set(['global', 'member', 'args', 'ok', 'error', 'settle'])
|
|
22
35
|
const OPERATION_FIELDS = new Set(['action', 'name'])
|
|
@@ -166,28 +179,64 @@ function normalizeRewritePolicy(value) {
|
|
|
166
179
|
})
|
|
167
180
|
}
|
|
168
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
|
+
|
|
169
203
|
function migrateJournal(value, resolveLegacyConfirm) {
|
|
170
|
-
if (value.version
|
|
171
|
-
|
|
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
|
|
172
213
|
return {
|
|
173
|
-
...
|
|
214
|
+
...rest,
|
|
174
215
|
version: JOURNAL_VERSION,
|
|
175
|
-
|
|
176
|
-
|
|
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,
|
|
177
225
|
}
|
|
178
226
|
}
|
|
179
227
|
|
|
180
228
|
/** Validate and detach one journal emitted by the runtime. */
|
|
181
229
|
export function normalizeJournal(value, options = {}) {
|
|
182
230
|
if (!isRecord(value)) throw new Error('invalid dsh-ptc-plus journal')
|
|
183
|
-
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)
|
|
184
232
|
|| !STATUSES.has(value.status)) {
|
|
185
233
|
throw new Error('invalid dsh-ptc-plus journal')
|
|
186
234
|
}
|
|
187
235
|
const migrated = migrateJournal(value, options.resolveLegacyConfirm)
|
|
188
236
|
assertOwnFields(migrated, JOURNAL_FIELDS, 'dsh-ptc-plus journal')
|
|
189
|
-
|
|
237
|
+
const bindingPolicy = normalizeBindingPolicy(migrated.bindingPolicy)
|
|
190
238
|
const rewritePolicy = normalizeRewritePolicy(migrated.rewritePolicy)
|
|
239
|
+
const moduleSemantics = normalizeModuleSemantics(migrated.moduleSemantics)
|
|
191
240
|
const calls = normalizeCalls(migrated.calls)
|
|
192
241
|
const operations = normalizeOperations(migrated.operations)
|
|
193
242
|
const confirms = normalizeConfirms(migrated.confirms)
|
|
@@ -208,8 +257,9 @@ export function normalizeJournal(value, options = {}) {
|
|
|
208
257
|
}
|
|
209
258
|
return Object.freeze({
|
|
210
259
|
version: JOURNAL_VERSION,
|
|
211
|
-
|
|
260
|
+
bindingPolicy,
|
|
212
261
|
rewritePolicy,
|
|
262
|
+
moduleSemantics,
|
|
213
263
|
status: migrated.status,
|
|
214
264
|
calls: Object.freeze(calls),
|
|
215
265
|
operations: Object.freeze(operations),
|
|
@@ -365,7 +415,7 @@ export function migrateRecoveryBoundaryEvents(events) {
|
|
|
365
415
|
|
|
366
416
|
/** Resolve the persisted event identity for one named tool call being dispatched. */
|
|
367
417
|
export function liveToolCallSeq(session, callId, toolName) {
|
|
368
|
-
const events = session
|
|
418
|
+
const events = sessionEvents(session)
|
|
369
419
|
if (!Array.isArray(events) || typeof callId !== 'string' || callId.length === 0
|
|
370
420
|
|| typeof toolName !== 'string' || toolName.length === 0) return undefined
|
|
371
421
|
|
|
@@ -393,12 +443,21 @@ export function liveToolCallSeq(session, callId, toolName) {
|
|
|
393
443
|
}
|
|
394
444
|
|
|
395
445
|
/** Start a mutable journal for one live cell. */
|
|
396
|
-
export function createJournal(confirms = [],
|
|
397
|
-
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
|
+
}
|
|
398
454
|
return {
|
|
399
455
|
version: JOURNAL_VERSION,
|
|
400
|
-
|
|
456
|
+
bindingPolicy: normalizeBindingPolicy(bindingPolicy),
|
|
401
457
|
rewritePolicy: normalizeRewritePolicy(rewritePolicy),
|
|
458
|
+
moduleSemantics: normalizeModuleSemantics({
|
|
459
|
+
defaultExportBinding: LIVE_DEFAULT_EXPORT_BINDING,
|
|
460
|
+
}),
|
|
402
461
|
calls: [],
|
|
403
462
|
operations: [],
|
|
404
463
|
confirms: [...confirms],
|
|
@@ -455,11 +514,6 @@ function applyOperations(state, operations, nodeIndex) {
|
|
|
455
514
|
function applyRecord(state, record, invalidCallSeqs) {
|
|
456
515
|
const { call, code, result } = record
|
|
457
516
|
if (invalidCallSeqs.has(call.seq)) return
|
|
458
|
-
if (code === undefined || result?.journal === undefined) {
|
|
459
|
-
state.trusted = false
|
|
460
|
-
state.volatileSuffix.push({ seq: call.seq, code, reason: result?.error ?? 'missing dsh-ptc-plus journal result' })
|
|
461
|
-
return
|
|
462
|
-
}
|
|
463
517
|
const journal = result.journal
|
|
464
518
|
if (journal.status === 'noop') return
|
|
465
519
|
if (journal.status === 'discarded') {
|
|
@@ -475,7 +529,7 @@ function applyRecord(state, record, invalidCallSeqs) {
|
|
|
475
529
|
applyOperations(state, journal.operations, undefined)
|
|
476
530
|
return
|
|
477
531
|
}
|
|
478
|
-
if (!state.trusted) {
|
|
532
|
+
if (!state.trusted && !state.surfaceContracted) {
|
|
479
533
|
state.trusted = true
|
|
480
534
|
state.volatileSuffix.length = 0
|
|
481
535
|
}
|
|
@@ -487,38 +541,31 @@ function applyRecord(state, record, invalidCallSeqs) {
|
|
|
487
541
|
})
|
|
488
542
|
const index = state.nodes.push(node) - 1
|
|
489
543
|
state.head = index
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
if (state.head === -1) throw new Error('recovery boundary frontier is not reconstructable')
|
|
498
|
-
state.trusted = true
|
|
499
|
-
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
|
+
}
|
|
500
551
|
}
|
|
501
552
|
|
|
502
553
|
function recordEventSeq(record) {
|
|
503
554
|
return record.result?.eventSeq ?? record.call.seq
|
|
504
555
|
}
|
|
505
556
|
|
|
506
|
-
function foldRecords(records, invalidCallSeqs,
|
|
557
|
+
function foldRecords(records, invalidCallSeqs, options = {}) {
|
|
507
558
|
const state = {
|
|
508
559
|
nodes: [],
|
|
509
560
|
head: undefined,
|
|
510
561
|
checkpoints: new Map(),
|
|
511
562
|
volatileSuffix: [],
|
|
512
563
|
trusted: true,
|
|
564
|
+
surfaceContracted: options.surfaceContracted === true,
|
|
513
565
|
}
|
|
514
|
-
let boundaryIndex = 0
|
|
515
566
|
for (const record of records) {
|
|
516
|
-
while (boundaryIndex < boundaries.length && boundaries[boundaryIndex].eventSeq < recordEventSeq(record)) {
|
|
517
|
-
forceRecoveryHead(state, boundaries[boundaryIndex++])
|
|
518
|
-
}
|
|
519
567
|
applyRecord(state, record, invalidCallSeqs)
|
|
520
568
|
}
|
|
521
|
-
while (boundaryIndex < boundaries.length) forceRecoveryHead(state, boundaries[boundaryIndex++])
|
|
522
569
|
return state
|
|
523
570
|
}
|
|
524
571
|
|
|
@@ -529,6 +576,77 @@ function dependsOn(nodes, index, ancestor) {
|
|
|
529
576
|
return false
|
|
530
577
|
}
|
|
531
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
|
+
|
|
532
650
|
function timelineRun(call, result, eventIndex, journal) {
|
|
533
651
|
let args
|
|
534
652
|
try {
|
|
@@ -588,6 +706,7 @@ export function foldSessionTimeline(events) {
|
|
|
588
706
|
const claimedEditTargets = new Set()
|
|
589
707
|
const editClaims = new Map()
|
|
590
708
|
const journalResultSeqs = new Set()
|
|
709
|
+
let pruneReplacementWindow
|
|
591
710
|
let scope = 0
|
|
592
711
|
|
|
593
712
|
const resetTurn = (openTurn) => {
|
|
@@ -602,6 +721,11 @@ export function foldSessionTimeline(events) {
|
|
|
602
721
|
|
|
603
722
|
for (let eventIndex = 0; eventIndex < events.length; eventIndex += 1) {
|
|
604
723
|
const event = events[eventIndex]
|
|
724
|
+
if (pruneReplacementWindow !== undefined
|
|
725
|
+
&& event?.type !== 'compaction/prune'
|
|
726
|
+
&& eventIndex !== pruneReplacementWindow.lastEventIndex + 1) {
|
|
727
|
+
pruneReplacementWindow = undefined
|
|
728
|
+
}
|
|
605
729
|
if (event?.type === 'turn/start') {
|
|
606
730
|
resetTurn(true)
|
|
607
731
|
continue
|
|
@@ -611,7 +735,23 @@ export function foldSessionTimeline(events) {
|
|
|
611
735
|
continue
|
|
612
736
|
}
|
|
613
737
|
if (event?.type === RECOVERY_BOUNDARY_EVENT) {
|
|
614
|
-
|
|
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 }
|
|
615
755
|
continue
|
|
616
756
|
}
|
|
617
757
|
if (event?.type === 'tool/call' && typeof event.data?.callId === 'string') {
|
|
@@ -651,6 +791,31 @@ export function foldSessionTimeline(events) {
|
|
|
651
791
|
const sourceSeq = event.sourceEventSeqs?.[0]
|
|
652
792
|
const callId = event.data?.message?.source?.callId
|
|
653
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
|
+
}
|
|
654
819
|
if (entry === undefined && !Number.isSafeInteger(sourceSeq) && typeof callId === 'string') {
|
|
655
820
|
entry = pendingByCallId.get(callId)
|
|
656
821
|
}
|
|
@@ -667,23 +832,38 @@ export function foldSessionTimeline(events) {
|
|
|
667
832
|
let normalized
|
|
668
833
|
if (isRecord(meta) && Object.hasOwn(meta, JOURNAL_KEY)) {
|
|
669
834
|
if (Number.isSafeInteger(sourceSeq) && journalResultSeqs.has(sourceSeq)) {
|
|
670
|
-
|
|
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
|
|
671
843
|
}
|
|
672
844
|
if (Number.isSafeInteger(sourceSeq)) journalResultSeqs.add(sourceSeq)
|
|
673
845
|
if (entry === undefined) {
|
|
674
|
-
if (Number.isSafeInteger(sourceSeq))
|
|
846
|
+
if (Number.isSafeInteger(sourceSeq)) {
|
|
847
|
+
state.unavailableResultSeq ??= sourceSeq
|
|
848
|
+
}
|
|
675
849
|
continue
|
|
676
850
|
}
|
|
677
|
-
const resultSeq =
|
|
851
|
+
const resultSeq = prunedReplacement
|
|
852
|
+
? callSeq
|
|
853
|
+
: Number.isSafeInteger(sourceSeq) ? sourceSeq : callSeq
|
|
678
854
|
const raw = {
|
|
679
855
|
meta,
|
|
680
856
|
eventSeq: Number.isSafeInteger(event.seq) && event.seq >= 0 ? event.seq : resultSeq,
|
|
681
857
|
eventIndex,
|
|
682
858
|
}
|
|
683
859
|
if (Object.hasOwn(meta, RECOVERY_BOUNDARY_KEY)) {
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
860
|
+
try {
|
|
861
|
+
state.boundaries.push(...normalizeRecoveryBoundaries(
|
|
862
|
+
meta[RECOVERY_BOUNDARY_KEY], raw.eventSeq,
|
|
863
|
+
))
|
|
864
|
+
} catch {
|
|
865
|
+
state.unavailableResultSeq ??= callSeq
|
|
866
|
+
}
|
|
687
867
|
}
|
|
688
868
|
try {
|
|
689
869
|
if (call.data.name === 'edit_run_code') {
|
|
@@ -741,64 +921,105 @@ export function foldSessionTimeline(events) {
|
|
|
741
921
|
|
|
742
922
|
/** Fold the session log into the last exactly replayable frontier. */
|
|
743
923
|
export function recoverJournal(session, currentCallSeq, options = {}) {
|
|
744
|
-
const events = session
|
|
924
|
+
const events = sessionEvents(session)
|
|
745
925
|
if (!Array.isArray(events)) {
|
|
746
926
|
return { nodes: [], head: undefined, checkpoints: new Map(), volatileSuffix: [], available: true }
|
|
747
927
|
}
|
|
748
928
|
const timeline = foldSessionTimeline(events)
|
|
749
929
|
const calls = timeline.calls.filter(call => call.seq !== currentCallSeq)
|
|
750
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
|
+
}
|
|
751
940
|
const extraBoundaries = options?.extraBoundaries ?? []
|
|
941
|
+
let normalizedExtraBoundaries = []
|
|
942
|
+
try {
|
|
943
|
+
normalizedExtraBoundaries = normalizeRecoveryBoundaries(extraBoundaries, undefined)
|
|
944
|
+
} catch {
|
|
945
|
+
normalizedExtraBoundaries = []
|
|
946
|
+
}
|
|
752
947
|
const boundaries = [
|
|
753
948
|
...timeline.boundaries,
|
|
754
|
-
...
|
|
949
|
+
...normalizedExtraBoundaries.map(boundary => ({
|
|
755
950
|
...boundary,
|
|
756
951
|
eventSeq: Number.POSITIVE_INFINITY,
|
|
757
952
|
})),
|
|
758
953
|
].sort((left, right) => left.eventSeq - right.eventSeq)
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
}
|
|
954
|
+
// An unavailable historical result contracts the replay frontier. It must
|
|
955
|
+
// not prevent the current request from executing on the verified prefix.
|
|
762
956
|
|
|
763
957
|
const confirmedNoops = new Set()
|
|
764
|
-
|
|
958
|
+
const invalidResultReasons = new Map()
|
|
959
|
+
for (const [resultSeq, { journal, eventIndex }] of results.entries()) {
|
|
960
|
+
const candidateConfirms = []
|
|
765
961
|
for (const callSeq of journal?.confirms ?? []) {
|
|
766
962
|
const confirmed = executableCalls.get(callSeq)
|
|
767
963
|
if (confirmed === undefined || confirmed.eventIndex >= eventIndex || results.has(callSeq)) {
|
|
768
|
-
|
|
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
|
|
769
969
|
}
|
|
770
|
-
|
|
970
|
+
candidateConfirms.push(callSeq)
|
|
971
|
+
}
|
|
972
|
+
if (!invalidResultReasons.has(resultSeq)) {
|
|
973
|
+
for (const callSeq of candidateConfirms) confirmedNoops.add(callSeq)
|
|
771
974
|
}
|
|
772
975
|
}
|
|
773
976
|
const records = []
|
|
774
977
|
const orderedRecords = []
|
|
775
978
|
const invalidCallSeqs = new Set()
|
|
776
|
-
const appliedBoundaries = []
|
|
777
979
|
let state = foldRecords(records, invalidCallSeqs)
|
|
980
|
+
let blockedUnavailableSeq = unavailableSeq
|
|
778
981
|
let boundaryIndex = 0
|
|
779
982
|
const applyBoundariesBefore = (seq) => {
|
|
780
983
|
while (boundaryIndex < boundaries.length && boundaries[boundaryIndex].eventSeq <= seq) {
|
|
781
984
|
const boundary = boundaries[boundaryIndex++]
|
|
985
|
+
const failedCall = executableCalls.get(boundary.failedCallSeq)
|
|
986
|
+
if (failedCall === undefined || failedCall.event.seq >= boundary.eventSeq) continue
|
|
782
987
|
const failedIndex = state.nodes.findIndex(node => node.callSeq === boundary.failedCallSeq)
|
|
783
|
-
|
|
784
|
-
const expectedFrontier = state.nodes[failedIndex].parent
|
|
988
|
+
const expectedFrontier = failedIndex < 0 ? state.head : state.nodes[failedIndex].parent
|
|
785
989
|
const frontierIndex = boundary.frontierCallSeq === null
|
|
786
990
|
? undefined
|
|
787
991
|
: state.nodes.findIndex(node => node.callSeq === boundary.frontierCallSeq)
|
|
788
|
-
if (frontierIndex !== expectedFrontier)
|
|
789
|
-
|
|
790
|
-
|
|
992
|
+
if (frontierIndex !== expectedFrontier) continue
|
|
993
|
+
const resolvesUnavailable = blockedUnavailableSeq === boundary.failedCallSeq
|
|
994
|
+
if (failedIndex < 0 && !resolvesUnavailable) continue
|
|
791
995
|
for (let index = 0; index < state.nodes.length; index += 1) {
|
|
792
996
|
if (dependsOn(state.nodes, index, failedIndex)) invalidCallSeqs.add(state.nodes[index].callSeq)
|
|
793
997
|
}
|
|
794
|
-
|
|
795
|
-
|
|
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
|
+
}
|
|
796
1010
|
}
|
|
797
1011
|
}
|
|
798
1012
|
for (const call of calls) {
|
|
799
1013
|
const result = results.get(call.seq)
|
|
800
1014
|
const code = call.data?.name === 'edit_run_code' ? result?.derived?.code : sourceForRunCall(call)
|
|
801
|
-
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
|
+
}
|
|
802
1023
|
if (call.data?.name === 'edit_run_code' && record.code === undefined && result === undefined) continue
|
|
803
1024
|
if (result?.journal === undefined && confirmedNoops.has(call.seq)) continue
|
|
804
1025
|
orderedRecords.push(record)
|
|
@@ -809,15 +1030,62 @@ export function recoverJournal(session, currentCallSeq, options = {}) {
|
|
|
809
1030
|
for (const record of orderedRecords) {
|
|
810
1031
|
applyBoundariesBefore(recordEventSeq(record))
|
|
811
1032
|
records.push(record)
|
|
812
|
-
|
|
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
|
+
}
|
|
813
1072
|
}
|
|
814
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
|
+
}
|
|
815
1083
|
return {
|
|
816
1084
|
nodes: state.nodes,
|
|
817
1085
|
head: state.head,
|
|
818
1086
|
checkpoints: state.checkpoints,
|
|
819
1087
|
volatileSuffix: state.volatileSuffix,
|
|
820
|
-
available: found,
|
|
1088
|
+
available: found && !state.surfaceContracted && blockedUnavailableSeq === undefined,
|
|
821
1089
|
}
|
|
822
1090
|
}
|
|
823
1091
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
foldSessionTimeline,
|
|
3
3
|
} from './session-journal.js'
|
|
4
|
+
import { sessionEvents } from './session-events.js'
|
|
4
5
|
|
|
5
6
|
const SYSTEM_PROMPT_PLUGIN = '@deepseek-ai/dsh-system-prompt'
|
|
6
7
|
|
|
@@ -43,7 +44,7 @@ function cordisTranscriptFacts(timeline) {
|
|
|
43
44
|
|
|
44
45
|
/** Project session events into immutable facts consumed by prompt presentation. */
|
|
45
46
|
export function projectSessionLog(agent, requestedEdit) {
|
|
46
|
-
const events = agent?.session
|
|
47
|
+
const events = sessionEvents(agent?.session)
|
|
47
48
|
if (!Array.isArray(events)) {
|
|
48
49
|
return Object.freeze({
|
|
49
50
|
openTurn: false,
|