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.
- package/README.en.md +10 -4
- package/README.md +10 -4
- package/client.cjs +1623 -104
- package/compiler-core.cjs +132 -132
- package/compiler-core.cjs.map +3 -3
- package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +2 -0
- package/docs/adr/0017-track-the-latest-dsh-public-surface.md +4 -2
- package/docs/adr/0019-plugin-settings-and-kill-switch.md +2 -2
- package/docs/adr/0023-global-user-bindings.md +12 -0
- package/docs/adr/0027-run-the-session-kernel-in-a-killable-helper-process.md +11 -1
- package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +1 -1
- package/docs/adr/0030-serve-both-typert-codec-generations-from-one-codec.md +2 -2
- package/docs/adr/0032-own-session-log-facts-in-a-host-only-projection.md +31 -1
- package/docs/adr/0044-coexist-with-the-official-persistent-repl.md +4 -4
- package/docs/adr/0045-customize-model-visible-prompt-text.md +38 -0
- package/docs/adr/0046-gate-the-commit-with-the-semantic-evidence-tier.md +35 -0
- package/docs/adr/0047-store-journal-argument-digests-in-the-projection.md +88 -0
- package/docs/adr/0048-supersede-a-rewritten-cell.md +48 -0
- package/docs/adr/0049-prove-a-settlement-against-its-published-record.md +53 -0
- package/docs/adr/0050-read-host-surfaces-through-their-published-contract.md +48 -0
- package/docs/adr/README.md +7 -1
- package/docs/architecture.md +3 -3
- package/docs/client-ui.md +5 -2
- package/docs/durability-design.md +15 -2
- package/docs/installation.md +9 -9
- package/docs/releases/v0.5.2.md +91 -0
- package/docs/releases/v0.5.3.md +77 -0
- package/docs/runtime-reference.md +43 -3
- package/docs/unmeasured-cell-recovery-repair-proposal.md +387 -0
- package/docs/user-bindings.en.md +15 -0
- package/docs/user-bindings.md +15 -0
- package/docs/v0.5.3-goals.md +117 -0
- package/docs/verification.md +7 -3
- package/index.js +100 -31
- package/internal/binding-adoption.js +136 -0
- package/internal/bounded-literal.js +85 -0
- package/internal/capability-command-receipt.js +18 -4
- package/internal/capability-delivery-proof.js +70 -0
- package/internal/capability-environment.js +200 -11
- package/internal/capability-plan.js +11 -8
- package/internal/cell-error.js +13 -0
- package/internal/config-spec.js +126 -6
- package/internal/cordis-tools-owner.js +2 -1
- package/internal/direct-surface-owner.js +39 -21
- package/internal/edit-transport-owner.js +81 -15
- package/internal/execution-provider-owner.js +6 -0
- package/internal/execution-seam-compat.js +25 -0
- package/internal/host-tool-sdk.js +308 -0
- package/internal/isolated-worker.js +134 -17
- package/internal/kernel-worker.js +167 -3
- package/internal/legacy-cell-analysis.js +14 -2
- package/internal/legacy-repl-convenience.js +16 -3
- package/internal/official-repl-priority.js +31 -10
- package/internal/program-bindings.js +12 -1
- package/internal/projection-retention.js +238 -0
- package/internal/prompt-text.js +858 -0
- package/internal/recovery-tips.js +50 -11
- package/internal/rejected-cell-editor.js +3 -2
- package/internal/repl-memory-projection.js +21 -2
- package/internal/runtime-bridge-owner.js +114 -9
- package/internal/runtime-config.js +2 -0
- package/internal/runtime-contexts.js +14 -5
- package/internal/runtime-messages.js +48 -15
- package/internal/session-cell-executor.js +132 -11
- package/internal/session-journal-recovery.js +1204 -116
- package/internal/session-journal-schema.js +10 -5
- package/internal/session-journal.js +214 -14
- package/internal/session-log-view.js +133 -26
- package/internal/session-runtime.js +746 -38
- package/internal/session-state.js +27 -0
- package/internal/stateful-root-compiler.js +11 -0
- package/internal/stateful-root-runtime.js +17 -0
- package/internal/user-binding-console.js +7 -0
- package/internal/user-bindings-owner.js +112 -6
- package/internal/user-bindings.js +2 -2
- package/internal/validated-parse-repair.js +6 -1
- package/internal/worker-client.js +27 -4
- package/package.json +16 -6
- package/src/client-activity.js +24 -4
- package/src/client-capabilities-view.js +504 -27
- package/src/client-copy.js +187 -2
- package/src/client-indicator-view.js +2 -1
- package/src/client-repl-view.js +35 -11
- package/src/client-styles.js +64 -2
- 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
|
|
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
|
|
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: '
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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: {
|
|
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.
|
|
3
|
+
"version": "0.5.3",
|
|
4
4
|
"icon": "assets/ptc-plus-icon.svg",
|
|
5
5
|
"description": "A session-bound agent-native REPL for DeepSeek Harness PTC mode.",
|
|
6
6
|
"type": "module",
|
|
@@ -69,9 +69,15 @@
|
|
|
69
69
|
"@deepseek-ai/dsh-skill-filesystem": "*",
|
|
70
70
|
"@deepseek-ai/dsh-spill-policy": "*",
|
|
71
71
|
"@deepseek-ai/dsh-tool-cordis": "*",
|
|
72
|
+
"@deepseek-ai/dsh-tool-executor-ptc": "*",
|
|
72
73
|
"@deepseek-ai/dsh-tools": "*",
|
|
73
74
|
"@deepseek-ai/dsh-typert-protocol": "*"
|
|
74
75
|
},
|
|
76
|
+
"peerDependenciesMeta": {
|
|
77
|
+
"@deepseek-ai/dsh-tool-executor-ptc": {
|
|
78
|
+
"optional": true
|
|
79
|
+
}
|
|
80
|
+
},
|
|
75
81
|
"scripts": {
|
|
76
82
|
"build": "node scripts/client-bundle.mjs build && node scripts/compiler-bundle.mjs build",
|
|
77
83
|
"build:check": "node scripts/client-bundle.mjs check && node scripts/compiler-bundle.mjs check",
|
|
@@ -88,8 +94,9 @@
|
|
|
88
94
|
"test:client:fallback": "node scripts/client-fallback-smoke.mjs",
|
|
89
95
|
"test:client:layout": "node scripts/client-binding-layout.mjs",
|
|
90
96
|
"readme:screenshots": "node scripts/readme-screenshots.mjs",
|
|
91
|
-
"host:
|
|
92
|
-
"host:
|
|
97
|
+
"host:channels:update": "node scripts/host-channel-manifest.mjs update",
|
|
98
|
+
"host:channels:graph": "node scripts/host-channel-manifest.mjs graph",
|
|
99
|
+
"host:channels:verify": "node scripts/host-channel-manifest.mjs verify",
|
|
93
100
|
"test:host-contract": "node scripts/dsh-host-contract-suite.mjs",
|
|
94
101
|
"prepack": "npm run build:check",
|
|
95
102
|
"hooks:install": "node scripts/review-findings.mjs install-hook",
|
|
@@ -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:
|
|
112
|
-
"verify:
|
|
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",
|
package/src/client-activity.js
CHANGED
|
@@ -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
|
-
|
|
256
|
-
|
|
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
|
-
|
|
393
|
-
|
|
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
|
}
|