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
|
@@ -24,6 +24,8 @@ import {
|
|
|
24
24
|
toolCapabilityMetadata,
|
|
25
25
|
} from './program-bindings.js'
|
|
26
26
|
import { deepFreeze } from './record-utils.js'
|
|
27
|
+
import { adaptRunCodeSchema } from './direct-surface-owner.js'
|
|
28
|
+
import { hostTransportDefinition, isBatchSubmission } from './host-tool-sdk.js'
|
|
27
29
|
import { REPL_MEMORY_META_KEY, withReplMemorySnapshot } from './repl-memory-projection.js'
|
|
28
30
|
import { withUserBindingDraftCapability } from './user-binding-draft-projection.js'
|
|
29
31
|
import { POST_EDIT_KEY } from './session-journal-schema.js'
|
|
@@ -160,6 +162,7 @@ export function createRuntimeBridgeOwner({
|
|
|
160
162
|
userBindingDraftForAgent,
|
|
161
163
|
bindingSubmissionForAgent,
|
|
162
164
|
capabilityEnvironment,
|
|
165
|
+
readPromptText = () => undefined,
|
|
163
166
|
}) {
|
|
164
167
|
const scope = new AsyncLocalStorage()
|
|
165
168
|
const childScope = new AsyncLocalStorage()
|
|
@@ -185,6 +188,8 @@ export function createRuntimeBridgeOwner({
|
|
|
185
188
|
})
|
|
186
189
|
let currentConfig = sessions.config
|
|
187
190
|
const patchedDefinitions = new Map()
|
|
191
|
+
/** The host descriptors this attachment replaced on each definition's slots. */
|
|
192
|
+
const slotPatches = new Map()
|
|
188
193
|
const pending = new WeakMap()
|
|
189
194
|
const nativeRunCodeHandoffs = new Set()
|
|
190
195
|
let active = true
|
|
@@ -342,7 +347,11 @@ export function createRuntimeBridgeOwner({
|
|
|
342
347
|
)))
|
|
343
348
|
const metadata = Object.freeze([
|
|
344
349
|
...(capabilityEnvironment?.toolMetadata(executionToken?.agent, toolCapabilityMetadata(schemas, annotations), schemas) ?? toolCapabilityMetadata(schemas, annotations)),
|
|
345
|
-
|
|
350
|
+
// The same set, as the third argument's own evidence: a derived row (`skills.*`,
|
|
351
|
+
// `mcp.*`) states the invocation path it still has, and the operations it can name
|
|
352
|
+
// are the members this program was handed — not the ones the composition froze when
|
|
353
|
+
// the session was last composed, which a step that delivers less does not rewrite.
|
|
354
|
+
...(capabilityEnvironment?.metadata(executionToken?.agent, schemas) ?? []),
|
|
346
355
|
...PROGRAM_CAPABILITY_METADATA.map(entry => entry.namespace === 'code'
|
|
347
356
|
&& cellConfig.userBindingsEnabled === true
|
|
348
357
|
? { ...entry, members: [...entry.members, BINDING_SUBMISSION] }
|
|
@@ -393,14 +402,94 @@ export function createRuntimeBridgeOwner({
|
|
|
393
402
|
return { request: { ...request, bindings: projected }, release }
|
|
394
403
|
}
|
|
395
404
|
|
|
405
|
+
/**
|
|
406
|
+
* Write one editable run_code slot onto the host's definition.
|
|
407
|
+
*
|
|
408
|
+
* A host may publish the slot as writable data or as a getter-only accessor over
|
|
409
|
+
* its own text, and strict-mode assignment throws on the latter. An owned property
|
|
410
|
+
* definition reaches both shapes, so the plugin never fails silently to deliver the
|
|
411
|
+
* text it was asked to deliver.
|
|
412
|
+
* @param {object} definition - the host's live run_code definition.
|
|
413
|
+
* @param {string} key - the definition field being replaced.
|
|
414
|
+
* @param {unknown} value - the text the plugin owns.
|
|
415
|
+
* @returns {boolean} whether the field now holds that value.
|
|
416
|
+
*/
|
|
417
|
+
function writeSlot(definition, key, value) {
|
|
418
|
+
const own = Object.getOwnPropertyDescriptor(definition, key)
|
|
419
|
+
try {
|
|
420
|
+
Object.defineProperty(definition, key, {
|
|
421
|
+
configurable: own?.configurable ?? true,
|
|
422
|
+
enumerable: own?.enumerable ?? true,
|
|
423
|
+
writable: true,
|
|
424
|
+
value,
|
|
425
|
+
})
|
|
426
|
+
return true
|
|
427
|
+
} catch {
|
|
428
|
+
return false
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** Put one slot back to the descriptor the host published before this plugin. */
|
|
433
|
+
function restoreSlot(definition, key, descriptor) {
|
|
434
|
+
try {
|
|
435
|
+
if (descriptor === undefined) delete definition[key]
|
|
436
|
+
else Object.defineProperty(definition, key, descriptor)
|
|
437
|
+
} catch {
|
|
438
|
+
// A host that made the slot non-configurable after the plugin replaced it keeps
|
|
439
|
+
// the last written text; the restore cannot be forced, and the caller that
|
|
440
|
+
// rewrote it owns the consequence.
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* The editable run_code slots have to reach whichever definition the host lets this
|
|
446
|
+
* plugin own. Two writes, one decision: if the host's parameter texts cannot be
|
|
447
|
+
* replaced, the definition keeps all of its own text rather than half of ours. The
|
|
448
|
+
* descriptors the write replaced are recorded once, so re-applying the current
|
|
449
|
+
* prompt document on every request can never mistake the plugin's own text for the
|
|
450
|
+
* host's, and disposal can restore what the host still publishes.
|
|
451
|
+
*/
|
|
452
|
+
function applyEditableSlots(definition, promptText, slotPatches) {
|
|
453
|
+
if (promptText === undefined) return
|
|
454
|
+
let adapted
|
|
455
|
+
try {
|
|
456
|
+
adapted = adaptRunCodeSchema(definition, promptText)
|
|
457
|
+
} catch {
|
|
458
|
+
return
|
|
459
|
+
}
|
|
460
|
+
const recorded = slotPatches.get(definition)
|
|
461
|
+
if (recorded !== undefined) {
|
|
462
|
+
// Re-applied on every request: the descriptors recorded by the first write are
|
|
463
|
+
// the ones disposal restores, so a later application can never mistake the
|
|
464
|
+
// plugin's own text for the host's. A slot the host has since made unreplaceable
|
|
465
|
+
// keeps the text this attachment already delivered.
|
|
466
|
+
writeSlot(definition, 'parameters', adapted.parameters)
|
|
467
|
+
writeSlot(definition, 'description', adapted.description)
|
|
468
|
+
return
|
|
469
|
+
}
|
|
470
|
+
const ownParameters = Object.getOwnPropertyDescriptor(definition, 'parameters')
|
|
471
|
+
const ownDescription = Object.getOwnPropertyDescriptor(definition, 'description')
|
|
472
|
+
if (!writeSlot(definition, 'parameters', adapted.parameters)) return
|
|
473
|
+
if (!writeSlot(definition, 'description', adapted.description)) {
|
|
474
|
+
restoreSlot(definition, 'parameters', ownParameters)
|
|
475
|
+
return
|
|
476
|
+
}
|
|
477
|
+
slotPatches.set(definition, new Map([['parameters', ownParameters], ['description', ownDescription]]))
|
|
478
|
+
}
|
|
479
|
+
|
|
396
480
|
const patchRunCodeDefinition = (agent) => {
|
|
397
|
-
const definition = ctx.tools
|
|
481
|
+
const definition = hostTransportDefinition(ctx.tools, agent, RUN_CODE)
|
|
398
482
|
if (definition === undefined) {
|
|
399
483
|
throw new Error('ptc-plus: run_code definition is unavailable for the owning session')
|
|
400
484
|
}
|
|
401
485
|
if (definition.output === undefined) {
|
|
402
486
|
throw new Error('ptc-plus: run_code definition has no output projection')
|
|
403
487
|
}
|
|
488
|
+
// A host that declares its own model-facing tools never hands this plugin a
|
|
489
|
+
// tools array to adapt, so the editable prompt slots reach the model only on
|
|
490
|
+
// the definition the executor projects. Re-applied on every request, because
|
|
491
|
+
// the prompt document can change while the definition stays the same object.
|
|
492
|
+
applyEditableSlots(definition, readPromptText(), slotPatches)
|
|
404
493
|
if (patchedDefinitions.has(definition)) return
|
|
405
494
|
const output = definition.output
|
|
406
495
|
const ownPresentationMeta = Object.getOwnPropertyDescriptor(output, 'presentationMeta')
|
|
@@ -463,10 +552,10 @@ export function createRuntimeBridgeOwner({
|
|
|
463
552
|
// nested success. Capture its canonical value before policy; the private
|
|
464
553
|
// relation is admitted only if policy retains that same frozen value.
|
|
465
554
|
if (current?.deferredSettlement?.current === current && derived !== undefined
|
|
466
|
-
&& current.exec === exec && exec.token !== undefined
|
|
555
|
+
&& current.exec === exec && exec.token !== undefined
|
|
467
556
|
&& current.id === sessionId(exec.agent) && derived.agent === exec.agent
|
|
468
|
-
&& derived
|
|
469
|
-
&&
|
|
557
|
+
&& isDerivedExecution(derived, exec)
|
|
558
|
+
&& deepEqualJson(derived.arguments, exec.arguments)
|
|
470
559
|
&& current.presentationMetaCalls === undefined && result.isError === false
|
|
471
560
|
&& result.value !== null && typeof result.value === 'object'
|
|
472
561
|
&& Object.isFrozen(result.value) && result.meta?.[JOURNAL_KEY] === undefined
|
|
@@ -610,13 +699,30 @@ export function createRuntimeBridgeOwner({
|
|
|
610
699
|
|
|
611
700
|
const releaseSeam = seam.installExecute(patchedRun)
|
|
612
701
|
|
|
702
|
+
/**
|
|
703
|
+
* Whether this execution is the derived execution the edit transport submitted.
|
|
704
|
+
*
|
|
705
|
+
* A registry host runs the very object the plugin built, so the call id, the root
|
|
706
|
+
* call and the parent token it carries all prove the relation. The batch contract
|
|
707
|
+
* assigns its own identity instead: the parent token is the host's own and the
|
|
708
|
+
* root call is whichever call that host treats as direct. What survives there is
|
|
709
|
+
* the call id this plugin chose, which the host matched the batch result back to,
|
|
710
|
+
* together with the dispatch record; the derived source is compared beside it.
|
|
711
|
+
*/
|
|
712
|
+
const isDerivedExecution = (derived, exec) => {
|
|
713
|
+
if (derived === null || typeof derived !== 'object' || exec === null || typeof exec !== 'object') return false
|
|
714
|
+
if (derived.callId !== exec.callId) return false
|
|
715
|
+
if (isBatchSubmission(derived)) return true
|
|
716
|
+
return exec.parent !== undefined
|
|
717
|
+
&& derived.rootCallId === exec.rootCallId && derived.parent === exec.parent
|
|
718
|
+
}
|
|
719
|
+
|
|
613
720
|
const ownedDerivedScope = (exec, executionArguments = exec.arguments) => {
|
|
614
721
|
const inherited = scope.getStore()
|
|
615
722
|
const derived = inherited?.derivedExecution
|
|
616
723
|
if (inherited?.deferredSettlement === undefined
|
|
617
724
|
|| inherited.id !== sessionId(exec.agent) || derived?.agent !== exec.agent
|
|
618
|
-
|| derived
|
|
619
|
-
|| derived.parent !== exec.parent || exec.name !== RUN_CODE
|
|
725
|
+
|| !isDerivedExecution(derived, exec) || exec.name !== RUN_CODE
|
|
620
726
|
|| !deepEqualJson(derived.arguments, executionArguments)) return undefined
|
|
621
727
|
return inherited
|
|
622
728
|
}
|
|
@@ -800,8 +906,7 @@ export function createRuntimeBridgeOwner({
|
|
|
800
906
|
const derived = current.derivedExecution
|
|
801
907
|
const privateMetadata = projection !== undefined && projection.exec === exec
|
|
802
908
|
&& deferred.current === current && current.exec === exec && current.id === id
|
|
803
|
-
&& derived?.agent === exec.agent && derived
|
|
804
|
-
&& derived.rootCallId === exec.rootCallId && derived.parent === exec.parent
|
|
909
|
+
&& derived?.agent === exec.agent && isDerivedExecution(derived, exec)
|
|
805
910
|
&& deepEqualJson(derived.arguments, exec.arguments)
|
|
806
911
|
&& projection.token === exec.token && projection.value === result?.value
|
|
807
912
|
&& result.isError === false && deepEqualJson(projection.meta, result.meta)
|
|
@@ -868,6 +973,10 @@ export function createRuntimeBridgeOwner({
|
|
|
868
973
|
}
|
|
869
974
|
}
|
|
870
975
|
patchedDefinitions.clear()
|
|
976
|
+
for (const [definition, recorded] of slotPatches) {
|
|
977
|
+
for (const [key, descriptor] of recorded) restoreSlot(definition, key, descriptor)
|
|
978
|
+
}
|
|
979
|
+
slotPatches.clear()
|
|
871
980
|
try {
|
|
872
981
|
await sessions.dispose()
|
|
873
982
|
} catch (error) {
|
|
@@ -130,6 +130,16 @@ export function resolveConfig(config = {}) {
|
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
resolved.capabilityPolicy = JSON.stringify(capabilityPolicyForConfig(provided.has('capabilityPolicy') ? resolved : { ...resolved, capabilityPolicy: undefined }))
|
|
133
|
+
// Whether the record holds this policy is a fact about the input document, not about this
|
|
134
|
+
// resolution's output: a configuration this function already resolved carries the answer,
|
|
135
|
+
// and resolving that answer's own product again must not re-infer it from the policy this
|
|
136
|
+
// function just wrote. Callers that reconcile by resolving a source that may itself be
|
|
137
|
+
// resolved (the plugin resolves its raw document and then re-resolves the result) would
|
|
138
|
+
// otherwise report every policy as saved, which is what makes a synthesized bottom layer
|
|
139
|
+
// decide the saved column. The marker only means something beside a policy the document
|
|
140
|
+
// itself holds, so a document that writes the marker without one states no provenance.
|
|
141
|
+
resolved.capabilityPolicySaved = provided.has('capabilityPolicy')
|
|
142
|
+
&& (typeof config.capabilityPolicySaved === 'boolean' ? config.capabilityPolicySaved : true)
|
|
133
143
|
if (!provided.has('capabilityPolicy') && resolved.capabilityManagementEnabled) {
|
|
134
144
|
if (!provided.has('capabilityRecoveryEnabled')) resolved.capabilityRecoveryEnabled = true
|
|
135
145
|
if (!provided.has('modelPreviewEnabled')) resolved.modelPreviewEnabled = true
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
} from './prompt-text.js'
|
|
6
6
|
|
|
7
7
|
import { isPtcMessageSource, PTC_MESSAGE_SOURCE_KIND } from './message-sources.js'
|
|
8
|
+
import { BINDING_ADOPTION_FORM, readBindingAdoption } from './binding-adoption.js'
|
|
8
9
|
|
|
9
10
|
export const PTC_DELIVERY_CONTEXT = 'tools:ptc-plus-message-delivery'
|
|
10
11
|
export const PTC_BINDING_CATALOG = 'tools:ptc-plus-user-binding-defaults'
|
|
@@ -116,6 +117,10 @@ export function readRuntimeMessage(message) {
|
|
|
116
117
|
const text = message.content[0].text
|
|
117
118
|
const availability = readRuntimeAvailability(message)
|
|
118
119
|
if (availability !== undefined) return { form: 'runtime-availability', state: availability }
|
|
120
|
+
// A session's binding adoption is recorded as one of its own messages, so the
|
|
121
|
+
// log is its only owner and replay reconstructs it without process memory.
|
|
122
|
+
const adoption = readBindingAdoption(message)
|
|
123
|
+
if (adoption !== undefined) return { form: BINDING_ADOPTION_FORM, declined: [...adoption.declined] }
|
|
119
124
|
if (source.form === 'snapshot') {
|
|
120
125
|
const sections = stateSections(source.sections)
|
|
121
126
|
if (sections !== undefined && [STATE_PREFIX, PREVIOUS_STATE_PREFIX, LEGACY_STATE_PREFIX]
|
|
@@ -9,6 +9,9 @@ import {
|
|
|
9
9
|
RECOVERY_BOUNDARY_EVENT,
|
|
10
10
|
DIAGNOSTIC_WORDING_KEY,
|
|
11
11
|
DIAGNOSTIC_WORDING_FIELDS,
|
|
12
|
+
SETTLEMENT_PROOF_KEY,
|
|
13
|
+
SETTLEMENT_PROOF_FIELDS,
|
|
14
|
+
SETTLEMENT_PROOF_VERSION,
|
|
12
15
|
LEGACY_JOURNAL_VERSION,
|
|
13
16
|
REPL_TOOL_NAMES,
|
|
14
17
|
REWRITES_KEY,
|
|
@@ -23,6 +26,7 @@ import {
|
|
|
23
26
|
normalizeJournal,
|
|
24
27
|
normalizeRecoveryBoundaries,
|
|
25
28
|
reduceStateOperations,
|
|
29
|
+
settlementProofDigest,
|
|
26
30
|
userBindingsForJournal,
|
|
27
31
|
validatedRewrites,
|
|
28
32
|
} from './session-journal.js'
|
|
@@ -104,14 +108,13 @@ function reachableFrom(nodes, head) {
|
|
|
104
108
|
}
|
|
105
109
|
|
|
106
110
|
/**
|
|
107
|
-
* Binding names a
|
|
111
|
+
* Binding names a settlement journal recorded.
|
|
108
112
|
*
|
|
109
113
|
* The recorded values are the only evidence that names a durable binding today;
|
|
110
114
|
* `unrecordedBindings` names the ones the codec could not transfer, and a
|
|
111
115
|
* `kind: "binding"` save names one explicitly. A checkpoint save never counts.
|
|
112
116
|
*/
|
|
113
|
-
function
|
|
114
|
-
const journal = node?.journal
|
|
117
|
+
function journalBindingNames(journal) {
|
|
115
118
|
if (journal === undefined) return []
|
|
116
119
|
return [...new Set([
|
|
117
120
|
...(journal.values ?? []).map(value => value.name),
|
|
@@ -120,6 +123,11 @@ function recordedBindingNames(node) {
|
|
|
120
123
|
].filter(name => typeof name === 'string'))]
|
|
121
124
|
}
|
|
122
125
|
|
|
126
|
+
/** Binding names an installed node recorded. */
|
|
127
|
+
function recordedBindingNames(node) {
|
|
128
|
+
return journalBindingNames(node?.journal)
|
|
129
|
+
}
|
|
130
|
+
|
|
123
131
|
function applyOperations(state, operations, nodeIndex) {
|
|
124
132
|
const previousHead = state.head
|
|
125
133
|
const transition = reduceStateOperations(state, operations, nodeIndex)
|
|
@@ -145,6 +153,117 @@ function applyOperations(state, operations, nodeIndex) {
|
|
|
145
153
|
}
|
|
146
154
|
}
|
|
147
155
|
|
|
156
|
+
/**
|
|
157
|
+
* Adopt the relation a record states about the cell it revised.
|
|
158
|
+
*
|
|
159
|
+
* The relation belongs to the fold, because only the fold knows the path that
|
|
160
|
+
* builds the environment. An accepted edit claims the revision *replaces* the cell
|
|
161
|
+
* it edited, so the revision takes that cell's place in the recorded order: it
|
|
162
|
+
* continues from the target's parent and every cell that followed the target
|
|
163
|
+
* continues from the revision. Appending the revision after those cells instead
|
|
164
|
+
* leaves them replaying an environment the edit already rewrote, with the
|
|
165
|
+
* declarations they read gone and the revision not yet in place.
|
|
166
|
+
*
|
|
167
|
+
* `installed` says whether this settlement puts a cell on the path. A contracted
|
|
168
|
+
* revision installs nothing, so the cells that followed the target bridge to the
|
|
169
|
+
* target's parent, and only a head that was the target itself has to move.
|
|
170
|
+
*
|
|
171
|
+
* The returned record of what changed is what lets a rejected settlement put the
|
|
172
|
+
* path back: nothing this adoption did survives a settlement the fold refuses.
|
|
173
|
+
*/
|
|
174
|
+
function adoptSupersession(state, record, { installed }) {
|
|
175
|
+
const superseded = state.editRewritesHistory && record.supersedes !== undefined
|
|
176
|
+
? state.nodes.findIndex(node => node.callSeq === record.supersedes) : -1
|
|
177
|
+
const adoption = {
|
|
178
|
+
superseded,
|
|
179
|
+
previousHead: state.head,
|
|
180
|
+
supersededBindings: new Set(state.supersededBindings),
|
|
181
|
+
supersessions: state.supersessions.length,
|
|
182
|
+
children: [],
|
|
183
|
+
parent: state.head,
|
|
184
|
+
head: undefined,
|
|
185
|
+
}
|
|
186
|
+
if (superseded < 0) {
|
|
187
|
+
// No relation: the settlement appends to the head, as it always has.
|
|
188
|
+
adoption.head = installed ? undefined : state.head
|
|
189
|
+
return adoption
|
|
190
|
+
}
|
|
191
|
+
const parent = state.nodes[superseded].parent
|
|
192
|
+
adoption.parent = parent
|
|
193
|
+
for (let child = 0; child < state.nodes.length; child += 1) {
|
|
194
|
+
if (state.nodes[child].parent === superseded) adoption.children.push(child)
|
|
195
|
+
}
|
|
196
|
+
// The head is the last recorded cell. It keeps its place when the target is on
|
|
197
|
+
// its path behind other cells, because those cells now reach the same head
|
|
198
|
+
// through the revision; otherwise the revision is the newest cell and the head.
|
|
199
|
+
const targetOnPath = reachableFrom(state.nodes, state.head).has(superseded)
|
|
200
|
+
if (installed) adoption.head = targetOnPath && state.head !== superseded ? state.head : undefined
|
|
201
|
+
else adoption.head = state.head === superseded ? parent : state.head
|
|
202
|
+
return adoption
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Record the relation as the fact the model is owed.
|
|
207
|
+
*
|
|
208
|
+
* Only names the record proves count. A durable declaration that the replay
|
|
209
|
+
* re-establishes is not evidence about the superseded cell, and claiming it
|
|
210
|
+
* would report a binding this fold cannot see. What the revision supplies is the
|
|
211
|
+
* caller's answer, because a settlement the fold refused to install supplies
|
|
212
|
+
* nothing; a cell the fold installs supplies the names its journal recorded.
|
|
213
|
+
*/
|
|
214
|
+
function recordSupersession(state, record, adoption, suppliedNames) {
|
|
215
|
+
if (adoption.superseded < 0) return
|
|
216
|
+
const supplied = new Set(suppliedNames)
|
|
217
|
+
for (const name of recordedBindingNames(state.nodes[adoption.superseded])) {
|
|
218
|
+
if (!supplied.has(name)) state.supersededBindings.add(name)
|
|
219
|
+
}
|
|
220
|
+
state.supersessions.push(Object.freeze({
|
|
221
|
+
supersededCallSeq: record.supersedes,
|
|
222
|
+
supersedingCallSeq: record.call.seq,
|
|
223
|
+
}))
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Fold one accepted record as a settlement of the recorded path.
|
|
228
|
+
*
|
|
229
|
+
* `installed` is false for a settlement the fold contracts: it puts no cell on the
|
|
230
|
+
* path and supplies no name, while the relation it states is still the log's fact.
|
|
231
|
+
* Either way the relation is adopted first and the state operations are applied
|
|
232
|
+
* last, so a settlement that throws leaves the path, the children it re-parented
|
|
233
|
+
* and the relation it recorded exactly as it found them.
|
|
234
|
+
*/
|
|
235
|
+
function applyAdoptedRecord(state, record, { journal, installed = true, restoredFromRecord = false }) {
|
|
236
|
+
const adoption = adoptSupersession(state, record, { installed })
|
|
237
|
+
const index = installed
|
|
238
|
+
? state.nodes.push(Object.freeze({
|
|
239
|
+
code: record.code,
|
|
240
|
+
journal,
|
|
241
|
+
callSeq: record.call.seq,
|
|
242
|
+
parent: adoption.parent,
|
|
243
|
+
...(restoredFromRecord ? { restoredFromRecord: true } : {}),
|
|
244
|
+
...(record.result.userBindings === undefined ? {} : { userBindings: record.result.userBindings }),
|
|
245
|
+
})) - 1
|
|
246
|
+
: undefined
|
|
247
|
+
const inherited = installed ? index : adoption.parent
|
|
248
|
+
for (const child of adoption.children) {
|
|
249
|
+
state.nodes[child] = Object.freeze({ ...state.nodes[child], parent: inherited })
|
|
250
|
+
}
|
|
251
|
+
state.head = adoption.head === undefined ? index : adoption.head
|
|
252
|
+
try {
|
|
253
|
+
recordSupersession(state, record, adoption, installed ? journalBindingNames(journal) : [])
|
|
254
|
+
applyOperations(state, journal.operations, index)
|
|
255
|
+
} catch (error) {
|
|
256
|
+
if (index !== undefined) state.nodes.pop()
|
|
257
|
+
state.head = adoption.previousHead
|
|
258
|
+
for (const child of adoption.children) {
|
|
259
|
+
state.nodes[child] = Object.freeze({ ...state.nodes[child], parent: adoption.superseded })
|
|
260
|
+
}
|
|
261
|
+
state.supersededBindings = adoption.supersededBindings
|
|
262
|
+
state.supersessions.length = adoption.supersessions
|
|
263
|
+
throw error
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
148
267
|
/**
|
|
149
268
|
* Binding names a recorded journal operation list saves.
|
|
150
269
|
*
|
|
@@ -212,17 +331,10 @@ function applyRecord(state, record, invalidCallSeqs) {
|
|
|
212
331
|
// the owner that read `Date.now()` or `process.pid` is gone, and the next
|
|
213
332
|
// environment inherits the durable computation, not the live cache.
|
|
214
333
|
if (!state.liveOnlyLost && journal.values !== undefined && journal.values.length > 0) {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
parent: state.head,
|
|
220
|
-
restoredFromRecord: true,
|
|
221
|
-
...(result.userBindings === undefined ? {} : { userBindings: result.userBindings }),
|
|
222
|
-
})
|
|
223
|
-
const index = state.nodes.push(node) - 1
|
|
224
|
-
state.head = index
|
|
225
|
-
applyOperations(state, journal.operations, index)
|
|
334
|
+
// The relation is adopted for this settlement too: an accepted revision that
|
|
335
|
+
// settled volatile is still the revision the record names, so it replaces its
|
|
336
|
+
// target on the path and installs its own recorded values.
|
|
337
|
+
applyAdoptedRecord(state, record, { journal, restoredFromRecord: true })
|
|
226
338
|
return
|
|
227
339
|
}
|
|
228
340
|
// A released live environment is reported as a contracted cell: the model
|
|
@@ -236,29 +348,22 @@ function applyRecord(state, record, invalidCallSeqs) {
|
|
|
236
348
|
: journal.volatileReason ?? 'volatile cell',
|
|
237
349
|
...(volatileBindings.length === 0 || state.liveOnlyLost === true ? {} : { bindings: volatileBindings }),
|
|
238
350
|
})
|
|
239
|
-
|
|
351
|
+
// A contracted revision still replaces the cell it edited. Leaving the target
|
|
352
|
+
// on the path would keep building an environment the accepted edit already
|
|
353
|
+
// ended, and the relation is what tells the model those names are withdrawn
|
|
354
|
+
// rather than lost. The settlement installs nothing, so it supplies no name:
|
|
355
|
+
// every name the target recorded is withdrawn by this record.
|
|
356
|
+
applyAdoptedRecord(state, record, { journal, installed: false })
|
|
240
357
|
return
|
|
241
358
|
}
|
|
242
359
|
if (!state.trusted && !state.surfaceContracted) {
|
|
243
360
|
state.trusted = true
|
|
244
361
|
state.volatileSuffix.length = 0
|
|
245
362
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
parent: state.head,
|
|
251
|
-
...(result.userBindings === undefined ? {} : { userBindings: result.userBindings }),
|
|
252
|
-
})
|
|
253
|
-
const index = state.nodes.push(node) - 1
|
|
254
|
-
state.head = index
|
|
255
|
-
try {
|
|
256
|
-
applyOperations(state, journal.operations, index)
|
|
257
|
-
} catch (error) {
|
|
258
|
-
state.nodes.pop()
|
|
259
|
-
state.head = node.parent
|
|
260
|
-
throw error
|
|
261
|
-
}
|
|
363
|
+
// A record may declare that it is the revision of an earlier cell. Adoption of
|
|
364
|
+
// that relation, its re-parented children and the node this record puts on the
|
|
365
|
+
// path are one settlement: a record the fold refuses leaves none of them behind.
|
|
366
|
+
applyAdoptedRecord(state, record, { journal })
|
|
262
367
|
}
|
|
263
368
|
|
|
264
369
|
function recordEventSeq(record) {
|
|
@@ -277,9 +382,15 @@ function foldRecords(records, invalidCallSeqs, options = {}) {
|
|
|
277
382
|
volatileSuffix: [],
|
|
278
383
|
provenBindings: new Set(),
|
|
279
384
|
deletedBindings: new Set(),
|
|
385
|
+
// One recorded revision replaced the cell it edited. The relation, not a
|
|
386
|
+
// deletion, is what the log proves: the superseded cell stays in the record
|
|
387
|
+
// and off the path to the head.
|
|
388
|
+
supersessions: [],
|
|
389
|
+
supersededBindings: new Set(),
|
|
280
390
|
trusted: true,
|
|
281
391
|
surfaceContracted: options.surfaceContracted === true,
|
|
282
392
|
liveOnlyLost: options.liveOnlyLost === true,
|
|
393
|
+
editRewritesHistory: options.editRewritesHistory === true,
|
|
283
394
|
}
|
|
284
395
|
for (const record of records) {
|
|
285
396
|
applyRecord(state, record, invalidCallSeqs)
|
|
@@ -1459,6 +1570,12 @@ function normalizePostEditFact(facts, call, resultEvent, result, replayPostEdit,
|
|
|
1459
1570
|
let derived
|
|
1460
1571
|
try {
|
|
1461
1572
|
derived = normalizeDeferredEditSettlement(post.meta, target?.callSeq)
|
|
1573
|
+
// The derived metadata is a published settlement of its own, so it carries the
|
|
1574
|
+
// same proof the root journal does. Comparing the two recorded journal copies
|
|
1575
|
+
// proves only that they agree with each other; an edit that changed both and
|
|
1576
|
+
// left this proof behind no longer reproduces the settlement it published, and
|
|
1577
|
+
// its journal is then not evidence for the bindings the revision declares.
|
|
1578
|
+
requireRecordedSettlementProof(post.meta, derived.journal)
|
|
1462
1579
|
} catch (error) {
|
|
1463
1580
|
return fail(error.message)
|
|
1464
1581
|
}
|
|
@@ -1517,6 +1634,40 @@ function requireRecordedDiagnosticWording(meta, journal) {
|
|
|
1517
1634
|
}
|
|
1518
1635
|
}
|
|
1519
1636
|
|
|
1637
|
+
/**
|
|
1638
|
+
* A published settlement proves its own facts, and the Host's copy of its outcome
|
|
1639
|
+
* must still agree with them.
|
|
1640
|
+
*
|
|
1641
|
+
* Recovery reads a record as evidence for the bindings its cell declared, and the
|
|
1642
|
+
* record is the only account of its cell: editing the status, the completion or a
|
|
1643
|
+
* diagnostic leaves the record self-consistent, because the completion is rendered
|
|
1644
|
+
* from the same diagnostic that was edited. The proof recorded at publication is
|
|
1645
|
+
* one digest over exactly those facts, so an edited record no longer reproduces it.
|
|
1646
|
+
* A record that carries no proof predates it and keeps the older evidence.
|
|
1647
|
+
*
|
|
1648
|
+
* The retained `isError` is the Host's copy of the same outcome and not a fact this
|
|
1649
|
+
* plugin can recompute, so an edit that also rewrites the proof still has to agree
|
|
1650
|
+
* with it. That relation is the one a deferred edit already proves before it is
|
|
1651
|
+
* published.
|
|
1652
|
+
*/
|
|
1653
|
+
function requireRecordedSettlementProof(meta, journal, isError = undefined) {
|
|
1654
|
+
if (isError !== undefined && isError !== (journal.status === 'discarded'
|
|
1655
|
+
|| journal.completion?.kind === 'throw')) {
|
|
1656
|
+
throw new Error('journal settlement does not agree with the tool result it settled')
|
|
1657
|
+
}
|
|
1658
|
+
if (!isRecord(meta) || !Object.hasOwn(meta, SETTLEMENT_PROOF_KEY)) return
|
|
1659
|
+
const recorded = meta[SETTLEMENT_PROOF_KEY]
|
|
1660
|
+
if (!isRecord(recorded) || Reflect.ownKeys(recorded).length !== SETTLEMENT_PROOF_FIELDS.size
|
|
1661
|
+
|| Reflect.ownKeys(recorded).some(key => !SETTLEMENT_PROOF_FIELDS.has(key))
|
|
1662
|
+
|| recorded.version !== SETTLEMENT_PROOF_VERSION
|
|
1663
|
+
|| typeof recorded.recordedMessage !== 'string') {
|
|
1664
|
+
throw new Error('invalid recorded settlement proof')
|
|
1665
|
+
}
|
|
1666
|
+
if (recorded.recordedMessage !== settlementProofDigest(journal)) {
|
|
1667
|
+
throw new Error('journal facts do not match their recorded settlement proof')
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1520
1671
|
function advanceTimelineEvent(previous, inputEvent, eventIndex, replayFact, retainedCalls) {
|
|
1521
1672
|
const nested = inputEvent?.type === 'tool/ptc-dispatch-start'
|
|
1522
1673
|
|| inputEvent?.type === 'tool/ptc-dispatch'
|
|
@@ -1767,6 +1918,7 @@ function advanceTimelineEvent(previous, inputEvent, eventIndex, replayFact, reta
|
|
|
1767
1918
|
...(derived.userBindings === undefined ? {} : { userBindings: derived.userBindings }),
|
|
1768
1919
|
}
|
|
1769
1920
|
requireRecordedDiagnosticWording(meta, derived.journal)
|
|
1921
|
+
requireRecordedSettlementProof(meta, derived.journal)
|
|
1770
1922
|
} else if (call.data.name === 'run_code') {
|
|
1771
1923
|
const rawJournal = meta[JOURNAL_KEY]
|
|
1772
1924
|
const resolveLegacyConfirm = rawJournal?.version === LEGACY_JOURNAL_VERSION
|
|
@@ -1800,6 +1952,15 @@ function advanceTimelineEvent(previous, inputEvent, eventIndex, replayFact, reta
|
|
|
1800
1952
|
...(userBindings === undefined ? {} : { userBindings }),
|
|
1801
1953
|
}
|
|
1802
1954
|
requireRecordedDiagnosticWording(meta, journal)
|
|
1955
|
+
// The Host states the outcome of the cell it settled; a record whose
|
|
1956
|
+
// completion or status contradicts it was edited after publication. The
|
|
1957
|
+
// Host records that outcome on the result message it appends, so the
|
|
1958
|
+
// message is read first; a log that carries it on the event instead is
|
|
1959
|
+
// read the same way.
|
|
1960
|
+
const hostIsError = typeof event.data?.message?.isError === 'boolean'
|
|
1961
|
+
? event.data.message.isError
|
|
1962
|
+
: typeof event.data?.isError === 'boolean' ? event.data.isError : undefined
|
|
1963
|
+
requireRecordedSettlementProof(meta, journal, hostIsError)
|
|
1803
1964
|
let postEdit
|
|
1804
1965
|
try {
|
|
1805
1966
|
postEdit = normalizePostEditFact(state.eventFacts, call, event, normalized, undefined, state.executableCalls)
|
|
@@ -2299,7 +2460,8 @@ export function recoverJournal(session, currentCallSeq, options = {}) {
|
|
|
2299
2460
|
// A live environment that was released cannot lend its recorded live-only
|
|
2300
2461
|
// values to the environment that replaces it. The flag belongs to the fold, so
|
|
2301
2462
|
// every re-fold below carries it.
|
|
2302
|
-
const foldOptions = { liveOnlyLost: options?.liveOnlyLost === true
|
|
2463
|
+
const foldOptions = { liveOnlyLost: options?.liveOnlyLost === true,
|
|
2464
|
+
editRewritesHistory: options?.editRewritesHistory === true }
|
|
2303
2465
|
let state = foldRecords(records, invalidCallSeqs, foldOptions)
|
|
2304
2466
|
let boundaryIndex = 0
|
|
2305
2467
|
const carrierCallSeqForBoundary = boundary => boundary.carrierCallSeq
|
|
@@ -2415,6 +2577,9 @@ export function recoverJournal(session, currentCallSeq, options = {}) {
|
|
|
2415
2577
|
code: postEdit.derived.code,
|
|
2416
2578
|
result: { eventSeq: result.eventSeq, journal: postEdit.derived.journal,
|
|
2417
2579
|
derived: postEdit.derived, userBindings: postEdit.derived.userBindings },
|
|
2580
|
+
// The revision names the cell it edits. Whether that relation replaces
|
|
2581
|
+
// the target or appends to it is the fold's decision, not the log's.
|
|
2582
|
+
supersedes: postEdit.targetCallSeq,
|
|
2418
2583
|
hidden: visibleCalls instanceof Set && (!visibleCalls.has(call.seq)
|
|
2419
2584
|
|| !visibleCalls.has(postEdit.targetCallSeq)),
|
|
2420
2585
|
})
|
|
@@ -2482,6 +2647,19 @@ export function recoverJournal(session, currentCallSeq, options = {}) {
|
|
|
2482
2647
|
enumerable: false,
|
|
2483
2648
|
configurable: true,
|
|
2484
2649
|
})
|
|
2650
|
+
// The supersession relation is recovery evidence of the same kind: which cell
|
|
2651
|
+
// each revision replaced, and which proved names the path no longer supplies
|
|
2652
|
+
// because of it. Both stay off the enumerable shape for the same reason.
|
|
2653
|
+
Object.defineProperty(history, 'supersessions', {
|
|
2654
|
+
value: [...state.supersessions],
|
|
2655
|
+
enumerable: false,
|
|
2656
|
+
configurable: true,
|
|
2657
|
+
})
|
|
2658
|
+
Object.defineProperty(history, 'supersededBindings', {
|
|
2659
|
+
value: [...state.supersededBindings],
|
|
2660
|
+
enumerable: false,
|
|
2661
|
+
configurable: true,
|
|
2662
|
+
})
|
|
2485
2663
|
return history
|
|
2486
2664
|
}
|
|
2487
2665
|
|
|
@@ -12,6 +12,7 @@ export const POST_EDIT_KEY = 'dshPtcPlusPostEdit'
|
|
|
12
12
|
export const REWRITES_KEY = 'dshPtcPlusRewrites'
|
|
13
13
|
export const RECOVERY_BOUNDARY_KEY = 'dshPtcPlusRecoveryBoundaries'
|
|
14
14
|
export const DIAGNOSTIC_WORDING_KEY = 'dshPtcPlusDiagnosticWording'
|
|
15
|
+
export const SETTLEMENT_PROOF_KEY = 'dshPtcPlusSettlementProof'
|
|
15
16
|
export const IMPORT_BOUNDARY_JOURNAL_VERSION = 7
|
|
16
17
|
export const PER_NAME_USER_BINDINGS_JOURNAL_VERSION = 8
|
|
17
18
|
export const LANGUAGE_SEMANTICS_JOURNAL_VERSION = 9
|
|
@@ -62,6 +63,8 @@ export const ERROR_FIELDS = new Set(['kind', 'message'])
|
|
|
62
63
|
export const EDIT_TARGET_FIELDS = new Set(['targetCallSeq'])
|
|
63
64
|
export const DERIVED_RUN_FIELDS = new Set(['code', 'codeDigest', 'codeBytes', 'description'])
|
|
64
65
|
export const DIAGNOSTIC_WORDING_FIELDS = new Set(['digest'])
|
|
66
|
+
export const SETTLEMENT_PROOF_VERSION = 1
|
|
67
|
+
export const SETTLEMENT_PROOF_FIELDS = new Set(['version', 'recordedMessage'])
|
|
65
68
|
|
|
66
69
|
export const JOURNAL_VERSIONS = new Set([LEGACY_JOURNAL_VERSION, INTERMEDIATE_JOURNAL_VERSION, PREVIOUS_JOURNAL_VERSION, USER_BINDING_RELATIONLESS_JOURNAL_VERSION, FINGERPRINT_REUSE_JOURNAL_VERSION, VERSIONED_BINDING_REUSE_JOURNAL_VERSION, IMPORT_BOUNDARY_JOURNAL_VERSION, PER_NAME_USER_BINDINGS_JOURNAL_VERSION, LANGUAGE_SEMANTICS_JOURNAL_VERSION, MODULE_TRANSFORM_JOURNAL_VERSION, JOURNAL_VERSION])
|
|
67
70
|
export const USER_BINDINGS_REUSE_POLICIES = new Set([LEGACY_USER_BINDINGS_REUSE_POLICY, LIVE_USER_BINDINGS_REUSE_POLICY])
|