dsh-ptc-plus 0.5.2 → 0.5.3
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 +3 -1
- package/README.md +3 -1
- package/client.cjs +165 -29
- 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/durability-design.md +9 -2
- package/docs/installation.md +9 -9
- package/docs/releases/v0.5.2.md +26 -14
- package/docs/releases/v0.5.3.md +77 -0
- package/docs/runtime-reference.md +7 -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 +67 -19
- package/internal/binding-adoption.js +136 -0
- package/internal/capability-command-receipt.js +18 -4
- package/internal/capability-delivery-proof.js +70 -0
- package/internal/capability-environment.js +38 -1
- package/internal/config-spec.js +14 -5
- package/internal/cordis-tools-owner.js +2 -1
- package/internal/direct-surface-owner.js +27 -13
- 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/official-repl-priority.js +31 -10
- package/internal/program-bindings.js +12 -1
- package/internal/repl-memory-projection.js +21 -2
- package/internal/runtime-bridge-owner.js +113 -8
- 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 +22 -2
- package/internal/session-runtime.js +106 -39
- package/internal/user-bindings-owner.js +109 -6
- package/package.json +15 -7
- package/src/client-capabilities-view.js +45 -3
- package/src/client-copy.js +24 -0
- package/src/client-indicator-view.js +2 -1
- package/src/client-repl-view.js +35 -16
- package/src/client.js +3 -1
|
@@ -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) {
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { PTC_MESSAGE_SOURCE_KIND } from './message-sources.js'
|
|
2
|
+
import { sessionExecutionAvailable } from './host-tool-sdk.js'
|
|
2
3
|
import { bindingToggleMessage } from './binding-toggle-receipt.js'
|
|
4
|
+
import {
|
|
5
|
+
bindingAdoptionMessage,
|
|
6
|
+
clearedBindingAdoption,
|
|
7
|
+
declinedBindingEntries,
|
|
8
|
+
withBindingAdoption,
|
|
9
|
+
} from './binding-adoption.js'
|
|
3
10
|
import { RPC_CONTRACTS } from './rpc-contract.js'
|
|
4
11
|
import { randomUUID } from 'node:crypto'
|
|
5
12
|
import { dirname } from 'node:path'
|
|
@@ -26,6 +33,7 @@ import {
|
|
|
26
33
|
createUserBindingsSnapshot,
|
|
27
34
|
normalizeUserBindingEntry,
|
|
28
35
|
normalizeUserBindingsDocument,
|
|
36
|
+
selectUserBindingsSnapshot,
|
|
29
37
|
storedUserBindingsDocument,
|
|
30
38
|
} from './user-bindings.js'
|
|
31
39
|
|
|
@@ -78,6 +86,40 @@ function parseAuthoringCommand(rawInput) {
|
|
|
78
86
|
return { kind: 'help' }
|
|
79
87
|
}
|
|
80
88
|
|
|
89
|
+
/**
|
|
90
|
+
* The adoption subcommands of `/binding`. They state which supplied entries this
|
|
91
|
+
* session uses; authoring and adoption share one command because both answer
|
|
92
|
+
* questions about the same global catalog, and a second command would suggest
|
|
93
|
+
* they were separate features.
|
|
94
|
+
*/
|
|
95
|
+
function parseAdoptionCommand(rawInput) {
|
|
96
|
+
const input = typeof rawInput === 'string' ? rawInput.trim() : ''
|
|
97
|
+
if (input === 'status') return { kind: 'status' }
|
|
98
|
+
if (input === 'reset') return { kind: 'reset' }
|
|
99
|
+
const decision = /^(use|skip)\s+(\S+)$/.exec(input)
|
|
100
|
+
if (decision === null) return undefined
|
|
101
|
+
return { kind: decision[1] === 'use' ? 'use' : 'skip', id: decision[2] }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function adoptionStatusText(catalog, adoption) {
|
|
105
|
+
const declined = declinedBindingEntries(adoption)
|
|
106
|
+
const supplied = catalog.entries
|
|
107
|
+
const listed = supplied.map(entry => `${entry.id} (${entry.name}): ${declined.has(entry.id)
|
|
108
|
+
? 'not used by this session' : 'used by this session'}`)
|
|
109
|
+
const unavailable = [...declined].filter(id => !supplied.some(entry => entry.id === id))
|
|
110
|
+
return [
|
|
111
|
+
'Global binding catalog supply and this session\'s adoption of it. Adoption decides which supplied entries this session uses; it does not report whether the worker installed them.',
|
|
112
|
+
...(listed.length === 0 ? ['The global catalog supplies no enabled entries.'] : listed),
|
|
113
|
+
...(unavailable.length === 0 ? [] : [`Declined but not supplied by the global catalog, therefore not used and not revivable here: ${JSON.stringify(unavailable)}`]),
|
|
114
|
+
].join('\n')
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function adoptionReceipt(adoption) {
|
|
118
|
+
return `Session binding adoption recorded. This session declines ${JSON.stringify([...declinedBindingEntries(adoption)])}. `
|
|
119
|
+
+ 'The global catalog and other sessions are unchanged, and adoption can only withdraw a supplied entry. '
|
|
120
|
+
+ 'Whether the worker installed the entries this session still uses is reported with the next accepted cell, not by this receipt.'
|
|
121
|
+
}
|
|
122
|
+
|
|
81
123
|
function authoringTask(command, current) {
|
|
82
124
|
if (command.kind === 'new') {
|
|
83
125
|
return `Create a Global User Binding draft for this requirement:\n\n${command.requirement}`
|
|
@@ -265,6 +307,48 @@ export function createUserBindingsOwner(ctx, options = {}) {
|
|
|
265
307
|
const requireEnabled = () => {
|
|
266
308
|
if (!enabled) throw new Error('Global User Bindings are disabled')
|
|
267
309
|
}
|
|
310
|
+
// Adoption is read from the session's own recorded decision, never from a
|
|
311
|
+
// second in-memory map: the log is the owner, so a restart reconstructs the
|
|
312
|
+
// same environment without a migration step.
|
|
313
|
+
const adoptionForAgent = (agent) => {
|
|
314
|
+
if (typeof options.bindingAdoptionForAgent !== 'function' || agent === undefined || agent === null) return undefined
|
|
315
|
+
return options.bindingAdoptionForAgent(agent)
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Supply minus this session's declines. The catalog decides which entries
|
|
319
|
+
* exist; adoption only withdraws one, so no entry can appear here that the
|
|
320
|
+
* global configuration did not supply.
|
|
321
|
+
*/
|
|
322
|
+
const adoptedCatalog = (snapshot, adoption) => {
|
|
323
|
+
const declined = declinedBindingEntries(adoption)
|
|
324
|
+
if (declined.size === 0) return snapshot
|
|
325
|
+
const kept = snapshot.entries.filter(entry => !declined.has(entry.id))
|
|
326
|
+
if (kept.length === snapshot.entries.length) return snapshot
|
|
327
|
+
return selectUserBindingsSnapshot(snapshot, kept.map(entry => entry.id))
|
|
328
|
+
}
|
|
329
|
+
const recordAdoption = (agent, adoption) => {
|
|
330
|
+
const session = agent?.session
|
|
331
|
+
if (session === undefined || typeof session.append !== 'function') {
|
|
332
|
+
throw new Error('binding adoption requires a live session')
|
|
333
|
+
}
|
|
334
|
+
if (session.append('user/message', bindingAdoptionMessage(adoption), { surfaceOp: 'append' }) === undefined) {
|
|
335
|
+
throw new Error('the session did not record the binding adoption decision')
|
|
336
|
+
}
|
|
337
|
+
return { kind: 'success', text: adoptionReceipt(adoption) }
|
|
338
|
+
}
|
|
339
|
+
const stateAdoption = async (agent, command) => {
|
|
340
|
+
requireEnabled()
|
|
341
|
+
// Status and validation read the full supplied catalog: an adopted subset
|
|
342
|
+
// cannot prove what it was derived from.
|
|
343
|
+
const catalog = await store.snapshot()
|
|
344
|
+
const current = adoptionForAgent(agent)
|
|
345
|
+
if (command.kind === 'status') return { kind: 'success', text: adoptionStatusText(catalog, current) }
|
|
346
|
+
if (command.kind === 'reset') return recordAdoption(agent, clearedBindingAdoption())
|
|
347
|
+
if (!catalog.entries.some(entry => entry.id === command.id)) {
|
|
348
|
+
throw new Error(`the global binding catalog does not supply entry ${JSON.stringify(command.id)}; this session can decline a supplied entry but cannot add or revive one`)
|
|
349
|
+
}
|
|
350
|
+
return recordAdoption(agent, withBindingAdoption(current, command.id, command.kind === 'use'))
|
|
351
|
+
}
|
|
268
352
|
const draftFor = (capability) => {
|
|
269
353
|
if (typeof capability !== 'string' || capability === '') {
|
|
270
354
|
throw new TypeError('draft capability must be a non-empty string')
|
|
@@ -482,7 +566,7 @@ export function createUserBindingsOwner(ctx, options = {}) {
|
|
|
482
566
|
const requireCurrentAgent = () => {
|
|
483
567
|
if (!agentIsCurrent(agent, sessionId, agentGeneration)
|
|
484
568
|
|| commandRegistrations.get(sessionId)?.agent !== agent
|
|
485
|
-
|| ctx.tools
|
|
569
|
+
|| !sessionExecutionAvailable(ctx.tools, agent)) {
|
|
486
570
|
throw new Error('this binding authoring Agent is no longer active')
|
|
487
571
|
}
|
|
488
572
|
}
|
|
@@ -617,6 +701,14 @@ export function createUserBindingsOwner(ctx, options = {}) {
|
|
|
617
701
|
|
|
618
702
|
const commandHandler = async ({ agent, rawInput, signal, commandId }) => {
|
|
619
703
|
signal?.throwIfAborted?.()
|
|
704
|
+
const adoption = parseAdoptionCommand(rawInput)
|
|
705
|
+
if (adoption !== undefined) {
|
|
706
|
+
try {
|
|
707
|
+
return await stateAdoption(agent, adoption)
|
|
708
|
+
} catch (error) {
|
|
709
|
+
return { kind: 'error', text: error instanceof Error ? error.message : String(error) }
|
|
710
|
+
}
|
|
711
|
+
}
|
|
620
712
|
const command = parseAuthoringCommand(rawInput)
|
|
621
713
|
if (command.kind === 'help') return { kind: 'error', text: `Usage: ${COMMAND_USAGE}` }
|
|
622
714
|
try {
|
|
@@ -660,14 +752,14 @@ export function createUserBindingsOwner(ctx, options = {}) {
|
|
|
660
752
|
// still claiming one.
|
|
661
753
|
if (disposed || !enabled || lifecycleGeneration !== registration.generation
|
|
662
754
|
|| !agentIsCurrent(agent, sessionId, agentGeneration)
|
|
663
|
-
|| ctx.tools
|
|
755
|
+
|| !sessionExecutionAvailable(ctx.tools, agent)
|
|
664
756
|
|| commandRegistrations.get(sessionId) !== registration) {
|
|
665
757
|
throw new Error('Global User Bindings binding command registration is no longer eligible')
|
|
666
758
|
}
|
|
667
759
|
registerOwned(commandCtx, commandCtx.commands, {
|
|
668
760
|
name: 'binding',
|
|
669
|
-
description: '
|
|
670
|
-
input: { hint: 'new <requirement> | edit <id> <requirement>' },
|
|
761
|
+
description: 'Author a Global User Binding draft, or state which supplied entries this session uses',
|
|
762
|
+
input: { hint: 'new <requirement> | edit <id> <requirement> | status | use <id> | skip <id> | reset' },
|
|
671
763
|
handler: commandHandler,
|
|
672
764
|
}, 'binding command')
|
|
673
765
|
})
|
|
@@ -712,13 +804,13 @@ export function createUserBindingsOwner(ctx, options = {}) {
|
|
|
712
804
|
}
|
|
713
805
|
if (!agentIsCurrent(agent, sessionId, agentGeneration)) return
|
|
714
806
|
const eligible = enabled && draftProjectionAvailable && !disposed
|
|
715
|
-
&& ctx.tools
|
|
807
|
+
&& sessionExecutionAvailable(ctx.tools, agent)
|
|
716
808
|
const current = commandRegistrations.get(sessionId)
|
|
717
809
|
if (eligible && current?.agent === agent) return
|
|
718
810
|
if (current !== undefined) await removeCommandFor(sessionId, current)
|
|
719
811
|
if (!agentIsCurrent(agent, sessionId, agentGeneration)) return
|
|
720
812
|
const stillEligible = enabled && draftProjectionAvailable && !disposed
|
|
721
|
-
&& ctx.tools
|
|
813
|
+
&& sessionExecutionAvailable(ctx.tools, agent)
|
|
722
814
|
if (stillEligible) {
|
|
723
815
|
registerCommandFor(agent)
|
|
724
816
|
return
|
|
@@ -918,6 +1010,17 @@ export function createUserBindingsOwner(ctx, options = {}) {
|
|
|
918
1010
|
async snapshot() {
|
|
919
1011
|
return enabled ? store.snapshot() : undefined
|
|
920
1012
|
},
|
|
1013
|
+
/**
|
|
1014
|
+
* The catalog entries one session's worker should receive. Without a stated
|
|
1015
|
+
* adoption this is exactly the supplied catalog, so 0.5.2 behavior is
|
|
1016
|
+
* unchanged for every session that never declines an entry.
|
|
1017
|
+
*/
|
|
1018
|
+
async snapshotForAgent(agent) {
|
|
1019
|
+
if (!enabled) return undefined
|
|
1020
|
+
const snapshot = await store.snapshot()
|
|
1021
|
+
const adoption = adoptionForAgent(agent)
|
|
1022
|
+
return adoption === undefined ? snapshot : adoptedCatalog(snapshot, adoption)
|
|
1023
|
+
},
|
|
921
1024
|
async list() {
|
|
922
1025
|
requireEnabled()
|
|
923
1026
|
return store.list()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-ptc-plus",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
4
4
|
"icon": "assets/ptc-plus-icon.svg",
|
|
5
5
|
"description": "A session-bound agent-native REPL for DeepSeek Harness PTC mode.",
|
|
6
6
|
"type": "module",
|
|
@@ -69,9 +69,15 @@
|
|
|
69
69
|
"@deepseek-ai/dsh-skill-filesystem": "*",
|
|
70
70
|
"@deepseek-ai/dsh-spill-policy": "*",
|
|
71
71
|
"@deepseek-ai/dsh-tool-cordis": "*",
|
|
72
|
+
"@deepseek-ai/dsh-tool-executor-ptc": "*",
|
|
72
73
|
"@deepseek-ai/dsh-tools": "*",
|
|
73
74
|
"@deepseek-ai/dsh-typert-protocol": "*"
|
|
74
75
|
},
|
|
76
|
+
"peerDependenciesMeta": {
|
|
77
|
+
"@deepseek-ai/dsh-tool-executor-ptc": {
|
|
78
|
+
"optional": true
|
|
79
|
+
}
|
|
80
|
+
},
|
|
75
81
|
"scripts": {
|
|
76
82
|
"build": "node scripts/client-bundle.mjs build && node scripts/compiler-bundle.mjs build",
|
|
77
83
|
"build:check": "node scripts/client-bundle.mjs check && node scripts/compiler-bundle.mjs check",
|
|
@@ -88,8 +94,9 @@
|
|
|
88
94
|
"test:client:fallback": "node scripts/client-fallback-smoke.mjs",
|
|
89
95
|
"test:client:layout": "node scripts/client-binding-layout.mjs",
|
|
90
96
|
"readme:screenshots": "node scripts/readme-screenshots.mjs",
|
|
91
|
-
"host:
|
|
92
|
-
"host:
|
|
97
|
+
"host:channels:update": "node scripts/host-channel-manifest.mjs update",
|
|
98
|
+
"host:channels:graph": "node scripts/host-channel-manifest.mjs graph",
|
|
99
|
+
"host:channels:verify": "node scripts/host-channel-manifest.mjs verify",
|
|
93
100
|
"test:host-contract": "node scripts/dsh-host-contract-suite.mjs",
|
|
94
101
|
"prepack": "npm run build:check",
|
|
95
102
|
"hooks:install": "node scripts/review-findings.mjs install-hook",
|
|
@@ -109,14 +116,15 @@
|
|
|
109
116
|
"test:backend": "node scripts/test-suite-files.mjs",
|
|
110
117
|
"test:backend:fast": "node scripts/test-suite-files.mjs --fast",
|
|
111
118
|
"test:ci:runtime": "node --test --test-concurrency=1 test/package-contract.test.js test/compiler-platform.test.js test/direct-surface-policy.test.js test/isolated-entrance.test.js test/plugin-native-call-transport.test.js",
|
|
112
|
-
"verify": "npm run review:validate && npm run host:
|
|
113
|
-
"verify:check": "npm run review:validate && npm run host:
|
|
114
|
-
"verify:ci": "npm run review:validate && npm run host:
|
|
119
|
+
"verify": "npm run review:validate && npm run host:channels:verify && npm run host:shape-check && npm run semantic:check && npm run build:check && node --check index.js && node --check internal/kernel-worker.js && node --check scripts/migrate-session-log.mjs && node --check scripts/expensive-headless-acceptance.mjs && node --check scripts/ab-headless-trajectories.mjs && node scripts/deterministic-test-groups.mjs",
|
|
120
|
+
"verify:check": "npm run review:validate && npm run host:channels:verify && npm run host:shape-check && npm run semantic:check && npm run build:check && node --check index.js && node --check internal/kernel-worker.js && node scripts/deterministic-test-groups.mjs --check",
|
|
121
|
+
"verify:ci": "npm run review:validate && npm run host:channels:verify && npm run host:shape-check && npm run semantic:check && npm run build:check && node --check index.js && node --check internal/kernel-worker.js && node --check scripts/migrate-session-log.mjs && node scripts/deterministic-test-groups.mjs --ci",
|
|
115
122
|
"check": "node scripts/review-findings.mjs check",
|
|
116
123
|
"verify:docs": "node scripts/review-findings.mjs docs",
|
|
117
124
|
"test:expensive": "node scripts/expensive-headless-acceptance.mjs",
|
|
118
125
|
"test:ab": "node scripts/ab-headless-trajectories.mjs",
|
|
119
|
-
"test:package:consumer": "node scripts/npm-consumer-smoke.mjs"
|
|
126
|
+
"test:package:consumer": "node scripts/npm-consumer-smoke.mjs",
|
|
127
|
+
"host:shape-check": "node scripts/check-host-version-literals.mjs"
|
|
120
128
|
},
|
|
121
129
|
"keywords": [
|
|
122
130
|
"deepseek-harness",
|
|
@@ -9,10 +9,28 @@ import {
|
|
|
9
9
|
accountPromptText, parsePromptTextDocument, resolvePromptText,
|
|
10
10
|
} from '../internal/prompt-text.js'
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* One capability, three separate facts, in the reader's language. Saved,
|
|
14
|
+
* delivered and usable are different questions, so a reader must be able to see
|
|
15
|
+
* which one failed instead of reading one blanket "applied".
|
|
16
|
+
*/
|
|
17
|
+
export function capabilityProofCopy(t, proof) {
|
|
18
|
+
const delivery = ['delivered', 'pending', 'absent'].includes(proof?.delivery) ? proof.delivery : undefined
|
|
19
|
+
const usability = ['proven', 'unproven'].includes(proof?.usability) ? proof.usability : undefined
|
|
20
|
+
if (delivery === undefined || usability === undefined) return t('capabilities.quick.unknown')
|
|
21
|
+
return t('capabilities.proof.line', {
|
|
22
|
+
saved: t(proof.saved === true ? 'capabilities.proof.saved.yes' : 'capabilities.proof.saved.no'),
|
|
23
|
+
delivery: t(`capabilities.proof.delivery.${delivery}`),
|
|
24
|
+
usability: t(`capabilities.proof.usability.${usability}`),
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
12
28
|
export function capabilityCommandText(t, text) {
|
|
13
29
|
if (text?.key) return t(text.key, text.params)
|
|
14
30
|
const receipt = readCapabilityCommandReceipt(text)
|
|
15
|
-
|
|
31
|
+
if (!receipt) return text
|
|
32
|
+
const line = t(`capabilities.receipt.${receipt.state}`, { session: receipt.sessionId, error: receipt.error ?? '' })
|
|
33
|
+
return receipt.proof === undefined ? line : `${line} ${capabilityProofCopy(t, receipt.proof)}`
|
|
16
34
|
}
|
|
17
35
|
|
|
18
36
|
// Host-owned context the model sees in PTC mode. The plugin never receives the host's
|
|
@@ -115,9 +133,14 @@ export function createCapabilityCommandCard(React) {
|
|
|
115
133
|
: node.outcome ? (typeof text === 'string' && !text.trim() ? undefined : capabilityCommandText(t, text))
|
|
116
134
|
?? t(node.outcome.kind === 'error' ? 'capabilities.commandFailed' : 'capabilities.refreshAcknowledged')
|
|
117
135
|
: t('capabilities.refreshing')
|
|
136
|
+
// A command that names one capability carries that capability's three
|
|
137
|
+
// facts; the notice states them beside its session outcome so "saved"
|
|
138
|
+
// cannot be read as "delivered" or "usable".
|
|
139
|
+
const notice = receipt?.proof === undefined || outcome === undefined
|
|
140
|
+
? outcome : `${outcome} ${capabilityProofCopy(t, receipt.proof)}`
|
|
118
141
|
return h('p', { className: 'ptcPlusCapabilityCommandNotice',
|
|
119
142
|
role: node.outcome?.kind === 'error' ? 'alert' : 'status',
|
|
120
|
-
'data-state': node.outcome?.kind ?? 'pending' },
|
|
143
|
+
'data-state': node.outcome?.kind ?? 'pending' }, notice)
|
|
121
144
|
}
|
|
122
145
|
let json
|
|
123
146
|
if (typeof text === 'string') {
|
|
@@ -1427,6 +1450,10 @@ export function createCapabilityRefreshView(React, commands) {
|
|
|
1427
1450
|
export function createSessionCapabilityControlsView(React, { commands, Modal, Select, inspectSessionCatalog }) {
|
|
1428
1451
|
const h = React.createElement
|
|
1429
1452
|
const emptyDrafts = () => ({})
|
|
1453
|
+
// The panel reports the three capability facts only where it holds evidence:
|
|
1454
|
+
// the receipts its own commands returned. A catalogue row says what the plan
|
|
1455
|
+
// will do; it cannot say a cell reached the capability.
|
|
1456
|
+
const emptyProofs = () => ({})
|
|
1430
1457
|
return function SessionCapabilityControls({ sessionId, open, enabled, t, onClose }) {
|
|
1431
1458
|
const source = React.useMemo(() => commands.source(sessionId), [sessionId])
|
|
1432
1459
|
const available = React.useSyncExternalStore(source.subscribe, source.getSnapshot)
|
|
@@ -1448,6 +1475,7 @@ export function createSessionCapabilityControlsView(React, { commands, Modal, Se
|
|
|
1448
1475
|
const [sort, setSort] = React.useState(null)
|
|
1449
1476
|
const [focusDimension, setFocusDimension] = React.useState(null)
|
|
1450
1477
|
const [status, setStatus] = React.useState(null)
|
|
1478
|
+
const [proofs, setProofs] = React.useState(emptyProofs)
|
|
1451
1479
|
const [pending, setPending] = React.useState(false)
|
|
1452
1480
|
const request = React.useRef(null)
|
|
1453
1481
|
const applying = React.useRef(false)
|
|
@@ -1480,6 +1508,7 @@ export function createSessionCapabilityControlsView(React, { commands, Modal, Se
|
|
|
1480
1508
|
setPane('list')
|
|
1481
1509
|
setFocusDimension(null)
|
|
1482
1510
|
setStatus(null)
|
|
1511
|
+
setProofs(emptyProofs())
|
|
1483
1512
|
setPending(false)
|
|
1484
1513
|
setError(null)
|
|
1485
1514
|
}, [sessionId])
|
|
@@ -1646,15 +1675,20 @@ export function createSessionCapabilityControlsView(React, { commands, Modal, Se
|
|
|
1646
1675
|
let pendingCount = 0
|
|
1647
1676
|
let failure = null
|
|
1648
1677
|
let savedOnly = null
|
|
1678
|
+
const observed = {}
|
|
1649
1679
|
for (const [id, selection, dimension] of staged) {
|
|
1650
1680
|
if (controller.signal.aborted) break
|
|
1681
|
+
const line = `set ${targetScope} ${encodeURIComponent(id)} ${dimension ? dimension + ' ' : ''}${selection}`
|
|
1651
1682
|
try {
|
|
1652
|
-
const result = await commands.run(sessionId,
|
|
1683
|
+
const result = await commands.run(sessionId, line, controller.signal)
|
|
1653
1684
|
const receipt = readCapabilityCommandReceipt(result.text)
|
|
1654
1685
|
if (!receipt || receipt.sessionId !== sessionId || !['recorded', 'pending'].includes(receipt.state)) {
|
|
1655
1686
|
throw new Error(receipt ? capabilityCommandText(t, result.text) : t('capabilities.quick.unknownReceipt'))
|
|
1656
1687
|
}
|
|
1657
1688
|
if (receipt?.state === 'pending') pendingCount += 1
|
|
1689
|
+
// A receipt that names the capability carries the only proof this
|
|
1690
|
+
// panel can hold: what the save did and did not establish.
|
|
1691
|
+
if (receipt.proof !== undefined) observed[receipt.proof.id] = { proof: receipt.proof, command: `/capabilities ${line}` }
|
|
1658
1692
|
completed += 1
|
|
1659
1693
|
} catch (cause) {
|
|
1660
1694
|
failure = String(cause?.message ?? cause)
|
|
@@ -1663,6 +1697,7 @@ export function createSessionCapabilityControlsView(React, { commands, Modal, Se
|
|
|
1663
1697
|
break
|
|
1664
1698
|
}
|
|
1665
1699
|
}
|
|
1700
|
+
if (mounted.current && Object.keys(observed).length) setProofs(current => ({ ...current, ...observed }))
|
|
1666
1701
|
if (request.current !== controller || controller.signal.aborted || !mounted.current) return
|
|
1667
1702
|
const refreshed = await load(controller.signal)
|
|
1668
1703
|
if (request.current !== controller || controller.signal.aborted || !mounted.current) return
|
|
@@ -1900,6 +1935,13 @@ export function createSessionCapabilityControlsView(React, { commands, Modal, Se
|
|
|
1900
1935
|
h('dt', null, t('capabilities.quick.effective')), h('dd', null, statusLabel(selected.effective)),
|
|
1901
1936
|
selected.sourceDimensions ? h(React.Fragment, null, h('dt', null, t('capabilities.policy.frozenSource')), h('dd', null, dimensionFacts(selected.sourceDimensions))) : null,
|
|
1902
1937
|
selected.effectiveDimensions ? h(React.Fragment, null, h('dt', null, t('capabilities.policy.effectiveDimensions')), h('dd', null, dimensionFacts(selected.effectiveDimensions))) : null),
|
|
1938
|
+
h('div', { className: 'ptcPlusCapabilityProof', 'data-capability-proof': selected.id },
|
|
1939
|
+
h('h4', null, t('capabilities.proof.title')),
|
|
1940
|
+
proofs[selected.id]
|
|
1941
|
+
? h(React.Fragment, null,
|
|
1942
|
+
h('p', { className: 'ptcPlusDetail', role: 'status' }, capabilityProofCopy(t, proofs[selected.id].proof)),
|
|
1943
|
+
h('small', null, t('capabilities.proof.observed', { command: proofs[selected.id].command })))
|
|
1944
|
+
: h('p', { className: 'ptcPlusDetail' }, t('capabilities.proof.unobserved'))),
|
|
1903
1945
|
selected.source?.kind === 'mcp' && selected.category !== 'server'
|
|
1904
1946
|
? (() => {
|
|
1905
1947
|
const server = catalog.find(entry => entry.category === 'server' && entry.source?.server === selected.source.server)
|