dsh-ptc-plus 0.5.2 → 0.5.4
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 +4 -2
- package/README.md +4 -2
- package/client.cjs +3392 -2811
- package/compiler-core.cjs +127 -127
- package/compiler-core.cjs.map +2 -2
- package/docs/README.md +1 -0
- package/docs/adr/0017-track-the-latest-dsh-public-surface.md +4 -2
- package/docs/adr/0019-plugin-settings-and-kill-switch.md +2 -2
- package/docs/adr/0023-global-user-bindings.md +12 -0
- package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +1 -1
- package/docs/adr/0030-serve-both-typert-codec-generations-from-one-codec.md +2 -2
- package/docs/adr/0044-coexist-with-the-official-persistent-repl.md +4 -4
- package/docs/adr/0046-gate-the-commit-with-the-semantic-evidence-tier.md +1 -1
- package/docs/adr/0048-supersede-a-rewritten-cell.md +48 -0
- package/docs/adr/0049-prove-a-settlement-against-its-published-record.md +53 -0
- package/docs/adr/0050-read-host-surfaces-through-their-published-contract.md +48 -0
- package/docs/adr/README.md +7 -1
- package/docs/architecture.md +1 -1
- package/docs/capability-planning-components.md +6 -6
- package/docs/capability-projection.md +1 -1
- package/docs/capability-surface.md +133 -0
- package/docs/client-ui.md +17 -15
- package/docs/client-visual-contract.md +7 -7
- package/docs/durability-design.md +9 -2
- package/docs/installation.md +17 -10
- package/docs/panel-concept.md +171 -0
- package/docs/releases/v0.5.2.md +26 -14
- package/docs/releases/v0.5.3.md +77 -0
- package/docs/releases/v0.5.4.md +75 -0
- package/docs/runtime-reference.md +21 -1
- package/docs/user-bindings.en.md +15 -0
- package/docs/user-bindings.md +15 -0
- package/docs/v0.5.3-goals.md +117 -0
- package/docs/verification.md +4 -4
- package/index.js +176 -41
- package/internal/binding-adoption.js +228 -0
- package/internal/capability-command-receipt.js +106 -8
- package/internal/capability-environment.js +1109 -312
- package/internal/capability-plan.js +18 -2
- package/internal/capability-policy.js +13 -0
- package/internal/capability-selection.js +330 -0
- package/internal/config-spec.js +14 -5
- package/internal/cordis-tools-owner.js +2 -1
- package/internal/direct-surface-owner.js +44 -17
- package/internal/edit-transport-owner.js +6 -5
- package/internal/execution-provider-owner.js +6 -0
- package/internal/execution-seam-compat.js +25 -0
- package/internal/host-tool-sdk.js +308 -0
- package/internal/message-sources.js +21 -0
- package/internal/official-repl-priority.js +31 -10
- package/internal/program-bindings.js +25 -5
- package/internal/repl-memory-projection.js +21 -2
- package/internal/runtime-bridge-owner.js +118 -9
- package/internal/runtime-config.js +10 -0
- package/internal/runtime-messages.js +5 -0
- package/internal/session-journal-recovery.js +210 -32
- package/internal/session-journal-schema.js +3 -0
- package/internal/session-journal.js +46 -9
- package/internal/session-log-view.js +69 -7
- package/internal/session-runtime.js +106 -39
- package/internal/settings-compat.js +6 -0
- package/internal/user-bindings-owner.js +126 -6
- package/package.json +59 -37
- package/src/client-authoring-view.js +7 -8
- package/src/client-capabilities-view.js +874 -909
- package/src/client-capability-sort.js +22 -9
- package/src/client-copy.js +118 -272
- package/src/client-indicator-view.js +2 -1
- package/src/client-repl-view.js +35 -16
- package/src/client-settings-view.js +90 -33
- package/src/client-styles.js +67 -19
- package/src/client.js +49 -22
|
@@ -64,6 +64,9 @@ import {
|
|
|
64
64
|
USER_BINDINGS_REUSE_POLICIES,
|
|
65
65
|
RECOVERY_BOUNDARY_FIELDS,
|
|
66
66
|
DIAGNOSTIC_WORDING_KEY,
|
|
67
|
+
SETTLEMENT_PROOF_KEY,
|
|
68
|
+
SETTLEMENT_PROOF_FIELDS,
|
|
69
|
+
SETTLEMENT_PROOF_VERSION,
|
|
67
70
|
REPL_TOOL_NAMES,
|
|
68
71
|
REWRITE_FIELDS,
|
|
69
72
|
REWRITE_KINDS,
|
|
@@ -78,6 +81,9 @@ export {
|
|
|
78
81
|
RECOVERY_BOUNDARY_KEY,
|
|
79
82
|
DIAGNOSTIC_WORDING_KEY,
|
|
80
83
|
DIAGNOSTIC_WORDING_FIELDS,
|
|
84
|
+
SETTLEMENT_PROOF_KEY,
|
|
85
|
+
SETTLEMENT_PROOF_FIELDS,
|
|
86
|
+
SETTLEMENT_PROOF_VERSION,
|
|
81
87
|
JOURNAL_VERSION,
|
|
82
88
|
LIVE_USER_BINDINGS_REUSE_POLICY,
|
|
83
89
|
RECOVERY_BOUNDARY_EVENT,
|
|
@@ -129,15 +135,46 @@ export function diagnosticWordingDigest(journal) {
|
|
|
129
135
|
return digestOf(diagnostics.map(diagnosticWordingFacts))
|
|
130
136
|
}
|
|
131
137
|
|
|
132
|
-
/**
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
138
|
+
/**
|
|
139
|
+
* Digest of the one settlement a journal states: the status of its cell, its
|
|
140
|
+
* completion, and the wording of its diagnostics.
|
|
141
|
+
*
|
|
142
|
+
* A published record is the only account of its own cell, so a record edited
|
|
143
|
+
* afterwards stays self-consistent: the completion is checkable against a
|
|
144
|
+
* diagnostic, and the diagnostic against the wording digest, but all three are
|
|
145
|
+
* the same settlement. The proof is taken at publication, from the facts the
|
|
146
|
+
* settlement was published with, and every fact is represented here exactly once,
|
|
147
|
+
* so a record whose status, completion or diagnostic wording changed no longer
|
|
148
|
+
* reproduces it.
|
|
149
|
+
*/
|
|
150
|
+
export function settlementProofDigest(journal) {
|
|
151
|
+
const completion = journal?.completion
|
|
152
|
+
const completionFacts = completion === undefined ? null
|
|
153
|
+
: completion.kind === 'return'
|
|
154
|
+
? [completion.kind, completion.hasValue,
|
|
155
|
+
completion.hasValue ? digestOf(completion.value) : null]
|
|
156
|
+
: [completion.kind, completion.error.kind, completion.error.message]
|
|
157
|
+
return digestOf([
|
|
158
|
+
journal?.status ?? null,
|
|
159
|
+
completionFacts,
|
|
160
|
+
diagnosticWordingDigest(journal) ?? null,
|
|
161
|
+
])
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Attach the published settlement's proof beside a journal.
|
|
166
|
+
*
|
|
167
|
+
* Every settlement states a status, so every publication carries one; a record
|
|
168
|
+
* that carries none predates this proof and keeps the older evidence.
|
|
169
|
+
*/
|
|
170
|
+
export function withSettlementProof(meta, journal) {
|
|
171
|
+
return {
|
|
172
|
+
...meta,
|
|
173
|
+
[SETTLEMENT_PROOF_KEY]: {
|
|
174
|
+
version: SETTLEMENT_PROOF_VERSION,
|
|
175
|
+
recordedMessage: settlementProofDigest(journal),
|
|
176
|
+
},
|
|
139
177
|
}
|
|
140
|
-
return { ...meta, [DIAGNOSTIC_WORDING_KEY]: { digest } }
|
|
141
178
|
}
|
|
142
179
|
|
|
143
180
|
function cloneJson(value) {
|
|
@@ -1272,7 +1309,7 @@ export function withJournal(meta, journal) {
|
|
|
1272
1309
|
const base = isRecord(meta) ? { ...meta } : meta === undefined ? {} : { value: cloneJson(meta) }
|
|
1273
1310
|
const normalized = normalizeJournal(journal)
|
|
1274
1311
|
base[JOURNAL_KEY] = normalized
|
|
1275
|
-
return
|
|
1312
|
+
return withSettlementProof(base, normalized)
|
|
1276
1313
|
}
|
|
1277
1314
|
|
|
1278
1315
|
/** Validate and detach one rewrite record emitted by the runtime. */
|
|
@@ -9,9 +9,38 @@ import { deepEqualJson, isJsonValue } from '@deepseek-ai/dsh-util-values'
|
|
|
9
9
|
import { deepFreeze, isRecord } from './record-utils.js'
|
|
10
10
|
import { advanceRuntimeHistory, createRuntimeHistory } from './recovery-tips.js'
|
|
11
11
|
|
|
12
|
-
import { isHostRuntimeContextSource } from './message-sources.js'
|
|
12
|
+
import { isHostRuntimeContextSource, isPtcMessageSource } from './message-sources.js'
|
|
13
|
+
import { BINDING_ADOPTION_FORM, readBindingAdoption } from './binding-adoption.js'
|
|
13
14
|
const SYSTEM_PROMPT_CLEARED = 'Current runtime context: none. Earlier runtime-context snapshots no longer apply.'
|
|
14
15
|
|
|
16
|
+
/**
|
|
17
|
+
* A PTC binding-adoption statement, readable or not.
|
|
18
|
+
*
|
|
19
|
+
* The fold has to recognize the statement even when this build cannot resolve its
|
|
20
|
+
* wording: the fact it keeps is what a restored checkpoint replays, so a fact that
|
|
21
|
+
* dropped the form could not reproduce the record the fold appended.
|
|
22
|
+
*/
|
|
23
|
+
function isBindingAdoptionStatement(message) {
|
|
24
|
+
const source = message?.source
|
|
25
|
+
return isPtcMessageSource(source) && source.form === BINDING_ADOPTION_FORM
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* An adoption statement this build cannot resolve, kept as the statement it is.
|
|
30
|
+
*
|
|
31
|
+
* Both the panel and the `/binding` command answer from this projection, and the
|
|
32
|
+
* answer has to be this session's *newest* statement. A projection that dropped a
|
|
33
|
+
* statement it could not resolve would let the record the session superseded answer
|
|
34
|
+
* instead, so the statement is retained with no declined list — the absence is what
|
|
35
|
+
* marks it unreadable. It never reaches the model-visible surface, which is built
|
|
36
|
+
* from resolvable runtime messages only.
|
|
37
|
+
*/
|
|
38
|
+
function unreadableBindingAdoption(message) {
|
|
39
|
+
if (!isBindingAdoptionStatement(message)) return undefined
|
|
40
|
+
if (readBindingAdoption(message) !== undefined) return undefined
|
|
41
|
+
return Object.freeze({ form: BINDING_ADOPTION_FORM })
|
|
42
|
+
}
|
|
43
|
+
|
|
15
44
|
/** DSH's empty aggregate uses an exact owner marker without a snapshot form. */
|
|
16
45
|
export function systemPromptSnapshotSections(message) {
|
|
17
46
|
const source = message?.source
|
|
@@ -72,7 +101,8 @@ export function runtimeMessageFacts(records, initial = {}) {
|
|
|
72
101
|
|
|
73
102
|
function presentationEvent(event) {
|
|
74
103
|
const recognized = event?.type === 'user/message'
|
|
75
|
-
&& (systemPromptSnapshotSections(event.data) !== undefined || readRuntimeMessage(event.data) !== undefined
|
|
104
|
+
&& (systemPromptSnapshotSections(event.data) !== undefined || readRuntimeMessage(event.data) !== undefined
|
|
105
|
+
|| isBindingAdoptionStatement(event.data))
|
|
76
106
|
const data = recognized ? { source: event.data.source, content: event.data.content }
|
|
77
107
|
: event?.type === 'user/message' && typeof event.data?.source?.kind === 'string'
|
|
78
108
|
? { source: { kind: event.data.source.kind } } : undefined
|
|
@@ -86,11 +116,14 @@ function applyLogEvent(state, event, index = event?.seq,
|
|
|
86
116
|
timeline = advanceSessionTimeline(state.timeline, event, index)) {
|
|
87
117
|
const sections = event?.type === 'user/message' ? systemPromptSnapshotSections(event.data) : undefined
|
|
88
118
|
const ptc = event?.type === 'user/message' ? readRuntimeMessage(event.data) : undefined
|
|
119
|
+
// An adoption statement this build cannot resolve is still this session's statement,
|
|
120
|
+
// so it stays in the projection as the newest one rather than disappearing.
|
|
121
|
+
const unresolvedAdoption = ptc === undefined ? unreadableBindingAdoption(event?.data) : undefined
|
|
89
122
|
const contextStep = state.contextStep + (isContextStep(event) ? 1 : 0)
|
|
90
123
|
const surfaceNodes = advanceSessionSurface(state.surfaceNodes, event)
|
|
91
124
|
let visibleRuntimeMessages = state.visibleRuntimeMessages
|
|
92
|
-
if (timeline === state.timeline && sections === undefined && ptc === undefined &&
|
|
93
|
-
&& surfaceNodes === state.surfaceNodes) return state
|
|
125
|
+
if (timeline === state.timeline && sections === undefined && ptc === undefined && unresolvedAdoption === undefined
|
|
126
|
+
&& contextStep === state.contextStep && surfaceNodes === state.surfaceNodes) return state
|
|
94
127
|
const next = { ...state, timeline, contextStep,
|
|
95
128
|
logFacts: [...state.logFacts, deepFreeze({ index, event: presentationEvent(event) })],
|
|
96
129
|
}
|
|
@@ -110,6 +143,11 @@ function applyLogEvent(state, event, index = event?.seq,
|
|
|
110
143
|
const record = Object.freeze({ ...ptc, index, contextStep: state.contextStep, producer: 'ptc-plus' })
|
|
111
144
|
next.ptcMessages = [...state.ptcMessages, record]
|
|
112
145
|
next.runtimeMessagesBySeq = { ...next.runtimeMessagesBySeq, [`k${event.seq}`]: record }
|
|
146
|
+
} else if (unresolvedAdoption !== undefined) {
|
|
147
|
+
// Listed as this session's newest statement, but not as a resolvable runtime
|
|
148
|
+
// message: the ordered surface below is built from `runtimeMessagesBySeq`.
|
|
149
|
+
next.ptcMessages = [...state.ptcMessages,
|
|
150
|
+
Object.freeze({ ...unresolvedAdoption, index, contextStep: state.contextStep, producer: 'ptc-plus' })]
|
|
113
151
|
}
|
|
114
152
|
if (event?.surfaceOp === 'append') {
|
|
115
153
|
const record = next.runtimeMessagesBySeq[`k${event.seq}`]
|
|
@@ -133,7 +171,11 @@ export function createSessionLogProjection(options = {}) {
|
|
|
133
171
|
const replayBudget = admissionReplayBudget(options.replayBudgetBytes)
|
|
134
172
|
return Object.freeze({
|
|
135
173
|
key: SESSION_LOG_PROJECTION_KEY,
|
|
136
|
-
|
|
174
|
+
// 14 records the binding-adoption statement this build cannot resolve: the fact it
|
|
175
|
+
// keeps now carries the form its own replay needs, and `ptcMessages` gained a record
|
|
176
|
+
// for it. A checkpoint written by an earlier fold answers a different question, so
|
|
177
|
+
// the version moves and stored checkpoints are refolded rather than admitted.
|
|
178
|
+
stateVersion: 14,
|
|
137
179
|
stateSchema: Object.freeze({ parse(value) {
|
|
138
180
|
if (!isJsonValue(value) || !isRecord(value) || !isRecord(value.timeline)
|
|
139
181
|
|| typeof value.timeline.openTurn !== 'boolean' || typeof value.timeline.found !== 'boolean'
|
|
@@ -249,6 +291,24 @@ export function createSessionLogProjection(options = {}) {
|
|
|
249
291
|
})
|
|
250
292
|
}
|
|
251
293
|
|
|
294
|
+
/**
|
|
295
|
+
* The disposer a session projection registration handed back.
|
|
296
|
+
*
|
|
297
|
+
* Two registration contracts are published: an earlier host returns a disposer and a
|
|
298
|
+
* newer one returns a `ProjectionHandle` (`stateOf`/`readerFor`/`dispose`, possibly
|
|
299
|
+
* async). Every projection this plugin registers goes through this reader, because a
|
|
300
|
+
* registration that only understands the earlier shape drops the projection — and
|
|
301
|
+
* with it the fact that the host accepted it — on the newer contract.
|
|
302
|
+
* @param {unknown} registration - what `sessionProjections.register` returned.
|
|
303
|
+
* @returns {(() => unknown)|undefined} the release call, or undefined when the answer
|
|
304
|
+
* names neither contract.
|
|
305
|
+
*/
|
|
306
|
+
export function projectionRegistrationDisposer(registration) {
|
|
307
|
+
if (typeof registration === 'function') return () => registration()
|
|
308
|
+
if (typeof registration?.dispose === 'function') return () => registration.dispose()
|
|
309
|
+
return undefined
|
|
310
|
+
}
|
|
311
|
+
|
|
252
312
|
export function createSessionLogOwner(ctx) {
|
|
253
313
|
const projection = createSessionLogProjection()
|
|
254
314
|
let registeredService
|
|
@@ -256,8 +316,10 @@ export function createSessionLogOwner(ctx) {
|
|
|
256
316
|
try {
|
|
257
317
|
scope.effect(() => {
|
|
258
318
|
const registry = scope.sessionProjections
|
|
259
|
-
const unregister = registry.register(projection)
|
|
260
|
-
if (
|
|
319
|
+
const unregister = projectionRegistrationDisposer(registry.register(projection))
|
|
320
|
+
if (unregister === undefined) {
|
|
321
|
+
throw new Error('ptc-plus: sessionProjections.register returned neither a disposer nor a handle')
|
|
322
|
+
}
|
|
261
323
|
registeredService = registry
|
|
262
324
|
return async () => {
|
|
263
325
|
await unregister()
|
|
@@ -180,6 +180,34 @@ export function vanishedBindingDiagnostic(names) {
|
|
|
180
180
|
})
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
/**
|
|
184
|
+
* Notice for bindings a recorded revision superseded.
|
|
185
|
+
*
|
|
186
|
+
* A revision that rewrites its target puts the target off the recorded path, so
|
|
187
|
+
* the target's declarations stop building the environment. They are not missing:
|
|
188
|
+
* the log says which revision replaced them, and reporting that relation keeps
|
|
189
|
+
* the model from reading a later `x is not defined` as its own mistake.
|
|
190
|
+
*/
|
|
191
|
+
export function supersededDiagnostic(bindings, supersessions = []) {
|
|
192
|
+
const names = bindings.length === 0 ? '' : ` (${bindings.join(', ')})`
|
|
193
|
+
const relations = supersessions.map(item =>
|
|
194
|
+
`cell ${item.supersededCallSeq} by cell ${item.supersedingCallSeq}`).join('; ')
|
|
195
|
+
return diagnostic({
|
|
196
|
+
code: 'PTC-R007',
|
|
197
|
+
severity: 'warning',
|
|
198
|
+
phase: 'recover',
|
|
199
|
+
message: (bindings.length === 0
|
|
200
|
+
? 'A recorded revision replaced the cell it edited'
|
|
201
|
+
: `Superseded ${bindings.length} binding(s)${names} because a recorded revision replaced the cell that declared them`)
|
|
202
|
+
+ `${relations === '' ? '' : ` (${relations})`}. The replaced cell stays in the session log but no longer builds this environment.`,
|
|
203
|
+
stateEffect: 'rolled-back',
|
|
204
|
+
help: [
|
|
205
|
+
'this is not a recovery failure: an accepted edit rewrote the cell that declared these bindings, and the revision does not declare them',
|
|
206
|
+
'redeclare a superseded name in a new cell before using it again',
|
|
207
|
+
],
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
|
|
183
211
|
function unverifiedCompletionDiagnostic(count) {
|
|
184
212
|
return diagnostic({
|
|
185
213
|
code: 'PTC-R003',
|
|
@@ -329,19 +357,21 @@ class SessionKernel {
|
|
|
329
357
|
this.liveCallSeqs = new Set()
|
|
330
358
|
this.unverifiedCompletions = []
|
|
331
359
|
this.restoredFromRecord = []
|
|
332
|
-
this.
|
|
360
|
+
this.recoveryNotices = disposedCells > 0
|
|
333
361
|
// A kernel whose owning agent or session was disposed must not resurrect
|
|
334
362
|
// the disposed environment: the history is gone by contract, and a later
|
|
335
363
|
// cell starts from an empty REPL with the loss reported once.
|
|
336
|
-
? recoveryDiagnostic([], disposedCells)
|
|
364
|
+
? [recoveryDiagnostic([], disposedCells)]
|
|
337
365
|
: history.volatileSuffix.length === 0
|
|
338
|
-
?
|
|
339
|
-
: recoveryDiagnostic(droppedBindingNames(history), history.volatileSuffix.length)
|
|
340
|
-
this.recoveryNoticeShown = false
|
|
366
|
+
? []
|
|
367
|
+
: [recoveryDiagnostic(droppedBindingNames(history), history.volatileSuffix.length)]
|
|
341
368
|
// Bindings a recovery told the model were live, and the last help pair that
|
|
342
369
|
// explained a recovery. Both exist to keep one event from being reported
|
|
343
370
|
// twice and to notice a binding that stops existing afterwards.
|
|
344
371
|
this.announcedLive = new Set()
|
|
372
|
+
// A recorded supersession is a durable fact; announcing it once per runtime
|
|
373
|
+
// keeps later folds from repeating a relation the model already read.
|
|
374
|
+
this.announcedSupersessions = new Set()
|
|
345
375
|
this.recoveryHelpSignature = undefined
|
|
346
376
|
this.recoveryEventCount = 0
|
|
347
377
|
this.active = undefined
|
|
@@ -558,27 +588,29 @@ class SessionKernel {
|
|
|
558
588
|
if (worker !== undefined) await this.resetWorker(worker)
|
|
559
589
|
this.rollbackToDurable()
|
|
560
590
|
if (config.durableReplay) {
|
|
561
|
-
this.
|
|
591
|
+
this.recoveryNotices = []
|
|
562
592
|
try {
|
|
563
593
|
this.history = recoverJournal(observedSession, request.callSeq, {
|
|
564
594
|
visibleCallSeqs,
|
|
595
|
+
editRewritesHistory: config.editRewritesHistory === true,
|
|
565
596
|
})
|
|
566
597
|
} catch (error) {
|
|
567
|
-
/* c8 ignore start --
|
|
598
|
+
/* c8 ignore start -- the fold reports damaged history as data, so no supported session reaches this catch */
|
|
568
599
|
const boundary = recoveryBoundaryForHistory(this.history, undefined, { reset: true })
|
|
569
600
|
if (boundary !== undefined) recoveryBoundaries.push(boundary)
|
|
570
601
|
this.history = emptyHistory()
|
|
571
|
-
this.
|
|
602
|
+
this.recoveryNotices = [recoveryDiagnostic([], 1)]
|
|
572
603
|
this.initialRecoveryBoundary = undefined
|
|
573
604
|
/* c8 ignore stop */
|
|
574
605
|
}
|
|
575
|
-
/* c8 ignore start -- volatile suffix on the first fold;
|
|
606
|
+
/* c8 ignore start -- volatile suffix on the first fold; no case settles a volatile suffix before the first replay */
|
|
576
607
|
if (this.history.volatileSuffix.length > 0) {
|
|
577
|
-
this.
|
|
608
|
+
this.recoveryNotices = [recoveryDiagnostic(
|
|
609
|
+
droppedBindingNames(this.history), this.history.volatileSuffix.length)]
|
|
578
610
|
}
|
|
579
611
|
/* c8 ignore stop */
|
|
580
612
|
this.initialRecoveryBoundary = recoveryBoundaryForHistory(this.history)
|
|
581
|
-
/* c8 ignore start -- first-fold recovery boundary;
|
|
613
|
+
/* c8 ignore start -- first-fold recovery boundary; no case makes the first replay record one */
|
|
582
614
|
if (this.initialRecoveryBoundary !== undefined) {
|
|
583
615
|
recoveryBoundaries.push(this.initialRecoveryBoundary)
|
|
584
616
|
this.initialRecoveryBoundary = undefined
|
|
@@ -587,7 +619,7 @@ class SessionKernel {
|
|
|
587
619
|
} else {
|
|
588
620
|
this.history = emptyHistory()
|
|
589
621
|
this.replayed = true
|
|
590
|
-
this.
|
|
622
|
+
this.recoveryNotices = [recoveryDiagnostic([], 1)]
|
|
591
623
|
}
|
|
592
624
|
}
|
|
593
625
|
}
|
|
@@ -680,6 +712,7 @@ class SessionKernel {
|
|
|
680
712
|
const recovered = recoverJournal(observedSession, request.callSeq, {
|
|
681
713
|
extraBoundaries: [boundary],
|
|
682
714
|
visibleCallSeqs: visibleExecutableCallSeqs(observedSession),
|
|
715
|
+
editRewritesHistory: config.editRewritesHistory === true,
|
|
683
716
|
})
|
|
684
717
|
const nextPathLength = pathToHead(recovered).length
|
|
685
718
|
if (nextPathLength >= previousPathLength) throw new Error('recovery did not contract the historical frontier')
|
|
@@ -698,10 +731,12 @@ class SessionKernel {
|
|
|
698
731
|
this.durability = durabilityState()
|
|
699
732
|
}
|
|
700
733
|
}
|
|
701
|
-
// One recovery event produces one
|
|
702
|
-
//
|
|
703
|
-
// an earlier overwrite dropped the restored-name list whenever
|
|
704
|
-
// arrived, and a
|
|
734
|
+
// One recovery event produces one report. The facts below describe the same
|
|
735
|
+
// event, so each one that holds is collected instead of overwriting the ones
|
|
736
|
+
// already found: an earlier overwrite dropped the restored-name list whenever
|
|
737
|
+
// a later fact arrived, and a relation dropped that way was then cleared
|
|
738
|
+
// without ever reaching the model. The facts never name the same binding, so
|
|
739
|
+
// collecting them states the event once per fact rather than twice per name.
|
|
705
740
|
const restoredCount = this.restoredFromRecord.length
|
|
706
741
|
const allLive = [...new Set(this.restoredFromRecord.flatMap(item => item.bindings))]
|
|
707
742
|
// Report only what this recovery added. A later recovery that installs
|
|
@@ -716,6 +751,8 @@ const failedNames = [...new Set(this.restoredFromRecord
|
|
|
716
751
|
.filter(name => typeof name === 'string'))]
|
|
717
752
|
const droppedNames = this.missingBindings === undefined ? [] : [...new Set(this.missingBindings)]
|
|
718
753
|
const endedNames = this.rewindEndedBindings === undefined ? [] : [...new Set(this.rewindEndedBindings)]
|
|
754
|
+
const supersededNames = this.supersededEndedBindings === undefined
|
|
755
|
+
? [] : [...new Set(this.supersededEndedBindings)]
|
|
719
756
|
const alreadyReported = this.recoveryEventCount > 0
|
|
720
757
|
// A complete restore is invisible to the model. Every recorded binding was
|
|
721
758
|
// installed, nothing was dropped or ended, and the next cell behaves exactly
|
|
@@ -726,58 +763,78 @@ const failedNames = [...new Set(this.restoredFromRecord
|
|
|
726
763
|
const completeRestore = failedNames.length === 0
|
|
727
764
|
&& droppedNames.length === 0
|
|
728
765
|
&& endedNames.length === 0
|
|
766
|
+
&& supersededNames.length === 0
|
|
729
767
|
if (restoredCount > 0 && completeRestore) {
|
|
730
768
|
this.restoredFromRecord = []
|
|
731
|
-
/* c8 ignore start -- needs a rewind that ended an announced name;
|
|
769
|
+
/* c8 ignore start -- needs a rewind that ended an announced name after a reported recovery; no case does */
|
|
732
770
|
} else if (restoredCount > 0 && liveNames.length === 0 && failedNames.length === 0 && alreadyReported) {
|
|
733
771
|
this.restoredFromRecord = []
|
|
734
772
|
/* c8 ignore stop */
|
|
735
773
|
} else if (restoredCount > 0) {
|
|
736
774
|
this.recoveryEventCount += 1
|
|
737
|
-
this.
|
|
775
|
+
this.recoveryNotices.push(restoredFromRecordDiagnostic(
|
|
738
776
|
restoredCount,
|
|
739
777
|
liveNames,
|
|
740
778
|
failedNames,
|
|
741
779
|
alreadyReported,
|
|
742
|
-
)
|
|
780
|
+
))
|
|
743
781
|
}
|
|
744
|
-
/* c8 ignore start -- needs a proven name the replay never visits;
|
|
782
|
+
/* c8 ignore start -- needs a proven name the replay never visits; no case records one */
|
|
745
783
|
if (droppedNames.length > 0) {
|
|
746
|
-
this.
|
|
784
|
+
this.recoveryNotices.push(recoveryDiagnostic(droppedNames, droppedNames.length))
|
|
747
785
|
/* c8 ignore stop */
|
|
748
786
|
} else if (restoredCount === 0 && skipped > 0) {
|
|
749
|
-
this.
|
|
787
|
+
this.recoveryNotices.push(recoveryDiagnostic([], skipped))
|
|
750
788
|
}
|
|
751
789
|
// A rewind that ended a binding is recorded, not a loss: report it only when
|
|
752
790
|
// the replay installed nothing under that name anywhere (path, off-path or
|
|
753
|
-
// catalog), so a correct value never carries a "dropped" notice.
|
|
754
|
-
|
|
755
|
-
|
|
791
|
+
// catalog), so a correct value never carries a "dropped" notice. A revision
|
|
792
|
+
// that superseded a cell is the other recorded ending: the environment it
|
|
793
|
+
// built differs from the cells the log still lists, so the relation is
|
|
794
|
+
// reported once per runtime instead of on every later fold.
|
|
795
|
+
const freshSupersessions = (this.supersessions ?? [])
|
|
796
|
+
.filter(item => !this.announcedSupersessions.has(item.supersedingCallSeq))
|
|
797
|
+
// A restored cell and a superseded one are two facts about the same recovery,
|
|
798
|
+
// not alternatives: the names this relation withdrew are excluded from the
|
|
799
|
+
// missing and rewind lists, so a report that chose the restore notice over it
|
|
800
|
+
// left a withdrawn name reported nowhere.
|
|
801
|
+
if (freshSupersessions.length > 0 || supersededNames.length > 0) {
|
|
802
|
+
this.recoveryNotices.push(supersededDiagnostic(supersededNames, freshSupersessions))
|
|
803
|
+
for (const item of freshSupersessions) this.announcedSupersessions.add(item.supersedingCallSeq)
|
|
756
804
|
}
|
|
757
|
-
if (
|
|
758
|
-
this.
|
|
805
|
+
if (endedNames.length > 0) {
|
|
806
|
+
this.recoveryNotices.push(rewindEndedDiagnostic(endedNames))
|
|
759
807
|
}
|
|
760
808
|
// A completion the replay could not certify is not a lost binding: the
|
|
761
809
|
// replay established them, and only the recorded payload stays unproved.
|
|
762
|
-
if (this.
|
|
810
|
+
if (this.unverifiedCompletions.length > 0) {
|
|
811
|
+
this.recoveryNotices.push(unverifiedCompletionDiagnostic(this.unverifiedCompletions.length))
|
|
812
|
+
}
|
|
813
|
+
if (this.recoveryNotices.length > 0) {
|
|
763
814
|
// Remember what was promised as live, so a binding that disappears later
|
|
764
815
|
// is reported by name instead of surfacing as an unexplained ReferenceError.
|
|
765
816
|
for (const name of liveNames) this.announcedLive.add(name)
|
|
766
817
|
// The help pair explains the recovery once. Repeating it on every later
|
|
767
818
|
// recovery in the same session is noise the model cannot act on twice.
|
|
768
|
-
const signature = this.
|
|
769
|
-
if (this.recoveryHelpSignature === signature)
|
|
770
|
-
|
|
819
|
+
const signature = this.recoveryNotices.map(item => item.message).join('\n')
|
|
820
|
+
if (this.recoveryHelpSignature === signature) {
|
|
821
|
+
this.recoveryNotices = this.recoveryNotices.map(item => ({ ...item, help: [] }))
|
|
822
|
+
} else {
|
|
823
|
+
this.recoveryHelpSignature = signature
|
|
824
|
+
}
|
|
771
825
|
}
|
|
772
826
|
this.restoredFromRecord = []
|
|
773
827
|
this.missingBindings = undefined
|
|
774
828
|
this.rewindEndedBindings = undefined
|
|
829
|
+
this.supersededEndedBindings = undefined
|
|
830
|
+
this.supersessions = undefined
|
|
775
831
|
}
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
832
|
+
// Every recovery reports the facts it established. Suppressing a later recovery's
|
|
833
|
+
// notice hid a relation the runtime had already marked as announced, so a name the
|
|
834
|
+
// revision withdrew was reported nowhere; what is deduplicated is the explanation
|
|
835
|
+
// (below) and each name or relation this runtime already stated.
|
|
836
|
+
const leadingDiagnostics = this.recoveryNotices
|
|
837
|
+
this.recoveryNotices = []
|
|
781
838
|
if (request.journal !== undefined) request.journal.diagnostics.push(...leadingDiagnostics)
|
|
782
839
|
let result
|
|
783
840
|
try {
|
|
@@ -938,9 +995,17 @@ const failedNames = [...new Set(this.restoredFromRecord
|
|
|
938
995
|
// correct.
|
|
939
996
|
const installed = new Set(replayedNames)
|
|
940
997
|
for (const name of this.bindingCatalog.inputs().knownBindings) installed.add(name)
|
|
941
|
-
|
|
998
|
+
// A superseded name is ended by the record that replaced its cell, so it is
|
|
999
|
+
// reported apart from a rewind and never as a binding recovery failed to
|
|
1000
|
+
// install: the log says which revision replaced it.
|
|
1001
|
+
const supersededEnded = new Set((this.history.supersededBindings ?? [])
|
|
1002
|
+
.filter(name => !installed.has(name)))
|
|
1003
|
+
this.rewindEndedBindings = [...endedBindings]
|
|
1004
|
+
.filter(name => !installed.has(name) && !supersededEnded.has(name))
|
|
1005
|
+
this.supersededEndedBindings = [...supersededEnded]
|
|
1006
|
+
this.supersessions = this.history.supersessions ?? []
|
|
942
1007
|
this.missingBindings = (this.history.provenBindings ?? [])
|
|
943
|
-
.filter(name => !installed.has(name) && !endedBindings.has(name))
|
|
1008
|
+
.filter(name => !installed.has(name) && !endedBindings.has(name) && !supersededEnded.has(name))
|
|
944
1009
|
}
|
|
945
1010
|
|
|
946
1011
|
completeJournal(journal, status, result, volatileReason, diagnostics = [], completion = undefined, values = undefined) {
|
|
@@ -1372,7 +1437,8 @@ export class SessionRuntime {
|
|
|
1372
1437
|
kernel.surfaceGeneration = surfaceGeneration(session)
|
|
1373
1438
|
} else {
|
|
1374
1439
|
const timeline = foldSessionTimeline(observed.events)
|
|
1375
|
-
const history = recoverJournal(observed, undefined, { visibleCallSeqs: visible
|
|
1440
|
+
const history = recoverJournal(observed, undefined, { visibleCallSeqs: visible,
|
|
1441
|
+
editRewritesHistory: this.config.editRewritesHistory === true })
|
|
1376
1442
|
const head = history.nodes[history.head]
|
|
1377
1443
|
if (head === undefined) return unavailableReplMemorySnapshot()
|
|
1378
1444
|
// A cold inventory can describe a proved recorded frontier without
|
|
@@ -1468,6 +1534,7 @@ export class SessionRuntime {
|
|
|
1468
1534
|
? emptyHistory()
|
|
1469
1535
|
: recoverJournal(sessionLog, callSeq, {
|
|
1470
1536
|
visibleCallSeqs: visibleExecutableCallSeqs(sessionLog),
|
|
1537
|
+
editRewritesHistory: cellConfig.editRewritesHistory === true,
|
|
1471
1538
|
...(liveOnlyLost ? { liveOnlyLost: true } : {}),
|
|
1472
1539
|
})
|
|
1473
1540
|
} catch (error) {
|
|
@@ -74,6 +74,12 @@ export function installSettingsSectionCompat({
|
|
|
74
74
|
&& typeof writeNamespace === 'string' && writeNamespace.length > 0
|
|
75
75
|
? Object.freeze({
|
|
76
76
|
update: (patch, expectedRevision) => provider.update(writeNamespace, patch, expectedRevision),
|
|
77
|
+
// The service applies field edits as `{ op: 'set' | 'unset', path }`; `update` can only
|
|
78
|
+
// restate values, so expose the primitive when it exists instead of narrowing the callers
|
|
79
|
+
// to a writer that cannot remove a field.
|
|
80
|
+
...(typeof provider.mutate === 'function'
|
|
81
|
+
? { mutate: (ops, expectedRevision) => provider.mutate(writeNamespace, ops, expectedRevision) }
|
|
82
|
+
: {}),
|
|
77
83
|
read: () => provider.describe?.().find(descriptor => descriptor.ns === writeNamespace),
|
|
78
84
|
}) : undefined
|
|
79
85
|
const unbind = onProvider(provider, writer)
|