dsh-ptc-plus 0.4.3 → 0.4.10

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.
Files changed (66) hide show
  1. package/README.en.md +15 -3
  2. package/README.md +15 -3
  3. package/client.js +1157 -533
  4. package/cordis.patch.yml +12 -2
  5. package/docs/README.md +2 -0
  6. package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +60 -4
  7. package/docs/adr/0017-track-the-latest-dsh-public-surface.md +5 -1
  8. package/docs/adr/0019-plugin-settings-and-kill-switch.md +14 -3
  9. package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +2 -2
  10. package/docs/adr/0023-global-user-bindings.md +1 -1
  11. package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +82 -18
  12. package/docs/adr/0030-serve-both-typert-codec-generations-from-one-codec.md +3 -3
  13. package/docs/adr/0032-own-session-log-facts-in-a-host-only-projection.md +27 -0
  14. package/docs/adr/0033-gate-client-visual-usability-in-a-browser.md +23 -0
  15. package/docs/architecture.md +6 -4
  16. package/docs/binding-review-placement-design.md +29 -21
  17. package/docs/client-ui.md +20 -8
  18. package/docs/client-visual-contract.md +48 -0
  19. package/docs/durability-design.md +12 -0
  20. package/docs/installation.md +48 -9
  21. package/docs/publishing.md +4 -4
  22. package/docs/release-notes.md +109 -0
  23. package/docs/runtime-reference.md +8 -2
  24. package/docs/user-bindings.en.md +2 -2
  25. package/docs/user-bindings.md +3 -3
  26. package/docs/verification-optimization-work.md +26 -0
  27. package/docs/verification.md +7 -1
  28. package/index.js +28 -7
  29. package/internal/compiler-platform.js +5 -3
  30. package/internal/compiler-service.js +2 -9
  31. package/internal/cordis-tools-owner.js +75 -16
  32. package/internal/direct-surface-owner.js +333 -7
  33. package/internal/edit-transport-owner.js +8 -5
  34. package/internal/execution-provider-owner.js +157 -0
  35. package/internal/execution-seam-compat.js +69 -113
  36. package/internal/isolated-worker.js +12 -8
  37. package/internal/recovery-tips.js +50 -21
  38. package/internal/rejected-cell-editor.js +11 -21
  39. package/internal/runtime-bridge-owner.js +88 -57
  40. package/internal/runtime-contexts.js +13 -13
  41. package/internal/session-events.js +40 -4
  42. package/internal/session-journal-recovery.js +678 -181
  43. package/internal/session-log-view.js +189 -43
  44. package/internal/session-runtime.js +58 -10
  45. package/internal/settings-compat.js +12 -2
  46. package/internal/user-bindings-owner.js +6 -5
  47. package/locale/en.json +6 -0
  48. package/locale/zh.json +6 -0
  49. package/package.json +18 -13
  50. package/src/client-authoring-view.js +222 -141
  51. package/src/client-binding-review.js +2 -0
  52. package/src/client-code-editor.js +6 -0
  53. package/src/client-copy.js +10 -0
  54. package/src/client-feature-gates.js +12 -5
  55. package/src/client-host-compat.js +23 -2
  56. package/src/client-primitives.js +249 -0
  57. package/src/client-repl-view.js +3 -3
  58. package/src/client-settings-view.js +2 -29
  59. package/src/client-styles.js +92 -33
  60. package/src/client-tool-view.js +8 -3
  61. package/src/client-workbench.js +9 -7
  62. package/src/client.js +72 -46
  63. package/assets/ptc-plus-bindings-en.png +0 -0
  64. package/assets/ptc-plus-bindings-zh.png +0 -0
  65. package/assets/ptc-plus-repair-en.png +0 -0
  66. package/assets/ptc-plus-repair-zh.png +0 -0
@@ -1,9 +1,12 @@
1
1
  import {
2
- foldSessionTimeline,
2
+ advanceSessionTimeline, createSessionTimelineState, replaySessionTimelineFact, validateSessionTimelineState,
3
3
  } from './session-journal-recovery.js'
4
4
  import { isCanonicalSequence } from './session-journal.js'
5
- import { sessionEvents } from './session-events.js'
5
+ import { advanceSessionSurface, readSessionLog, sessionEvents } from './session-events.js'
6
6
  import { readRuntimeMessage } from './runtime-messages.js'
7
+ import { deepEqualJson, isJsonValue } from '@deepseek-ai/dsh-util-values'
8
+ import { deepFreeze, isRecord } from './record-utils.js'
9
+ import { advanceRuntimeHistory, createRuntimeHistory } from './recovery-tips.js'
7
10
 
8
11
  import { isHostRuntimeContextSource } from './message-sources.js'
9
12
  const SYSTEM_PROMPT_CLEARED = 'Current runtime context: none. Earlier runtime-context snapshots no longer apply.'
@@ -35,30 +38,185 @@ function isContextStep(event) {
35
38
  || (event?.type === 'user/message' && event.data?.source?.kind === 'user')
36
39
  }
37
40
 
