dsh-ptc-plus 0.2.4 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.en.md +177 -0
  2. package/README.md +85 -69
  3. package/assets/ptc-plus-bindings-en.png +0 -0
  4. package/assets/ptc-plus-bindings-zh.png +0 -0
  5. package/assets/ptc-plus-repair-en.png +0 -0
  6. package/assets/ptc-plus-settings-en.png +0 -0
  7. package/assets/ptc-plus-settings-zh.png +0 -0
  8. package/client.js +1168 -144
  9. package/docs/adr/0006-rewrite-module-syntax-with-an-ast.md +1 -1
  10. package/docs/adr/0007-separate-worker-transport-from-session-semantics.md +15 -3
  11. package/docs/adr/0013-isolate-repl-redeclaration-convenience.md +7 -3
  12. package/docs/adr/0014-persist-cell-rewrite-policy.md +7 -5
  13. package/docs/adr/0019-plugin-settings-and-kill-switch.md +19 -4
  14. package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +2 -2
  15. package/docs/adr/0021-separate-host-bootstrap-from-session-runtime.md +5 -3
  16. package/docs/adr/0022-stage-npm-releases-with-oidc.md +2 -2
  17. package/docs/architecture.md +23 -14
  18. package/docs/client-ui.md +29 -11
  19. package/docs/durability-design.md +42 -20
  20. package/docs/installation.md +11 -3
  21. package/docs/program-data-plane.md +6 -0
  22. package/docs/runtime-reference.md +27 -14
  23. package/docs/value-wire.md +1 -1
  24. package/index.js +38 -4
  25. package/internal/cell-analysis.js +89 -12
  26. package/internal/cell-rewriter.js +162 -4
  27. package/internal/config-spec.js +192 -117
  28. package/internal/direct-surface-owner.js +56 -5
  29. package/internal/edit-transport-owner.js +11 -2
  30. package/internal/failure-reporting.js +15 -0
  31. package/internal/kernel-worker.js +11 -0
  32. package/internal/rejected-cell-editor.js +21 -12
  33. package/internal/repl-convenience.js +94 -6
  34. package/internal/repl-memory-projection.js +277 -0
  35. package/internal/repl-rewrite-contract.js +15 -0
  36. package/internal/run-code-description.js +34 -0
  37. package/internal/runtime-bridge-owner.js +63 -15
  38. package/internal/runtime-config.js +3 -0
  39. package/internal/session-cell-executor.js +74 -14
  40. package/internal/session-events.js +7 -0
  41. package/internal/session-journal.js +331 -63
  42. package/internal/session-log-view.js +2 -1
  43. package/internal/session-runtime.js +110 -9
  44. package/internal/session-state.js +116 -4
  45. package/package.json +10 -7
  46. package/src/client-activity.js +501 -0
  47. package/src/client.js +456 -20
  48. package/README.zh.md +0 -161
  49. /package/assets/{ptc-plus-repair.png → ptc-plus-repair-zh.png} +0 -0
@@ -7,12 +7,17 @@ import {
7
7
  pathToHead,
8
8
  reduceStateOperations,
9
9
  recoverJournal,
10
+ visibleExecutableCallSeqs,
10
11
  } from './session-journal.js'
11
12
  import { normalizeBindingDescriptors } from './binding-descriptors.js'
12
13
  import { resolveConfig } from './runtime-config.js'
13
14
  import { WorkerClient } from './worker-client.js'
14
15
  import { BindingCatalog, durabilityState, transitionDurability } from './session-state.js'
15
16
  import { SessionCellExecutor } from './session-cell-executor.js'
17
+ import {
18
+ createReplMemorySnapshot,
19
+ unavailableReplMemorySnapshot,
20
+ } from './repl-memory-projection.js'
16
21
 
17
22
  const WORKER_URL = new URL('./kernel-worker.js', import.meta.url)
18
23
  function recoveryDiagnostic(count) {
@@ -37,6 +42,13 @@ function rewritePolicy(config) {
37
42
  }
38
43
  }
39
44
 
45
+ function bindingPolicy(config) {
46
+ return {
47
+ variableRedeclarations: config.looseTopLevelRedeclarations,
48
+ functionClassRedeclarations: config.looseTopLevelFunctionClassRedeclarations,
49
+ }
50
+ }
51
+
40
52
  function resolvedRuntimeConfig(config) {
41
53
  return Object.freeze(resolveConfig(config))
42
54
  }
