dsh-ptc-plus 0.5.1 → 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.
Files changed (85) hide show
  1. package/README.en.md +10 -4
  2. package/README.md +10 -4
  3. package/client.cjs +1623 -104
  4. package/compiler-core.cjs +132 -132
  5. package/compiler-core.cjs.map +3 -3
  6. package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +2 -0
  7. package/docs/adr/0017-track-the-latest-dsh-public-surface.md +4 -2
  8. package/docs/adr/0019-plugin-settings-and-kill-switch.md +2 -2
  9. package/docs/adr/0023-global-user-bindings.md +12 -0
  10. package/docs/adr/0027-run-the-session-kernel-in-a-killable-helper-process.md +11 -1
  11. package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +1 -1
  12. package/docs/adr/0030-serve-both-typert-codec-generations-from-one-codec.md +2 -2
  13. package/docs/adr/0032-own-session-log-facts-in-a-host-only-projection.md +31 -1
  14. package/docs/adr/0044-coexist-with-the-official-persistent-repl.md +4 -4
  15. package/docs/adr/0045-customize-model-visible-prompt-text.md +38 -0
  16. package/docs/adr/0046-gate-the-commit-with-the-semantic-evidence-tier.md +35 -0
  17. package/docs/adr/0047-store-journal-argument-digests-in-the-projection.md +88 -0
  18. package/docs/adr/0048-supersede-a-rewritten-cell.md +48 -0
  19. package/docs/adr/0049-prove-a-settlement-against-its-published-record.md +53 -0
  20. package/docs/adr/0050-read-host-surfaces-through-their-published-contract.md +48 -0
  21. package/docs/adr/README.md +7 -1
  22. package/docs/architecture.md +3 -3
  23. package/docs/client-ui.md +5 -2
  24. package/docs/durability-design.md +15 -2
  25. package/docs/installation.md +9 -9
  26. package/docs/releases/v0.5.2.md +91 -0
  27. package/docs/releases/v0.5.3.md +77 -0
  28. package/docs/runtime-reference.md +43 -3
  29. package/docs/unmeasured-cell-recovery-repair-proposal.md +387 -0
  30. package/docs/user-bindings.en.md +15 -0
  31. package/docs/user-bindings.md +15 -0
  32. package/docs/v0.5.3-goals.md +117 -0
  33. package/docs/verification.md +7 -3
  34. package/index.js +100 -31
  35. package/internal/binding-adoption.js +136 -0
  36. package/internal/bounded-literal.js +85 -0
  37. package/internal/capability-command-receipt.js +18 -4
  38. package/internal/capability-delivery-proof.js +70 -0
  39. package/internal/capability-environment.js +200 -11
  40. package/internal/capability-plan.js +11 -8
  41. package/internal/cell-error.js +13 -0
  42. package/internal/config-spec.js +126 -6
  43. package/internal/cordis-tools-owner.js +2 -1
  44. package/internal/direct-surface-owner.js +39 -21
  45. package/internal/edit-transport-owner.js +81 -15
  46. package/internal/execution-provider-owner.js +6 -0
  47. package/internal/execution-seam-compat.js +25 -0
  48. package/internal/host-tool-sdk.js +308 -0
  49. package/internal/isolated-worker.js +134 -17
  50. package/internal/kernel-worker.js +167 -3
  51. package/internal/legacy-cell-analysis.js +14 -2
  52. package/internal/legacy-repl-convenience.js +16 -3
  53. package/internal/official-repl-priority.js +31 -10
  54. package/internal/program-bindings.js +12 -1
  55. package/internal/projection-retention.js +238 -0
  56. package/internal/prompt-text.js +858 -0
  57. package/internal/recovery-tips.js +50 -11
  58. package/internal/rejected-cell-editor.js +3 -2
  59. package/internal/repl-memory-projection.js +21 -2
  60. package/internal/runtime-bridge-owner.js +114 -9
  61. package/internal/runtime-config.js +2 -0
  62. package/internal/runtime-contexts.js +14 -5
  63. package/internal/runtime-messages.js +48 -15
  64. package/internal/session-cell-executor.js +132 -11
  65. package/internal/session-journal-recovery.js +1204 -116
  66. package/internal/session-journal-schema.js +10 -5
  67. package/internal/session-journal.js +214 -14
  68. package/internal/session-log-view.js +133 -26
  69. package/internal/session-runtime.js +746 -38
  70. package/internal/session-state.js +27 -0
  71. package/internal/stateful-root-compiler.js +11 -0
  72. package/internal/stateful-root-runtime.js +17 -0
  73. package/internal/user-binding-console.js +7 -0
  74. package/internal/user-bindings-owner.js +112 -6
  75. package/internal/user-bindings.js +2 -2
  76. package/internal/validated-parse-repair.js +6 -1
  77. package/internal/worker-client.js +27 -4
  78. package/package.json +16 -6
  79. package/src/client-activity.js +24 -4
  80. package/src/client-capabilities-view.js +504 -27
  81. package/src/client-copy.js +187 -2
  82. package/src/client-indicator-view.js +2 -1
  83. package/src/client-repl-view.js +35 -11
  84. package/src/client-styles.js +64 -2
  85. package/src/client.js +3 -1