38
- function cordisTranscriptFacts(timeline) {
39
- let calls = 0
40
- let inspections = 0
41
- for (const result of timeline.results.values()) {
42
- for (const call of result?.journal?.calls ?? []) {
43
- if (call.global !== 'tools' || typeof call.member !== 'string'
44
- || !call.member.startsWith('cordis_')) continue
45
- calls += 1
46
- if (call.ok === true && call.member.startsWith('cordis_inspect')) inspections += 1
41
+ export const SESSION_LOG_PROJECTION_KEY = 'ptcPlusSessionLog'
42
+
43
+ function emptyLogState() {
44
+ return { timeline: createSessionTimelineState(), contextStep: 0,
45
+ runtimeHistory: createRuntimeHistory(), visibleRuntimeFacts: {},
46
+ systemPromptSnapshots: [], ptcMessages: [], runtimeMessagesBySeq: {}, visibleRuntimeMessages: [], surfaceNodes: [], logFacts: [] }
47
+ }
48
+
49
+ export function runtimeMessageFacts(records, initial = {}) {
50
+ const facts = { ...initial }
51
+ for (const record of records) {
52
+ if (record.producer === 'aggregate') facts.aggregate = record
53
+ else if (record.form === 'snapshot') facts.snapshot = record
54
+ else if (record.form === 'catalog') facts.catalog = record
55
+ }
56
+ return facts
57
+ }
58
+
59
+ function presentationEvent(event) {
60
+ const recognized = event?.type === 'user/message'
61
+ && (systemPromptSnapshotSections(event.data) !== undefined || readRuntimeMessage(event.data) !== undefined)
62
+ const data = recognized ? { source: event.data.source, content: event.data.content }
63
+ : event?.type === 'user/message' && typeof event.data?.source?.kind === 'string'
64
+ ? { source: { kind: event.data.source.kind } } : undefined
65
+ return JSON.parse(JSON.stringify({ seq: event.seq, type: event.type,
66
+ ...(data === undefined ? {} : { data }),
67
+ ...(event.surfaceOp === undefined ? {} : { surfaceOp: event.surfaceOp }),
68
+ }))
69
+ }
70
+
71
+ function applyLogEvent(state, event, index = event?.seq,
72
+ timeline = advanceSessionTimeline(state.timeline, event, index)) {
73
+ const sections = event?.type === 'user/message' ? systemPromptSnapshotSections(event.data) : undefined
74
+ const ptc = event?.type === 'user/message' ? readRuntimeMessage(event.data) : undefined
75
+ const contextStep = state.contextStep + (isContextStep(event) ? 1 : 0)
76
+ const surfaceNodes = advanceSessionSurface(state.surfaceNodes, event)
77
+ let visibleRuntimeMessages = state.visibleRuntimeMessages
78
+ if (timeline === state.timeline && sections === undefined && ptc === undefined && contextStep === state.contextStep
79
+ && surfaceNodes === state.surfaceNodes) return state
80
+ const next = { ...state, timeline, contextStep,
81
+ logFacts: [...state.logFacts, deepFreeze({ index, event: presentationEvent(event) })],
82
+ }
83
+ next.runtimeHistory = advanceRuntimeHistory(state.runtimeHistory, {
84
+ sections, notice: ptc?.form === 'notice' ? ptc.name : undefined,
85
+ snapshot: ptc?.form === 'snapshot', catalog: ptc?.form === 'catalog',
86
+ index, contextStep: state.contextStep, lastSuccessfulRunIndex: timeline.lastSuccessfulRunIndex,
87
+ resetSuccess: timeline.lastSuccessfulRunIndex !== state.timeline.lastSuccessfulRunIndex,
88
+ })
89
+ if (sections !== undefined) {
90
+ const snapshot = Object.freeze({ index, contextStep: state.contextStep, sections })
91
+ next.systemPromptSnapshots = [...state.systemPromptSnapshots, snapshot]
92
+ next.runtimeMessagesBySeq = { ...state.runtimeMessagesBySeq,
93
+ [`k${event.seq}`]: Object.freeze({ ...snapshot, producer: 'aggregate', form: 'snapshot' }) }
94
+ }
95
+ if (ptc !== undefined) {
96
+ const record = Object.freeze({ ...ptc, index, contextStep: state.contextStep, producer: 'ptc-plus' })
97
+ next.ptcMessages = [...state.ptcMessages, record]
98
+ next.runtimeMessagesBySeq = { ...next.runtimeMessagesBySeq, [`k${event.seq}`]: record }
99
+ }
100
+ if (event?.surfaceOp === 'append') {
101
+ const record = next.runtimeMessagesBySeq[`k${event.seq}`]
102
+ if (record !== undefined) {
103
+ visibleRuntimeMessages = [...visibleRuntimeMessages, record]
104
+ next.visibleRuntimeFacts = runtimeMessageFacts([record], state.visibleRuntimeFacts)
47
105
  }
106
+ } else if (surfaceNodes !== state.surfaceNodes) {
107
+ visibleRuntimeMessages = surfaceNodes.flatMap(seq => {
108
+ const record = next.runtimeMessagesBySeq[`k${seq}`]
109
+ return record === undefined ? [] : [record]
110
+ })
111
+ next.visibleRuntimeFacts = runtimeMessageFacts(visibleRuntimeMessages)
48
112
  }
49
- return Object.freeze({ calls, inspections })
113
+ next.surfaceNodes = surfaceNodes
114
+ next.visibleRuntimeMessages = Object.freeze(visibleRuntimeMessages)
115
+ return next
116
+ }
117
+
118
+ export function createSessionLogProjection() {
119
+ return Object.freeze({
120
+ key: SESSION_LOG_PROJECTION_KEY,
121
+ stateVersion: 8,
122
+ stateSchema: Object.freeze({ parse(value) {
123
+ if (!isJsonValue(value) || !isRecord(value) || !isRecord(value.timeline)
124
+ || typeof value.timeline.openTurn !== 'boolean' || typeof value.timeline.found !== 'boolean'
125
+ || !Number.isSafeInteger(value.timeline.scope) || value.timeline.scope < 0
126
+ || !Number.isSafeInteger(value.contextStep) || value.contextStep < 0
127
+ || !Array.isArray(value.systemPromptSnapshots) || !Array.isArray(value.ptcMessages)
128
+ || !Array.isArray(value.visibleRuntimeMessages)
129
+ || !isRecord(value.runtimeHistory) || !isRecord(value.runtimeHistory.tips)
130
+ || !isRecord(value.runtimeHistory.seenNames) || !isRecord(value.visibleRuntimeFacts)
131
+ || !Array.isArray(value.surfaceNodes) || !value.surfaceNodes.every(isCanonicalSequence)
132
+ || new Set(value.surfaceNodes).size !== value.surfaceNodes.length
133
+ || !Array.isArray(value.timeline.calls) || !Array.isArray(value.timeline.boundaries)
134
+ || !isRecord(value.runtimeMessagesBySeq) || !isRecord(value.timeline.cordisTranscript)) {
135
+ throw new TypeError('invalid PTC session-log projection checkpoint')
136
+ }
137
+ validateSessionTimelineState(value.timeline)
138
+ if (!Array.isArray(value.logFacts)) throw new TypeError('invalid PTC session-log projection checkpoint facts')
139
+ const timelineFacts = new Map(value.timeline.eventFacts.map(fact => [fact.eventIndex, fact]))
140
+ let reconstructed = emptyLogState()
141
+ let lastIndex = -1
142
+ let timelineCount = 0
143
+ for (const fact of value.logFacts) {
144
+ if (!isRecord(fact) || Object.keys(fact).some(key => !['index', 'event'].includes(key))
145
+ || !isCanonicalSequence(fact.index) || fact.index <= lastIndex
146
+ || !isRecord(fact.event) || !isCanonicalSequence(fact.event.seq) || typeof fact.event.type !== 'string'
147
+ || !deepEqualJson(presentationEvent(fact.event), fact.event)) {
148
+ throw new TypeError('invalid PTC session-log projection checkpoint facts')
149
+ }
150
+ lastIndex = fact.index
151
+ const timelineFact = timelineFacts.get(fact.index)
152
+ if (timelineFact !== undefined) {
153
+ if (!deepEqualJson(presentationEvent(timelineFact.event), fact.event)) {
154
+ throw new TypeError('inconsistent PTC session-log projection checkpoint event identity')
155
+ }
156
+ timelineCount += 1
157
+ }
158
+ if (timelineFact === undefined
159
+ && advanceSessionTimeline(reconstructed.timeline, fact.event, fact.index) !== reconstructed.timeline) {
160
+ throw new TypeError('inconsistent PTC session-log projection checkpoint missing timeline facts')
161
+ }
162
+ const timeline = timelineFact === undefined ? reconstructed.timeline
163
+ : replaySessionTimelineFact(reconstructed.timeline, timelineFact)
164
+ reconstructed = applyLogEvent(reconstructed, fact.event, fact.index, timeline)
165
+ }
166
+ if (timelineCount !== timelineFacts.size || !deepEqualJson(reconstructed, value)) {
167
+ throw new TypeError('inconsistent PTC session-log projection checkpoint facts')
168
+ }
169
+ return deepFreeze(value)
170
+ } }),
171
+ init: emptyLogState,
172
+ apply: applyLogEvent,
173
+ })
174
+ }
175
+
176
+ export function createSessionLogOwner(ctx) {
177
+ const projection = createSessionLogProjection()
178
+ let registeredService
179
+ const injection = ctx.inject(['sessionProjections'], scope => {
180
+ try {
181
+ scope.effect(() => {
182
+ const registry = scope.sessionProjections
183
+ const unregister = registry.register(projection)
184
+ if (typeof unregister !== 'function') throw new Error('ptc-plus: sessionProjections.register did not return a disposer')
185
+ registeredService = registry
186
+ return async () => {
187
+ await unregister()
188
+ if (registeredService === registry) registeredService = undefined
189
+ }
190
+ }, 'ptc-plus host-only session-log projection')
191
+ } catch (error) {
192
+ ctx.logger?.warn?.('ptc-plus: session-log projection unavailable', error)
193
+ }
194
+ })
195
+ ctx.effect(() => () => typeof injection === 'function' ? injection() : injection?.dispose?.(),
196
+ 'ptc-plus session-log projection injection lifecycle')
197
+ const service = name => typeof ctx.get === 'function' ? ctx.get(name) : ctx[name]
198
+ return Object.freeze({
199
+ read: (session, signal) => readSessionLog(session, service('sessionQuery'), signal),
200
+ project(agent, requestedEdit) {
201
+ if (Array.isArray(agent?.session?.events)) return projectSessionLog(agent, requestedEdit)
202
+ const registry = registeredService ?? service('sessionProjections')
203
+ const state = agent?.session === undefined ? undefined : registry?.stateOf?.(agent.session, SESSION_LOG_PROJECTION_KEY)
204
+ return projectSessionLog(agent, requestedEdit, state)
205
+ },
206
+ })
50
207
  }
51
208
 
52
209
  /** Project session events into immutable facts consumed by prompt presentation. */
53
- export function projectSessionLog(agent, requestedEdit) {
54
- const events = sessionEvents(agent?.session)
55
- if (!Array.isArray(events)) {
210
+ export function projectSessionLog(agent, requestedEdit, projectedState) {
211
+ const events = projectedState === undefined ? sessionEvents(agent?.session) : undefined
212
+ if (projectedState === undefined && !Array.isArray(events)) {
56
213
  return Object.freeze({
57
214
  openTurn: false,
58
215
  contextStep: 0,
59
216
  systemPromptSnapshots: Object.freeze([]),
60
217
  ptcMessages: Object.freeze([]),
61
218
  visibleRuntimeMessages: undefined,
219
+ runtimeHistory: Object.freeze(createRuntimeHistory()),
62
220
  lastSuccessfulRunIndex: undefined,
63
221
  latestRun: undefined,
64
222
  editableRun: undefined,
@@ -66,45 +224,30 @@ export function projectSessionLog(agent, requestedEdit) {
66
224
  cordisTranscript: Object.freeze({ calls: 0, inspections: 0 }),
67
225
  })
68
226
  }
69
- let contextStep = 0
70
- const systemPromptSnapshots = []
71
- const ptcMessages = []
72
- const runtimeMessagesBySeq = new Map()
73
- for (let index = 0; index < events.length; index += 1) {
74
- const event = events[index]
75
- const sections = event?.type === 'user/message' ? systemPromptSnapshotSections(event.data) : undefined
76
- if (sections !== undefined) {
77
- const snapshot = Object.freeze({ index, contextStep, sections })
78
- systemPromptSnapshots.push(snapshot)
79
- runtimeMessagesBySeq.set(event.seq, Object.freeze({ ...snapshot, producer: 'aggregate', form: 'snapshot' }))
80
- }
81
- const ptc = event?.type === 'user/message' ? readRuntimeMessage(event.data) : undefined
82
- if (ptc !== undefined) {
83
- const record = Object.freeze({ ...ptc, index, contextStep, producer: 'ptc-plus' })
84
- ptcMessages.push(record)
85
- runtimeMessagesBySeq.set(event.seq, record)
86
- }
87
- if (isContextStep(event)) contextStep += 1
227
+ let state = projectedState ?? emptyLogState()
228
+ if (projectedState === undefined) {
229
+ for (let index = 0; index < events.length; index += 1) state = applyLogEvent(state, events[index], index)
88
230
  }
89
- const timeline = foldSessionTimeline(events)
231
+ const { timeline, contextStep, systemPromptSnapshots, ptcMessages, runtimeMessagesBySeq } = state
90
232
  const requestedEntry = requestedEdit === undefined
91
233
  ? undefined
92
- : timeline.executableCalls.get(requestedEdit.callSeq)
234
+ : timeline.executableCalls[`k${requestedEdit.callSeq}`]?.[1]
93
235
  const requestedEditTarget = requestedEdit !== undefined
94
236
  && requestedEntry?.event.data?.callId === requestedEdit.callId
95
237
  && requestedEntry.event.data.name === 'edit_run_code'
96
- ? timeline.editTargets.get(requestedEdit.callSeq)
238
+ ? timeline.editTargets[`k${requestedEdit.callSeq}`]?.[1] ?? undefined
97
239
  : undefined
98
240
  const latestRun = timeline.openTurn ? timeline.latestRun : undefined
99
241
  const editableRun = timeline.openTurn ? timeline.editableRun : undefined
100
- const cordisTranscript = cordisTranscriptFacts(timeline)
101
- let visibleRuntimeMessages
102
- try {
242
+ const cordisTranscript = Object.freeze(timeline.cordisTranscript)
243
+ let visibleRuntimeMessages = projectedState === undefined ? undefined : state.visibleRuntimeMessages
244
+ if (projectedState === undefined) try {
103
245
  const nodes = agent?.session?.surface?.nodes
104
- const knownSeqs = new Set(events.map(event => event?.seq))
105
- if (Array.isArray(nodes) && nodes.every(seq => isCanonicalSequence(seq) && knownSeqs.has(seq))) {
246
+ const knownSeqs = events === undefined ? undefined : new Set(events.map(event => event?.seq))
247
+ if (Array.isArray(nodes) && nodes.every(seq => isCanonicalSequence(seq)
248
+ && (knownSeqs === undefined ? seq < agent.session.seq : knownSeqs.has(seq)))) {
106
249
  visibleRuntimeMessages = Object.freeze(nodes.flatMap(seq => {
107
- const record = runtimeMessagesBySeq.get(seq)
250
+ const record = runtimeMessagesBySeq[`k${seq}`]
108
251
  return record === undefined ? [] : [record]
109
252
  }))
110
253
  }
@@ -115,6 +258,9 @@ export function projectSessionLog(agent, requestedEdit) {
115
258
  systemPromptSnapshots: Object.freeze(systemPromptSnapshots),
116
259
  ptcMessages: Object.freeze(ptcMessages),
117
260
  visibleRuntimeMessages,
261
+ visibleRuntimeFacts: projectedState === undefined && visibleRuntimeMessages !== undefined
262
+ ? runtimeMessageFacts(visibleRuntimeMessages) : state.visibleRuntimeFacts,
263
+ runtimeHistory: state.runtimeHistory,
118
264
  lastSuccessfulRunIndex: timeline.lastSuccessfulRunIndex,
119
265
  latestRun,
120
266
  editableRun,
@@ -244,6 +244,7 @@ class SessionKernel {
244
244
  }
245
245
 
246
246
  async execute(request, config) {
247
+ const observedSession = request.sessionLog ?? this.session
247
248
  const recoveryBoundaries = this.initialRecoveryBoundary === undefined
248
249
  ? []
249
250
  : [this.initialRecoveryBoundary]
@@ -256,7 +257,7 @@ class SessionKernel {
256
257
  if (this.surfaceProvenanceRequired
257
258
  && (currentGeneration !== this.surfaceGeneration || hasLiveState)) {
258
259
  this.surfaceGeneration = currentGeneration
259
- const visibleCallSeqs = visibleExecutableCallSeqs(this.session)
260
+ const visibleCallSeqs = visibleExecutableCallSeqs(observedSession)
260
261
  const contractsLiveState = [...this.liveCallSeqs].some(callSeq => (
261
262
  !(visibleCallSeqs instanceof Set) || !visibleCallSeqs.has(callSeq)
262
263
  ))
@@ -267,7 +268,7 @@ class SessionKernel {
267
268
  if (config.durableReplay) {
268
269
  this.recoveryNotice = undefined
269
270
  try {
270
- this.history = recoverJournal(this.session, request.callSeq, {
271
+ this.history = recoverJournal(observedSession, request.callSeq, {
271
272
  visibleCallSeqs,
272
273
  })
273
274
  } catch (error) {
@@ -309,9 +310,9 @@ class SessionKernel {
309
310
  if (!(error instanceof ReplayFailure)) throw error
310
311
  const previousPathLength = pathToHead(this.history).length
311
312
  const boundary = recoveryBoundaryForHistory(this.history, error.node)
312
- const recovered = recoverJournal(this.session, request.callSeq, {
313
+ const recovered = recoverJournal(observedSession, request.callSeq, {
313
314
  extraBoundaries: [boundary],
314
- visibleCallSeqs: visibleExecutableCallSeqs(this.session),
315
+ visibleCallSeqs: visibleExecutableCallSeqs(observedSession),
315
316
  })
316
317
  const nextPathLength = pathToHead(recovered).length
317
318
  if (nextPathLength >= previousPathLength) throw new Error('recovery did not contract the historical frontier')
@@ -631,6 +632,8 @@ export class SessionRuntime {
631
632
  this.kernels = new Map()
632
633
  this.pendingNoops = new Map()
633
634
  this.settlements = new WeakSet()
635
+ this.preparingCells = new Set()
636
+ this.submissions = new WeakSet()
634
637
  this.disposed = false
635
638
  this.userBindingsCwd = options.userBindingsCwd ?? process.cwd()
636
639
  if (typeof this.userBindingsCwd !== 'string' || !isAbsolute(this.userBindingsCwd)) {
@@ -638,6 +641,7 @@ export class SessionRuntime {
638
641
  }
639
642
  this.withInitiator = typeof options.withInitiator === 'function' ? options.withInitiator : undefined
640
643
  this.observeSession = options.observeSession ?? (() => false)
644
+ this.readSessionLog = options.readSessionLog ?? (async session => session)
641
645
  this.disposal = undefined
642
646
  }
643
647
 
@@ -647,8 +651,31 @@ export class SessionRuntime {
647
651
  return execution.result
648
652
  }
649
653
 
654
+ beginSubmission(sessionContext, signal) {
655
+ const admission = { sessionId: sessionOf(sessionContext).id, config: this.config,
656
+ signal, cancelled: this.disposed }
657
+ this.submissions.add(admission)
658
+ if (!this.disposed) this.preparingCells.add(admission)
659
+ return admission
660
+ }
661
+
662
+ releaseSubmission(admission) {
663
+ this.preparingCells.delete(admission)
664
+ this.submissions.delete(admission)
665
+ }
666
+
667
+ assertSubmission(admission) {
668
+ if (this.disposed) throw new Error('PTC runtime disposed')
669
+ if (admission.cancelled || !this.submissions.has(admission)) throw new Error('PTC session disposed')
670
+ }
671
+
650
672
  reconfigure(config) {
651
673
  const resolved = resolvedRuntimeConfig(config)
674
+ for (const admission of this.preparingCells) {
675
+ if (!admission.signal?.aborted && resolved.maxOldGenerationSizeMb !== admission.config.maxOldGenerationSizeMb) {
676
+ throw new Error('ptc-plus: maxOldGenerationSizeMb cannot change while a session worker is active; retry after the session is disposed')
677
+ }
678
+ }
652
679
  const kernels = [...this.kernels.values()]
653
680
  for (const kernel of kernels) kernel.assertReconfigurationAllowed(resolved)
654
681
  for (const kernel of kernels) kernel.reconfigure(resolved)
@@ -665,7 +692,6 @@ export class SessionRuntime {
665
692
  async runTentative(sessionContext, request) {
666
693
  const completed = result => Object.freeze({ result, settlement: undefined })
667
694
  if (this.disposed) return completed({ logs: [], error: { kind: 'abort', message: 'PTC runtime disposed' } })
668
- const cellConfig = this.config
669
695
  let bindingDescriptors
670
696
  try {
671
697
  bindingDescriptors = normalizeBindingDescriptors(request?.bindings)
@@ -683,34 +709,49 @@ export class SessionRuntime {
683
709
  } = sessionOf(sessionContext)
684
710
  let callSeq
685
711
  let sourceCallSeq
712
+ let sessionLog
713
+ const admission = sessionContext?.submission ?? this.beginSubmission(sessionContext, request.signal)
714
+ if (!this.submissions.has(admission) || admission.sessionId !== sessionId) {
715
+ return completed({ logs: [], error: { kind: 'exception', message: 'PTC submission does not belong to this session runtime' } })
716
+ }
717
+ const cellConfig = admission.config
718
+ const aborted = () => this.disposed || admission.cancelled
719
+ const abortResult = () => completed({ logs: [], error: { kind: 'abort',
720
+ message: request.signal?.aborted ? String(request.signal.reason)
721
+ : this.disposed ? 'PTC runtime disposed' : 'PTC session disposed' } })
686
722
  try {
723
+ sessionLog = sessionContext?.sessionLog ?? await this.readSessionLog(session, request.signal)
724
+ if (aborted()) return abortResult()
687
725
  if (persistedCallSeq !== undefined
688
726
  && !isCanonicalSequence(persistedCallSeq)) {
689
727
  throw new Error('persisted tool call sequence must be a non-negative safe integer')
690
728
  }
691
729
  if (cellConfig.durableReplay) {
692
- sourceCallSeq = liveToolCallSeq(session, callId, 'run_code')
730
+ sourceCallSeq = liveToolCallSeq(sessionLog, callId, 'run_code')
693
731
  callSeq = persistedCallSeq ?? sourceCallSeq
694
732
  } else {
695
733
  sourceCallSeq = persistedCallSeq
696
734
  if (sourceCallSeq === undefined) {
697
735
  try {
698
- sourceCallSeq = liveToolCallSeq(session, callId, 'run_code')
736
+ sourceCallSeq = liveToolCallSeq(sessionLog, callId, 'run_code')
699
737
  } catch {
700
738
  sourceCallSeq = undefined
701
739
  }
702
740
  }
703
741
  }
704
742
  } catch (error) {
743
+ if (aborted() || request.signal?.aborted) return abortResult()
705
744
  return completed({ logs: [], error: { kind: 'recovery', message: `cannot identify current run_code call in session log: ${messageOf(error)}` } })
745
+ } finally {
746
+ this.releaseSubmission(admission)
706
747
  }
707
748
  let kernel = this.kernels.get(sessionId)
708
749
  if (kernel === undefined) {
709
750
  let history
710
751
  try {
711
752
  history = cellConfig.durableReplay
712
- ? recoverJournal(session, callSeq, {
713
- visibleCallSeqs: visibleExecutableCallSeqs(session),
753
+ ? recoverJournal(sessionLog, callSeq, {
754
+ visibleCallSeqs: visibleExecutableCallSeqs(sessionLog),
714
755
  })
715
756
  : emptyHistory()
716
757
  } catch (error) {
@@ -742,7 +783,7 @@ export class SessionRuntime {
742
783
  const workerReservation = kernel.reserveWorkerConfiguration(cellConfig)
743
784
  let result
744
785
  try {
745
- result = await kernel.run({ ...request, journal, callSeq, sourceCallSeq }, cellConfig)
786
+ result = await kernel.run({ ...request, journal, callSeq, sourceCallSeq, sessionLog }, cellConfig)
746
787
  } finally {
747
788
  kernel.releaseWorkerConfiguration(workerReservation)
748
789
  }
@@ -793,6 +834,12 @@ export class SessionRuntime {
793
834
 
794
835
  async disposeSession(sessionId) {
795
836
  const id = String(sessionId)
837
+ for (const admission of this.preparingCells) {
838
+ if (admission.sessionId === id) {
839
+ admission.cancelled = true
840
+ this.preparingCells.delete(admission)
841
+ }
842
+ }
796
843
  const kernel = this.kernels.get(id)
797
844
  this.pendingNoops.delete(id)
798
845
  if (kernel === undefined) return
@@ -815,6 +862,7 @@ export class SessionRuntime {
815
862
 
816
863
  async #dispose() {
817
864
  this.disposed = true
865
+ this.preparingCells.clear()
818
866
  const entries = [...this.kernels]
819
867
  const results = await Promise.allSettled(entries.map(async ([id, kernel]) => {
820
868
  await kernel.dispose()
@@ -63,7 +63,14 @@ export function installSettingsSectionCompat({
63
63
  }) {
64
64
  ctx.inject(['settings'], settingsContext => {
65
65
  const provider = settingsContext.settings
66
- onProvider(provider)
66
+ const sectionInstalled = typeof provider?.installSection === 'function'
67
+ || typeof settingsModule?.installSettingsSection === 'function'
68
+ const writeNamespace = sectionInstalled ? namespace : (ownerFiber ?? ctx.fiber)?.entry?.options?.id
69
+ const writer = typeof provider?.update === 'function'
70
+ && typeof writeNamespace === 'string' && writeNamespace.length > 0
71
+ ? Object.freeze({ update: patch => provider.update(writeNamespace, patch) }) : undefined
72
+ const unbind = onProvider(provider, writer)
73
+ if (typeof unbind === 'function') settingsContext.effect(() => unbind, 'ptc-plus settings writer')
67
74
  if (typeof provider?.installSection === 'function') {
68
75
  provider.installSection(ctx, namespace, schema, entry, hooks)
69
76
  return
@@ -79,7 +86,10 @@ export function installSettingsSectionCompat({
79
86
  // fiber, so the owner is the plugin's entry context rather than the
80
87
  // scope this installation happens to run in.
81
88
  const dispose = provider.configure({ auto: false }, ownerFiber ?? ctx.fiber)
82
- ctx.effect?.(() => dispose, 'ptc-plus settings page policy')
89
+ // The policy belongs to the settings surface that accepted it, so its
90
+ // disposer is released when that surface goes away instead of
91
+ // accumulating on the plugin scope across settings reloads.
92
+ settingsContext.effect?.(() => dispose, 'ptc-plus settings page policy')
83
93
  }
84
94
  return
85
95
  }
@@ -274,7 +274,7 @@ export function createUserBindingsOwner(ctx, options = {}) {
274
274
  entry: current.entry,
275
275
  })
276
276
  const candidateView = current => Object.freeze({ requestId: current.requestId, commandId: current.commandId, ...draftView(current) })
277
- const settleDraft = (current, state, enabled = false) => {
277
+ const settleDraft = async (current, state, enabled = false) => {
278
278
  const retainReview = draftsByCapability.get(current.capability) === current
279
279
  removeDraft(current)
280
280
  const action = Object.freeze({ requestId: current.requestId, id: current.entry.id, state, enabled })
@@ -282,7 +282,8 @@ export function createUserBindingsOwner(ctx, options = {}) {
282
282
  reviewsByCapability.set(current.capability, { agent: current.agent, candidate: candidateView(current), action })
283
283
  }
284
284
  try {
285
- const accepting = sessionEvents(current.agent.session)?.find(event => (
285
+ const sessionLog = await (options.readSessionLog ?? (async session => session))(current.agent.session)
286
+ const accepting = sessionEvents(sessionLog)?.find(event => (
286
287
  event.type === 'tool/result' && event.surfaceOp === 'append'
287
288
  && event.data?.meta?.[USER_BINDING_DRAFT_META_KEY]?.candidate?.requestId === current.requestId
288
289
  ))
@@ -303,7 +304,7 @@ export function createUserBindingsOwner(ctx, options = {}) {
303
304
  drafts.delete(current.sessionId)
304
305
  }
305
306
  }
306
- const clearDraft = (capability, version) => {
307
+ const clearDraft = async (capability, version) => {
307
308
  const current = draftFor(capability)
308
309
  if (current === null) return null
309
310
  if (!Number.isSafeInteger(version) || version < 1 || version !== current.version) {
@@ -314,7 +315,7 @@ export function createUserBindingsOwner(ctx, options = {}) {
314
315
  if (current.state !== 'ready') {
315
316
  throw Object.assign(new Error('binding draft is being saved'), { code: 'BINDINGS_BUSY' })
316
317
  }
317
- settleDraft(current, 'discarded')
318
+ await settleDraft(current, 'discarded')
318
319
  return null
319
320
  }
320
321
  const saveDraft = async (capability, version, expectedRevision, activate = false) => {
@@ -333,7 +334,7 @@ export function createUserBindingsOwner(ctx, options = {}) {
333
334
  const result = current.mode === 'new'
334
335
  ? await store.create(entry, expectedRevision)
335
336
  : await store.update(entry, expectedRevision)
336
- settleDraft(current, 'saved', activate === true)
337
+ await settleDraft(current, 'saved', activate === true)
337
338
  return result
338
339
  } catch (error) {
339
340
  if (drafts.get(current.sessionId) === current && current.state === 'saving') {
package/locale/en.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "PTC Plus",
4
+ "description": "A session-bound agent-native REPL for DeepSeek Harness PTC mode."
5
+ }
6
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "PTC Plus",
4
+ "description": "面向 DeepSeek Harness PTC 模式的会话级 Agent 原生 REPL。"
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,14 +1,16 @@
1
1
  {
2
2
  "name": "dsh-ptc-plus",
3
- "version": "0.4.3",
3
+ "version": "0.4.10",
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",
7
7
  "main": "./index.js",
8
8
  "exports": {
9
9
  ".": "./index.js",
10
+ "./execution-provider": "./internal/execution-provider-owner.js",
10
11
  "./client": "./client.js",
11
- "./package.json": "./package.json"
12
+ "./package.json": "./package.json",
13
+ "./locale/*.json": "./locale/*.json"
12
14
  },
13
15
  "files": [
14
16
  "index.js",
@@ -29,15 +31,12 @@
29
31
  "assets/ptc-plus-icon.svg",
30
32
  "assets/dsh-ptc-plus-banner-en.webp",
31
33
  "assets/dsh-ptc-plus-banner-zh.webp",
32
- "assets/ptc-plus-bindings-en.png",
33
- "assets/ptc-plus-bindings-zh.png",
34
- "assets/ptc-plus-repair-zh.png",
35
- "assets/ptc-plus-repair-en.png",
36
34
  "assets/ptc-plus-repl-workspace-en.png",
37
35
  "assets/ptc-plus-repl-workspace-zh.png",
38
36
  "assets/ptc-plus-settings-en.png",
39
37
  "assets/ptc-plus-settings-zh.png",
40
38
  "cordis.patch.yml",
39
+ "locale",
41
40
  "README.md",
42
41
  "README.en.md",
43
42
  "LICENSE"
@@ -64,6 +63,7 @@
64
63
  "@codemirror/state": "^6.7.4",
65
64
  "@codemirror/view": "^6.43.11",
66
65
  "@deepseek-ai/schemastery": "^3.18.3",
66
+ "@deepseek-ai/dsh-util-values": "^0.2.0-rc.2 || ^0.2.1-alpha.1",
67
67
  "@jridgewell/trace-mapping": "^0.3.31",
68
68
  "@lezer/highlight": "^1.2.3",
69
69
  "acorn": "^8.15.0",
@@ -71,6 +71,7 @@
71
71
  "codemirror": "^6.0.2",
72
72
  "fflate": "^0.8.3",
73
73
  "pathe": "^2.0.3",
74
+ "react-is": "^19.3.0",
74
75
  "tslib": "^2.8.1",
75
76
  "typescript": "^6.0.3"
76
77
  },
@@ -88,10 +89,15 @@
88
89
  "build": "node scripts/client-bundle.mjs build && node scripts/compiler-bundle.mjs build",
89
90
  "build:check": "node scripts/client-bundle.mjs check && node scripts/compiler-bundle.mjs check",
90
91
  "test:client": "vitest run --config scripts/client-test.config.mjs",
92
+ "test:client:visual": "node scripts/client-visual-check.mjs",
91
93
  "test:semantics": "node --test --test-concurrency=1 test/semantic-test262.test.js test/semantic-dialect.test.js test/semantic-boundaries.test.js",
92
94
  "semantic:check": "node scripts/semantic-obligations.mjs",
93
95
  "test:client:web": "node scripts/client-web-smoke.mjs",
96
+ "test:client:fallback": "node scripts/client-fallback-smoke.mjs",
94
97
  "test:client:layout": "node scripts/client-binding-layout.mjs",
98
+ "host:baseline:update": "node scripts/dsh-dependency-baseline.mjs update",
99
+ "host:baseline:verify": "node scripts/dsh-dependency-baseline.mjs verify",
100
+ "test:host-contract": "node scripts/dsh-host-contract-suite.mjs",
95
101
  "prepack": "npm run build:check",
96
102
  "hooks:install": "node scripts/review-findings.mjs install-hook",
97
103
  "review:new": "node scripts/review-findings.mjs create",
@@ -106,7 +112,9 @@
106
112
  "session:migrate": "node scripts/migrate-session-log.mjs",
107
113
  "coverage": "node scripts/coverage.mjs",
108
114
  "coverage:focus": "node scripts/coverage.mjs --focus",
109
- "verify": "npm run review:validate && 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 && npm run test:client && npm run coverage",
115
+ "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",
116
+ "verify": "npm run review:validate && npm run host:baseline:verify && 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 && npm run test:client:visual && npm run coverage",
117
+ "verify:ci": "npm run review:validate && npm run host:baseline:verify && 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 && npm run test:client:visual && npm run test:ci:runtime",
110
118
  "check": "node scripts/review-findings.mjs check",
111
119
  "test:expensive": "node scripts/expensive-headless-acceptance.mjs",
112
120
  "test:ab": "node scripts/ab-headless-trajectories.mjs"
@@ -134,23 +142,20 @@
134
142
  },
135
143
  "client": {
136
144
  "platform": "web",
137
- "external": [
138
- "react"
139
- ],
145
+ "immediately": true,
140
146
  "inject": [
141
147
  "@deepseek-ai/dsh-api-gateway",
142
148
  "@deepseek-ai/dsh-api-remotes",
143
- "@deepseek-ai/dsh-client-runtime",
144
149
  "@deepseek-ai/dsh-client-locale",
145
150
  "@deepseek-ai/dsh-client-ui-settings",
146
151
  "@deepseek-ai/dsh-client-ui-settings-plugins",
147
152
  "@deepseek-ai/dsh-client-ui-conversation",
148
- "@deepseek-ai/dsh-client-connection",
149
- "@deepseek-ai/dsh-client-ui-primitives"
153
+ "@deepseek-ai/dsh-client-connection"
150
154
  ]
151
155
  }
152
156
  },
153
157
  "devDependencies": {
158
+ "@deepseek-ai/dsh": "next",
154
159
  "@deepseek-ai/dsh-agent-loop": "next",
155
160
  "@deepseek-ai/dsh-api-gateway": "next",
156
161
  "@deepseek-ai/dsh-api-remotes": "next",