@@ -63,12 +75,25 @@ class SessionKernel {
63
75
  constructor({ config, history, cwd, session, withInitiator }) {
64
76
  this.config = config
65
77
  this.history = history
78
+ this.initialRecoveryBoundary = history.available === false && history.volatileSuffix.length > 0
79
+ ? {
80
+ failedCallSeq: history.volatileSuffix[0].seq,
81
+ frontierCallSeq: history.head === undefined ? null : history.nodes[history.head]?.callSeq ?? null,
82
+ }
83
+ : undefined
84
+ this.surfaceGeneration = undefined
85
+ try {
86
+ this.surfaceGeneration = session?.surface?.replaceGeneration
87
+ } catch {
88
+ this.surfaceGeneration = undefined
89
+ }
66
90
  this.cwd = cwd
67
91
  this.session = session
68
92
  this.withInitiator = withInitiator
69
93
  this.durability = durabilityState()
70
94
  this.bindingCatalog = new BindingCatalog()
71
95
  this.replayed = false
96
+ this.liveCallSeqs = new Set()
72
97
  this.recoveryNotice = history.volatileSuffix.length === 0
73
98
  ? undefined
74
99
  : recoveryDiagnostic(history.volatileSuffix.length)
@@ -132,10 +157,62 @@ class SessionKernel {
132
157
  }
133
158
 
134
159
  async execute(request, config) {
135
- const recoveryBoundaries = []
160
+ const recoveryBoundaries = this.initialRecoveryBoundary === undefined
161
+ ? []
162
+ : [this.initialRecoveryBoundary]
163
+ this.initialRecoveryBoundary = undefined
136
164
  const finishResult = result => recoveryBoundaries.length === 0
137
165
  ? result
138
166
  : { ...result, recoveryBoundaries: recoveryBoundaries.map(boundary => ({ ...boundary })) }
167
+ let currentGeneration
168
+ try {
169
+ currentGeneration = this.session?.surface?.replaceGeneration
170
+ } catch {
171
+ currentGeneration = undefined
172
+ }
173
+ if (currentGeneration !== undefined && currentGeneration !== this.surfaceGeneration) {
174
+ this.surfaceGeneration = currentGeneration
175
+ const visibleCallSeqs = visibleExecutableCallSeqs(this.session)
176
+ const contractsLiveState = [...this.liveCallSeqs].some(callSeq => (
177
+ !(visibleCallSeqs instanceof Set) || !visibleCallSeqs.has(callSeq)
178
+ ))
179
+ if ((config.durableReplay && !this.replayed) || contractsLiveState) {
180
+ const worker = this.client.worker
181
+ if (worker !== undefined) await this.client.reset(worker)
182
+ this.rollbackToDurable()
183
+ if (config.durableReplay) {
184
+ this.recoveryNotice = undefined
185
+ try {
186
+ this.history = recoverJournal(this.session, request.callSeq, {
187
+ visibleCallSeqs,
188
+ })
189
+ } catch (error) {
190
+ this.history = emptyHistory()
191
+ this.recoveryNotice = recoveryDiagnostic(1)
192
+ this.initialRecoveryBoundary = undefined
193
+ }
194
+ if (this.history.volatileSuffix.length > 0) {
195
+ this.recoveryNotice = recoveryDiagnostic(this.history.volatileSuffix.length)
196
+ }
197
+ this.initialRecoveryBoundary = this.history.available === false && this.history.volatileSuffix.length > 0
198
+ ? {
199
+ failedCallSeq: this.history.volatileSuffix[0].seq,
200
+ frontierCallSeq: this.history.head === undefined
201
+ ? null
202
+ : this.history.nodes[this.history.head]?.callSeq ?? null,
203
+ }
204
+ : undefined
205
+ if (this.initialRecoveryBoundary !== undefined) {
206
+ recoveryBoundaries.push(this.initialRecoveryBoundary)
207
+ this.initialRecoveryBoundary = undefined
208
+ }
209
+ } else {
210
+ this.history = emptyHistory()
211
+ this.replayed = true
212
+ this.recoveryNotice = recoveryDiagnostic(1)
213
+ }
214
+ }
215
+ }
139
216
  if (!this.replayed && config.durableReplay) {
140
217
  let skipped = 0
141
218
  while (!this.replayed) {
@@ -171,6 +248,7 @@ class SessionKernel {
171
248
  }
172
249
  const recovered = recoverJournal(this.session, request.callSeq, {
173
250
  extraBoundaries: [boundary],
251
+ visibleCallSeqs: visibleExecutableCallSeqs(this.session),
174
252
  })
175
253
  recoveryBoundaries.push(boundary)
176
254
  this.history = recovered
@@ -241,6 +319,7 @@ class SessionKernel {
241
319
  throw new ReplayFailure(node, error)
242
320
  }
243
321
  }
322
+ for (const node of path) this.liveCallSeqs.add(node.callSeq)
244
323
  }
245
324
 
246
325
  completeJournal(journal, status, result, volatileReason, diagnostics = [], completion = undefined) {
@@ -268,6 +347,7 @@ class SessionKernel {
268
347
  this.durability = durabilityState()
269
348
  this.replayed = false
270
349
  this.bindingCatalog = new BindingCatalog()
350
+ this.liveCallSeqs.clear()
271
351
  }
272
352
 
273
353
  settleCell(active, result, terminate = false) {
@@ -300,14 +380,17 @@ class SessionKernel {
300
380
  callSeq: request.callSeq,
301
381
  program: request.program,
302
382
  bindingCatalog: active.appliedBindingCatalog
303
- ?? active.priorBindingCatalog.advance(prepared),
383
+ ?? active.priorBindingCatalog.advance(prepared, request.program),
304
384
  worker,
305
385
  })
306
386
  }
307
387
  }
308
388
  if (replay !== undefined && !terminate) {
309
389
  this.bindingCatalog = active.appliedBindingCatalog
310
- ?? active.priorBindingCatalog.advance(prepared)
390
+ ?? active.priorBindingCatalog.advance(prepared, request.program)
391
+ }
392
+ if (replay === undefined && !terminate) {
393
+ this.liveCallSeqs.add(request.callSeq ?? request.sourceCallSeq)
311
394
  }
312
395
  this.active = undefined
313
396
  if (terminate) void this.client.reset(worker)
@@ -353,6 +436,18 @@ class SessionKernel {
353
436
  }
354
437
  }
355
438
 
439
+ replMemoryFor(journal) {
440
+ const tentative = this.tentatives.get(journal)
441
+ if (journal.status === 'discarded'
442
+ || journal.operations.some(operation => operation.action === 'restore')) {
443
+ return unavailableReplMemorySnapshot()
444
+ }
445
+ if (tentative === undefined) {
446
+ return createReplMemorySnapshot(this.bindingCatalog.snapshot())
447
+ }
448
+ return createReplMemorySnapshot(tentative.bindingCatalog.snapshot())
449
+ }
450
+
356
451
  finishStateOperations(operations, index, worker) {
357
452
  const transition = reduceStateOperations(this.history, operations, index)
358
453
  this.history.head = transition.head
@@ -442,10 +537,13 @@ export class SessionRuntime {
442
537
  sourceCallSeq = liveToolCallSeq(session, callId, 'run_code')
443
538
  callSeq = persistedCallSeq ?? sourceCallSeq
444
539
  } else {
445
- try {
446
- sourceCallSeq = liveToolCallSeq(session, callId, 'run_code')
447
- } catch {
448
- sourceCallSeq = undefined
540
+ sourceCallSeq = persistedCallSeq
541
+ if (sourceCallSeq === undefined) {
542
+ try {
543
+ sourceCallSeq = liveToolCallSeq(session, callId, 'run_code')
544
+ } catch {
545
+ sourceCallSeq = undefined
546
+ }
449
547
  }
450
548
  }
451
549
  } catch (error) {
@@ -456,7 +554,9 @@ export class SessionRuntime {
456
554
  let history
457
555
  try {
458
556
  history = cellConfig.durableReplay
459
- ? recoverJournal(session, callSeq)
557
+ ? recoverJournal(session, callSeq, {
558
+ visibleCallSeqs: visibleExecutableCallSeqs(session),
559
+ })
460
560
  : emptyHistory()
461
561
  } catch (error) {
462
562
  return completed({ logs: [], error: { kind: 'recovery', message: `cannot reconstruct REPL from session log: ${messageOf(error)}` } })
@@ -473,7 +573,7 @@ export class SessionRuntime {
473
573
  const journal = createJournal(
474
574
  /* c8 ignore next */
475
575
  this.pendingNoops.get(sessionId) ?? [],
476
- cellConfig.looseTopLevelRedeclarations ? 'loose' : 'strict',
576
+ bindingPolicy(cellConfig),
477
577
  rewritePolicy(cellConfig),
478
578
  )
479
579
  const workerReservation = kernel.reserveWorkerConfiguration(cellConfig)
@@ -487,6 +587,7 @@ export class SessionRuntime {
487
587
  journal,
488
588
  kernel,
489
589
  sessionId,
590
+ replMemory: kernel.replMemoryFor(journal),
490
591
  ...(result.recoveryBoundaries === undefined
491
592
  ? {}
492
593
  : { recoveryBoundaries: result.recoveryBoundaries }),
@@ -19,13 +19,28 @@ export function transitionDurability(state, transition) {
19
19
 
20
20
  export class BindingCatalog {
21
21
  #known
22
+ #kinds
23
+ #definitions
22
24
  #imports
23
25
  #namespaces
26
+ #writable
24
27
 
25
- constructor(known = new Set(), imports = new Map(), namespaces = new Set()) {
28
+ constructor(
29
+ known = new Set(),
30
+ imports = new Map(),
31
+ namespaces = new Set(),
32
+ kinds = new Map(),
33
+ definitions = new Map(),
34
+ writable = new Set(),
35
+ ) {
26
36
  this.#known = new Set(known)
37
+ this.#kinds = new Map([...this.#known].map(name => [name, kinds.get(name) ?? 'variable']))
38
+ this.#definitions = new Map([...this.#known]
39
+ .filter(name => definitions.has(name))
40
+ .map(name => [name, definitions.get(name)]))
27
41
  this.#imports = new Map(imports)
28
42
  this.#namespaces = new Set(namespaces)
43
+ this.#writable = new Set([...writable].filter(name => this.#known.has(name)))
29
44
  Object.freeze(this)
30
45
  }
31
46
 
@@ -34,12 +49,109 @@ export class BindingCatalog {
34
49
  knownBindings: new Set(this.#known),
35
50
  importBindings: new Map(this.#imports),
36
51
  importNamespaces: new Set(this.#namespaces),
52
+ writableBindings: new Set(this.#writable),
37
53
  }
38
54
  }
39
55
 
40
- advance(prepared) {
56
+ advance(prepared, source = undefined, committedRedeclarations = undefined) {
41
57
  const known = new Set(this.#known)
42
- for (const name of prepared.declared) known.add(name)
43
- return new BindingCatalog(known, prepared.imports, prepared.importNamespaces)
58
+ const kinds = new Map(this.#kinds)
59
+ const definitions = new Map(this.#definitions)
60
+ const writable = new Set(this.#writable)
61
+ const touched = new Set()
62
+ const redeclared = new Set((prepared.redeclared ?? []).map(declaration => declaration.name))
63
+ const commitGated = prepared.commitTargets
64
+ const committed = committedRedeclarations instanceof Set
65
+ ? committedRedeclarations
66
+ : new Set([...redeclared, ...commitGated])
67
+ const uncommitted = new Set()
68
+ const extractDefinition = sourceDefinitionExtractor(source)
69
+ for (const declaration of prepared.declarations ?? []) {
70
+ if (typeof declaration?.name !== 'string') continue
71
+ const dependency = typeof declaration.commitDependency === 'string'
72
+ && commitGated.has(declaration.commitDependency)
73
+ ? declaration.commitDependency
74
+ : commitGated.has(declaration.name) ? declaration.name : undefined
75
+ if (dependency !== undefined && !committed.has(dependency)) {
76
+ uncommitted.add(declaration.name)
77
+ continue
78
+ }
79
+ touched.add(declaration.name)
80
+ kinds.set(declaration.name, declaration.kind ?? 'variable')
81
+ const definition = extractDefinition(declaration.definitionSpan)
82
+ if (definition !== undefined) definitions.set(declaration.name, definition)
83
+ if (!redeclared.has(declaration.name)) {
84
+ if (declaration.writable === true) writable.add(declaration.name)
85
+ else writable.delete(declaration.name)
86
+ }
87
+ }
88
+ for (const name of prepared.declared) {
89
+ if (!uncommitted.has(name)) touched.add(name)
90
+ }
91
+ for (const name of touched) {
92
+ known.delete(name)
93
+ known.add(name)
94
+ }
95
+ const imports = new Map(prepared.imports)
96
+ for (const [name, binding] of imports) {
97
+ if (typeof binding?.commitDependency !== 'string'
98
+ || !commitGated.has(binding.commitDependency)
99
+ || committed.has(binding.commitDependency)) continue
100
+ if (this.#imports.has(name)) imports.set(name, this.#imports.get(name))
101
+ else imports.delete(name)
102
+ }
103
+ return new BindingCatalog(known, imports, prepared.importNamespaces, kinds, definitions, writable)
104
+ }
105
+
106
+ snapshot() {
107
+ return [...this.#known].reverse()
108
+ .filter(name => !this.#namespaces.has(name))
109
+ .map(name => ({
110
+ name,
111
+ kind: this.#kinds.get(name) ?? 'variable',
112
+ ...(this.#definitions.has(name) ? { definition: this.#definitions.get(name) } : {}),
113
+ }))
114
+ }
115
+ }
116
+
117
+ const MAX_DEFINITION_SOURCE_LENGTH = 1024
118
+
119
+ function lineOffsets(source) {
120
+ const offsets = [0]
121
+ for (let index = 0; index < source.length; index += 1) {
122
+ if (source.charCodeAt(index) === 10) offsets.push(index + 1)
123
+ }
124
+ return offsets
125
+ }
126
+
127
+ function offsetAt(offsets, sourceLength, position) {
128
+ if (position === null || typeof position !== 'object'
129
+ || !Number.isSafeInteger(position.line) || position.line < 1
130
+ || !Number.isSafeInteger(position.column) || position.column < 1
131
+ || position.line > offsets.length) return undefined
132
+ const lineStart = offsets[position.line - 1]
133
+ const lineEnd = position.line < offsets.length ? offsets[position.line] - 1 : sourceLength
134
+ const offset = lineStart + position.column - 1
135
+ return offset <= lineEnd ? offset : undefined
136
+ }
137
+
138
+ function sourceDefinitionExtractor(source) {
139
+ const offsets = typeof source === 'string' ? lineOffsets(source) : undefined
140
+ const extracted = new Map()
141
+ return span => {
142
+ if (offsets === undefined || span === null || typeof span !== 'object') return undefined
143
+ const start = offsetAt(offsets, source.length, span)
144
+ const end = offsetAt(offsets, source.length, span.end)
145
+ if (start === undefined || end === undefined || end <= start) return undefined
146
+ const key = `${start}:${end}`
147
+ if (extracted.has(key)) return extracted.get(key)
148
+ const length = end - start
149
+ const bounded = length <= MAX_DEFINITION_SOURCE_LENGTH
150
+ ? source.slice(start, end)
151
+ : `${source.slice(start, start + MAX_DEFINITION_SOURCE_LENGTH - 3)}...`
152
+ if (bounded.length === 0) return undefined
153
+ const definition = Object.freeze({ source: bounded, line: span.line, column: span.column })
154
+ extracted.set(key, definition)
155
+ return definition
44
156
  }
45
157
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-ptc-plus",
3
- "version": "0.2.4",
3
+ "version": "0.3.2",
4
4
  "description": "A session-bound agent-native REPL for DeepSeek Harness PTC mode.",
5
5
  "type": "module",
6
6
  "main": "./index.js",
@@ -17,12 +17,15 @@
17
17
  "docs",
18
18
  "assets/dsh-ptc-plus-banner-en.webp",
19
19
  "assets/dsh-ptc-plus-banner-zh.webp",
20
- "assets/ptc-plus-repair.png",
20
+ "assets/ptc-plus-bindings-en.png",
21
+ "assets/ptc-plus-bindings-zh.png",
22
+ "assets/ptc-plus-repair-zh.png",
23
+ "assets/ptc-plus-repair-en.png",
21
24
  "assets/ptc-plus-settings-en.png",
22
25
  "assets/ptc-plus-settings-zh.png",
23
26
  "cordis.patch.yml",
24
27
  "README.md",
25
- "README.zh.md",
28
+ "README.en.md",
26
29
  "LICENSE"
27
30
  ],
28
31
  "engines": {
@@ -96,10 +99,10 @@
96
99
  }
97
100
  },
98
101
  "devDependencies": {
99
- "@deepseek-ai/dsh-settings": "alpha",
100
- "@deepseek-ai/dsh-skill-filesystem": "alpha",
101
- "@deepseek-ai/dsh-tool-cordis": "alpha",
102
- "@deepseek-ai/dsh-tools": "alpha",
102
+ "@deepseek-ai/dsh-settings": "next",
103
+ "@deepseek-ai/dsh-skill-filesystem": "next",
104
+ "@deepseek-ai/dsh-tool-cordis": "next",
105
+ "@deepseek-ai/dsh-tools": "next",
103
106
  "esbuild": "0.28.2",
104
107
  "yaml": "^2.9.0"
105
108
  }