dsh-ptc-plus 0.2.1
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/LICENSE +21 -0
- package/README.md +137 -0
- package/README.zh.md +137 -0
- package/assets/dsh-ptc-plus-banner.webp +0 -0
- package/assets/ptc-plus-repair.png +0 -0
- package/assets/ptc-plus-settings.png +0 -0
- package/client.js +366 -0
- package/cordis.patch.yml +5 -0
- package/docs/README.md +21 -0
- package/docs/adr/0001-delegate-governance-to-dsh.md +3 -0
- package/docs/adr/0002-limit-work-map-scope.md +3 -0
- package/docs/adr/0003-prefer-native-program-surfaces.md +3 -0
- package/docs/adr/0004-declare-program-bindings-at-the-host.md +5 -0
- package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +129 -0
- package/docs/adr/0006-rewrite-module-syntax-with-an-ast.md +45 -0
- package/docs/adr/0007-separate-worker-transport-from-session-semantics.md +39 -0
- package/docs/adr/0008-define-agent-operating-contract.md +25 -0
- package/docs/adr/0009-bound-expensive-acceptance-default.md +39 -0
- package/docs/adr/0010-session-log-derived-recovery-tips.md +26 -0
- package/docs/adr/0011-isolate-the-local-recovery-tip-provider.md +20 -0
- package/docs/adr/0012-preserve-mixed-destructuring-initialization.md +23 -0
- package/docs/adr/0013-isolate-repl-redeclaration-convenience.md +21 -0
- package/docs/adr/0014-persist-cell-rewrite-policy.md +25 -0
- package/docs/adr/0015-preserve-source-positions-through-rewrites.md +25 -0
- package/docs/adr/0016-archive-resolved-review-findings.md +43 -0
- package/docs/adr/0017-track-the-latest-dsh-public-surface.md +23 -0
- package/docs/adr/0018-use-versioned-fixture-for-ab-benchmark.md +25 -0
- package/docs/adr/0019-plugin-settings-and-kill-switch.md +32 -0
- package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +36 -0
- package/docs/adr/0021-separate-host-bootstrap-from-session-runtime.md +68 -0
- package/docs/adr/README.md +18 -0
- package/docs/architecture.md +134 -0
- package/docs/capability-projection.md +48 -0
- package/docs/client-ui.md +47 -0
- package/docs/durability-design.md +265 -0
- package/docs/evaluation.md +62 -0
- package/docs/installation.md +82 -0
- package/docs/program-data-plane.md +61 -0
- package/docs/publishing.md +37 -0
- package/docs/runtime-reference.md +119 -0
- package/docs/value-wire.md +61 -0
- package/index.js +532 -0
- package/internal/ast-traversal.js +20 -0
- package/internal/binding-descriptors.js +95 -0
- package/internal/binding-pattern.js +54 -0
- package/internal/builtin-esm-sync.js +9 -0
- package/internal/cell-analysis.js +467 -0
- package/internal/cell-rewriter.js +520 -0
- package/internal/config-spec.js +179 -0
- package/internal/cordis-tools-owner.js +363 -0
- package/internal/diagnostic.js +133 -0
- package/internal/direct-surface-owner.js +326 -0
- package/internal/edit-transport-owner.js +287 -0
- package/internal/failure-reporting.js +139 -0
- package/internal/kernel-worker.js +484 -0
- package/internal/module-policy.js +46 -0
- package/internal/program-bindings.js +187 -0
- package/internal/record-utils.js +53 -0
- package/internal/recovery-tips.js +81 -0
- package/internal/rejected-cell-editor.js +328 -0
- package/internal/repl-convenience.js +201 -0
- package/internal/runtime-bridge-owner.js +417 -0
- package/internal/runtime-config.js +77 -0
- package/internal/runtime-contexts.js +31 -0
- package/internal/sdk-projection.js +88 -0
- package/internal/session-cell-executor.js +612 -0
- package/internal/session-journal.js +889 -0
- package/internal/session-log-view.js +80 -0
- package/internal/session-runtime.js +536 -0
- package/internal/session-state.js +45 -0
- package/internal/source-position-map.js +224 -0
- package/internal/tool-call-canonicalizer.js +339 -0
- package/internal/value-wire.js +421 -0
- package/internal/worker-client.js +218 -0
- package/internal/worker-cwd-virtualization.js +155 -0
- package/package.json +100 -0
- package/src/client.js +165 -0
package/index.js
ADDED
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-bound REPL for DeepSeek Harness PTC mode.
|
|
3
|
+
*
|
|
4
|
+
* DSH's run_code bridge does not pass session identity to CodeRuntime.run().
|
|
5
|
+
* The tools/execute around-hook carries that identity into the runtime bridge,
|
|
6
|
+
* which redirects only those runs to a persistent per-session kernel.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import Schema from '@deepseek-ai/schemastery'
|
|
10
|
+
import { createDirectSurfaceOwner } from './internal/direct-surface-owner.js'
|
|
11
|
+
import { createCordisToolsOwner } from './internal/cordis-tools-owner.js'
|
|
12
|
+
import { createEditTransportOwner, EDIT_RUN_CODE } from './internal/edit-transport-owner.js'
|
|
13
|
+
import { createRuntimeBridgeOwner, RUN_CODE } from './internal/runtime-bridge-owner.js'
|
|
14
|
+
import { resolveConfig } from './internal/runtime-config.js'
|
|
15
|
+
import {
|
|
16
|
+
CONFIG_FIELDS,
|
|
17
|
+
SETTINGS_NAMESPACE,
|
|
18
|
+
} from './internal/config-spec.js'
|
|
19
|
+
import { installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-settings'
|
|
20
|
+
|
|
21
|
+
const INSTALL_CLEANUP = Symbol('ptc-plus install cleanup')
|
|
22
|
+
|
|
23
|
+
/** Plugin name used by loader diagnostics. */
|
|
24
|
+
export const name = 'ptc-plus'
|
|
25
|
+
|
|
26
|
+
/** Runtime limits and behavior exposed to Cordis configuration. */
|
|
27
|
+
function configSchemaField(field) {
|
|
28
|
+
const base = field.type === 'boolean'
|
|
29
|
+
? Schema.boolean().default(field.default)
|
|
30
|
+
: Schema.number().step(1).min(field.min).max(field.max).default(field.default)
|
|
31
|
+
return base.description(field.description)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const Config = Schema.object(Object.fromEntries(
|
|
35
|
+
CONFIG_FIELDS.map(field => [field.key, configSchemaField(field)]),
|
|
36
|
+
))
|
|
37
|
+
|
|
38
|
+
/** Services required by the plugin. */
|
|
39
|
+
export const inject = ['tools', 'codeRuntime', 'systemPrompt', 'agents', 'llm']
|
|
40
|
+
|
|
41
|
+
function replGuidance(
|
|
42
|
+
looseTopLevelRedeclarations,
|
|
43
|
+
durableReplay,
|
|
44
|
+
autoRewriteImports,
|
|
45
|
+
autoStripExports,
|
|
46
|
+
autoSplitRedeclarations,
|
|
47
|
+
cordisToolsEnabled,
|
|
48
|
+
) {
|
|
49
|
+
const redeclaration = looseTopLevelRedeclarations
|
|
50
|
+
? 'Repeated top-level `const`/`let` declarations replace existing bindings.'
|
|
51
|
+
: 'Redeclaring an existing top-level name fails before execution, so reuse it or place one-off declarations inside a block.'
|
|
52
|
+
const moduleSyntax = autoRewriteImports && autoStripExports
|
|
53
|
+
? 'static `import` declarations are adapted with live, read-only bindings and top-level `export` modifiers are stripped automatically.'
|
|
54
|
+
: autoRewriteImports
|
|
55
|
+
? 'static `import` declarations are adapted with live, read-only bindings; top-level `export` modifiers remain unsupported.'
|
|
56
|
+
: autoStripExports
|
|
57
|
+
? 'top-level `export` modifiers are stripped automatically; static `import` declarations remain unsupported.'
|
|
58
|
+
: 'static `import` declarations and top-level `export` modifiers remain unsupported.'
|
|
59
|
+
const splitSyntax = autoSplitRedeclarations
|
|
60
|
+
? 'Mixed new/existing top-level destructuring is split automatically while preserving assignment semantics.'
|
|
61
|
+
: 'Mixed new/existing top-level destructuring remains unsupported; separate the declaration from the assignment.'
|
|
62
|
+
const recovery = durableReplay
|
|
63
|
+
? 'Direct Node/OS access remains live but is not replayed after a kernel restart.'
|
|
64
|
+
: 'Durable replay is disabled for this profile. Bindings remain reusable only in the current process; a new kernel starts empty.'
|
|
65
|
+
const cordisRecovery = cordisToolsEnabled
|
|
66
|
+
? 'When using Cordis tools, keep large host or client source in a top-level binding before the Cordis call. A Cordis parse or validation error is a runtime failure: bindings assigned before that failure remain live, so retry only the Cordis call with the existing binding instead of resending the source. Treat that binding as the read-only input for the retry.'
|
|
67
|
+
: ''
|
|
68
|
+
return `\`run_code\` continues one persistent PTC REPL. Ordinary top-level bindings remain available to later cells, so reuse them instead of resending setup code. Choose the smallest cell that answers the request and return only the value the next step needs.
|
|
69
|
+
|
|
70
|
+
The host may append a bounded recovery context after a qualifying failure. Treat that context as a session-log-derived diagnostic: use \`edit_run_code\` only when it explicitly proves a complete-cell rerun is safe; otherwise inspect live state in a new short \`run_code\` cell.
|
|
71
|
+
|
|
72
|
+
## Cell conventions
|
|
73
|
+
Expressions that are neither returned nor printed produce no output. Keep large inspection results in bindings or reduce them to targeted excerpts: \`tools.read\` is bounded inspection, not a lossless whole-file reader. Cells are async function bodies; ${moduleSyntax} Use dynamic import or require explicitly when static module syntax is unsupported. ${redeclaration} ${splitSyntax}
|
|
74
|
+
|
|
75
|
+
## Available capabilities
|
|
76
|
+
Use \`capabilities.tree()\`, \`capabilities.find()\`, and \`capabilities.inspect()\` to discover the current request's live \`tools.*\` members before calling an unfamiliar binding. Prefer direct current-cell work; reserve \`code.run\` for source already held as data.
|
|
77
|
+
|
|
78
|
+
Native tool availability, executable names, shells, and path syntax depend on the current DSH profile and execution world; inspect them instead of assuming Windows, WSL, POSIX, or a particular shell. ${recovery}${cordisToolsEnabled ? ` ${cordisRecovery}` : ''}`
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Register the session-bound REPL runtime. */
|
|
82
|
+
function installPtCRuntime(ctx, resolvedConfig, toolSchemasForAgent, sessionId) {
|
|
83
|
+
let activeConfig = resolvedConfig
|
|
84
|
+
let cordisTools
|
|
85
|
+
let runtimeBridge
|
|
86
|
+
let editTransport
|
|
87
|
+
let directSurface
|
|
88
|
+
let ready
|
|
89
|
+
let pendingCordisActivation
|
|
90
|
+
const disposers = []
|
|
91
|
+
let disposed = false
|
|
92
|
+
async function dispose() {
|
|
93
|
+
if (disposed) return
|
|
94
|
+
disposed = true
|
|
95
|
+
pendingCordisActivation?.cancel()
|
|
96
|
+
const failures = []
|
|
97
|
+
for (const dispose of [...disposers].reverse()) {
|
|
98
|
+
if (typeof dispose !== 'function') continue
|
|
99
|
+
try {
|
|
100
|
+
await dispose()
|
|
101
|
+
} catch (error) {
|
|
102
|
+
failures.push(error)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
for (const owner of [directSurface, editTransport, runtimeBridge, cordisTools]) {
|
|
106
|
+
try {
|
|
107
|
+
await owner?.dispose()
|
|
108
|
+
} catch (error) {
|
|
109
|
+
failures.push(error)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (failures.length > 0) throw new AggregateError(failures, 'ptc-plus runtime disposal failed')
|
|
113
|
+
}
|
|
114
|
+
const awaitCordisToolsOwner = async (owner, cancelled) => {
|
|
115
|
+
try {
|
|
116
|
+
await Promise.race([owner.ready, cancelled])
|
|
117
|
+
return owner
|
|
118
|
+
} catch (error) {
|
|
119
|
+
let rollbackError
|
|
120
|
+
try {
|
|
121
|
+
await owner.dispose()
|
|
122
|
+
} catch (caught) {
|
|
123
|
+
rollbackError = caught
|
|
124
|
+
}
|
|
125
|
+
if (cordisTools === owner) cordisTools = undefined
|
|
126
|
+
if (rollbackError !== undefined) {
|
|
127
|
+
throw new AggregateError(
|
|
128
|
+
[error, rollbackError],
|
|
129
|
+
'ptc-plus: Cordis activation and rollback failed',
|
|
130
|
+
{ cause: error },
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
throw error
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const activateCordisToolsOwner = async () => {
|
|
137
|
+
const owner = createCordisToolsOwner(ctx)
|
|
138
|
+
let cancel
|
|
139
|
+
const cancelled = new Promise((_resolve, reject) => {
|
|
140
|
+
cancel = () => reject(new Error('ptc-plus: Cordis activation cancelled'))
|
|
141
|
+
})
|
|
142
|
+
const activation = { owner, cancel }
|
|
143
|
+
cordisTools = owner
|
|
144
|
+
pendingCordisActivation = activation
|
|
145
|
+
try {
|
|
146
|
+
return await awaitCordisToolsOwner(owner, cancelled)
|
|
147
|
+
} finally {
|
|
148
|
+
if (pendingCordisActivation === activation) pendingCordisActivation = undefined
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const cancelCordisActivation = () => pendingCordisActivation?.cancel()
|
|
152
|
+
try {
|
|
153
|
+
cordisTools = activeConfig.cordisToolsEnabled
|
|
154
|
+
? createCordisToolsOwner(ctx)
|
|
155
|
+
: undefined
|
|
156
|
+
if (cordisTools !== undefined) {
|
|
157
|
+
const initialCordisTools = cordisTools
|
|
158
|
+
ready = awaitCordisToolsOwner(initialCordisTools, new Promise(() => {})).catch(error => {
|
|
159
|
+
if (cordisTools === initialCordisTools) cordisTools = undefined
|
|
160
|
+
throw error
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
runtimeBridge = createRuntimeBridgeOwner({
|
|
164
|
+
ctx,
|
|
165
|
+
sessionConfig: activeConfig,
|
|
166
|
+
maxNestedRunCodeDepth: activeConfig.maxNestedRunCodeDepth,
|
|
167
|
+
sessionId,
|
|
168
|
+
toolSchemasForAgent,
|
|
169
|
+
})
|
|
170
|
+
editTransport = createEditTransportOwner(ctx, {
|
|
171
|
+
durableReplay: activeConfig.durableReplay,
|
|
172
|
+
executeTentative: runtimeBridge.executeTentative,
|
|
173
|
+
sessionId,
|
|
174
|
+
toolSchemasForAgent,
|
|
175
|
+
})
|
|
176
|
+
directSurface = createDirectSurfaceOwner({
|
|
177
|
+
editTransport,
|
|
178
|
+
runtimeConfig: activeConfig,
|
|
179
|
+
canonicalizeToolCalls: activeConfig.canonicalizeToolCalls,
|
|
180
|
+
sessionId,
|
|
181
|
+
toolSchemasForAgent,
|
|
182
|
+
})
|
|
183
|
+
disposers.push(ctx.systemPrompt.section({
|
|
184
|
+
name: 'tools:ptc-plus-repl',
|
|
185
|
+
order: 98,
|
|
186
|
+
text: context => {
|
|
187
|
+
if (ctx.tools.get(RUN_CODE, context?.scope) === undefined) return ''
|
|
188
|
+
return replGuidance(
|
|
189
|
+
activeConfig.looseTopLevelRedeclarations,
|
|
190
|
+
activeConfig.durableReplay,
|
|
191
|
+
activeConfig.autoRewriteImports,
|
|
192
|
+
activeConfig.autoStripExports,
|
|
193
|
+
activeConfig.autoSplitRedeclarations,
|
|
194
|
+
activeConfig.cordisToolsEnabled,
|
|
195
|
+
)
|
|
196
|
+
},
|
|
197
|
+
}))
|
|
198
|
+
disposers.push(ctx.on('system-prompt/assemble', (assembly, context, next) => (
|
|
199
|
+
directSurface.assemble(assembly, context, next)
|
|
200
|
+
)))
|
|
201
|
+
disposers.push(ctx.on('llm/stream', (options, next) => directSurface.stream(options, next), { global: true }))
|
|
202
|
+
disposers.push(ctx.on('tools/execute', (exec, next) => {
|
|
203
|
+
const rejected = directSurface.executionRejection(exec)
|
|
204
|
+
if (rejected !== undefined) return rejected
|
|
205
|
+
if (exec.name === RUN_CODE) return runtimeBridge.handleExecute(exec, next)
|
|
206
|
+
return next()
|
|
207
|
+
}))
|
|
208
|
+
disposers.push(ctx.on('tools/result', (exec, result) => {
|
|
209
|
+
directSurface.handleResult(exec)
|
|
210
|
+
if (exec.name === EDIT_RUN_CODE) return editTransport.handleResult(exec, result)
|
|
211
|
+
if (exec.name === RUN_CODE) return runtimeBridge.handleResult(exec, result)
|
|
212
|
+
}))
|
|
213
|
+
disposers.push(ctx.on('agent/disposed', ({ agent }) => {
|
|
214
|
+
directSurface.disposeAgent(agent)
|
|
215
|
+
editTransport.disposeAgent(agent)
|
|
216
|
+
return runtimeBridge.disposeAgent(agent)
|
|
217
|
+
}))
|
|
218
|
+
disposers.push(ctx.on('session/disposed', (session) => {
|
|
219
|
+
directSurface.disposeSession(session)
|
|
220
|
+
editTransport.disposeSession(session)
|
|
221
|
+
return runtimeBridge.disposeSession(session)
|
|
222
|
+
}))
|
|
223
|
+
} catch (error) {
|
|
224
|
+
const cleanup = dispose()
|
|
225
|
+
if (error !== null && typeof error === 'object') {
|
|
226
|
+
Object.defineProperty(error, INSTALL_CLEANUP, { value: cleanup })
|
|
227
|
+
}
|
|
228
|
+
throw error
|
|
229
|
+
}
|
|
230
|
+
async function reconfigure(nextConfig) {
|
|
231
|
+
if (disposed) return
|
|
232
|
+
const previousConfig = activeConfig
|
|
233
|
+
const rollbacks = []
|
|
234
|
+
try {
|
|
235
|
+
runtimeBridge.reconfigure(nextConfig)
|
|
236
|
+
rollbacks.push(() => runtimeBridge.reconfigure(previousConfig))
|
|
237
|
+
editTransport.reconfigure(nextConfig)
|
|
238
|
+
rollbacks.push(() => editTransport.reconfigure(previousConfig))
|
|
239
|
+
directSurface.reconfigure(nextConfig)
|
|
240
|
+
rollbacks.push(() => directSurface.reconfigure(previousConfig))
|
|
241
|
+
|
|
242
|
+
if (nextConfig.cordisToolsEnabled && cordisTools === undefined) {
|
|
243
|
+
await activateCordisToolsOwner()
|
|
244
|
+
} else if (!nextConfig.cordisToolsEnabled && cordisTools !== undefined) {
|
|
245
|
+
const currentCordis = cordisTools
|
|
246
|
+
try {
|
|
247
|
+
await currentCordis.dispose()
|
|
248
|
+
cordisTools = undefined
|
|
249
|
+
} catch (error) {
|
|
250
|
+
try {
|
|
251
|
+
await activateCordisToolsOwner()
|
|
252
|
+
/* c8 ignore next */
|
|
253
|
+
} catch (rollbackError) {
|
|
254
|
+
cordisTools = currentCordis
|
|
255
|
+
throw new AggregateError([error, rollbackError], 'ptc-plus: Cordis reconfiguration and rollback failed', { cause: error })
|
|
256
|
+
}
|
|
257
|
+
throw error
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
activeConfig = nextConfig
|
|
261
|
+
} catch (error) {
|
|
262
|
+
const rollbackFailures = []
|
|
263
|
+
/* c8 ignore next -- the rollback loop's rejection branch is host-specific. */
|
|
264
|
+
for (const rollback of rollbacks.reverse()) {
|
|
265
|
+
try {
|
|
266
|
+
await rollback()
|
|
267
|
+
/* c8 ignore next */
|
|
268
|
+
} catch (rollbackError) { rollbackFailures.push(rollbackError) }
|
|
269
|
+
}
|
|
270
|
+
/* c8 ignore next */
|
|
271
|
+
if (rollbackFailures.length > 0) { throw new AggregateError([error, ...rollbackFailures], 'ptc-plus: live runtime reconfiguration rollback failed', { cause: error }) }
|
|
272
|
+
throw error
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return Object.freeze({ cancelCordisActivation, dispose, reconfigure, ready })
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Register the session-bound REPL runtime. */
|
|
280
|
+
export function apply(ctx, config = {}) {
|
|
281
|
+
const resolvedConfig = resolveConfig(config)
|
|
282
|
+
const toolSchemasForAgent = agent => typeof ctx.tools.schemas === 'function'
|
|
283
|
+
? ctx.tools.schemas(agent)
|
|
284
|
+
: []
|
|
285
|
+
const sessionId = agent => {
|
|
286
|
+
const id = agent?.session?.id ?? agent?.id
|
|
287
|
+
return id === undefined ? undefined : String(id)
|
|
288
|
+
}
|
|
289
|
+
let committed
|
|
290
|
+
let activating
|
|
291
|
+
let disposed = false
|
|
292
|
+
let transitionTail = Promise.resolve()
|
|
293
|
+
let pendingTransitions = 0
|
|
294
|
+
const trackTransition = operation => {
|
|
295
|
+
pendingTransitions += 1
|
|
296
|
+
const tracked = Promise.resolve(operation)
|
|
297
|
+
transitionTail = Promise.allSettled([tracked])
|
|
298
|
+
const settle = () => { pendingTransitions -= 1 }
|
|
299
|
+
tracked.then(settle, settle)
|
|
300
|
+
return tracked
|
|
301
|
+
}
|
|
302
|
+
const enqueueTransition = task => {
|
|
303
|
+
const operation = transitionTail.then(task, task)
|
|
304
|
+
return trackTransition(operation)
|
|
305
|
+
}
|
|
306
|
+
const deferred = () => {
|
|
307
|
+
let resolve
|
|
308
|
+
let reject
|
|
309
|
+
const promise = new Promise((onResolve, onReject) => {
|
|
310
|
+
resolve = onResolve
|
|
311
|
+
reject = onReject
|
|
312
|
+
})
|
|
313
|
+
return { promise, resolve, reject }
|
|
314
|
+
}
|
|
315
|
+
const disposeRuntime = record => {
|
|
316
|
+
if (record?.disposal !== undefined) return record.disposal
|
|
317
|
+
record.disposal = record.runtime.dispose()
|
|
318
|
+
return record.disposal
|
|
319
|
+
}
|
|
320
|
+
const activationError = (error, cleanupError) => new AggregateError(
|
|
321
|
+
[error, cleanupError],
|
|
322
|
+
'ptc-plus: runtime activation and cleanup failed',
|
|
323
|
+
{ cause: error },
|
|
324
|
+
)
|
|
325
|
+
const cancelActivation = record => {
|
|
326
|
+
if (record === undefined) return Promise.resolve()
|
|
327
|
+
record.cancelled = true
|
|
328
|
+
if (activating === record) activating = undefined
|
|
329
|
+
const cleanup = record.runtime === undefined ? Promise.resolve() : disposeRuntime(record)
|
|
330
|
+
if (record.runtime !== undefined && record.trackedDisposal === undefined) {
|
|
331
|
+
record.trackedDisposal = trackTransition(cleanup)
|
|
332
|
+
}
|
|
333
|
+
cleanup.then(
|
|
334
|
+
() => record.completion.resolve({ status: 'superseded' }),
|
|
335
|
+
error => record.completion.reject(error),
|
|
336
|
+
)
|
|
337
|
+
return cleanup
|
|
338
|
+
}
|
|
339
|
+
const startActivation = record => {
|
|
340
|
+
if (record.cancelled || disposed || activating !== record) {
|
|
341
|
+
record.completion.resolve({ status: 'superseded' })
|
|
342
|
+
return
|
|
343
|
+
}
|
|
344
|
+
if (ctx.codeRuntime.language !== 'typescript') {
|
|
345
|
+
throw new Error('ptc-plus: unsupported code runtime language ' + JSON.stringify(ctx.codeRuntime.language) + '; only "typescript" is supported')
|
|
346
|
+
}
|
|
347
|
+
let candidate
|
|
348
|
+
try {
|
|
349
|
+
candidate = installPtCRuntime(ctx, record.config, toolSchemasForAgent, sessionId)
|
|
350
|
+
record.runtime = candidate
|
|
351
|
+
} catch (error) {
|
|
352
|
+
const cleanup = error?.[INSTALL_CLEANUP]
|
|
353
|
+
if (cleanup === undefined) throw error
|
|
354
|
+
const trackedCleanup = trackTransition(cleanup)
|
|
355
|
+
void trackedCleanup.then(
|
|
356
|
+
() => record.completion.reject(error),
|
|
357
|
+
cleanupError => record.completion.reject(activationError(error, cleanupError)),
|
|
358
|
+
)
|
|
359
|
+
if (activating === record) activating = undefined
|
|
360
|
+
return
|
|
361
|
+
}
|
|
362
|
+
const promote = () => {
|
|
363
|
+
if (record.cancelled || disposed || activating !== record) return
|
|
364
|
+
activating = undefined
|
|
365
|
+
committed = record
|
|
366
|
+
record.completion.resolve({ status: 'applied' })
|
|
367
|
+
}
|
|
368
|
+
if (candidate.ready === undefined) {
|
|
369
|
+
promote()
|
|
370
|
+
return
|
|
371
|
+
}
|
|
372
|
+
void Promise.resolve(candidate.ready).then(promote, async error => {
|
|
373
|
+
if (record.cancelled) return
|
|
374
|
+
const cleanup = disposeRuntime(record)
|
|
375
|
+
const trackedCleanup = record.trackedDisposal === undefined
|
|
376
|
+
? (record.trackedDisposal = trackTransition(cleanup))
|
|
377
|
+
: record.trackedDisposal
|
|
378
|
+
if (activating === record) activating = undefined
|
|
379
|
+
try {
|
|
380
|
+
await trackedCleanup
|
|
381
|
+
} catch (cleanupError) {
|
|
382
|
+
record.completion.reject(activationError(error, cleanupError))
|
|
383
|
+
return
|
|
384
|
+
}
|
|
385
|
+
record.completion.reject(error)
|
|
386
|
+
})
|
|
387
|
+
}
|
|
388
|
+
const beginActivation = (nextConfig, generation, prerequisite) => {
|
|
389
|
+
const completion = deferred()
|
|
390
|
+
const record = {
|
|
391
|
+
config: nextConfig,
|
|
392
|
+
generation,
|
|
393
|
+
runtime: undefined,
|
|
394
|
+
disposal: undefined,
|
|
395
|
+
trackedDisposal: undefined,
|
|
396
|
+
cancelled: false,
|
|
397
|
+
completion,
|
|
398
|
+
}
|
|
399
|
+
activating = record
|
|
400
|
+
const start = () => startActivation(record)
|
|
401
|
+
if (prerequisite !== undefined || pendingTransitions > 0) {
|
|
402
|
+
void enqueueTransition(async () => {
|
|
403
|
+
if (prerequisite !== undefined) await prerequisite
|
|
404
|
+
start()
|
|
405
|
+
}).catch(error => {
|
|
406
|
+
if (activating === record) activating = undefined
|
|
407
|
+
completion.reject(error)
|
|
408
|
+
})
|
|
409
|
+
} else {
|
|
410
|
+
try {
|
|
411
|
+
start()
|
|
412
|
+
} catch (error) {
|
|
413
|
+
if (activating === record) activating = undefined
|
|
414
|
+
throw error
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return completion.promise
|
|
418
|
+
}
|
|
419
|
+
const controller = {
|
|
420
|
+
apply(nextConfig, generation) {
|
|
421
|
+
if (disposed) return Promise.resolve({ status: 'superseded' })
|
|
422
|
+
if (activating !== undefined) {
|
|
423
|
+
const cleanup = cancelActivation(activating)
|
|
424
|
+
return beginActivation(nextConfig, generation, cleanup)
|
|
425
|
+
}
|
|
426
|
+
if (committed === undefined) return beginActivation(nextConfig, generation)
|
|
427
|
+
const current = committed
|
|
428
|
+
if (!nextConfig.cordisToolsEnabled) current.runtime.cancelCordisActivation()
|
|
429
|
+
return enqueueTransition(async () => {
|
|
430
|
+
if (disposed || committed !== current) return { status: 'superseded' }
|
|
431
|
+
await current.runtime.reconfigure(nextConfig)
|
|
432
|
+
current.config = nextConfig
|
|
433
|
+
current.generation = generation
|
|
434
|
+
return { status: 'applied' }
|
|
435
|
+
})
|
|
436
|
+
},
|
|
437
|
+
uninstall() {
|
|
438
|
+
const activationCleanup = activating === undefined
|
|
439
|
+
? Promise.resolve()
|
|
440
|
+
: cancelActivation(activating)
|
|
441
|
+
const current = committed
|
|
442
|
+
committed = undefined
|
|
443
|
+
const committedCleanup = current === undefined
|
|
444
|
+
? Promise.resolve()
|
|
445
|
+
: trackTransition(disposeRuntime(current))
|
|
446
|
+
return Promise.all([activationCleanup, committedCleanup]).then(() => undefined)
|
|
447
|
+
},
|
|
448
|
+
committedConfig() {
|
|
449
|
+
return committed?.config
|
|
450
|
+
},
|
|
451
|
+
async dispose() {
|
|
452
|
+
disposed = true
|
|
453
|
+
await controller.uninstall()
|
|
454
|
+
},
|
|
455
|
+
}
|
|
456
|
+
ctx.effect(() => async () => controller.dispose(), 'ptc-plus runtime lifecycle')
|
|
457
|
+
|
|
458
|
+
let configSource = () => resolvedConfig
|
|
459
|
+
let configurationGeneration = 0
|
|
460
|
+
let settingsWriter
|
|
461
|
+
let configurationRollback = false
|
|
462
|
+
const reportActivationFailure = (error) => {
|
|
463
|
+
ctx.logger?.warn?.('ptc-plus: runtime activation failed', error)
|
|
464
|
+
}
|
|
465
|
+
const handleConfigurationFailure = async (error, generation) => {
|
|
466
|
+
reportActivationFailure(error)
|
|
467
|
+
if (generation !== configurationGeneration || configurationRollback) return
|
|
468
|
+
configurationRollback = true
|
|
469
|
+
try {
|
|
470
|
+
if (settingsWriter?.update === undefined) return
|
|
471
|
+
const previousConfig = controller.committedConfig()
|
|
472
|
+
const patch = previousConfig === undefined
|
|
473
|
+
? { enabled: false }
|
|
474
|
+
: Object.fromEntries(CONFIG_FIELDS.map(field => [field.key, previousConfig[field.key]]))
|
|
475
|
+
try {
|
|
476
|
+
await settingsWriter.update(settingsNamespace(SETTINGS_NAMESPACE), patch)
|
|
477
|
+
} catch (rollbackError) {
|
|
478
|
+
reportActivationFailure(new Error(
|
|
479
|
+
`ptc-plus: failed to roll back runtime configuration: ${rollbackError.message}`,
|
|
480
|
+
{ cause: error },
|
|
481
|
+
))
|
|
482
|
+
}
|
|
483
|
+
} finally {
|
|
484
|
+
configurationRollback = false
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
const reconcile = (propagateFailure = false) => {
|
|
488
|
+
const current = resolveConfig(configSource())
|
|
489
|
+
const generation = ++configurationGeneration
|
|
490
|
+
if (!current.enabled) {
|
|
491
|
+
const operation = controller.uninstall()
|
|
492
|
+
if (propagateFailure) return operation
|
|
493
|
+
void operation.catch(error => reportActivationFailure(new Error(
|
|
494
|
+
`ptc-plus: runtime disable failed: ${error.message}`,
|
|
495
|
+
{ cause: error },
|
|
496
|
+
)))
|
|
497
|
+
return
|
|
498
|
+
}
|
|
499
|
+
if (configurationRollback) return
|
|
500
|
+
try {
|
|
501
|
+
const operation = Promise.resolve(controller.apply(current, generation))
|
|
502
|
+
if (propagateFailure) {
|
|
503
|
+
return operation.catch(error => {
|
|
504
|
+
reportActivationFailure(error)
|
|
505
|
+
throw error
|
|
506
|
+
})
|
|
507
|
+
}
|
|
508
|
+
void operation.catch(error => handleConfigurationFailure(error, generation))
|
|
509
|
+
} catch (error) {
|
|
510
|
+
if (settingsWriter === undefined) throw error
|
|
511
|
+
void handleConfigurationFailure(error, generation)
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
if (typeof ctx.inject === 'function') {
|
|
515
|
+
ctx.inject(['settings'], settings => {
|
|
516
|
+
settingsWriter = settings.settings
|
|
517
|
+
})
|
|
518
|
+
installSettingsSection(
|
|
519
|
+
ctx,
|
|
520
|
+
settingsNamespace(SETTINGS_NAMESPACE),
|
|
521
|
+
Config,
|
|
522
|
+
resolvedConfig,
|
|
523
|
+
{
|
|
524
|
+
setSource(source) {
|
|
525
|
+
configSource = source
|
|
526
|
+
},
|
|
527
|
+
onChange: reconcile,
|
|
528
|
+
},
|
|
529
|
+
)
|
|
530
|
+
}
|
|
531
|
+
if (configurationGeneration === 0) return reconcile(settingsWriter === undefined)
|
|
532
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const AST_METADATA_KEYS = new Set(['start', 'end', 'loc', 'range'])
|
|
2
|
+
|
|
3
|
+
export const SKIP_AST_CHILDREN = Symbol('skip AST children')
|
|
4
|
+
|
|
5
|
+
/** Walk ESTree-shaped values while ignoring source-location metadata. */
|
|
6
|
+
export function walkAst(node, enter, leave = undefined, context = undefined, parent = undefined, parentKey = undefined) {
|
|
7
|
+
if (node === null || typeof node !== 'object') return
|
|
8
|
+
const entered = enter(node, parent, parentKey, context)
|
|
9
|
+
if (entered === SKIP_AST_CHILDREN) return
|
|
10
|
+
const childContext = entered === undefined ? context : entered
|
|
11
|
+
for (const [key, value] of Object.entries(node)) {
|
|
12
|
+
if (AST_METADATA_KEYS.has(key)) continue
|
|
13
|
+
if (Array.isArray(value)) {
|
|
14
|
+
for (const child of value) walkAst(child, enter, leave, childContext, node, key)
|
|
15
|
+
} else {
|
|
16
|
+
walkAst(value, enter, leave, childContext, node, key)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
leave?.(node, parent, parentKey, childContext)
|
|
20
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { record } from './record-utils.js'
|
|
2
|
+
|
|
3
|
+
const SOFT_PLUGIN_NAMESPACES = new Set(['capabilities', 'code', 'repl'])
|
|
4
|
+
|
|
5
|
+
function name(value, label) {
|
|
6
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
7
|
+
throw new TypeError(`ptc-plus: ${label} must be a non-empty string`)
|
|
8
|
+
}
|
|
9
|
+
return value
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function normalizeErrorClass(value, globalName) {
|
|
13
|
+
if (value === undefined) return undefined
|
|
14
|
+
const descriptor = record(value, `binding ${globalName} errorClass`)
|
|
15
|
+
return Object.freeze({
|
|
16
|
+
name: name(descriptor.name, `binding ${globalName} errorClass.name`),
|
|
17
|
+
memberNameProperty: name(
|
|
18
|
+
descriptor.memberNameProperty,
|
|
19
|
+
`binding ${globalName} errorClass.memberNameProperty`,
|
|
20
|
+
),
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function normalizeEmptyObjectMembers(value, globalName, members) {
|
|
25
|
+
if (value === undefined) return Object.freeze([])
|
|
26
|
+
if (globalName !== 'tools' || !Array.isArray(value)) {
|
|
27
|
+
throw new TypeError(`ptc-plus: binding ${globalName} emptyObjectMembers must be an array for tools`)
|
|
28
|
+
}
|
|
29
|
+
const available = new Set(members)
|
|
30
|
+
const seen = new Set()
|
|
31
|
+
const normalized = value.map((member) => {
|
|
32
|
+
const memberName = name(member, `binding ${globalName} emptyObjectMembers entry`)
|
|
33
|
+
if (!available.has(memberName)) {
|
|
34
|
+
throw new TypeError(`ptc-plus: binding ${globalName} emptyObjectMembers contains unknown member ${JSON.stringify(memberName)}`)
|
|
35
|
+
}
|
|
36
|
+
if (seen.has(memberName)) {
|
|
37
|
+
throw new TypeError(`ptc-plus: binding ${globalName} emptyObjectMembers contains duplicate member ${JSON.stringify(memberName)}`)
|
|
38
|
+
}
|
|
39
|
+
seen.add(memberName)
|
|
40
|
+
return memberName
|
|
41
|
+
})
|
|
42
|
+
return Object.freeze(normalized)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function normalizeBindingDescriptors(bindings) {
|
|
46
|
+
if (!Array.isArray(bindings)) throw new TypeError('ptc-plus: bindings must be an array')
|
|
47
|
+
const reservedNames = new Set()
|
|
48
|
+
const namespaceNames = new Set()
|
|
49
|
+
const namespaces = bindings.map((value) => {
|
|
50
|
+
const namespace = record(value, 'binding namespace')
|
|
51
|
+
const globalName = name(namespace.global, 'binding namespace global')
|
|
52
|
+
if (namespaceNames.has(globalName)) {
|
|
53
|
+
throw new TypeError(`ptc-plus: duplicate binding namespace global ${JSON.stringify(globalName)}`)
|
|
54
|
+
}
|
|
55
|
+
namespaceNames.add(globalName)
|
|
56
|
+
const functions = record(namespace.functions, `binding ${globalName} functions`)
|
|
57
|
+
const members = Reflect.ownKeys(functions).map((member) => {
|
|
58
|
+
if (typeof member !== 'string' || member.length === 0) {
|
|
59
|
+
throw new TypeError(`ptc-plus: binding ${globalName} member names must be non-empty strings`)
|
|
60
|
+
}
|
|
61
|
+
const descriptor = Object.getOwnPropertyDescriptor(functions, member)
|
|
62
|
+
if (descriptor === undefined || !Object.hasOwn(descriptor, 'value') || typeof descriptor.value !== 'function') {
|
|
63
|
+
throw new TypeError(`ptc-plus: binding ${globalName}.${member} is not an own callable value`)
|
|
64
|
+
}
|
|
65
|
+
return member
|
|
66
|
+
})
|
|
67
|
+
const errorClass = normalizeErrorClass(namespace.errorClass, globalName)
|
|
68
|
+
const emptyObjectMembers = normalizeEmptyObjectMembers(
|
|
69
|
+
namespace.emptyObjectMembers,
|
|
70
|
+
globalName,
|
|
71
|
+
members,
|
|
72
|
+
)
|
|
73
|
+
if (!SOFT_PLUGIN_NAMESPACES.has(globalName)) reservedNames.add(globalName)
|
|
74
|
+
if (errorClass !== undefined) reservedNames.add(errorClass.name)
|
|
75
|
+
return Object.freeze({
|
|
76
|
+
global: globalName,
|
|
77
|
+
functions,
|
|
78
|
+
members: Object.freeze(members),
|
|
79
|
+
...(emptyObjectMembers.length === 0 ? {} : { emptyObjectMembers }),
|
|
80
|
+
...(errorClass === undefined ? {} : { errorClass }),
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
return Object.freeze({
|
|
84
|
+
namespaces: Object.freeze(namespaces),
|
|
85
|
+
reservedNames,
|
|
86
|
+
workerDescriptors: Object.freeze(namespaces.map(namespace => Object.freeze({
|
|
87
|
+
global: namespace.global,
|
|
88
|
+
members: namespace.members,
|
|
89
|
+
...(namespace.emptyObjectMembers === undefined
|
|
90
|
+
? {}
|
|
91
|
+
: { emptyObjectMembers: namespace.emptyObjectMembers }),
|
|
92
|
+
...(namespace.errorClass === undefined ? {} : { errorClass: namespace.errorClass }),
|
|
93
|
+
}))),
|
|
94
|
+
})
|
|
95
|
+
}
|