dsh-ptc-plus 0.5.1 → 0.5.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.
- package/README.en.md +7 -3
- package/README.md +7 -3
- package/client.cjs +1467 -84
- 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/0027-run-the-session-kernel-in-a-killable-helper-process.md +11 -1
- package/docs/adr/0032-own-session-log-facts-in-a-host-only-projection.md +31 -1
- 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/architecture.md +2 -2
- package/docs/client-ui.md +5 -2
- package/docs/durability-design.md +7 -1
- package/docs/releases/v0.5.2.md +79 -0
- package/docs/runtime-reference.md +36 -2
- package/docs/unmeasured-cell-recovery-repair-proposal.md +387 -0
- package/docs/verification.md +6 -2
- package/index.js +36 -15
- package/internal/bounded-literal.js +85 -0
- package/internal/capability-environment.js +162 -10
- package/internal/capability-plan.js +11 -8
- package/internal/cell-error.js +13 -0
- package/internal/config-spec.js +113 -2
- package/internal/direct-surface-owner.js +14 -10
- package/internal/edit-transport-owner.js +79 -14
- 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/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/runtime-bridge-owner.js +1 -1
- package/internal/runtime-config.js +2 -0
- package/internal/runtime-contexts.js +14 -5
- package/internal/runtime-messages.js +43 -15
- package/internal/session-cell-executor.js +132 -11
- package/internal/session-journal-recovery.js +1009 -99
- package/internal/session-journal-schema.js +7 -5
- package/internal/session-journal.js +177 -14
- package/internal/session-log-view.js +111 -24
- package/internal/session-runtime.js +674 -33
- 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 +3 -0
- 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 +3 -1
- package/src/client-activity.js +24 -4
- package/src/client-capabilities-view.js +459 -24
- package/src/client-copy.js +163 -2
- package/src/client-repl-view.js +6 -1
- package/src/client-styles.js +64 -2
|
@@ -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
|
|
@@ -111,6 +111,7 @@ function runCandidate(source, invocation, options, signal, owner) {
|
|
|
111
111
|
workerData: { source, invocation, valueLimits: options.valueLimits, cwd: options.cwd },
|
|
112
112
|
env: normalizeWorkerEnvironment(process.env),
|
|
113
113
|
resourceLimits: { maxOldGenerationSizeMb: options.maxOldGenerationSizeMb },
|
|
114
|
+
reclaimBoundMs: options.workerReclaimMs,
|
|
114
115
|
})
|
|
115
116
|
const worker = started.transport
|
|
116
117
|
const outputLimitError = () => Object.assign(
|
|
@@ -183,6 +184,7 @@ export function createUserBindingsOwner(ctx, options = {}) {
|
|
|
183
184
|
maxWallMs: options.maxWallMs,
|
|
184
185
|
maxOutputBytes: options.maxOutputBytes,
|
|
185
186
|
maxOldGenerationSizeMb: options.maxOldGenerationSizeMb,
|
|
187
|
+
workerReclaimMs: options.workerReclaimMs,
|
|
186
188
|
valueLimits: options.valueLimits,
|
|
187
189
|
}
|
|
188
190
|
let disposed = false
|
|
@@ -975,6 +977,7 @@ export function createUserBindingsOwner(ctx, options = {}) {
|
|
|
975
977
|
maxWallMs: nextConfig.maxWallMs,
|
|
976
978
|
maxOutputBytes: nextConfig.maxOutputBytes,
|
|
977
979
|
maxOldGenerationSizeMb: nextConfig.maxOldGenerationSizeMb,
|
|
980
|
+
workerReclaimMs: nextConfig.workerReclaimMs,
|
|
978
981
|
valueLimits: valueLimitsFromConfig(nextConfig),
|
|
979
982
|
}
|
|
980
983
|
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.2",
|
|
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",
|
|
@@ -107,8 +107,10 @@
|
|
|
107
107
|
"coverage:focus": "node scripts/coverage.mjs --focus",
|
|
108
108
|
"test:backend:focus": "node scripts/test-suite-files.mjs --focus",
|
|
109
109
|
"test:backend": "node scripts/test-suite-files.mjs",
|
|
110
|
+
"test:backend:fast": "node scripts/test-suite-files.mjs --fast",
|
|
110
111
|
"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
112
|
"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",
|
|
113
|
+
"verify:check": "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 scripts/deterministic-test-groups.mjs --check",
|
|
112
114
|
"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",
|
|
113
115
|
"check": "node scripts/review-findings.mjs check",
|
|
114
116
|
"verify:docs": "node scripts/review-findings.mjs docs",
|
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
|
}
|