dsh-ptc-plus 0.3.1 → 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.
@@ -7,6 +7,7 @@ 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'
@@ -41,6 +42,13 @@ function rewritePolicy(config) {
41
42
  }
42
43
  }
43
44
 
45
+ function bindingPolicy(config) {
46
+ return {
47
+ variableRedeclarations: config.looseTopLevelRedeclarations,
48
+ functionClassRedeclarations: config.looseTopLevelFunctionClassRedeclarations,
49
+ }
50
+ }
51
+
44
52
  function resolvedRuntimeConfig(config) {
45
53
  return Object.freeze(resolveConfig(config))
46
54
  }
@@ -67,12 +75,25 @@ class SessionKernel {
67
75
  constructor({ config, history, cwd, session, withInitiator }) {
68
76
  this.config = config
69
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
+ }
70
90
  this.cwd = cwd
71
91
  this.session = session
72
92
  this.withInitiator = withInitiator
73
93
  this.durability = durabilityState()
74
94
  this.bindingCatalog = new BindingCatalog()
75
95
  this.replayed = false
96
+ this.liveCallSeqs = new Set()
76
97
  this.recoveryNotice = history.volatileSuffix.length === 0
77
98
  ? undefined
78
99
  : recoveryDiagnostic(history.volatileSuffix.length)
@@ -136,10 +157,62 @@ class SessionKernel {
136
157
  }
137
158
 
138
159
  async execute(request, config) {
139
- const recoveryBoundaries = []
160
+ const recoveryBoundaries = this.initialRecoveryBoundary === undefined
161
+ ? []
162
+ : [this.initialRecoveryBoundary]
163
+ this.initialRecoveryBoundary = undefined
140
164
  const finishResult = result => recoveryBoundaries.length === 0
141
165
  ? result
142
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
+ }
143
216
  if (!this.replayed && config.durableReplay) {
144
217
  let skipped = 0
145
218
  while (!this.replayed) {
@@ -175,6 +248,7 @@ class SessionKernel {
175
248
  }
176
249
  const recovered = recoverJournal(this.session, request.callSeq, {
177
250
  extraBoundaries: [boundary],
251
+ visibleCallSeqs: visibleExecutableCallSeqs(this.session),
178
252
  })
179
253
  recoveryBoundaries.push(boundary)
180
254
  this.history = recovered
@@ -245,6 +319,7 @@ class SessionKernel {
245
319
  throw new ReplayFailure(node, error)
246
320
  }
247
321
  }
322
+ for (const node of path) this.liveCallSeqs.add(node.callSeq)
248
323
  }
249
324
 
250
325
  completeJournal(journal, status, result, volatileReason, diagnostics = [], completion = undefined) {
@@ -272,6 +347,7 @@ class SessionKernel {
272
347
  this.durability = durabilityState()
273
348
  this.replayed = false
274
349
  this.bindingCatalog = new BindingCatalog()
350
+ this.liveCallSeqs.clear()
275
351
  }
276
352
 
277
353
  settleCell(active, result, terminate = false) {
@@ -313,6 +389,9 @@ class SessionKernel {
313
389
  this.bindingCatalog = active.appliedBindingCatalog
314
390
  ?? active.priorBindingCatalog.advance(prepared, request.program)
315
391
  }
392
+ if (replay === undefined && !terminate) {
393
+ this.liveCallSeqs.add(request.callSeq ?? request.sourceCallSeq)
394
+ }
316
395
  this.active = undefined
317
396
  if (terminate) void this.client.reset(worker)
318
397
  active.finish(result)
@@ -458,10 +537,13 @@ export class SessionRuntime {
458
537
  sourceCallSeq = liveToolCallSeq(session, callId, 'run_code')
459
538
  callSeq = persistedCallSeq ?? sourceCallSeq
460
539
  } else {
461
- try {
462
- sourceCallSeq = liveToolCallSeq(session, callId, 'run_code')
463
- } catch {
464
- sourceCallSeq = undefined
540
+ sourceCallSeq = persistedCallSeq
541
+ if (sourceCallSeq === undefined) {
542
+ try {
543
+ sourceCallSeq = liveToolCallSeq(session, callId, 'run_code')
544
+ } catch {
545
+ sourceCallSeq = undefined
546
+ }
465
547
  }
466
548
  }
467
549
  } catch (error) {
@@ -472,7 +554,9 @@ export class SessionRuntime {
472
554
  let history
473
555
  try {
474
556
  history = cellConfig.durableReplay
475
- ? recoverJournal(session, callSeq)
557
+ ? recoverJournal(session, callSeq, {
558
+ visibleCallSeqs: visibleExecutableCallSeqs(session),
559
+ })
476
560
  : emptyHistory()
477
561
  } catch (error) {
478
562
  return completed({ logs: [], error: { kind: 'recovery', message: `cannot reconstruct REPL from session log: ${messageOf(error)}` } })
@@ -489,7 +573,7 @@ export class SessionRuntime {
489
573
  const journal = createJournal(
490
574
  /* c8 ignore next */
491
575
  this.pendingNoops.get(sessionId) ?? [],
492
- cellConfig.looseTopLevelRedeclarations ? 'loose' : 'strict',
576
+ bindingPolicy(cellConfig),
493
577
  rewritePolicy(cellConfig),
494
578
  )
495
579
  const workerReservation = kernel.reserveWorkerConfiguration(cellConfig)
@@ -23,6 +23,7 @@ export class BindingCatalog {
23
23
  #definitions
24
24
  #imports
25
25
  #namespaces
26
+ #writable
26
27
 
27
28
  constructor(
28
29
  known = new Set(),
@@ -30,6 +31,7 @@ export class BindingCatalog {
30
31
  namespaces = new Set(),
31
32
  kinds = new Map(),
32
33
  definitions = new Map(),
34
+ writable = new Set(),
33
35
  ) {
34
36
  this.#known = new Set(known)
35
37
  this.#kinds = new Map([...this.#known].map(name => [name, kinds.get(name) ?? 'variable']))
@@ -38,6 +40,7 @@ export class BindingCatalog {
38
40
  .map(name => [name, definitions.get(name)]))
39
41
  this.#imports = new Map(imports)
40
42
  this.#namespaces = new Set(namespaces)
43
+ this.#writable = new Set([...writable].filter(name => this.#known.has(name)))
41
44
  Object.freeze(this)
42
45
  }
43
46
 
@@ -46,28 +49,58 @@ export class BindingCatalog {
46
49
  knownBindings: new Set(this.#known),
47
50
  importBindings: new Map(this.#imports),
48
51
  importNamespaces: new Set(this.#namespaces),
52
+ writableBindings: new Set(this.#writable),
49
53
  }
50
54
  }
51
55
 
52
- advance(prepared, source = undefined) {
56
+ advance(prepared, source = undefined, committedRedeclarations = undefined) {
53
57
  const known = new Set(this.#known)
54
58
  const kinds = new Map(this.#kinds)
55
59
  const definitions = new Map(this.#definitions)
60
+ const writable = new Set(this.#writable)
56
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()
57
68
  const extractDefinition = sourceDefinitionExtractor(source)
58
69
  for (const declaration of prepared.declarations ?? []) {
59
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
+ }
60
79
  touched.add(declaration.name)
61
80
  kinds.set(declaration.name, declaration.kind ?? 'variable')
62
81
  const definition = extractDefinition(declaration.definitionSpan)
63
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)
64
90
  }
65
- for (const name of prepared.declared) touched.add(name)
66
91
  for (const name of touched) {
67
92
  known.delete(name)
68
93
  known.add(name)
69
94
  }
70
- return new BindingCatalog(known, prepared.imports, prepared.importNamespaces, kinds, definitions)
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)
71
104
  }
72
105
 
73
106
  snapshot() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-ptc-plus",
3
- "version": "0.3.1",
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,6 +17,8 @@
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-bindings-en.png",
21
+ "assets/ptc-plus-bindings-zh.png",
20
22
  "assets/ptc-plus-repair-zh.png",
21
23
  "assets/ptc-plus-repair-en.png",
22
24
  "assets/ptc-plus-settings-en.png",
@@ -97,10 +99,10 @@
97
99
  }
98
100
  },
99
101
  "devDependencies": {
100
- "@deepseek-ai/dsh-settings": "alpha",
101
- "@deepseek-ai/dsh-skill-filesystem": "alpha",
102
- "@deepseek-ai/dsh-tool-cordis": "alpha",
103
- "@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",
104
106
  "esbuild": "0.28.2",
105
107
  "yaml": "^2.9.0"
106
108
  }
@@ -1,14 +1,21 @@
1
- const JOURNAL_VERSIONS = new Set([1, 2, 3])
1
+ const JOURNAL_VERSIONS = new Set([1, 2, 3, 4])
2
2
  const JOURNAL_STATUSES = new Set(['durable', 'volatile', 'discarded', 'noop'])
3
3
  const BINDING_MODES = new Set(['loose', 'strict'])
4
4
  const JOURNAL_FIELDS = new Set([
5
+ 'version', 'bindingPolicy', 'rewritePolicy', 'moduleSemantics', 'status', 'calls', 'operations',
6
+ 'confirms', 'diagnostics', 'completion', 'volatileReason',
7
+ ])
8
+ const PREDECESSOR_JOURNAL_FIELDS = new Set([
5
9
  'version', 'bindingMode', 'rewritePolicy', 'status', 'calls', 'operations',
6
10
  'confirms', 'diagnostics', 'completion', 'volatileReason',
7
11
  ])
8
- const LEGACY_JOURNAL_FIELDS = new Set([...JOURNAL_FIELDS].filter(key => key !== 'rewritePolicy'))
12
+ const LEGACY_JOURNAL_FIELDS = new Set([...PREDECESSOR_JOURNAL_FIELDS].filter(key => key !== 'rewritePolicy'))
13
+ const BINDING_POLICY_FIELDS = new Set(['variableRedeclarations', 'functionClassRedeclarations'])
9
14
  const REWRITE_POLICY_FIELDS = new Set([
10
15
  'autoRewriteImports', 'autoStripExports', 'autoSplitRedeclarations',
11
16
  ])
17
+ const MODULE_SEMANTICS_FIELDS = new Set(['defaultExportBinding'])
18
+ const DEFAULT_EXPORT_BINDINGS = new Set(['legacy-variable', 'live-readonly'])
12
19
  const CALL_SUCCESS_FIELDS = new Set(['global', 'member', 'args', 'ok', 'value', 'settle'])
13
20
  const CALL_ERROR_FIELDS = new Set(['global', 'member', 'args', 'ok', 'error', 'settle'])
14
21
  const OPERATION_FIELDS = new Set(['action', 'name'])
@@ -30,9 +37,11 @@ const REWRITE_FIELDS = new Set(['kind', 'description', 'source'])
30
37
  const REWRITE_KINDS = new Set(['import', 'redeclaration', 'export'])
31
38
  const REWRITE_POLICY_BY_KIND = Object.freeze({
32
39
  import: 'autoRewriteImports',
33
- redeclaration: 'autoSplitRedeclarations',
34
40
  export: 'autoStripExports',
35
41
  })
42
+ const MIXED_REDECLARATION = 'split a mixed top-level declaration while preserving native pattern initialization'
43
+ const FUNCTION_REDECLARATION = 'reassigned an existing top-level function declaration for REPL continuity'
44
+ const CLASS_REDECLARATION = 'reassigned an existing top-level class declaration for REPL continuity'
36
45
  const EDIT_TARGET_FIELDS = new Set(['targetCallSeq'])
37
46
  const DERIVED_RUN_FIELDS = new Set(['code', 'description'])
38
47
  const RECOVERY_BOUNDARY_FIELDS = new Set(['failedCallSeq', 'frontierCallSeq'])
@@ -209,6 +218,16 @@ function isValidRewritePolicy(value) {
209
218
  && [...REWRITE_POLICY_FIELDS].every(key => typeof value[key] === 'boolean')
210
219
  }
211
220
 
221
+ function isValidBindingPolicy(value) {
222
+ return hasExactFields(value, BINDING_POLICY_FIELDS)
223
+ && [...BINDING_POLICY_FIELDS].every(key => typeof value[key] === 'boolean')
224
+ }
225
+
226
+ function isValidModuleSemantics(value) {
227
+ return hasExactFields(value, MODULE_SEMANTICS_FIELDS)
228
+ && DEFAULT_EXPORT_BINDINGS.has(value.defaultExportBinding)
229
+ }
230
+
212
231
  function isValidCall(value) {
213
232
  if (!isRecord(value) || (value.ok !== true && value.ok !== false)) return false
214
233
  const fields = value.ok ? CALL_SUCCESS_FIELDS : CALL_ERROR_FIELDS
@@ -249,10 +268,16 @@ function isReadableJournalUnchecked(value) {
249
268
  if (!isRecord(value) || !JOURNAL_VERSIONS.has(value.version) || !JOURNAL_STATUSES.has(value.status)) {
250
269
  return false
251
270
  }
252
- const fields = value.version === 1 ? LEGACY_JOURNAL_FIELDS : JOURNAL_FIELDS
253
- const required = ['version', 'bindingMode', 'status', 'calls', 'operations', 'diagnostics']
254
- if (value.version !== 1) required.push('rewritePolicy')
255
- if (!hasClosedFields(value, fields, required) || !BINDING_MODES.has(value.bindingMode)
271
+ const predecessor = value.version !== 4
272
+ const fields = value.version === 1 ? LEGACY_JOURNAL_FIELDS : predecessor ? PREDECESSOR_JOURNAL_FIELDS : JOURNAL_FIELDS
273
+ const required = predecessor
274
+ ? ['version', 'bindingMode', 'status', 'calls', 'operations', 'diagnostics']
275
+ : ['version', 'bindingPolicy', 'rewritePolicy', 'moduleSemantics', 'status', 'calls', 'operations', 'diagnostics']
276
+ if (value.version !== 1 && value.version !== 4) required.push('rewritePolicy')
277
+ if (!hasClosedFields(value, fields, required)
278
+ || (predecessor && !BINDING_MODES.has(value.bindingMode))
279
+ || (value.version === 4 && !isValidBindingPolicy(value.bindingPolicy))
280
+ || (value.version === 4 && !isValidModuleSemantics(value.moduleSemantics))
256
281
  || (value.version !== 1 && !isValidRewritePolicy(value.rewritePolicy))
257
282
  || !Array.isArray(value.calls) || !value.calls.every(isValidCall)
258
283
  || !Array.isArray(value.operations) || !value.operations.every(isValidOperation)
@@ -295,7 +320,12 @@ function isValidRewrites(value, journal) {
295
320
  && Array.isArray(value) && value.every(rewrite => (
296
321
  hasClosedFields(rewrite, REWRITE_FIELDS, ['kind', 'description'])
297
322
  && REWRITE_KINDS.has(rewrite.kind)
298
- && journal.rewritePolicy[REWRITE_POLICY_BY_KIND[rewrite.kind]] === true
323
+ && (rewrite.kind === 'redeclaration'
324
+ ? (rewrite.description === FUNCTION_REDECLARATION || rewrite.description === CLASS_REDECLARATION)
325
+ ? journal.version === 4 && isValidBindingPolicy(journal.bindingPolicy)
326
+ && journal.bindingPolicy.functionClassRedeclarations === true
327
+ : journal.rewritePolicy.autoSplitRedeclarations === true
328
+ : journal.rewritePolicy[REWRITE_POLICY_BY_KIND[rewrite.kind]] === true)
299
329
  && typeof rewrite.description === 'string' && rewrite.description.length > 0
300
330
  && (rewrite.source === undefined || typeof rewrite.source === 'string')
301
331
  ))
@@ -377,7 +407,6 @@ function resultText(block) {
377
407
  }
378
408
  }
379
409
 
380
- const MIXED_REDECLARATION = 'split a mixed top-level declaration while preserving native pattern initialization'
381
410
  const GENERATED_RUN_CODE_DESCRIPTION_KEY = 'dshPtcPlusRunCodeDescription'
382
411
 
383
412
  function rewriteFeature(rewrites, predicate, key) {