@@ -94,6 +94,33 @@ export class BindingCatalog {
94
94
  }
95
95
  }
96
96
 
97
+ /**
98
+ * Catalog after installing recorded values.
99
+ *
100
+ * A restored node is not compiled: its source may be dead (it can reference a
101
+ * binding a recorded rewind ended), so there is no preparation to take
102
+ * declarations from. The journal records the names and values but not their
103
+ * declaration kind, so a restored binding is catalogued as reassignable. That is
104
+ * deliberately permissive: refusing a later `let`-style redeclaration would break
105
+ * continuation, while accepting one only widens what the session may do with a
106
+ * binding whose original declaration the log cannot describe.
107
+ */
108
+ restore(names) {
109
+ if (!Array.isArray(names) || names.length === 0) return this
110
+ const entries = new Map(this.#entries)
111
+ for (const name of names) {
112
+ if (typeof name !== 'string' || entries.has(name)) continue
113
+ entries.set(name, { kind: 'let', writable: true })
114
+ }
115
+ return new BindingCatalog({
116
+ entries,
117
+ namespaces: this.#namespaces,
118
+ userBindingAncestry: this.#userBindingAncestry,
119
+ rootCandidates: this.#rootCandidates,
120
+ dynamicOrigins: this.#dynamicOrigins,
121
+ })
122
+ }
123
+
97
124
  advance(prepared, source = undefined, committedRedeclarations = undefined, rootBindingFacts = undefined) {
98
125
  const semantics = normalizeLanguageSemantics(prepared.languageSemantics ?? LEGACY_LANGUAGE_SEMANTICS)
99
126
  if (semantics === LEGACY_LANGUAGE_SEMANTICS) {
@@ -1,4 +1,5 @@
1
1
  /** Compile each cell into a private native frame backed by session identities. */
2
+ import { boundedLiteralEvidence } from './bounded-literal.js'
2
3
  import { adaptRootModuleImports } from './managed-module-operations.js'
3
4
  import { parse } from '@babel/parser'
4
5
  import { createHash } from 'node:crypto'
@@ -321,11 +322,21 @@ export function compileStatefulRoot(program, {
321
322
  return remember(text, [{ generatedStart: 6, generatedEnd: 6 + source.length,
322
323
  originalStart: original.start, originalEnd: original.end }])
323
324
  }
325
+ const firstDeclaration = declarations.length
324
326
  const target = record(names[0], 'variable', node, bindings[0], `:${declarator.start}`)
325
327
  for (const binding of bindings.slice(1)) {
326
328
  record(binding.name, 'variable', node, binding, `:${declarator.start}`)
327
329
  declarations.at(-1).commitDependency = target
328
330
  }
331
+ // A single `const name = { … }` initializer proves its own size from the
332
+ // source, so settlement may record that value without walking an object the
333
+ // codec cannot size. A destructuring pattern has no one initializer to prove.
334
+ if (declarator.id.type === 'Identifier' && supplied === undefined && declarator.init !== null) {
335
+ const evidence = boundedLiteralEvidence(declarator.init)
336
+ if (evidence !== undefined) {
337
+ for (const declaration of declarations.slice(firstDeclaration)) declaration.boundedLiteral = evidence
338
+ }
339
+ }
329
340
  if (declarator.init === null && supplied === undefined) {
330
341
  return emptyCompletion(`${runtime}.declare(${JSON.stringify(names[0])}, ${JSON.stringify(target)})`)
331
342
  }
@@ -333,6 +333,23 @@ export function createStatefulRootRuntime({ readAmbient, typeofAmbient, writeAmb
333
333
  const entry = mapGet(entries, name)
334
334
  return entry?.source === 'local' && entry.object !== true && entry.read === undefined ? { value: entry.value } : undefined
335
335
  },
336
+ /**
337
+ * Install a recorded binding without running the cell that produced it.
338
+ *
339
+ * A volatile cell cannot be replayed: re-executing its source would
340
+ * redispatch the ambient effects that made it volatile. Recovery therefore
341
+ * installs the value the journal recorded, as the committed local
342
+ * declaration the original cell's own commit produced. Returns false when
343
+ * the store cannot publish a session identity for the name, so the caller
344
+ * reports that binding instead of claiming it was restored.
345
+ */
346
+ restoreValue(name, value) {
347
+ const entry = storeValue(name, value, { declaration: true })
348
+ if (entry === undefined) return false
349
+ publish(name, () => read(name), next => write(name, next))
350
+ changed(name, 'local')
351
+ return true
352
+ },
336
353
  has: name => mapHas(entries, name) && mapGet(entries, name).object !== true && mapGet(entries, name).deleted !== true,
337
354
  facts() {
338
355
  const facts = []
@@ -55,6 +55,12 @@ export class UserBindingConsole {
55
55
  async reconfigure(options) {
56
56
  // Reconfiguration releases the current environments; the owner stays usable so
57
57
  // a later run can start again.
58
+ if (this.options.workerReclaimMs !== options.workerReclaimMs && Number.isSafeInteger(options.workerReclaimMs)) {
59
+ // A bound change needs no new worker: the close path reads the transport.
60
+ for (const environment of this.environments.values()) {
61
+ environment.worker.reclaimBoundMs = options.workerReclaimMs
62
+ }
63
+ }
58
64
  if (['maxWallMs', 'maxOutputBytes', 'maxOldGenerationSizeMb'].some(key => this.options[key] !== options[key])) {
59
65
  for (const capability of [...this.environments.keys()]) this.release(capability, 'reconfigured')
60
66
  const failures = await this.owner.releaseAll()
@@ -88,6 +94,7 @@ export class UserBindingConsole {
88
94
  workerData: { source, cwd: this.options.cwd, maxOutputBytes: this.options.maxOutputBytes },
89
95
  env: normalizeWorkerEnvironment(process.env),
90
96
  resourceLimits: { maxOldGenerationSizeMb: this.options.maxOldGenerationSizeMb },
97
+ reclaimBoundMs: this.options.workerReclaimMs,
91
98
  protocol: 'parent-port',
92
99
  })
93
100
  const worker = started.transport
@@ -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}`
@@ -111,6 +153,7 @@ function runCandidate(source, invocation, options, signal, owner) {
111
153
  workerData: { source, invocation, valueLimits: options.valueLimits, cwd: options.cwd },
112
154
  env: normalizeWorkerEnvironment(process.env),
113
155
  resourceLimits: { maxOldGenerationSizeMb: options.maxOldGenerationSizeMb },
156
+ reclaimBoundMs: options.workerReclaimMs,
114
157
  })
115
158
  const worker = started.transport
116
159
  const outputLimitError = () => Object.assign(
@@ -183,6 +226,7 @@ export function createUserBindingsOwner(ctx, options = {}) {
183
226
  maxWallMs: options.maxWallMs,
184
227
  maxOutputBytes: options.maxOutputBytes,
185
228
  maxOldGenerationSizeMb: options.maxOldGenerationSizeMb,
229
+ workerReclaimMs: options.workerReclaimMs,
186
230
  valueLimits: options.valueLimits,
187
231
  }
188
232
  let disposed = false
@@ -263,6 +307,48 @@ export function createUserBindingsOwner(ctx, options = {}) {
263
307
  const requireEnabled = () => {
264
308
  if (!enabled) throw new Error('Global User Bindings are disabled')
265
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
+ }
266
352
  const draftFor = (capability) => {
267
353
  if (typeof capability !== 'string' || capability === '') {
268
354
  throw new TypeError('draft capability must be a non-empty string')
@@ -480,7 +566,7 @@ export function createUserBindingsOwner(ctx, options = {}) {
480
566
  const requireCurrentAgent = () => {
481
567
  if (!agentIsCurrent(agent, sessionId, agentGeneration)
482
568
  || commandRegistrations.get(sessionId)?.agent !== agent
483
- || ctx.tools.get('run_code', agent) === undefined) {
569
+ || !sessionExecutionAvailable(ctx.tools, agent)) {
484
570
  throw new Error('this binding authoring Agent is no longer active')
485
571
  }
486
572
  }
@@ -615,6 +701,14 @@ export function createUserBindingsOwner(ctx, options = {}) {
615
701
 
616
702
  const commandHandler = async ({ agent, rawInput, signal, commandId }) => {
617
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
+ }
618
712
  const command = parseAuthoringCommand(rawInput)
619
713
  if (command.kind === 'help') return { kind: 'error', text: `Usage: ${COMMAND_USAGE}` }
620
714
  try {
@@ -658,14 +752,14 @@ export function createUserBindingsOwner(ctx, options = {}) {
658
752
  // still claiming one.
659
753
  if (disposed || !enabled || lifecycleGeneration !== registration.generation
660
754
  || !agentIsCurrent(agent, sessionId, agentGeneration)
661
- || ctx.tools.get('run_code', agent) === undefined
755
+ || !sessionExecutionAvailable(ctx.tools, agent)
662
756
  || commandRegistrations.get(sessionId) !== registration) {
663
757
  throw new Error('Global User Bindings binding command registration is no longer eligible')
664
758
  }
665
759
  registerOwned(commandCtx, commandCtx.commands, {
666
760
  name: 'binding',
667
- description: 'Create or revise a Global User Binding draft with the current agent',
668
- 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' },
669
763
  handler: commandHandler,
670
764
  }, 'binding command')
671
765
  })
@@ -710,13 +804,13 @@ export function createUserBindingsOwner(ctx, options = {}) {
710
804
  }
711
805
  if (!agentIsCurrent(agent, sessionId, agentGeneration)) return
712
806
  const eligible = enabled && draftProjectionAvailable && !disposed
713
- && ctx.tools.get('run_code', agent) !== undefined
807
+ && sessionExecutionAvailable(ctx.tools, agent)
714
808
  const current = commandRegistrations.get(sessionId)
715
809
  if (eligible && current?.agent === agent) return
716
810
  if (current !== undefined) await removeCommandFor(sessionId, current)
717
811
  if (!agentIsCurrent(agent, sessionId, agentGeneration)) return
718
812
  const stillEligible = enabled && draftProjectionAvailable && !disposed
719
- && ctx.tools.get('run_code', agent) !== undefined
813
+ && sessionExecutionAvailable(ctx.tools, agent)
720
814
  if (stillEligible) {
721
815
  registerCommandFor(agent)
722
816
  return
@@ -916,6 +1010,17 @@ export function createUserBindingsOwner(ctx, options = {}) {
916
1010
  async snapshot() {
917
1011
  return enabled ? store.snapshot() : undefined
918
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
+ },
919
1024
  async list() {
920
1025
  requireEnabled()
921
1026
  return store.list()
@@ -975,6 +1080,7 @@ export function createUserBindingsOwner(ctx, options = {}) {
975
1080
  maxWallMs: nextConfig.maxWallMs,
976
1081
  maxOutputBytes: nextConfig.maxOutputBytes,
977
1082
  maxOldGenerationSizeMb: nextConfig.maxOldGenerationSizeMb,
1083
+ workerReclaimMs: nextConfig.workerReclaimMs,
978
1084
  valueLimits: valueLimitsFromConfig(nextConfig),
979
1085
  }
980
1086
  if (nextEnabled === previousEnabled) {
@@ -268,7 +268,7 @@ export function userBindingsDeclaration(snapshot) {
268
268
  return normalized.entries.map(entry => entry.declaration).join('\n\n')
269
269
  }
270
270
 
271
- export function userBindingsConfiguredContext(snapshot) {
271
+ export function userBindingsConfiguredContext(snapshot, { entryLabel = 'Binding' } = {}) {
272
272
  const entries = normalizeUserBindingsSnapshot(snapshot).entries
273
273
  .filter(entry => {
274
274
  const { includeDeclaration, instructions } = bindingModelPreferences(entry.modelContext)
@@ -279,7 +279,7 @@ export function userBindingsConfiguredContext(snapshot) {
279
279
  const content = entries.map(entry => {
280
280
  const { includeDeclaration, instructions } = bindingModelPreferences(entry.modelContext)
281
281
  return [
282
- `Binding: ${entry.name}`,
282
+ `${entryLabel}: ${entry.name}`,
283
283
  instructions,
284
284
  includeDeclaration ? `\`\`\`ts\n${entry.declaration}\n\`\`\`` : '',
285
285
  ].filter(Boolean).join('\n\n')
@@ -31,7 +31,7 @@ function startsInsideSurrogatePair(source, index) {
31
31
  return current >= 0xdc00 && current <= 0xdfff && previous >= 0xd800 && previous <= 0xdbff
32
32
  }
33
33
 
34
- function boundedUniqueSuffix(source) {
34
+ export function boundedUniqueSuffix(source) {
35
35
  const first = Math.max(0, source.length - MAX_PARSE_REPAIR_ANCHOR_CODE_UNITS)
36
36
  for (let start = source.length - 1; start >= first; start -= 1) {
37
37
  if (startsInsideSurrogatePair(source, start)) continue
@@ -81,6 +81,11 @@ export function validatedEofClosureRepair({ source, position, prepare, targetCal
81
81
  const candidate = candidates[0]
82
82
  return Object.freeze({
83
83
  ...candidate,
84
+ // The diagnostic names the target cell instead of embedding the whole
85
+ // invocation: the JSON form is hundreds of characters the model has to copy
86
+ // before it can act, while the delimiter and the target are the two facts it
87
+ // needs.
88
+ targetCallSeq,
84
89
  invocation: renderInvocation(candidate.arguments),
85
90
  })
86
91
  }
@@ -1,4 +1,4 @@
1
- import { mkdtemp, rm } from 'node:fs/promises'
1
+ import { mkdir, mkdtemp, rm } from 'node:fs/promises'
2
2
  import { randomBytes } from 'node:crypto'
3
3
  import { tmpdir } from 'node:os'
4
4
  import { isAbsolute, join } from 'node:path'
@@ -46,7 +46,15 @@ export class WorkerClient {
46
46
  return lastLine === undefined ? undefined : lastLine.slice(0, 300)
47
47
  }
48
48
 
49
- async ensure(maxOldGenerationSizeMb) {
49
+ /**
50
+ * Start the session worker once and reuse it.
51
+ *
52
+ * `bounds` carries the startup bound for a worker that does not exist yet and
53
+ * the close bound this worker starts with; a later close reads the bound the
54
+ * owning kernel sets from its current configuration. It may be a getter, which
55
+ * is read after the scratch wait because a settings change can land there.
56
+ */
57
+ async ensure(maxOldGenerationSizeMb, bounds = {}) {
50
58
  if (this.pendingOutputFailure !== undefined) {
51
59
  const message = this.pendingOutputFailure
52
60
  this.pendingOutputFailure = undefined
@@ -61,7 +69,14 @@ export class WorkerClient {
61
69
  if (!isAbsolute(scratchRoot)) {
62
70
  throw new Error(`ptc-plus: host temporary directory must be absolute, got ${JSON.stringify(scratchRoot)}`)
63
71
  }
64
- this.scratchReady = mkdtemp(join(scratchRoot, 'dsh-ptc-plus-'))
72
+ this.scratchReady = mkdtemp(join(scratchRoot, 'dsh-ptc-plus-')).catch(async error => {
73
+ // A host that points TMPDIR at a directory it has not created yet (macOS CI)
74
+ // fails mkdtemp with ENOENT; create the root once and retry instead of
75
+ // failing the cell with a worker-exit error.
76
+ if (error?.code !== 'ENOENT') throw error
77
+ await mkdir(scratchRoot, { recursive: true })
78
+ return mkdtemp(join(scratchRoot, 'dsh-ptc-plus-'))
79
+ })
65
80
  }
66
81
  this.workerLimit = maxOldGenerationSizeMb
67
82
  let scratchDirectory
@@ -79,13 +94,21 @@ export class WorkerClient {
79
94
  /* c8 ignore next */
80
95
  if (this.worker !== undefined) return this.workerReady
81
96
  const environment = normalizeWorkerEnvironment(process.env)
97
+ // A settings change can land while the scratch directory is being created,
98
+ // so the bounds are read here rather than at the call.
99
+ const resolvedBounds = typeof bounds === 'function' ? bounds() : bounds
82
100
  let worker
83
101
  try {
84
102
  const started = this.owner.start({
85
103
  helper: KERNEL_HELPER,
86
104
  entry: this.workerUrl,
87
- workerData: { cwd: this.cwd, compilerCache: this.compilerCache?.() },
105
+ workerData: {
106
+ cwd: this.cwd,
107
+ compilerCache: this.compilerCache?.(),
108
+ startupProbeMs: resolvedBounds.startupProbeMs,
109
+ },
88
110
  resourceLimits: { maxOldGenerationSizeMb: this.workerLimit },
111
+ reclaimBoundMs: resolvedBounds.reclaimBoundMs,
89
112
  env: {
90
113
  ...environment,
91
114
  TEMP: scratchDirectory,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-ptc-plus",
3
- "version": "0.5.1",
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:baseline:update": "node scripts/dsh-dependency-baseline.mjs update",
92
- "host:baseline:verify": "node scripts/dsh-dependency-baseline.mjs verify",
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",
@@ -107,14 +114,17 @@
107
114
  "coverage:focus": "node scripts/coverage.mjs --focus",
108
115
  "test:backend:focus": "node scripts/test-suite-files.mjs --focus",
109
116
  "test:backend": "node scripts/test-suite-files.mjs",
117
+ "test:backend:fast": "node scripts/test-suite-files.mjs --fast",
110
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",
111
- "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 && node scripts/deterministic-test-groups.mjs",
112
- "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 && node scripts/deterministic-test-groups.mjs --ci",
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",
113
122
  "check": "node scripts/review-findings.mjs check",
114
123
  "verify:docs": "node scripts/review-findings.mjs docs",
115
124
  "test:expensive": "node scripts/expensive-headless-acceptance.mjs",
116
125
  "test:ab": "node scripts/ab-headless-trajectories.mjs",
117
- "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"
118
128
  },
119
129
  "keywords": [
120
130
  "deepseek-harness",
@@ -249,12 +249,30 @@ function isValidModuleSemantics(value, version) {
249
249
  && (legacy || IMPORT_EXPRESSION_BOUNDARIES.has(value.importExpressionBoundary))
250
250
  }
251
251
 
252
+ /**
253
+ * The retention window keeps everything recovery reads and replaces one payload
254
+ * with the digest of exactly the bytes it used to carry, so a recorded call keeps
255
+ * the Value V1 wire of its arguments or the digest of that wire, and a derived run
256
+ * keeps its source or the digest of that source. `normalizeCalls` and
257
+ * `normalizeDerivedRelation` accept exactly one of the two shapes and reject a
258
+ * record that claims both, so this boundary reports which shape it read.
259
+ */
260
+ function payloadShape(value, textField, digestField, bytesField) {
261
+ if (Object.hasOwn(value, textField)) {
262
+ return Object.hasOwn(value, digestField) || Object.hasOwn(value, bytesField) ? undefined : 'text'
263
+ }
264
+ return typeof value[digestField] === 'string' && /^[a-f0-9]{64}$/u.test(value[digestField])
265
+ && isSafeSequence(value[bytesField]) ? 'digest' : undefined
266
+ }
267
+
252
268
  function isValidCall(value) {
253
269
  if (!isRecord(value) || (value.ok !== true && value.ok !== false)) return false
254
270
  const fields = value.ok ? CALL_SUCCESS_FIELDS : CALL_ERROR_FIELDS
255
- if (!hasExactFields(value, fields) || typeof value.global !== 'string'
256
- || typeof value.member !== 'string' || !isValidValueWire(value.args)
271
+ const argsShape = payloadShape(value, 'args', 'argsDigest', 'argsBytes')
272
+ if (argsShape === undefined || !hasClosedFields(value, fields)
273
+ || typeof value.global !== 'string' || typeof value.member !== 'string'
257
274
  || !isSafeSequence(value.settle)) return false
275
+ if (argsShape === 'text' && !isValidValueWire(value.args)) return false
258
276
  return value.ok ? isValidValueWire(value.value) : typeof value.error === 'string'
259
277
  }
260
278
 
@@ -389,8 +407,10 @@ function isValidEditTarget(value) {
389
407
 
390
408
  function isValidDerivedRun(value) {
391
409
  try {
392
- return hasExactFields(value, DERIVED_RUN_FIELDS)
393
- && typeof value.code === 'string' && typeof value.description === 'string'
410
+ if (!isRecord(value) || !hasClosedFields(value, DERIVED_RUN_FIELDS)
411
+ || typeof value.description !== 'string') return false
412
+ const codeShape = payloadShape(value, 'code', 'codeDigest', 'codeBytes')
413
+ return codeShape === 'digest' || (codeShape === 'text' && typeof value.code === 'string')
394
414
  } catch {
395
415
  return false
396
416
  }