dsh-ptc-plus 0.3.2 → 0.4.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/README.en.md +65 -78
- package/README.md +65 -78
- package/assets/ptc-plus-bindings-en.png +0 -0
- package/assets/ptc-plus-bindings-zh.png +0 -0
- package/assets/ptc-plus-repair-en.png +0 -0
- package/assets/ptc-plus-repair-zh.png +0 -0
- package/assets/ptc-plus-repl-workspace-en.png +0 -0
- package/assets/ptc-plus-repl-workspace-zh.png +0 -0
- package/assets/ptc-plus-settings-en.png +0 -0
- package/assets/ptc-plus-settings-zh.png +0 -0
- package/client.js +30596 -742
- package/compiler-amaro.cjs +5 -0
- package/compiler-amaro.cjs.map +6 -0
- package/compiler-asset-124f80c1bba92ff5fe79cfec6e8ad518f476de4a42514cae3919538887f510e8.txt +1 -0
- package/compiler-assets.json +3 -0
- package/compiler-core.cjs +554 -0
- package/compiler-core.cjs.map +6 -0
- package/compiler-platform.cjs +2 -0
- package/compiler-platform.cjs.map +6 -0
- package/compiler-typescript.cjs +388 -0
- package/compiler-typescript.cjs.map +6 -0
- package/docs/README.md +6 -0
- package/docs/adr/0004-declare-program-bindings-at-the-host.md +1 -1
- package/docs/adr/0006-rewrite-module-syntax-with-an-ast.md +5 -3
- package/docs/adr/0007-separate-worker-transport-from-session-semantics.md +1 -1
- package/docs/adr/0009-bound-expensive-acceptance-default.md +2 -0
- package/docs/adr/0010-session-log-derived-recovery-tips.md +59 -5
- package/docs/adr/0011-isolate-the-local-recovery-tip-provider.md +1 -1
- package/docs/adr/0012-preserve-mixed-destructuring-initialization.md +3 -1
- package/docs/adr/0013-isolate-repl-redeclaration-convenience.md +3 -1
- package/docs/adr/0014-persist-cell-rewrite-policy.md +6 -2
- package/docs/adr/0015-preserve-source-positions-through-rewrites.md +1 -1
- package/docs/adr/0016-archive-resolved-review-findings.md +1 -1
- package/docs/adr/0017-track-the-latest-dsh-public-surface.md +49 -0
- package/docs/adr/0018-use-versioned-fixture-for-ab-benchmark.md +3 -3
- package/docs/adr/0019-plugin-settings-and-kill-switch.md +5 -3
- package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +2 -2
- package/docs/adr/0021-separate-host-bootstrap-from-session-runtime.md +12 -0
- package/docs/adr/0023-global-user-bindings.md +104 -0
- package/docs/adr/0024-repl-console-observation.md +33 -0
- package/docs/adr/0025-use-versioned-logical-binding-identities.md +119 -0
- package/docs/adr/0026-persist-the-delivery-verdict-against-the-verified-candidate.md +21 -0
- package/docs/adr/0027-run-the-session-kernel-in-a-killable-helper-process.md +37 -0
- package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +103 -0
- package/docs/adr/0029-keep-plugin-unavailability-out-of-the-host-agent-lifecycle.md +27 -0
- package/docs/adr/0030-serve-both-typert-codec-generations-from-one-codec.md +27 -0
- package/docs/architecture.md +73 -21
- package/docs/binding-review-placement-design.md +200 -0
- package/docs/capability-projection.md +1 -1
- package/docs/client-ui.md +95 -19
- package/docs/durability-design.md +25 -10
- package/docs/evaluation.md +78 -6
- package/docs/installation.md +41 -6
- package/docs/publishing.md +5 -3
- package/docs/runtime-reference.md +72 -25
- package/docs/semantic-validation.md +29 -0
- package/docs/stateful-binding-updates-proposal.md +755 -0
- package/docs/user-bindings.en.md +64 -0
- package/docs/user-bindings.md +64 -0
- package/docs/value-wire.md +3 -3
- package/docs/verification.md +57 -0
- package/index.js +369 -92
- package/internal/ast-traversal.js +5 -2
- package/internal/binding-descriptors.js +1 -0
- package/internal/binding-pattern-traversal.js +25 -0
- package/internal/binding-pattern.js +26 -29
- package/internal/binding-source-analysis.js +391 -0
- package/internal/binding-update-policy.js +66 -0
- package/internal/bounded-worker-round.js +56 -0
- package/internal/callable-definition-environment.js +42 -0
- package/internal/callable-source-catalog.js +129 -0
- package/internal/callable-source-encoding.js +47 -0
- package/internal/callable-source-facts.js +459 -0
- package/internal/callable-source-format.js +2 -0
- package/internal/callable-source-lowering.js +123 -0
- package/internal/cell-analysis-contract.js +25 -0
- package/internal/cell-analysis.js +240 -290
- package/internal/cell-completion.js +15 -0
- package/internal/cell-error.js +7 -0
- package/internal/cell-parser.js +66 -0
- package/internal/cell-rewriter.js +3 -677
- package/internal/commonjs-export-evidence.js +63 -0
- package/internal/commonjs-source-evidence.js +72 -0
- package/internal/compiler-amaro.cjs +2 -0
- package/internal/compiler-compression.js +12 -0
- package/internal/compiler-data.js +58 -0
- package/internal/compiler-descriptors.js +54 -0
- package/internal/compiler-dynamic-cache.js +48 -0
- package/internal/compiler-entry.js +35 -0
- package/internal/compiler-generated-names.js +17 -0
- package/internal/compiler-intrinsics.js +84 -0
- package/internal/compiler-module-links.js +34 -0
- package/internal/compiler-operations.js +61 -0
- package/internal/compiler-platform-factory.js +84 -0
- package/internal/compiler-platform.js +136 -0
- package/internal/compiler-record-roles.js +53 -0
- package/internal/compiler-region-output.js +259 -0
- package/internal/compiler-resource-lowering.js +103 -0
- package/internal/compiler-scope-facts.js +115 -0
- package/internal/compiler-service.js +109 -0
- package/internal/compiler-source-cache.js +32 -0
- package/internal/compiler-source-pieces.js +93 -0
- package/internal/compiler-source-regions.js +132 -0
- package/internal/compiler-storage-source.js +54 -0
- package/internal/compiler-text.js +15 -0
- package/internal/compiler-typescript.cjs +2 -0
- package/internal/config-spec.js +83 -27
- package/internal/cordis-tools-owner.js +130 -21
- package/internal/diagnostic.js +27 -1
- package/internal/direct-surface-owner.js +76 -23
- package/internal/dynamic-binding-evidence.js +16 -0
- package/internal/dynamic-environment-compiler.js +779 -0
- package/internal/dynamic-environment-integration.js +87 -0
- package/internal/dynamic-environment-runtime.js +362 -0
- package/internal/dynamic-native-calls.js +150 -0
- package/internal/dynamic-native-runtime.js +248 -0
- package/internal/dynamic-scope-analysis.js +90 -0
- package/internal/edit-transport-owner.js +27 -2
- package/internal/execution-seam-compat.js +193 -0
- package/internal/failure-reporting.js +118 -18
- package/internal/host-rpc.js +66 -0
- package/internal/isolated-worker.js +548 -0
- package/internal/kernel-child.js +139 -0
- package/internal/kernel-worker.js +904 -120
- package/internal/language-semantics.js +14 -0
- package/internal/legacy-binding-catalog.js +48 -0
- package/internal/legacy-cell-analysis.js +600 -0
- package/internal/legacy-cell-parser.js +50 -0
- package/internal/legacy-cell-rewriter.js +631 -0
- package/internal/legacy-repl-convenience.js +291 -0
- package/internal/managed-module-operations.js +170 -0
- package/internal/module-compilation.js +321 -0
- package/internal/module-runtime-platform.js +19 -0
- package/internal/module-transform-contract.js +12 -0
- package/internal/native-await-declarations.js +68 -0
- package/internal/native-root-compilation.js +54 -0
- package/internal/native-root-dynamic.js +67 -0
- package/internal/recovery-tips.js +27 -15
- package/internal/rejected-cell-editor.js +13 -8
- package/internal/repl-convenience.js +2 -289
- package/internal/repl-memory-projection.js +81 -12
- package/internal/repl-observation-interest.js +70 -0
- package/internal/repl-rewrite-contract.js +6 -0
- package/internal/repl-scope-normalizer.js +2091 -0
- package/internal/repl-value-observer.js +114 -0
- package/internal/rpc-contract.js +52 -0
- package/internal/runtime-bridge-owner.js +172 -28
- package/internal/runtime-config.js +48 -52
- package/internal/runtime-contexts.js +82 -10
- package/internal/runtime-messages.js +111 -0
- package/internal/session-cell-executor.js +401 -87
- package/internal/session-journal-recovery.js +684 -0
- package/internal/session-journal-schema.js +106 -0
- package/internal/session-journal.js +184 -654
- package/internal/session-log-view.js +42 -9
- package/internal/session-runtime.js +256 -95
- package/internal/session-state.js +247 -71
- package/internal/source-position-map.js +211 -71
- package/internal/stateful-module-compiler.js +154 -0
- package/internal/stateful-module-runtime.js +303 -0
- package/internal/stateful-root-compiler.js +598 -0
- package/internal/stateful-root-runtime.js +340 -0
- package/internal/typert-codec-compat.js +28 -0
- package/internal/typescript-transform.js +29 -0
- package/internal/user-binding-authoring.js +70 -0
- package/internal/user-binding-console-worker.js +184 -0
- package/internal/user-binding-console.js +148 -0
- package/internal/user-binding-draft-projection.js +335 -0
- package/internal/user-binding-model-context.js +31 -0
- package/internal/user-binding-runner.js +56 -0
- package/internal/user-bindings-owner.js +1065 -0
- package/internal/user-bindings-store.js +293 -0
- package/internal/user-bindings.js +353 -0
- package/internal/value-wire-schema.js +28 -0
- package/internal/value-wire.js +29 -20
- package/internal/worker-client.js +57 -67
- package/internal/worker-environment.js +45 -0
- package/internal/worker-output-relay.js +29 -0
- package/internal/worker-shutdown.js +14 -0
- package/package.json +75 -7
- package/src/client-activity.js +88 -53
- package/src/client-authoring-view.js +580 -0
- package/src/client-binding-review.js +230 -0
- package/src/client-bindings-data.js +29 -0
- package/src/client-catalog.js +127 -0
- package/src/client-code-editor.js +84 -0
- package/src/client-console.js +112 -0
- package/src/client-copy.js +370 -0
- package/src/client-feature-gates.js +62 -0
- package/src/client-host-compat.js +150 -0
- package/src/client-indicator-view.js +267 -0
- package/src/client-repl-view.js +292 -0
- package/src/client-rpc.js +20 -0
- package/src/client-settings-view.js +221 -0
- package/src/client-styles.js +106 -0
- package/src/client-tool-view.js +108 -0
- package/src/client-transport.js +159 -0
- package/src/client-workbench.js +521 -0
- package/src/client.js +233 -615
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto'
|
|
2
|
+
import { processExitDescription } from './failure-reporting.js'
|
|
3
|
+
import { IsolatedOwner } from './isolated-worker.js'
|
|
4
|
+
import {
|
|
5
|
+
createBoundedWorkerRound,
|
|
6
|
+
ROUND_CANCELLED,
|
|
7
|
+
ROUND_OUTPUT_LIMIT,
|
|
8
|
+
ROUND_TIMEOUT,
|
|
9
|
+
} from './bounded-worker-round.js'
|
|
10
|
+
import { normalizeWorkerEnvironment } from './worker-client.js'
|
|
11
|
+
const CONSOLE_HELPER = new URL('./kernel-child.js', import.meta.url)
|
|
12
|
+
|
|
13
|
+
export const CONSOLE_IDLE_MS = 10 * 60 * 1000
|
|
14
|
+
const WORKER_URL = new URL('./user-binding-console-worker.js', import.meta.url)
|
|
15
|
+
const MAX_CONSOLES = 4
|
|
16
|
+
const MAX_SOURCE_BYTES = 1024 * 1024
|
|
17
|
+
const ROUND_REASONS = Object.freeze({
|
|
18
|
+
[ROUND_TIMEOUT]: 'execution timed out',
|
|
19
|
+
[ROUND_CANCELLED]: 'stopped',
|
|
20
|
+
[ROUND_OUTPUT_LIMIT]: 'console output limit exceeded',
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
/** Own only user workbench workers; no Agent state, replay or persistence. */
|
|
24
|
+
export class UserBindingConsole {
|
|
25
|
+
constructor(options) {
|
|
26
|
+
this.options = options
|
|
27
|
+
this.environments = new Map()
|
|
28
|
+
// Every console transport is owned here until its real exit is observed.
|
|
29
|
+
this.owner = new IsolatedOwner()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
release(capability, reason = 'reset') {
|
|
33
|
+
const environment = this.environments.get(capability)
|
|
34
|
+
if (environment === undefined) return
|
|
35
|
+
this.environments.delete(capability)
|
|
36
|
+
clearTimeout(environment.idleTimer)
|
|
37
|
+
environment.active?.finish({ error: reason, released: true })
|
|
38
|
+
// One close per transport owns the release request, the kill deadline and the
|
|
39
|
+
// real exit; a refusal stays visible instead of becoming an unhandled rejection.
|
|
40
|
+
// The owner keeps this instance (and any refusal) until a real exit arrives.
|
|
41
|
+
void this.owner.stop(environment.hostId).catch(() => {})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Release every environment while this console's owner stays usable. */
|
|
45
|
+
async releaseAll(reason = 'released') {
|
|
46
|
+
for (const capability of [...this.environments.keys()]) this.release(capability, reason)
|
|
47
|
+
return this.owner.releaseAll()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async dispose() {
|
|
51
|
+
for (const capability of this.environments.keys()) this.release(capability, 'disposed')
|
|
52
|
+
return this.owner.dispose()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async reconfigure(options) {
|
|
56
|
+
// Reconfiguration releases the current environments; the owner stays usable so
|
|
57
|
+
// a later run can start again.
|
|
58
|
+
if (['maxWallMs', 'maxOutputBytes', 'maxOldGenerationSizeMb'].some(key => this.options[key] !== options[key])) {
|
|
59
|
+
for (const capability of [...this.environments.keys()]) this.release(capability, 'reconfigured')
|
|
60
|
+
const failures = await this.owner.releaseAll()
|
|
61
|
+
if (failures.length > 0) {
|
|
62
|
+
throw new AggregateError(failures, 'user binding console reconfiguration failed')
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
this.options = options
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async run({ environment: capability, source, code }, signal) {
|
|
69
|
+
for (const [label, value] of [['source', source], ['code', code]]) {
|
|
70
|
+
if (typeof value !== 'string' || value.trim() === '' || Buffer.byteLength(value) > MAX_SOURCE_BYTES) {
|
|
71
|
+
throw new TypeError(`console ${label} must be non-empty TypeScript within 1 MiB`)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
signal?.throwIfAborted()
|
|
75
|
+
let environment = this.environments.get(capability)
|
|
76
|
+
if (environment?.active !== undefined) throw new Error('console is already running')
|
|
77
|
+
const reset = capability !== undefined && (environment === undefined || environment.source !== source)
|
|
78
|
+
if (environment !== undefined && environment.source !== source) {
|
|
79
|
+
this.release(capability, 'source changed')
|
|
80
|
+
environment = undefined
|
|
81
|
+
}
|
|
82
|
+
if (environment === undefined) {
|
|
83
|
+
if (this.environments.size >= MAX_CONSOLES) throw new Error('too many active code consoles')
|
|
84
|
+
capability = randomUUID()
|
|
85
|
+
const started = this.owner.start({
|
|
86
|
+
helper: CONSOLE_HELPER,
|
|
87
|
+
entry: WORKER_URL.href,
|
|
88
|
+
workerData: { source, cwd: this.options.cwd, maxOutputBytes: this.options.maxOutputBytes },
|
|
89
|
+
env: normalizeWorkerEnvironment(process.env),
|
|
90
|
+
resourceLimits: { maxOldGenerationSizeMb: this.options.maxOldGenerationSizeMb },
|
|
91
|
+
protocol: 'parent-port',
|
|
92
|
+
})
|
|
93
|
+
const worker = started.transport
|
|
94
|
+
environment = { worker, hostId: started.id, source, round: undefined, active: undefined, sequence: 0 }
|
|
95
|
+
this.environments.set(capability, environment)
|
|
96
|
+
const fail = reason => this.release(capability, reason)
|
|
97
|
+
for (const [stream, channel] of [[worker.stdout, 'stdout'], [worker.stderr, 'stderr']]) {
|
|
98
|
+
stream.on('data', chunk => environment.round?.capture(channel, String(chunk)))
|
|
99
|
+
}
|
|
100
|
+
worker.on('error', error => fail(error.message))
|
|
101
|
+
worker.on('exit', (code, signal) => {
|
|
102
|
+
fail(`console worker exited (${processExitDescription(code, signal)})`)
|
|
103
|
+
})
|
|
104
|
+
worker.on('message', message => {
|
|
105
|
+
if (message === null || typeof message !== 'object') { fail('invalid console result'); return }
|
|
106
|
+
if (message.fatal === true) { fail(message.error); return }
|
|
107
|
+
if (message.id !== environment.active?.id) return
|
|
108
|
+
if (typeof message.output !== 'string' && typeof message.error !== 'string') {
|
|
109
|
+
fail('invalid console result'); return
|
|
110
|
+
}
|
|
111
|
+
const result = typeof message.error === 'string' ? { error: message.error } : { output: message.output }
|
|
112
|
+
if (environment.round.exceeds(result)) fail('console output limit exceeded')
|
|
113
|
+
else environment.active.finish(result)
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
clearTimeout(environment.idleTimer)
|
|
117
|
+
const id = ++environment.sequence
|
|
118
|
+
const started = Date.now()
|
|
119
|
+
return new Promise(resolve => {
|
|
120
|
+
const finish = result => {
|
|
121
|
+
environment.round = undefined
|
|
122
|
+
environment.active = undefined
|
|
123
|
+
const live = this.environments.get(capability) === environment
|
|
124
|
+
const expiresAt = live ? Date.now() + CONSOLE_IDLE_MS : null
|
|
125
|
+
if (live) {
|
|
126
|
+
environment.idleTimer = setTimeout(() => this.release(capability, 'idle'), CONSOLE_IDLE_MS)
|
|
127
|
+
environment.idleTimer.unref()
|
|
128
|
+
}
|
|
129
|
+
resolve({ ...result, id: undefined, environment: live ? capability : null,
|
|
130
|
+
reset, expiresAt, logs: round.logs, durationMs: Date.now() - started })
|
|
131
|
+
}
|
|
132
|
+
const round = createBoundedWorkerRound({
|
|
133
|
+
maxOutputBytes: this.options.maxOutputBytes,
|
|
134
|
+
maxWallMs: this.options.maxWallMs,
|
|
135
|
+
signal,
|
|
136
|
+
settle: ({ ok, result, reason }) => {
|
|
137
|
+
if (ok) { finish(result); return }
|
|
138
|
+
this.release(capability, ROUND_REASONS[reason])
|
|
139
|
+
finish({ error: ROUND_REASONS[reason], released: true })
|
|
140
|
+
},
|
|
141
|
+
})
|
|
142
|
+
environment.round = round
|
|
143
|
+
environment.active = { id, finish: result => round.succeed(result) }
|
|
144
|
+
try { environment.worker.postMessage({ id, code }) }
|
|
145
|
+
catch (error) { this.release(capability, error.message) }
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import { isRecord } from './record-utils.js'
|
|
2
|
+
import { normalizeBindingModelContext } from './user-binding-model-context.js'
|
|
3
|
+
|
|
4
|
+
export const USER_BINDING_DRAFT_KEY = 'ptcPlusBindingDraft'
|
|
5
|
+
export const USER_BINDING_DRAFT_META_KEY = 'dshPtcPlusBindingDraft'
|
|
6
|
+
|
|
7
|
+
const META_VERSION = 1
|
|
8
|
+
const MAX_GENERATION_LENGTH = 128
|
|
9
|
+
const MAX_CAPABILITY_LENGTH = 128
|
|
10
|
+
const MAX_COMMAND_ID_LENGTH = 256
|
|
11
|
+
const META_FIELDS = new Set(['version', 'generation', 'capability'])
|
|
12
|
+
const STATE_FIELDS = new Set([
|
|
13
|
+
'generation', 'phase', 'capability', 'commandId', 'fallbackReady', 'fallbackCommandId',
|
|
14
|
+
'turnStarted',
|
|
15
|
+
])
|
|
16
|
+
const VIEW_FIELDS = new Set(['phase', 'capability', 'commandId'])
|
|
17
|
+
const PHASES = new Set(['idle', 'pending', 'ready', 'failed'])
|
|
18
|
+
const ACTION_PREFIX = 'Global User Binding action (persistence does not imply session activation):\n'
|
|
19
|
+
|
|
20
|
+
function normalizeCandidate(value) {
|
|
21
|
+
if (!isRecord(value) || typeof value.requestId !== 'string' || value.requestId.length === 0
|
|
22
|
+
|| value.requestId.length > 128 || !Number.isSafeInteger(value.version) || value.version < 1
|
|
23
|
+
|| !['new', 'edit'].includes(value.mode) || !isRecord(value.entry)
|
|
24
|
+
|| !['namespace', 'top-level'].includes(value.entry.scope)
|
|
25
|
+
|| value.entry.enabled !== false || !Array.isArray(value.entry.symbols)
|
|
26
|
+
|| value.entry.symbols.some(symbol => typeof symbol !== 'string')
|
|
27
|
+
|| ['id', 'name', 'purpose', 'source'].some(key => typeof value.entry[key] !== 'string')
|
|
28
|
+
|| value.entry.source.length === 0 || value.entry.source.length > 65536) {
|
|
29
|
+
throw new Error('invalid dsh-ptc-plus accepted binding candidate')
|
|
30
|
+
}
|
|
31
|
+
const modelContext = normalizeBindingModelContext(value.entry.modelContext)
|
|
32
|
+
return Object.freeze({
|
|
33
|
+
requestId: value.requestId, commandId: normalizeCommandId(value.commandId), version: value.version, mode: value.mode,
|
|
34
|
+
entry: Object.freeze({
|
|
35
|
+
id: value.entry.id, name: value.entry.name, scope: value.entry.scope,
|
|
36
|
+
symbols: Object.freeze([...value.entry.symbols]), purpose: value.entry.purpose,
|
|
37
|
+
source: value.entry.source, enabled: false,
|
|
38
|
+
...(modelContext === undefined ? {} : { modelContext }),
|
|
39
|
+
}),
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normalizeAction(value) {
|
|
44
|
+
if (!exactFields(value, new Set(['requestId', 'id', 'state', 'enabled']))
|
|
45
|
+
|| typeof value.requestId !== 'string' || value.requestId.length === 0 || value.requestId.length > 128
|
|
46
|
+
|| typeof value.id !== 'string' || value.id.length === 0 || value.id.length > 64
|
|
47
|
+
|| !['saved', 'discarded'].includes(value.state) || typeof value.enabled !== 'boolean'
|
|
48
|
+
|| (value.state === 'discarded' && value.enabled)) throw new Error('invalid binding action receipt')
|
|
49
|
+
return Object.freeze({ requestId: value.requestId, id: value.id, state: value.state, enabled: value.enabled })
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function bindingActionNotice(action) {
|
|
53
|
+
return {
|
|
54
|
+
source: { kind: 'plugin', plugin: 'ptc-plus', form: 'notice', summary: 'Global User Binding action' },
|
|
55
|
+
content: [{ type: 'text', text: ACTION_PREFIX + JSON.stringify(normalizeAction(action)) }],
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function readBindingAction(message) {
|
|
60
|
+
if (message?.source?.kind !== 'plugin' || message.source.plugin !== 'ptc-plus'
|
|
61
|
+
|| message.source.form !== 'notice' || message.source.summary !== 'Global User Binding action'
|
|
62
|
+
|| message.content?.length !== 1 || message.content[0]?.type !== 'text') return undefined
|
|
63
|
+
try {
|
|
64
|
+
const text = message.content[0].text
|
|
65
|
+
if (typeof text !== 'string' || !text.startsWith(ACTION_PREFIX)) return undefined
|
|
66
|
+
const action = normalizeAction(JSON.parse(text.slice(ACTION_PREFIX.length)))
|
|
67
|
+
return text === ACTION_PREFIX + JSON.stringify(action) ? action : undefined
|
|
68
|
+
} catch {
|
|
69
|
+
return undefined
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function normalizeHistory(value) {
|
|
74
|
+
if (!Array.isArray(value)) throw new Error('invalid binding review history')
|
|
75
|
+
return Object.freeze(value.map(record => {
|
|
76
|
+
const candidate = normalizeCandidate(record.candidate)
|
|
77
|
+
const action = record.action === null ? null : normalizeAction(record.action)
|
|
78
|
+
if (action !== null && (action.requestId !== candidate.requestId || action.id !== candidate.entry.id)) {
|
|
79
|
+
throw new Error('binding action does not identify its candidate')
|
|
80
|
+
}
|
|
81
|
+
if (!Number.isSafeInteger(record.acceptedSeq) || record.acceptedSeq < 0) {
|
|
82
|
+
throw new Error('invalid binding acceptance sequence')
|
|
83
|
+
}
|
|
84
|
+
if (record.commandId !== candidate.commandId) throw new Error('binding review does not identify its command')
|
|
85
|
+
return Object.freeze({
|
|
86
|
+
commandId: normalizeCommandId(record.commandId), acceptedSeq: record.acceptedSeq, candidate, action,
|
|
87
|
+
})
|
|
88
|
+
}))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function historyFields(value) {
|
|
92
|
+
return value.history === undefined ? {} : { history: normalizeHistory(value.history) }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function exactFields(value, fields) {
|
|
96
|
+
if (!isRecord(value)) return false
|
|
97
|
+
const keys = Reflect.ownKeys(value)
|
|
98
|
+
return keys.length === fields.size && keys.every(key => (
|
|
99
|
+
typeof key === 'string' && fields.has(key)
|
|
100
|
+
&& Object.prototype.propertyIsEnumerable.call(value, key)
|
|
101
|
+
))
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function normalizeGeneration(value) {
|
|
105
|
+
if (typeof value !== 'string' || value.length === 0 || value.length > MAX_GENERATION_LENGTH) {
|
|
106
|
+
throw new Error('invalid dsh-ptc-plus binding draft generation')
|
|
107
|
+
}
|
|
108
|
+
return value
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function normalizeUserBindingDraftCapability(value) {
|
|
112
|
+
if (value === null) return null
|
|
113
|
+
if (typeof value !== 'string' || value.length === 0 || value.length > MAX_CAPABILITY_LENGTH) {
|
|
114
|
+
throw new Error('invalid dsh-ptc-plus binding draft capability')
|
|
115
|
+
}
|
|
116
|
+
return value
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function normalizePhase(value) {
|
|
120
|
+
if (typeof value !== 'string' || !PHASES.has(value)) {
|
|
121
|
+
throw new Error('invalid dsh-ptc-plus binding draft phase')
|
|
122
|
+
}
|
|
123
|
+
return value
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function normalizeCommandId(value) {
|
|
127
|
+
if (value === null) return null
|
|
128
|
+
if (typeof value !== 'string' || value.length === 0 || value.length > MAX_COMMAND_ID_LENGTH) {
|
|
129
|
+
throw new Error('invalid dsh-ptc-plus binding draft command id')
|
|
130
|
+
}
|
|
131
|
+
return value
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Read a current-generation draft locator from private result metadata. */
|
|
135
|
+
export function userBindingDraftCapabilityFromMeta(meta, generation) {
|
|
136
|
+
if (!isRecord(meta) || !Object.hasOwn(meta, USER_BINDING_DRAFT_META_KEY)) return undefined
|
|
137
|
+
try {
|
|
138
|
+
const normalized = normalizeMetadata(meta[USER_BINDING_DRAFT_META_KEY])
|
|
139
|
+
return normalized.generation === normalizeGeneration(generation)
|
|
140
|
+
? normalized.capability
|
|
141
|
+
: undefined
|
|
142
|
+
} catch {
|
|
143
|
+
return undefined
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function normalizeMetadata(value) {
|
|
148
|
+
const modern = value?.version === 2
|
|
149
|
+
if (!exactFields(value, modern ? new Set([...META_FIELDS, 'candidate']) : META_FIELDS)
|
|
150
|
+
|| (!modern && value.version !== META_VERSION)) {
|
|
151
|
+
throw new Error('invalid dsh-ptc-plus binding draft metadata')
|
|
152
|
+
}
|
|
153
|
+
return Object.freeze({
|
|
154
|
+
version: value.version,
|
|
155
|
+
generation: normalizeGeneration(value.generation),
|
|
156
|
+
capability: normalizeUserBindingDraftCapability(value.capability),
|
|
157
|
+
...(modern ? { candidate: value.candidate === null ? null : normalizeCandidate(value.candidate) } : {}),
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function projectionState(generation, options = {}) {
|
|
162
|
+
const state = {
|
|
163
|
+
generation: normalizeGeneration(generation),
|
|
164
|
+
phase: normalizePhase(options.phase ?? 'idle'),
|
|
165
|
+
capability: normalizeUserBindingDraftCapability(options.capability ?? null),
|
|
166
|
+
commandId: normalizeCommandId(options.commandId ?? null),
|
|
167
|
+
fallbackReady: options.fallbackReady === true,
|
|
168
|
+
fallbackCommandId: normalizeCommandId(options.fallbackCommandId ?? null),
|
|
169
|
+
turnStarted: options.turnStarted === true,
|
|
170
|
+
}
|
|
171
|
+
if (state.phase === 'pending' ? state.commandId === null : state.phase === 'idle' && state.commandId !== null) {
|
|
172
|
+
throw new Error('invalid dsh-ptc-plus binding draft command state')
|
|
173
|
+
}
|
|
174
|
+
if (state.phase !== 'pending' && state.turnStarted) {
|
|
175
|
+
throw new Error('invalid dsh-ptc-plus binding draft turn state')
|
|
176
|
+
}
|
|
177
|
+
if (state.fallbackReady && (state.phase !== 'pending' || state.capability === null)) {
|
|
178
|
+
throw new Error('invalid dsh-ptc-plus binding draft fallback state')
|
|
179
|
+
}
|
|
180
|
+
if (!state.fallbackReady && state.fallbackCommandId !== null) {
|
|
181
|
+
throw new Error('invalid dsh-ptc-plus binding draft fallback state')
|
|
182
|
+
}
|
|
183
|
+
return Object.freeze(state)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function failedAuthoringState(generation, state) {
|
|
187
|
+
return !state.fallbackReady
|
|
188
|
+
? projectionState(generation, {
|
|
189
|
+
phase: 'failed', capability: state.capability, commandId: state.commandId,
|
|
190
|
+
})
|
|
191
|
+
: projectionState(generation, {
|
|
192
|
+
phase: 'ready', capability: state.capability, commandId: state.fallbackCommandId,
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function normalizeProjectionState(value, generation) {
|
|
197
|
+
if (!exactFields(value, value?.history === undefined ? STATE_FIELDS : new Set([...STATE_FIELDS, 'history']))) {
|
|
198
|
+
throw new Error('invalid dsh-ptc-plus binding draft projection state')
|
|
199
|
+
}
|
|
200
|
+
const currentGeneration = normalizeGeneration(generation)
|
|
201
|
+
if (normalizeGeneration(value.generation) !== currentGeneration) {
|
|
202
|
+
return Object.freeze({ ...projectionState(currentGeneration), ...historyFields(value) })
|
|
203
|
+
}
|
|
204
|
+
return Object.freeze({ ...projectionState(currentGeneration, value), ...historyFields(value) })
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Validate authoring state and source-owned review history for a session Client. */
|
|
208
|
+
export function normalizeUserBindingDraftView(value) {
|
|
209
|
+
if (!exactFields(value, value?.history === undefined ? VIEW_FIELDS : new Set([...VIEW_FIELDS, 'history']))) {
|
|
210
|
+
throw new Error('invalid dsh-ptc-plus binding draft projection view')
|
|
211
|
+
}
|
|
212
|
+
return Object.freeze({
|
|
213
|
+
phase: normalizePhase(value.phase),
|
|
214
|
+
capability: normalizeUserBindingDraftCapability(value.capability),
|
|
215
|
+
commandId: normalizeCommandId(value.commandId ?? null),
|
|
216
|
+
...historyFields(value),
|
|
217
|
+
})
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Attach the revocable locator and optional accepted source to private result metadata. */
|
|
221
|
+
export function withUserBindingDraftCapability(meta, capability, generation, candidate) {
|
|
222
|
+
const base = isRecord(meta) ? { ...meta } : meta === undefined ? {} : { value: meta }
|
|
223
|
+
base[USER_BINDING_DRAFT_META_KEY] = normalizeMetadata({
|
|
224
|
+
version: candidate === undefined ? META_VERSION : 2,
|
|
225
|
+
generation,
|
|
226
|
+
capability,
|
|
227
|
+
...(candidate === undefined ? {} : { candidate }),
|
|
228
|
+
})
|
|
229
|
+
return base
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Project the latest current-generation draft locator for one session. */
|
|
233
|
+
export function createUserBindingDraftProjection(generation) {
|
|
234
|
+
const currentGeneration = normalizeGeneration(generation)
|
|
235
|
+
const projection = {
|
|
236
|
+
key: USER_BINDING_DRAFT_KEY,
|
|
237
|
+
stateVersion: 4,
|
|
238
|
+
stateSchema: Object.freeze({
|
|
239
|
+
parse: value => normalizeProjectionState(value, currentGeneration),
|
|
240
|
+
}),
|
|
241
|
+
init: () => projectionState(currentGeneration),
|
|
242
|
+
apply(state, event) {
|
|
243
|
+
if (event?.type === 'session/end-seed') return projectionState(currentGeneration)
|
|
244
|
+
if (event?.type === 'command/run' && event.data?.name === 'binding') {
|
|
245
|
+
try {
|
|
246
|
+
return projectionState(currentGeneration, {
|
|
247
|
+
phase: 'pending',
|
|
248
|
+
capability: state.capability,
|
|
249
|
+
commandId: event.data.commandId,
|
|
250
|
+
fallbackReady: state.phase === 'ready'
|
|
251
|
+
? state.capability !== null
|
|
252
|
+
: state.phase === 'pending' && state.fallbackReady,
|
|
253
|
+
fallbackCommandId: state.phase === 'ready'
|
|
254
|
+
? state.capability === null ? null : state.commandId
|
|
255
|
+
: state.phase === 'pending' ? state.fallbackCommandId : null,
|
|
256
|
+
})
|
|
257
|
+
} catch {
|
|
258
|
+
return state
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (event?.type === 'command/done' && state.phase === 'pending'
|
|
262
|
+
&& event.data?.commandId === state.commandId) {
|
|
263
|
+
return event.data.kind === 'error' ? failedAuthoringState(currentGeneration, state) : state
|
|
264
|
+
}
|
|
265
|
+
if (event?.type === 'turn/start' && state.phase === 'pending' && !state.turnStarted) {
|
|
266
|
+
return projectionState(currentGeneration, { ...state, turnStarted: true })
|
|
267
|
+
}
|
|
268
|
+
if (event?.type === 'turn/end' && state.phase === 'pending' && state.turnStarted) {
|
|
269
|
+
return failedAuthoringState(currentGeneration, state)
|
|
270
|
+
}
|
|
271
|
+
if (event?.type !== 'tool/result'
|
|
272
|
+
|| (event.surfaceOp !== undefined && event.surfaceOp !== 'append')) {
|
|
273
|
+
return state
|
|
274
|
+
}
|
|
275
|
+
const raw = event.data?.meta?.[USER_BINDING_DRAFT_META_KEY]
|
|
276
|
+
if (raw === undefined) return state
|
|
277
|
+
try {
|
|
278
|
+
const metadata = normalizeMetadata(raw)
|
|
279
|
+
if (metadata.generation !== currentGeneration) return state
|
|
280
|
+
if (metadata.version === 2 && (state.phase === 'pending' || state.phase === 'failed')
|
|
281
|
+
&& metadata.candidate?.commandId !== state.commandId) return state
|
|
282
|
+
if ((state.phase === 'pending' || state.phase === 'failed')
|
|
283
|
+
&& metadata.capability === state.capability) return state
|
|
284
|
+
if (metadata.capability !== null) {
|
|
285
|
+
return projectionState(currentGeneration, {
|
|
286
|
+
phase: 'ready',
|
|
287
|
+
capability: metadata.capability,
|
|
288
|
+
commandId: state.commandId,
|
|
289
|
+
})
|
|
290
|
+
}
|
|
291
|
+
return projectionState(currentGeneration)
|
|
292
|
+
} catch {
|
|
293
|
+
return state
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
wire: Object.freeze({
|
|
297
|
+
viewSchema: Object.freeze({ parse: normalizeUserBindingDraftView }),
|
|
298
|
+
view: state => normalizeUserBindingDraftView({
|
|
299
|
+
phase: state.phase,
|
|
300
|
+
capability: state.capability,
|
|
301
|
+
commandId: state.commandId,
|
|
302
|
+
...historyFields(state),
|
|
303
|
+
}),
|
|
304
|
+
}),
|
|
305
|
+
}
|
|
306
|
+
const reduce = projection.apply
|
|
307
|
+
projection.apply = (state, event) => {
|
|
308
|
+
const next = reduce(state, event)
|
|
309
|
+
let history = state.history
|
|
310
|
+
if (event?.type === 'tool/result' && event.surfaceOp === 'append') {
|
|
311
|
+
try {
|
|
312
|
+
const metadata = normalizeMetadata(event.data?.meta?.[USER_BINDING_DRAFT_META_KEY])
|
|
313
|
+
if (metadata.candidate != null && !history?.some(record => (
|
|
314
|
+
record.candidate.requestId === metadata.candidate.requestId
|
|
315
|
+
))) {
|
|
316
|
+
history = normalizeHistory([...(history ?? []), {
|
|
317
|
+
commandId: metadata.candidate.commandId, acceptedSeq: event.seq, candidate: metadata.candidate, action: null,
|
|
318
|
+
}])
|
|
319
|
+
}
|
|
320
|
+
} catch {
|
|
321
|
+
// Invalid historical presentation cannot invalidate current authoring.
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
const action = event?.type === 'user/message' && event.surfaceOp === 'append'
|
|
325
|
+
? readBindingAction(event.data) : undefined
|
|
326
|
+
if (action !== undefined && history !== undefined) {
|
|
327
|
+
history = normalizeHistory(history.map(record => record.action === null
|
|
328
|
+
&& record.candidate.requestId === action.requestId && record.candidate.entry.id === action.id
|
|
329
|
+
&& event.sourceEventSeqs?.includes(record.acceptedSeq)
|
|
330
|
+
? { ...record, action } : record))
|
|
331
|
+
}
|
|
332
|
+
return history === undefined ? next : Object.freeze({ ...next, history })
|
|
333
|
+
}
|
|
334
|
+
return Object.freeze(projection)
|
|
335
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { assertOwnFields, isRecord } from './record-utils.js'
|
|
2
|
+
|
|
3
|
+
const FIELDS = new Set(['includeDeclaration', 'instructions', 'enabled', 'declaration'])
|
|
4
|
+
|
|
5
|
+
/** Validate presentation metadata without loading the source parser in the Client. */
|
|
6
|
+
export function normalizeBindingModelContext(value) {
|
|
7
|
+
if (value === undefined) return undefined
|
|
8
|
+
if (!isRecord(value)) throw new TypeError('binding modelContext must be an object')
|
|
9
|
+
assertOwnFields(value, FIELDS, 'binding modelContext')
|
|
10
|
+
const { includeDeclaration = true, instructions = '', enabled = true, declaration = '' } = value
|
|
11
|
+
if (typeof includeDeclaration !== 'boolean') throw new TypeError('binding modelContext.includeDeclaration must be a boolean')
|
|
12
|
+
if (typeof enabled !== 'boolean') throw new TypeError('binding modelContext.enabled must be a boolean')
|
|
13
|
+
for (const [name, text, limit] of [['instructions', instructions, 4096], ['declaration', declaration, 8192]]) {
|
|
14
|
+
if (typeof text !== 'string' || text.length > limit) {
|
|
15
|
+
throw new TypeError(`binding modelContext.${name} must be a string of at most ${limit} characters`)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
// Preserve fingerprints of previously saved prompt metadata; new edits use only the current fields.
|
|
19
|
+
if (Object.hasOwn(value, 'enabled') || Object.hasOwn(value, 'declaration')) {
|
|
20
|
+
return Object.freeze({ enabled, instructions: instructions.trim(), declaration: declaration.trim(),
|
|
21
|
+
...(Object.hasOwn(value, 'includeDeclaration') ? { includeDeclaration } : {}) })
|
|
22
|
+
}
|
|
23
|
+
return Object.freeze({ includeDeclaration, instructions: instructions.trim() })
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function bindingModelPreferences(value) {
|
|
27
|
+
return {
|
|
28
|
+
includeDeclaration: value?.includeDeclaration ?? value?.enabled ?? true,
|
|
29
|
+
instructions: value?.enabled === false && value.includeDeclaration === undefined ? '' : value?.instructions ?? '',
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { registerHooks } from 'node:module'
|
|
2
|
+
import { isAbsolute, resolve } from 'node:path'
|
|
3
|
+
import { pathToFileURL } from 'node:url'
|
|
4
|
+
import { parentPort, workerData } from 'node:worker_threads'
|
|
5
|
+
import { encodeValue } from './value-wire.js'
|
|
6
|
+
import { managedModuleImport } from './stateful-module-runtime.js'
|
|
7
|
+
import { compileStatefulModule, createUserModuleCompilationHooks } from './stateful-module-compiler.js'
|
|
8
|
+
import { WORKER_SHUTDOWN_ACKNOWLEDGEMENT, WORKER_SHUTDOWN_REQUEST } from './worker-shutdown.js'
|
|
9
|
+
|
|
10
|
+
if (parentPort === null) throw new Error('user binding runner requires a parent port')
|
|
11
|
+
const candidateCwd = workerData.cwd
|
|
12
|
+
if (typeof candidateCwd !== 'string' || !isAbsolute(candidateCwd)) {
|
|
13
|
+
/* c8 ignore next */
|
|
14
|
+
throw new Error('user binding runner requires an absolute cwd')
|
|
15
|
+
}
|
|
16
|
+
const candidateParent = pathToFileURL(resolve(candidateCwd, 'ptc-plus-bindings-candidate')).href
|
|
17
|
+
let candidateUrl
|
|
18
|
+
const moduleCompilation = createUserModuleCompilationHooks()
|
|
19
|
+
registerHooks({
|
|
20
|
+
resolve(specifier, context, nextResolve) {
|
|
21
|
+
return moduleCompilation.resolve(specifier, context, (selected, original) =>
|
|
22
|
+
nextResolve(selected, original.parentURL === candidateUrl
|
|
23
|
+
? { ...original, parentURL: candidateParent } : original))
|
|
24
|
+
},
|
|
25
|
+
load: moduleCompilation.load,
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
parentPort.on('message', (message) => {
|
|
29
|
+
if (message?.type !== WORKER_SHUTDOWN_REQUEST) return
|
|
30
|
+
parentPort.postMessage({ type: WORKER_SHUTDOWN_ACKNOWLEDGEMENT })
|
|
31
|
+
parentPort.close()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const prepared = compileStatefulModule(workerData.source)
|
|
36
|
+
candidateUrl = `data:text/javascript,${encodeURIComponent(prepared.code)}#candidate`
|
|
37
|
+
moduleCompilation.mark(candidateUrl, { compiled: true, moduleInterface: prepared.moduleInterface, sourceRegions: prepared.sourceRegions })
|
|
38
|
+
const namespace = await managedModuleImport(candidateUrl, candidateUrl)
|
|
39
|
+
let value = { symbols: Object.keys(namespace).sort() }
|
|
40
|
+
if (workerData.invocation !== undefined) {
|
|
41
|
+
const { symbol, args } = workerData.invocation
|
|
42
|
+
if (!Object.hasOwn(namespace, symbol) || typeof namespace[symbol] !== 'function') {
|
|
43
|
+
throw new TypeError(`candidate export ${JSON.stringify(symbol)} is not callable`)
|
|
44
|
+
}
|
|
45
|
+
value = await namespace[symbol](...args)
|
|
46
|
+
}
|
|
47
|
+
parentPort.postMessage({ ok: true, value: encodeValue(value, workerData.valueLimits) })
|
|
48
|
+
// The result is delivered; nothing this worker owns may keep it alive now.
|
|
49
|
+
parentPort.unref()
|
|
50
|
+
} catch (error) {
|
|
51
|
+
parentPort.postMessage({
|
|
52
|
+
ok: false,
|
|
53
|
+
error: error instanceof Error ? `${error.name}: ${error.message}` : String(error),
|
|
54
|
+
})
|
|
55
|
+
parentPort.unref()
|
|
56
|
+
}
|