dsh-ptc-plus 0.3.1 → 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 +66 -73
- package/README.md +66 -73
- 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 +30610 -722
- 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 +6 -4
- package/docs/adr/0007-separate-worker-transport-from-session-semantics.md +15 -3
- 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 +9 -3
- package/docs/adr/0014-persist-cell-rewrite-policy.md +11 -5
- 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 +8 -6
- package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +2 -2
- package/docs/adr/0021-separate-host-bootstrap-from-session-runtime.md +17 -3
- 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 +82 -22
- package/docs/binding-review-placement-design.md +200 -0
- package/docs/capability-projection.md +1 -1
- package/docs/client-ui.md +96 -20
- package/docs/durability-design.md +61 -24
- package/docs/evaluation.md +78 -6
- package/docs/installation.md +41 -6
- package/docs/program-data-plane.md +6 -0
- package/docs/publishing.md +5 -3
- package/docs/runtime-reference.md +79 -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 +374 -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 +252 -242
- 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 -553
- 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 +91 -25
- 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 +28 -3
- 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 +911 -116
- 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 -201
- package/internal/repl-memory-projection.js +81 -12
- package/internal/repl-observation-interest.js +70 -0
- package/internal/repl-rewrite-contract.js +21 -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 -51
- package/internal/runtime-contexts.js +82 -10
- package/internal/runtime-messages.js +111 -0
- package/internal/session-cell-executor.js +455 -91
- package/internal/session-journal-recovery.js +684 -0
- package/internal/session-journal-schema.js +106 -0
- package/internal/session-journal.js +237 -440
- package/internal/session-log-view.js +42 -9
- package/internal/session-runtime.js +325 -80
- package/internal/session-state.js +249 -40
- 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 +81 -11
- package/src/client-activity.js +109 -45
- 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,1065 @@
|
|
|
1
|
+
import { RPC_CONTRACTS } from './rpc-contract.js'
|
|
2
|
+
import { randomUUID } from 'node:crypto'
|
|
3
|
+
import { dirname } from 'node:path'
|
|
4
|
+
import { IsolatedOwner } from './isolated-worker.js'
|
|
5
|
+
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
|
6
|
+
import { validateJsonSchemaValue } from '@deepseek-ai/dsh-tools'
|
|
7
|
+
import {
|
|
8
|
+
createBoundedWorkerRound,
|
|
9
|
+
ROUND_CANCELLED,
|
|
10
|
+
ROUND_OUTPUT_LIMIT,
|
|
11
|
+
ROUND_TIMEOUT,
|
|
12
|
+
} from './bounded-worker-round.js'
|
|
13
|
+
import { BINDING_SUBMISSION, bindingAuthoringInstructions } from './user-binding-authoring.js'
|
|
14
|
+
import { bindingActionNotice, USER_BINDING_DRAFT_META_KEY } from './user-binding-draft-projection.js'
|
|
15
|
+
import { bindingModelPreferences } from './user-binding-model-context.js'
|
|
16
|
+
import { sessionEvents } from './session-events.js'
|
|
17
|
+
import { processExitDescription } from './failure-reporting.js'
|
|
18
|
+
import { decodeValue } from './value-wire.js'
|
|
19
|
+
import { valueLimitsFromConfig } from './value-wire-schema.js'
|
|
20
|
+
import { normalizeWorkerEnvironment } from './worker-client.js'
|
|
21
|
+
import { UserBindingsStore } from './user-bindings-store.js'
|
|
22
|
+
import { UserBindingConsole } from './user-binding-console.js'
|
|
23
|
+
import {
|
|
24
|
+
createUserBindingsSnapshot,
|
|
25
|
+
normalizeUserBindingEntry,
|
|
26
|
+
normalizeUserBindingsDocument,
|
|
27
|
+
storedUserBindingsDocument,
|
|
28
|
+
} from './user-bindings.js'
|
|
29
|
+
|
|
30
|
+
const RPC_CONTRACT = RPC_CONTRACTS.bindings
|
|
31
|
+
const RUNNER_URL = new URL('./user-binding-runner.js', import.meta.url)
|
|
32
|
+
const RUNNER_HELPER = new URL('./kernel-child.js', import.meta.url)
|
|
33
|
+
const COMMAND_USAGE = '/binding new <requirement> or /binding edit <id> <requirement>'
|
|
34
|
+
const CANDIDATE_FAILURE_LOGS = Symbol('candidate failure logs')
|
|
35
|
+
|
|
36
|
+
function isRecord(value) {
|
|
37
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function errorResult(error) {
|
|
41
|
+
const logs = error?.[CANDIDATE_FAILURE_LOGS]
|
|
42
|
+
return {
|
|
43
|
+
ok: false,
|
|
44
|
+
error: {
|
|
45
|
+
code: typeof error?.code === 'string' ? error.code : 'bindings/error',
|
|
46
|
+
message: error instanceof Error ? error.message : String(error),
|
|
47
|
+
details: Array.isArray(logs) ? { logs } : {},
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function storedEntry(entry) {
|
|
53
|
+
return storedUserBindingsDocument({ entries: [entry] }).entries[0]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function sessionIdOf(agent) {
|
|
57
|
+
const value = agent?.session?.id ?? agent?.id
|
|
58
|
+
return value === undefined ? undefined : String(value)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function parseAuthoringCommand(rawInput) {
|
|
62
|
+
const input = typeof rawInput === 'string' ? rawInput.trim() : ''
|
|
63
|
+
if (input === '') return { kind: 'help' }
|
|
64
|
+
const newMatch = /^new(?:\s+([\s\S]+))?$/.exec(input)
|
|
65
|
+
if (newMatch !== null) {
|
|
66
|
+
const requirement = newMatch[1]?.trim() ?? ''
|
|
67
|
+
return requirement === '' ? { kind: 'help' } : { kind: 'new', requirement }
|
|
68
|
+
}
|
|
69
|
+
const editMatch = /^edit\s+(\S+)(?:\s+([\s\S]+))?$/.exec(input)
|
|
70
|
+
if (editMatch !== null) {
|
|
71
|
+
const requirement = editMatch[2]?.trim() ?? ''
|
|
72
|
+
return requirement === ''
|
|
73
|
+
? { kind: 'help' }
|
|
74
|
+
: { kind: 'edit', id: editMatch[1], requirement }
|
|
75
|
+
}
|
|
76
|
+
return { kind: 'help' }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function authoringTask(command, current) {
|
|
80
|
+
if (command.kind === 'new') {
|
|
81
|
+
return `Create a Global User Binding draft for this requirement:\n\n${command.requirement}`
|
|
82
|
+
}
|
|
83
|
+
const entry = { ...current, modelContext: bindingModelPreferences(current.modelContext) }
|
|
84
|
+
return `Revise the following Global User Binding for this requirement:\n\n${command.requirement}\n\nExisting entry:\n${JSON.stringify(entry, null, 2)}`
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function taskMessage(text) {
|
|
88
|
+
return createUserMessage({
|
|
89
|
+
content: [{ type: 'text', text }],
|
|
90
|
+
source: { kind: 'plugin', plugin: 'ptc-plus', form: 'instructions' },
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function candidateInvocation(value) {
|
|
95
|
+
if (value === undefined) return undefined
|
|
96
|
+
if (!isRecord(value) || typeof value.symbol !== 'string' || !Array.isArray(value.args)) {
|
|
97
|
+
throw new TypeError('candidate invocation must contain a symbol string and args array')
|
|
98
|
+
}
|
|
99
|
+
return { symbol: value.symbol, args: value.args }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function runCandidate(source, invocation, options, signal, owner) {
|
|
103
|
+
if (typeof source !== 'string' || source.length === 0) throw new TypeError('candidate source must be a non-empty string')
|
|
104
|
+
return new Promise((resolve, reject) => {
|
|
105
|
+
const started = owner.start({
|
|
106
|
+
helper: RUNNER_HELPER,
|
|
107
|
+
entry: RUNNER_URL.href,
|
|
108
|
+
protocol: 'parent-port',
|
|
109
|
+
workerData: { source, invocation, valueLimits: options.valueLimits, cwd: options.cwd },
|
|
110
|
+
env: normalizeWorkerEnvironment(process.env),
|
|
111
|
+
resourceLimits: { maxOldGenerationSizeMb: options.maxOldGenerationSizeMb },
|
|
112
|
+
})
|
|
113
|
+
const worker = started.transport
|
|
114
|
+
const outputLimitError = () => Object.assign(
|
|
115
|
+
new Error('candidate output exceeded the configured limit'),
|
|
116
|
+
{ code: 'bindings/output-limit' },
|
|
117
|
+
)
|
|
118
|
+
const round = createBoundedWorkerRound({
|
|
119
|
+
maxOutputBytes: options.maxOutputBytes,
|
|
120
|
+
maxWallMs: options.maxWallMs,
|
|
121
|
+
signal,
|
|
122
|
+
settle: ({ ok, result, reason }) => {
|
|
123
|
+
// The shared owner owns this close: the release request, the kill deadline
|
|
124
|
+
// and the real exit, with any refusal kept until a late exit arrives.
|
|
125
|
+
void owner.stop(started.id).catch(() => {})
|
|
126
|
+
if (ok) { resolve(result); return }
|
|
127
|
+
if (reason === ROUND_TIMEOUT) reject(new Error(`candidate execution exceeded ${options.maxWallMs}ms`))
|
|
128
|
+
else if (reason === ROUND_CANCELLED) {
|
|
129
|
+
reject(Object.assign(new Error('candidate execution was cancelled'), { code: 'gateway/cancelled' }))
|
|
130
|
+
} else if (reason === ROUND_OUTPUT_LIMIT) reject(outputLimitError())
|
|
131
|
+
else reject(reason)
|
|
132
|
+
},
|
|
133
|
+
})
|
|
134
|
+
const capture = (stream, channel) => stream?.on('data', chunk => round.capture(channel, String(chunk)))
|
|
135
|
+
capture(worker.stdout, 'stdout')
|
|
136
|
+
capture(worker.stderr, 'stderr')
|
|
137
|
+
worker.once('message', (message) => {
|
|
138
|
+
if (message?.ok === true) {
|
|
139
|
+
if (round.exceeds({ value: message.value }, { envelope: true })) {
|
|
140
|
+
round.fail(ROUND_OUTPUT_LIMIT)
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
let value
|
|
144
|
+
try {
|
|
145
|
+
value = decodeValue(message.value, options.valueLimits)
|
|
146
|
+
} catch (error) {
|
|
147
|
+
/* c8 ignore next */
|
|
148
|
+
round.fail(error)
|
|
149
|
+
/* c8 ignore next */
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
round.succeed({ logs: round.logs, value })
|
|
153
|
+
} else {
|
|
154
|
+
const error = typeof message?.error === 'string' ? message.error : 'candidate execution failed'
|
|
155
|
+
if (round.exceeds({ error }, { envelope: true })) round.fail(ROUND_OUTPUT_LIMIT)
|
|
156
|
+
else round.fail(new Error(error))
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
worker.once('error', error => {
|
|
160
|
+
error[CANDIDATE_FAILURE_LOGS] = round.logs.map(log => ({ ...log }))
|
|
161
|
+
round.fail(error)
|
|
162
|
+
})
|
|
163
|
+
worker.once('exit', (code, signal) => {
|
|
164
|
+
const error = new Error(`candidate worker exited before returning a result (${processExitDescription(code, signal)})`)
|
|
165
|
+
error[CANDIDATE_FAILURE_LOGS] = round.logs.map(log => ({ ...log }))
|
|
166
|
+
round.fail(error)
|
|
167
|
+
})
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Own user-binding persistence and the optional authenticated Client RPC surface. */
|
|
172
|
+
export function createUserBindingsOwner(ctx, options = {}) {
|
|
173
|
+
// This owner instance keeps its candidate transports only until real exit.
|
|
174
|
+
const candidateOwner = options.candidateOwner ?? new IsolatedOwner()
|
|
175
|
+
const store = options.store ?? new UserBindingsStore(options)
|
|
176
|
+
const bindingsCwd = options.cwd ?? dirname(store.filename)
|
|
177
|
+
let enabled = options.enabled === true
|
|
178
|
+
let draftProjectionAvailable = options.draftProjectionAvailable === true
|
|
179
|
+
let currentOptions = {
|
|
180
|
+
cwd: bindingsCwd,
|
|
181
|
+
maxWallMs: options.maxWallMs,
|
|
182
|
+
maxOutputBytes: options.maxOutputBytes,
|
|
183
|
+
maxOldGenerationSizeMb: options.maxOldGenerationSizeMb,
|
|
184
|
+
valueLimits: options.valueLimits,
|
|
185
|
+
}
|
|
186
|
+
let disposed = false
|
|
187
|
+
const codeConsole = new UserBindingConsole(currentOptions)
|
|
188
|
+
const commandRegistrations = new Map()
|
|
189
|
+
const sessionAgents = new Map()
|
|
190
|
+
const sessionGenerations = new Map()
|
|
191
|
+
const agentLifecycles = new WeakMap()
|
|
192
|
+
const activeAuthoring = new Map()
|
|
193
|
+
const pendingAuthoring = new Map()
|
|
194
|
+
const drafts = new Map()
|
|
195
|
+
const draftsByCapability = new Map()
|
|
196
|
+
const reviewsByCapability = new Map()
|
|
197
|
+
let rpcMount
|
|
198
|
+
let authoringInjection
|
|
199
|
+
let authoringGeneration = 0
|
|
200
|
+
let draftRevision = 0
|
|
201
|
+
let lifecycleGeneration = 0
|
|
202
|
+
|
|
203
|
+
const lifecycleFor = (agent) => {
|
|
204
|
+
let lifecycle = agentLifecycles.get(agent)
|
|
205
|
+
if (lifecycle === undefined) {
|
|
206
|
+
lifecycle = { active: false, generation: 0 }
|
|
207
|
+
agentLifecycles.set(agent, lifecycle)
|
|
208
|
+
}
|
|
209
|
+
return lifecycle
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const activateAgent = (agent) => {
|
|
213
|
+
const lifecycle = lifecycleFor(agent)
|
|
214
|
+
if (!lifecycle.active) {
|
|
215
|
+
lifecycle.active = true
|
|
216
|
+
lifecycle.generation += 1
|
|
217
|
+
}
|
|
218
|
+
return lifecycle.generation
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const invalidateAgent = (agent) => {
|
|
222
|
+
const lifecycle = lifecycleFor(agent)
|
|
223
|
+
lifecycle.active = false
|
|
224
|
+
lifecycle.generation += 1
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const agentIsCurrent = (agent, sessionId, generation) => {
|
|
228
|
+
const lifecycle = lifecycleFor(agent)
|
|
229
|
+
return lifecycle.active && lifecycle.generation === generation
|
|
230
|
+
&& sessionAgents.get(sessionId) === agent
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const disposeFiber = async (fiber) => {
|
|
234
|
+
if (typeof fiber === 'function') await fiber()
|
|
235
|
+
else if (typeof fiber?.dispose === 'function') await fiber.dispose()
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const registerOwned = (scope, registry, definition, label) => {
|
|
239
|
+
if (typeof scope?.effect !== 'function') {
|
|
240
|
+
throw new Error(`Global User Bindings ${label} registration requires an effect owner`)
|
|
241
|
+
}
|
|
242
|
+
return scope.effect(() => registry.register(definition), `ptc-plus: ${label}`)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const reportContainedFailure = (message, error) => {
|
|
246
|
+
try {
|
|
247
|
+
ctx.logger?.warn?.(message, error)
|
|
248
|
+
} catch {
|
|
249
|
+
// A contained failure must not regain control through its reporter.
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const contain = (task, message) => Promise.resolve(task).catch((error) => {
|
|
254
|
+
reportContainedFailure(message, error)
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
const detach = (task, message) => {
|
|
258
|
+
void contain(task, message)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const requireEnabled = () => {
|
|
262
|
+
if (!enabled) throw new Error('Global User Bindings are disabled')
|
|
263
|
+
}
|
|
264
|
+
const draftFor = (capability) => {
|
|
265
|
+
if (typeof capability !== 'string' || capability === '') {
|
|
266
|
+
throw new TypeError('draft capability must be a non-empty string')
|
|
267
|
+
}
|
|
268
|
+
return draftsByCapability.get(capability) ?? null
|
|
269
|
+
}
|
|
270
|
+
const draftView = current => current === null ? null : Object.freeze({
|
|
271
|
+
version: current.version,
|
|
272
|
+
mode: current.mode,
|
|
273
|
+
entry: current.entry,
|
|
274
|
+
})
|
|
275
|
+
const candidateView = current => Object.freeze({ requestId: current.requestId, commandId: current.commandId, ...draftView(current) })
|
|
276
|
+
const settleDraft = (current, state, enabled = false) => {
|
|
277
|
+
const retainReview = draftsByCapability.get(current.capability) === current
|
|
278
|
+
removeDraft(current)
|
|
279
|
+
const action = Object.freeze({ requestId: current.requestId, id: current.entry.id, state, enabled })
|
|
280
|
+
if (retainReview) {
|
|
281
|
+
reviewsByCapability.set(current.capability, { agent: current.agent, candidate: candidateView(current), action })
|
|
282
|
+
}
|
|
283
|
+
try {
|
|
284
|
+
const accepting = sessionEvents(current.agent.session)?.find(event => (
|
|
285
|
+
event.type === 'tool/result' && event.surfaceOp === 'append'
|
|
286
|
+
&& event.data?.meta?.[USER_BINDING_DRAFT_META_KEY]?.candidate?.requestId === current.requestId
|
|
287
|
+
))
|
|
288
|
+
if (accepting === undefined) throw new Error('binding acceptance is not yet present in the session log')
|
|
289
|
+
current.agent.session.append('user/message', createUserMessage(bindingActionNotice(action)), {
|
|
290
|
+
surfaceOp: 'append', sourceEventSeqs: [accepting.seq],
|
|
291
|
+
})
|
|
292
|
+
} catch (error) {
|
|
293
|
+
// Storage settlement is final even when its separate presentation append fails.
|
|
294
|
+
reportContainedFailure('ptc-plus: binding action completed but its session receipt could not be appended', error)
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
const removeDraft = (current) => {
|
|
298
|
+
if (draftsByCapability.get(current.capability) === current) {
|
|
299
|
+
draftsByCapability.delete(current.capability)
|
|
300
|
+
}
|
|
301
|
+
if (drafts.get(current.sessionId) === current) {
|
|
302
|
+
drafts.delete(current.sessionId)
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
const clearDraft = (capability, version) => {
|
|
306
|
+
const current = draftFor(capability)
|
|
307
|
+
if (current === null) return null
|
|
308
|
+
if (!Number.isSafeInteger(version) || version < 1 || version !== current.version) {
|
|
309
|
+
throw Object.assign(new Error('binding draft changed; refresh and retry'), {
|
|
310
|
+
code: 'BINDINGS_CONFLICT',
|
|
311
|
+
})
|
|
312
|
+
}
|
|
313
|
+
if (current.state !== 'ready') {
|
|
314
|
+
throw Object.assign(new Error('binding draft is being saved'), { code: 'BINDINGS_BUSY' })
|
|
315
|
+
}
|
|
316
|
+
settleDraft(current, 'discarded')
|
|
317
|
+
return null
|
|
318
|
+
}
|
|
319
|
+
const saveDraft = async (capability, version, expectedRevision, activate = false) => {
|
|
320
|
+
const current = draftFor(capability)
|
|
321
|
+
if (current === null || !Number.isSafeInteger(version) || version < 1 || version !== current.version) {
|
|
322
|
+
throw Object.assign(new Error('binding draft changed; refresh and retry'), {
|
|
323
|
+
code: 'BINDINGS_CONFLICT',
|
|
324
|
+
})
|
|
325
|
+
}
|
|
326
|
+
if (current.state !== 'ready') {
|
|
327
|
+
throw Object.assign(new Error('binding draft is being saved'), { code: 'BINDINGS_BUSY' })
|
|
328
|
+
}
|
|
329
|
+
current.state = 'saving'
|
|
330
|
+
const entry = { ...current.entry, enabled: activate === true }
|
|
331
|
+
try {
|
|
332
|
+
const result = current.mode === 'new'
|
|
333
|
+
? await store.create(entry, expectedRevision)
|
|
334
|
+
: await store.update(entry, expectedRevision)
|
|
335
|
+
settleDraft(current, 'saved', activate === true)
|
|
336
|
+
return result
|
|
337
|
+
} catch (error) {
|
|
338
|
+
if (drafts.get(current.sessionId) === current && current.state === 'saving') {
|
|
339
|
+
current.state = 'ready'
|
|
340
|
+
}
|
|
341
|
+
throw error
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
const validateEntry = (value) => {
|
|
345
|
+
const entry = normalizeUserBindingEntry(value)
|
|
346
|
+
return {
|
|
347
|
+
id: entry.id,
|
|
348
|
+
name: entry.name,
|
|
349
|
+
scope: entry.scope,
|
|
350
|
+
symbols: [...entry.symbols],
|
|
351
|
+
purpose: entry.purpose,
|
|
352
|
+
enabled: entry.enabled,
|
|
353
|
+
source: entry.source,
|
|
354
|
+
declaration: entry.declaration,
|
|
355
|
+
...(entry.modelContext === undefined ? {} : { modelContext: entry.modelContext }),
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
const saveEntry = async (input) => {
|
|
359
|
+
if (input.intent === 'create') return store.create(input.entry, input.expectedRevision)
|
|
360
|
+
if (input.intent !== 'update') {
|
|
361
|
+
throw new TypeError('binding save requires an explicit create or update intent')
|
|
362
|
+
}
|
|
363
|
+
const entry = normalizeUserBindingEntry(input.entry)
|
|
364
|
+
if (typeof input.originalId !== 'string' || input.originalId !== entry.id) {
|
|
365
|
+
throw new Error('binding update requires the original id of the entry it replaces')
|
|
366
|
+
}
|
|
367
|
+
// The store owns entry normalization and accepts only raw entry fields;
|
|
368
|
+
// the normalized product carries derived fingerprint/declaration state.
|
|
369
|
+
return store.update(input.entry, input.expectedRevision)
|
|
370
|
+
}
|
|
371
|
+
const reviewDraft = (capability) => {
|
|
372
|
+
const current = draftFor(capability)
|
|
373
|
+
if (current !== null) return { candidate: candidateView(current), action: null }
|
|
374
|
+
const review = reviewsByCapability.get(capability)
|
|
375
|
+
return review === undefined ? null : { candidate: review.candidate, action: review.action }
|
|
376
|
+
}
|
|
377
|
+
// Fixed endpoint tables grouped by domain; the dispatcher only looks up the
|
|
378
|
+
// operation, rejects unknown endpoints and hands over the validated input.
|
|
379
|
+
const storageEndpoints = Object.freeze({
|
|
380
|
+
list: () => store.list(),
|
|
381
|
+
load: input => store.entry(input.id),
|
|
382
|
+
reload: () => store.reload(),
|
|
383
|
+
save: saveEntry,
|
|
384
|
+
enable: input => store.setEnabled(input.id, true, input.expectedRevision),
|
|
385
|
+
disable: input => store.setEnabled(input.id, false, input.expectedRevision),
|
|
386
|
+
remove: input => store.remove(input.id, input.expectedRevision),
|
|
387
|
+
import: input => store.importFile(input.path, input.expectedRevision, input.options),
|
|
388
|
+
validate: input => validateEntry(input.entry),
|
|
389
|
+
})
|
|
390
|
+
const candidateEndpoints = Object.freeze({
|
|
391
|
+
run: (input, signal) => runCandidate(
|
|
392
|
+
input.source,
|
|
393
|
+
candidateInvocation(input.invocation),
|
|
394
|
+
currentOptions,
|
|
395
|
+
signal,
|
|
396
|
+
candidateOwner,
|
|
397
|
+
),
|
|
398
|
+
})
|
|
399
|
+
const consoleEndpoints = Object.freeze({
|
|
400
|
+
'console-run': (input, signal) => codeConsole.run(input, signal),
|
|
401
|
+
'console-release': (input) => {
|
|
402
|
+
codeConsole.release(input.environment)
|
|
403
|
+
return null
|
|
404
|
+
},
|
|
405
|
+
})
|
|
406
|
+
const draftEndpoints = Object.freeze({
|
|
407
|
+
draft: input => draftView(draftFor(input.capability)),
|
|
408
|
+
'draft-review': input => reviewDraft(input.capability),
|
|
409
|
+
'save-draft': input => saveDraft(
|
|
410
|
+
input.capability,
|
|
411
|
+
input.version,
|
|
412
|
+
input.expectedRevision,
|
|
413
|
+
input.activate === true,
|
|
414
|
+
),
|
|
415
|
+
'discard-draft': input => clearDraft(input.capability, input.version),
|
|
416
|
+
})
|
|
417
|
+
const ENDPOINTS = Object.freeze(Object.assign(
|
|
418
|
+
Object.create(null),
|
|
419
|
+
storageEndpoints,
|
|
420
|
+
candidateEndpoints,
|
|
421
|
+
consoleEndpoints,
|
|
422
|
+
draftEndpoints,
|
|
423
|
+
))
|
|
424
|
+
|
|
425
|
+
const handler = async (endpoint, payload, signal) => {
|
|
426
|
+
try {
|
|
427
|
+
if (disposed) throw new Error('Global User Bindings owner is disposed')
|
|
428
|
+
requireEnabled()
|
|
429
|
+
const operation = ENDPOINTS[endpoint]
|
|
430
|
+
if (typeof operation !== 'function') {
|
|
431
|
+
throw new Error(`unknown user binding operation ${JSON.stringify(endpoint)}`)
|
|
432
|
+
}
|
|
433
|
+
return { ok: true, value: await operation(isRecord(payload) ? payload : {}, signal) }
|
|
434
|
+
} catch (error) {
|
|
435
|
+
return errorResult(error)
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const stopAuthoring = async (
|
|
440
|
+
sessionId,
|
|
441
|
+
_reason = 'Agent authoring ended before a Global User Binding draft was accepted.',
|
|
442
|
+
expected,
|
|
443
|
+
) => {
|
|
444
|
+
const current = activeAuthoring.get(sessionId)
|
|
445
|
+
if (current === undefined || (expected !== undefined && current !== expected)) return
|
|
446
|
+
activeAuthoring.delete(sessionId)
|
|
447
|
+
current.abortCleanup?.()
|
|
448
|
+
current.abortCleanup = undefined
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const beginAuthoring = async (agent, command, signal, commandId) => {
|
|
452
|
+
signal?.throwIfAborted?.()
|
|
453
|
+
requireEnabled()
|
|
454
|
+
if (!draftProjectionAvailable) {
|
|
455
|
+
throw new Error('binding authoring requires the session draft projection')
|
|
456
|
+
}
|
|
457
|
+
if (disposed) throw new Error('Global User Bindings owner is disposed')
|
|
458
|
+
const sessionId = sessionIdOf(agent)
|
|
459
|
+
if (sessionId === undefined) throw new Error('binding authoring requires a live session identity')
|
|
460
|
+
const agentGeneration = lifecycleFor(agent).generation
|
|
461
|
+
const requireCurrentAgent = () => {
|
|
462
|
+
if (!agentIsCurrent(agent, sessionId, agentGeneration)
|
|
463
|
+
|| commandRegistrations.get(sessionId)?.agent !== agent
|
|
464
|
+
|| ctx.tools.get('run_code', agent) === undefined) {
|
|
465
|
+
throw new Error('this binding authoring Agent is no longer active')
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
requireCurrentAgent()
|
|
469
|
+
await stopAuthoring(
|
|
470
|
+
sessionId,
|
|
471
|
+
'A newer Global User Binding authoring request replaced this one before a draft was accepted.',
|
|
472
|
+
)
|
|
473
|
+
requireCurrentAgent()
|
|
474
|
+
const attempt = Object.freeze({ agent, agentGeneration, generation: lifecycleGeneration })
|
|
475
|
+
pendingAuthoring.set(sessionId, attempt)
|
|
476
|
+
const requireCurrentAttempt = () => {
|
|
477
|
+
if (disposed || !enabled || lifecycleGeneration !== attempt.generation
|
|
478
|
+
|| pendingAuthoring.get(sessionId) !== attempt
|
|
479
|
+
|| !agentIsCurrent(agent, sessionId, attempt.agentGeneration)) {
|
|
480
|
+
throw new Error('this binding authoring request is no longer active')
|
|
481
|
+
}
|
|
482
|
+
signal?.throwIfAborted?.()
|
|
483
|
+
}
|
|
484
|
+
let active
|
|
485
|
+
try {
|
|
486
|
+
const current = command.kind === 'edit'
|
|
487
|
+
? storedEntry((await store.entry(command.id)).entry)
|
|
488
|
+
: undefined
|
|
489
|
+
requireCurrentAttempt()
|
|
490
|
+
pendingAuthoring.delete(sessionId)
|
|
491
|
+
active = {
|
|
492
|
+
agent,
|
|
493
|
+
requestId: randomUUID(),
|
|
494
|
+
accepted: false,
|
|
495
|
+
submitting: false,
|
|
496
|
+
agentGeneration,
|
|
497
|
+
generation: lifecycleGeneration,
|
|
498
|
+
abortCleanup: undefined,
|
|
499
|
+
}
|
|
500
|
+
activeAuthoring.set(sessionId, active)
|
|
501
|
+
const requireActive = () => {
|
|
502
|
+
if (disposed || !enabled || lifecycleGeneration !== active.generation
|
|
503
|
+
|| activeAuthoring.get(sessionId) !== active || active.accepted
|
|
504
|
+
|| !agentIsCurrent(agent, sessionId, active.agentGeneration)) {
|
|
505
|
+
throw new Error('this binding draft handoff is no longer active')
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
signal?.throwIfAborted?.()
|
|
509
|
+
active.submit = async (value, ensureLease, onAccepted) => {
|
|
510
|
+
ensureLease()
|
|
511
|
+
requireActive()
|
|
512
|
+
const errors = validateJsonSchemaValue(BINDING_SUBMISSION.parameters, value)
|
|
513
|
+
if (errors.length > 0) throw new TypeError(`invalid binding submission: ${JSON.stringify(errors)}`)
|
|
514
|
+
if (value.requestId !== active.requestId) {
|
|
515
|
+
throw new Error('this binding draft request identity is no longer active')
|
|
516
|
+
}
|
|
517
|
+
const { entry } = value
|
|
518
|
+
if (active.submitting) throw new Error('this binding draft handoff is already processing a submission')
|
|
519
|
+
active.submitting = true
|
|
520
|
+
try {
|
|
521
|
+
const candidate = normalizeUserBindingEntry({ ...entry, enabled: false })
|
|
522
|
+
if (command.kind === 'edit' && candidate.id !== command.id) {
|
|
523
|
+
throw new Error(`edited binding id must remain ${JSON.stringify(command.id)}`)
|
|
524
|
+
}
|
|
525
|
+
const document = await store.validationDocument()
|
|
526
|
+
ensureLease()
|
|
527
|
+
requireActive()
|
|
528
|
+
if (command.kind === 'new' && document.entries.some(item => item.id === candidate.id)) {
|
|
529
|
+
throw new Error(`binding entry ${JSON.stringify(candidate.id)} already exists; choose a new id`)
|
|
530
|
+
}
|
|
531
|
+
const candidateEntry = storedEntry(candidate)
|
|
532
|
+
const entries = [
|
|
533
|
+
...document.entries.filter(item => item.id !== candidate.id),
|
|
534
|
+
candidateEntry,
|
|
535
|
+
]
|
|
536
|
+
normalizeUserBindingsDocument({ entries })
|
|
537
|
+
createUserBindingsSnapshot({
|
|
538
|
+
entries: entries.map(item => item.id === candidate.id
|
|
539
|
+
? { ...item, enabled: true }
|
|
540
|
+
: item),
|
|
541
|
+
}, document.revision)
|
|
542
|
+
active.accepted = true
|
|
543
|
+
const previous = drafts.get(sessionId)
|
|
544
|
+
if (previous !== undefined) removeDraft(previous)
|
|
545
|
+
const draft = {
|
|
546
|
+
agent,
|
|
547
|
+
capability: randomUUID(),
|
|
548
|
+
sessionId,
|
|
549
|
+
version: ++draftRevision,
|
|
550
|
+
mode: command.kind,
|
|
551
|
+
requestId: active.requestId,
|
|
552
|
+
commandId: commandId ?? null,
|
|
553
|
+
entry: Object.freeze(storedEntry(candidate)),
|
|
554
|
+
state: 'ready',
|
|
555
|
+
}
|
|
556
|
+
drafts.set(sessionId, draft)
|
|
557
|
+
draftsByCapability.set(draft.capability, draft)
|
|
558
|
+
onAccepted(candidateView(draft))
|
|
559
|
+
queueMicrotask(() => {
|
|
560
|
+
detach(
|
|
561
|
+
stopAuthoring(sessionId, undefined, active),
|
|
562
|
+
'ptc-plus: failed to clean up accepted Global User Bindings authoring',
|
|
563
|
+
)
|
|
564
|
+
})
|
|
565
|
+
return { accepted: true, id: candidate.id, requestId: active.requestId }
|
|
566
|
+
} finally {
|
|
567
|
+
active.submitting = false
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
signal?.throwIfAborted?.()
|
|
571
|
+
if (typeof signal?.addEventListener === 'function') {
|
|
572
|
+
const abort = () => {
|
|
573
|
+
detach(
|
|
574
|
+
stopAuthoring(sessionId, 'The binding authoring request was cancelled.', active),
|
|
575
|
+
'ptc-plus: failed to cancel Global User Bindings authoring',
|
|
576
|
+
)
|
|
577
|
+
}
|
|
578
|
+
active.abortCleanup = () => signal.removeEventListener('abort', abort)
|
|
579
|
+
signal.addEventListener('abort', abort, { once: true })
|
|
580
|
+
if (signal.aborted) abort()
|
|
581
|
+
}
|
|
582
|
+
agent.steer(taskMessage(`${bindingAuthoringInstructions(active.requestId, bindingsCwd)}\n\n${authoringTask(command, current)}`))
|
|
583
|
+
return { kind: 'success' }
|
|
584
|
+
} catch (error) {
|
|
585
|
+
if (pendingAuthoring.get(sessionId) === attempt) pendingAuthoring.delete(sessionId)
|
|
586
|
+
if (active !== undefined && activeAuthoring.get(sessionId) === active) {
|
|
587
|
+
await stopAuthoring(
|
|
588
|
+
sessionId,
|
|
589
|
+
error instanceof Error ? error.message : String(error),
|
|
590
|
+
active,
|
|
591
|
+
)
|
|
592
|
+
}
|
|
593
|
+
throw error
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const commandHandler = async ({ agent, rawInput, signal, commandId }) => {
|
|
598
|
+
signal?.throwIfAborted?.()
|
|
599
|
+
const command = parseAuthoringCommand(rawInput)
|
|
600
|
+
if (command.kind === 'help') return { kind: 'error', text: `Usage: ${COMMAND_USAGE}` }
|
|
601
|
+
try {
|
|
602
|
+
return await beginAuthoring(agent, command, signal, commandId)
|
|
603
|
+
} catch (error) {
|
|
604
|
+
return { kind: 'error', text: error instanceof Error ? error.message : String(error) }
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const removeCommandFor = async (sessionId, expected) => {
|
|
609
|
+
const current = commandRegistrations.get(sessionId)
|
|
610
|
+
if (current === undefined || (expected !== undefined && current !== expected)) return
|
|
611
|
+
if (current.disposing !== undefined) return current.disposing
|
|
612
|
+
current.disposing = (async () => {
|
|
613
|
+
try {
|
|
614
|
+
await disposeFiber(current.fiber)
|
|
615
|
+
} finally {
|
|
616
|
+
if (commandRegistrations.get(sessionId) === current) commandRegistrations.delete(sessionId)
|
|
617
|
+
}
|
|
618
|
+
})()
|
|
619
|
+
return current.disposing
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
const registerCommandFor = (agent) => {
|
|
623
|
+
const sessionId = sessionIdOf(agent)
|
|
624
|
+
if (sessionId === undefined || commandRegistrations.has(sessionId)) return
|
|
625
|
+
if (typeof agent?.ctx?.inject !== 'function') {
|
|
626
|
+
throw new Error('Global User Bindings command requires agent-scoped Cordis injection')
|
|
627
|
+
}
|
|
628
|
+
const agentGeneration = lifecycleFor(agent).generation
|
|
629
|
+
const registration = { agent, fiber: undefined, generation: lifecycleGeneration }
|
|
630
|
+
commandRegistrations.set(sessionId, registration)
|
|
631
|
+
let fiber
|
|
632
|
+
try {
|
|
633
|
+
fiber = agent.ctx.inject(['commands'], (commandCtx) => {
|
|
634
|
+
// This callback runs whenever the command service becomes available, not
|
|
635
|
+
// necessarily before inject() returns. When eligibility changed before it
|
|
636
|
+
// ran, the registration is stale: fail the activation explicitly so the
|
|
637
|
+
// fiber rejection below removes the placeholder registration. A silent
|
|
638
|
+
// return would leave the fiber alive with no command and the map entry
|
|
639
|
+
// still claiming one.
|
|
640
|
+
if (disposed || !enabled || lifecycleGeneration !== registration.generation
|
|
641
|
+
|| !agentIsCurrent(agent, sessionId, agentGeneration)
|
|
642
|
+
|| ctx.tools.get('run_code', agent) === undefined
|
|
643
|
+
|| commandRegistrations.get(sessionId) !== registration) {
|
|
644
|
+
throw new Error('Global User Bindings binding command registration is no longer eligible')
|
|
645
|
+
}
|
|
646
|
+
registerOwned(commandCtx, commandCtx.commands, {
|
|
647
|
+
name: 'binding',
|
|
648
|
+
description: 'Create or revise a Global User Binding draft with the current agent',
|
|
649
|
+
input: { hint: 'new <requirement> | edit <id> <requirement>' },
|
|
650
|
+
handler: commandHandler,
|
|
651
|
+
}, 'binding command')
|
|
652
|
+
})
|
|
653
|
+
} catch (error) {
|
|
654
|
+
if (commandRegistrations.get(sessionId) === registration) {
|
|
655
|
+
commandRegistrations.delete(sessionId)
|
|
656
|
+
}
|
|
657
|
+
throw error
|
|
658
|
+
}
|
|
659
|
+
registration.fiber = fiber
|
|
660
|
+
// A fiber that settles without running its callback is still pending on the
|
|
661
|
+
// command service; its registration must survive so the callback can install
|
|
662
|
+
// the command later. Only rejected fibers remove the placeholder registration.
|
|
663
|
+
detach(Promise.resolve(fiber).catch(async (error) => {
|
|
664
|
+
if (commandRegistrations.get(sessionId) === registration) {
|
|
665
|
+
commandRegistrations.delete(sessionId)
|
|
666
|
+
}
|
|
667
|
+
try {
|
|
668
|
+
await disposeFiber(fiber)
|
|
669
|
+
} catch (cleanupError) {
|
|
670
|
+
throw new AggregateError(
|
|
671
|
+
[error, cleanupError],
|
|
672
|
+
'Global User Bindings command activation and rollback failed',
|
|
673
|
+
)
|
|
674
|
+
}
|
|
675
|
+
throw error
|
|
676
|
+
}), 'ptc-plus: failed to roll back Global User Bindings command injection')
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
const reconcileAgent = async (agent) => {
|
|
680
|
+
const sessionId = sessionIdOf(agent)
|
|
681
|
+
if (sessionId === undefined) return
|
|
682
|
+
const agentGeneration = activateAgent(agent)
|
|
683
|
+
const previousAgent = sessionAgents.get(sessionId)
|
|
684
|
+
if (previousAgent !== agent) {
|
|
685
|
+
sessionGenerations.set(sessionId, (sessionGenerations.get(sessionId) ?? 0) + 1)
|
|
686
|
+
sessionAgents.set(sessionId, agent)
|
|
687
|
+
if (previousAgent !== undefined) {
|
|
688
|
+
invalidateAgent(previousAgent)
|
|
689
|
+
await clearAgentPresentation(previousAgent)
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
if (!agentIsCurrent(agent, sessionId, agentGeneration)) return
|
|
693
|
+
const eligible = enabled && draftProjectionAvailable && !disposed
|
|
694
|
+
&& ctx.tools.get('run_code', agent) !== undefined
|
|
695
|
+
const current = commandRegistrations.get(sessionId)
|
|
696
|
+
if (eligible && current?.agent === agent) return
|
|
697
|
+
if (current !== undefined) await removeCommandFor(sessionId, current)
|
|
698
|
+
if (!agentIsCurrent(agent, sessionId, agentGeneration)) return
|
|
699
|
+
const stillEligible = enabled && draftProjectionAvailable && !disposed
|
|
700
|
+
&& ctx.tools.get('run_code', agent) !== undefined
|
|
701
|
+
if (stillEligible) {
|
|
702
|
+
registerCommandFor(agent)
|
|
703
|
+
return
|
|
704
|
+
}
|
|
705
|
+
const active = activeAuthoring.get(sessionId)
|
|
706
|
+
if (active?.agent === agent) {
|
|
707
|
+
await stopAuthoring(
|
|
708
|
+
sessionId,
|
|
709
|
+
'This agent no longer exposes run_code; no Global User Binding draft was accepted.',
|
|
710
|
+
active,
|
|
711
|
+
)
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const setAgentPresentation = agent => reconcileAgent(agent)
|
|
716
|
+
|
|
717
|
+
const clearAgentPresentation = async (agent) => {
|
|
718
|
+
const sessionId = sessionIdOf(agent)
|
|
719
|
+
if (sessionId === undefined) return
|
|
720
|
+
invalidateAgent(agent)
|
|
721
|
+
if (sessionAgents.get(sessionId) === agent) sessionAgents.delete(sessionId)
|
|
722
|
+
const pending = pendingAuthoring.get(sessionId)
|
|
723
|
+
if (pending?.agent === agent) pendingAuthoring.delete(sessionId)
|
|
724
|
+
const active = activeAuthoring.get(sessionId)
|
|
725
|
+
if (active?.agent === agent) {
|
|
726
|
+
await stopAuthoring(
|
|
727
|
+
sessionId,
|
|
728
|
+
'The Agent ended before a Global User Binding draft was accepted.',
|
|
729
|
+
active,
|
|
730
|
+
)
|
|
731
|
+
}
|
|
732
|
+
const draft = drafts.get(sessionId)
|
|
733
|
+
if (draft?.agent === agent) removeDraft(draft)
|
|
734
|
+
for (const [capability, review] of reviewsByCapability) {
|
|
735
|
+
if (review.agent === agent) reviewsByCapability.delete(capability)
|
|
736
|
+
}
|
|
737
|
+
const registration = commandRegistrations.get(sessionId)
|
|
738
|
+
if (registration?.agent === agent) await removeCommandFor(sessionId, registration)
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
const clearSessionPresentation = async (sessionId) => {
|
|
742
|
+
if (sessionId === undefined || sessionId === null) return
|
|
743
|
+
const id = String(sessionId)
|
|
744
|
+
const generation = sessionGenerations.get(id) ?? 0
|
|
745
|
+
const pending = pendingAuthoring.get(id)
|
|
746
|
+
const sessionDrafts = [...drafts.values()].filter(draft => draft.sessionId === id)
|
|
747
|
+
const agents = new Set()
|
|
748
|
+
const known = [
|
|
749
|
+
sessionAgents.get(id),
|
|
750
|
+
pending?.agent,
|
|
751
|
+
activeAuthoring.get(id)?.agent,
|
|
752
|
+
commandRegistrations.get(id)?.agent,
|
|
753
|
+
...sessionDrafts.map(draft => draft.agent),
|
|
754
|
+
]
|
|
755
|
+
for (const agent of known) if (agent !== undefined) agents.add(agent)
|
|
756
|
+
if (pendingAuthoring.get(id) === pending) pendingAuthoring.delete(id)
|
|
757
|
+
for (const draft of sessionDrafts) {
|
|
758
|
+
if (drafts.get(id) === draft) removeDraft(draft)
|
|
759
|
+
}
|
|
760
|
+
const results = await Promise.allSettled([...agents].map(agent => clearAgentPresentation(agent)))
|
|
761
|
+
if ((sessionGenerations.get(id) ?? 0) !== generation) return
|
|
762
|
+
activeAuthoring.delete(id)
|
|
763
|
+
sessionAgents.delete(id)
|
|
764
|
+
sessionGenerations.delete(id)
|
|
765
|
+
const failures = results.filter(result => result.status === 'rejected').map(result => result.reason)
|
|
766
|
+
if (failures.length > 0) {
|
|
767
|
+
throw new AggregateError(failures, 'Global User Bindings session cleanup failed')
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
const unmountAuthoring = async () => {
|
|
772
|
+
authoringGeneration += 1
|
|
773
|
+
const currentInjection = authoringInjection
|
|
774
|
+
const operations = [
|
|
775
|
+
...[...activeAuthoring.keys()].map(sessionId => stopAuthoring(
|
|
776
|
+
sessionId,
|
|
777
|
+
'Global User Bindings were disabled before a draft was accepted.',
|
|
778
|
+
)),
|
|
779
|
+
...[...commandRegistrations.keys()].map(sessionId => removeCommandFor(sessionId)),
|
|
780
|
+
Promise.resolve().then(() => disposeFiber(currentInjection)).finally(() => {
|
|
781
|
+
if (authoringInjection === currentInjection) authoringInjection = undefined
|
|
782
|
+
}),
|
|
783
|
+
]
|
|
784
|
+
pendingAuthoring.clear()
|
|
785
|
+
const results = await Promise.allSettled(operations)
|
|
786
|
+
const failures = []
|
|
787
|
+
for (const result of results) {
|
|
788
|
+
if (result.status === 'rejected') failures.push(result.reason)
|
|
789
|
+
}
|
|
790
|
+
if (failures.length > 0) throw new AggregateError(failures, 'Global User Bindings authoring unmount failed')
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
const mountAuthoring = () => {
|
|
794
|
+
if (!enabled || disposed || authoringInjection !== undefined || typeof ctx.inject !== 'function') return
|
|
795
|
+
const generation = ++authoringGeneration
|
|
796
|
+
authoringInjection = ctx.inject(['tools'], (scope) => {
|
|
797
|
+
if (!enabled || disposed || authoringGeneration !== generation) return
|
|
798
|
+
const reconcile = agent => {
|
|
799
|
+
detach(
|
|
800
|
+
reconcileAgent(agent),
|
|
801
|
+
'ptc-plus: failed to reconcile Global User Bindings authoring',
|
|
802
|
+
)
|
|
803
|
+
}
|
|
804
|
+
for (const agent of ctx.agents?.list?.() ?? []) reconcile(agent)
|
|
805
|
+
scope.on?.('agent/created', ({ agent }) => reconcile(agent))
|
|
806
|
+
scope.on?.('tools/change', () => {
|
|
807
|
+
for (const agent of ctx.agents?.list?.() ?? []) reconcile(agent)
|
|
808
|
+
})
|
|
809
|
+
scope.on?.('agent/disposed', ({ agent }) => {
|
|
810
|
+
return contain(
|
|
811
|
+
clearAgentPresentation(agent),
|
|
812
|
+
'ptc-plus: failed to dispose Global User Bindings authoring',
|
|
813
|
+
)
|
|
814
|
+
})
|
|
815
|
+
scope.on?.('agent/turn-stopping', ({ agent }) => {
|
|
816
|
+
const sessionId = sessionIdOf(agent)
|
|
817
|
+
const active = sessionId === undefined ? undefined : activeAuthoring.get(sessionId)
|
|
818
|
+
if (active?.agent === agent) return stopAuthoring(sessionId, undefined, active)
|
|
819
|
+
})
|
|
820
|
+
scope.on?.('agent/error', ({ agent }) => {
|
|
821
|
+
const sessionId = sessionIdOf(agent)
|
|
822
|
+
const active = sessionId === undefined ? undefined : activeAuthoring.get(sessionId)
|
|
823
|
+
if (active?.agent === agent) return stopAuthoring(
|
|
824
|
+
sessionId,
|
|
825
|
+
'The Agent failed before a Global User Binding draft was accepted.',
|
|
826
|
+
active,
|
|
827
|
+
)
|
|
828
|
+
})
|
|
829
|
+
})
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
const mountRpcRegistration = (state, record) => {
|
|
833
|
+
if (!enabled || disposed || rpcMount !== state || record.registration !== undefined
|
|
834
|
+
|| typeof record.scope.ptcPlusRpc?.register !== 'function') return
|
|
835
|
+
const effectOwner = typeof record.scope.effect === 'function' ? record.scope : ctx
|
|
836
|
+
const registration = effectOwner.effect(() => record.scope.ptcPlusRpc.register(
|
|
837
|
+
RPC_CONTRACT,
|
|
838
|
+
handler,
|
|
839
|
+
), 'ptc-plus user bindings RPC')
|
|
840
|
+
if (typeof registration !== 'function') {
|
|
841
|
+
throw new Error('Global User Bindings RPC registration did not return a disposer')
|
|
842
|
+
}
|
|
843
|
+
record.registration = registration
|
|
844
|
+
}
|
|
845
|
+
const mountRpc = () => {
|
|
846
|
+
if (!enabled || disposed || typeof ctx.inject !== 'function') return
|
|
847
|
+
if (rpcMount !== undefined) {
|
|
848
|
+
for (const record of rpcMount.records) mountRpcRegistration(rpcMount, record)
|
|
849
|
+
return
|
|
850
|
+
}
|
|
851
|
+
const state = { injection: undefined, records: new Set() }
|
|
852
|
+
rpcMount = state
|
|
853
|
+
state.injection = ctx.inject(['ptcPlusRpc'], (scope) => {
|
|
854
|
+
if (!enabled || disposed || rpcMount !== state) return
|
|
855
|
+
const record = { scope, registration: undefined }
|
|
856
|
+
state.records.add(record)
|
|
857
|
+
scope.effect?.(() => () => { state.records.delete(record) }, 'ptc-plus: binding RPC scope')
|
|
858
|
+
try {
|
|
859
|
+
mountRpcRegistration(state, record)
|
|
860
|
+
} catch (error) {
|
|
861
|
+
state.records.delete(record)
|
|
862
|
+
throw error
|
|
863
|
+
}
|
|
864
|
+
})
|
|
865
|
+
}
|
|
866
|
+
const unmountRpc = async () => {
|
|
867
|
+
const state = rpcMount
|
|
868
|
+
if (state === undefined) return
|
|
869
|
+
const operations = [...state.records].flatMap(record => (
|
|
870
|
+
typeof record.registration !== 'function' ? [] : [
|
|
871
|
+
Promise.resolve().then(() => record.registration()).finally(() => {
|
|
872
|
+
record.registration = undefined
|
|
873
|
+
}),
|
|
874
|
+
]
|
|
875
|
+
))
|
|
876
|
+
const results = await Promise.allSettled(operations)
|
|
877
|
+
const failures = []
|
|
878
|
+
for (const result of results) {
|
|
879
|
+
if (result.status === 'rejected') failures.push(result.reason)
|
|
880
|
+
}
|
|
881
|
+
if (failures.length > 0) throw new AggregateError(failures, 'Global User Bindings RPC unmount failed')
|
|
882
|
+
try {
|
|
883
|
+
if (typeof state.injection === 'function') await state.injection()
|
|
884
|
+
else if (typeof state.injection?.dispose === 'function') await state.injection.dispose()
|
|
885
|
+
} catch (error) {
|
|
886
|
+
throw new AggregateError([error], 'Global User Bindings RPC unmount failed')
|
|
887
|
+
}
|
|
888
|
+
if (rpcMount === state) rpcMount = undefined
|
|
889
|
+
state.records.clear()
|
|
890
|
+
}
|
|
891
|
+
mountRpc()
|
|
892
|
+
mountAuthoring()
|
|
893
|
+
|
|
894
|
+
return Object.freeze({
|
|
895
|
+
path: store.filename,
|
|
896
|
+
cwd: bindingsCwd,
|
|
897
|
+
async snapshot() {
|
|
898
|
+
return enabled ? store.snapshot() : undefined
|
|
899
|
+
},
|
|
900
|
+
async list() {
|
|
901
|
+
requireEnabled()
|
|
902
|
+
return store.list()
|
|
903
|
+
},
|
|
904
|
+
setAgentPresentation,
|
|
905
|
+
clearAgentPresentation,
|
|
906
|
+
clearSessionPresentation,
|
|
907
|
+
async setDraftProjectionAvailable(available) {
|
|
908
|
+
const next = available === true
|
|
909
|
+
if (draftProjectionAvailable === next) return
|
|
910
|
+
draftProjectionAvailable = next
|
|
911
|
+
lifecycleGeneration += 1
|
|
912
|
+
if (next) {
|
|
913
|
+
await Promise.all([...new Set([
|
|
914
|
+
...(ctx.agents?.list?.() ?? []),
|
|
915
|
+
...sessionAgents.values(),
|
|
916
|
+
])].map(reconcileAgent))
|
|
917
|
+
return
|
|
918
|
+
}
|
|
919
|
+
pendingAuthoring.clear()
|
|
920
|
+
for (const draft of [...drafts.values()]) removeDraft(draft)
|
|
921
|
+
reviewsByCapability.clear()
|
|
922
|
+
const results = await Promise.allSettled([
|
|
923
|
+
...[...activeAuthoring.keys()].map(sessionId => stopAuthoring(
|
|
924
|
+
sessionId,
|
|
925
|
+
'The binding draft projection became unavailable before a draft was accepted.',
|
|
926
|
+
)),
|
|
927
|
+
...[...commandRegistrations.keys()].map(sessionId => removeCommandFor(sessionId)),
|
|
928
|
+
])
|
|
929
|
+
const failures = results.filter(result => result.status === 'rejected').map(result => result.reason)
|
|
930
|
+
if (failures.length > 0) {
|
|
931
|
+
throw new AggregateError(failures, 'Global User Bindings command projection cleanup failed')
|
|
932
|
+
}
|
|
933
|
+
},
|
|
934
|
+
draftCapabilityForAgent(agent) {
|
|
935
|
+
const sessionId = sessionIdOf(agent)
|
|
936
|
+
if (sessionId === undefined) return null
|
|
937
|
+
const draft = drafts.get(sessionId)
|
|
938
|
+
return draft?.agent === agent ? draft.capability : null
|
|
939
|
+
},
|
|
940
|
+
submissionForAgent(agent, ensureLease, onAccepted) {
|
|
941
|
+
const current = activeAuthoring.get(sessionIdOf(agent))
|
|
942
|
+
return async value => {
|
|
943
|
+
ensureLease()
|
|
944
|
+
if (current === undefined || current.agent !== agent) {
|
|
945
|
+
throw new Error('binding submission requires an active /binding request in the requesting Agent')
|
|
946
|
+
}
|
|
947
|
+
return current.submit(value, ensureLease, onAccepted)
|
|
948
|
+
}
|
|
949
|
+
},
|
|
950
|
+
async reconfigure(nextConfig) {
|
|
951
|
+
const nextEnabled = nextConfig.userBindingsEnabled === true
|
|
952
|
+
const previousEnabled = enabled
|
|
953
|
+
const previousOptions = currentOptions
|
|
954
|
+
const nextOptions = {
|
|
955
|
+
...currentOptions,
|
|
956
|
+
maxWallMs: nextConfig.maxWallMs,
|
|
957
|
+
maxOutputBytes: nextConfig.maxOutputBytes,
|
|
958
|
+
maxOldGenerationSizeMb: nextConfig.maxOldGenerationSizeMb,
|
|
959
|
+
valueLimits: valueLimitsFromConfig(nextConfig),
|
|
960
|
+
}
|
|
961
|
+
if (nextEnabled === previousEnabled) {
|
|
962
|
+
await codeConsole.reconfigure(nextOptions)
|
|
963
|
+
currentOptions = nextOptions
|
|
964
|
+
return
|
|
965
|
+
}
|
|
966
|
+
try {
|
|
967
|
+
await codeConsole.reconfigure(nextOptions)
|
|
968
|
+
if (!nextEnabled) {
|
|
969
|
+
const failures = await codeConsole.releaseAll('disabled')
|
|
970
|
+
if (failures.length > 0) {
|
|
971
|
+
throw new AggregateError(failures, 'Global User Bindings console release failed')
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
} catch (error) {
|
|
975
|
+
try {
|
|
976
|
+
await codeConsole.reconfigure(previousOptions)
|
|
977
|
+
} catch (rollbackError) {
|
|
978
|
+
throw new AggregateError(
|
|
979
|
+
[error, rollbackError],
|
|
980
|
+
'Global User Bindings console reconfiguration and rollback failed',
|
|
981
|
+
{ cause: error },
|
|
982
|
+
)
|
|
983
|
+
}
|
|
984
|
+
throw error
|
|
985
|
+
}
|
|
986
|
+
enabled = nextEnabled
|
|
987
|
+
currentOptions = nextOptions
|
|
988
|
+
lifecycleGeneration += 1
|
|
989
|
+
if (nextEnabled) {
|
|
990
|
+
try {
|
|
991
|
+
mountRpc()
|
|
992
|
+
mountAuthoring()
|
|
993
|
+
} catch (error) {
|
|
994
|
+
enabled = previousEnabled
|
|
995
|
+
currentOptions = previousOptions
|
|
996
|
+
lifecycleGeneration += 1
|
|
997
|
+
const rollbackResults = await Promise.allSettled([
|
|
998
|
+
codeConsole.reconfigure(previousOptions),
|
|
999
|
+
unmountRpc(),
|
|
1000
|
+
unmountAuthoring(),
|
|
1001
|
+
])
|
|
1002
|
+
const rollbackFailures = rollbackResults
|
|
1003
|
+
.filter(result => result.status === 'rejected')
|
|
1004
|
+
.map(result => result.reason)
|
|
1005
|
+
if (rollbackFailures.length > 0) {
|
|
1006
|
+
throw new AggregateError(
|
|
1007
|
+
[error, ...rollbackFailures],
|
|
1008
|
+
'Global User Bindings enablement and rollback failed',
|
|
1009
|
+
{ cause: error },
|
|
1010
|
+
)
|
|
1011
|
+
}
|
|
1012
|
+
throw error
|
|
1013
|
+
}
|
|
1014
|
+
return
|
|
1015
|
+
}
|
|
1016
|
+
try {
|
|
1017
|
+
await Promise.all([unmountRpc(), unmountAuthoring()])
|
|
1018
|
+
for (const draft of [...drafts.values()]) removeDraft(draft)
|
|
1019
|
+
reviewsByCapability.clear()
|
|
1020
|
+
} catch (error) {
|
|
1021
|
+
enabled = previousEnabled
|
|
1022
|
+
currentOptions = previousOptions
|
|
1023
|
+
lifecycleGeneration += 1
|
|
1024
|
+
const rollbackResults = await Promise.allSettled([
|
|
1025
|
+
codeConsole.reconfigure(previousOptions),
|
|
1026
|
+
Promise.resolve().then(() => mountRpc()),
|
|
1027
|
+
Promise.resolve().then(() => mountAuthoring()),
|
|
1028
|
+
])
|
|
1029
|
+
const rollbackFailures = rollbackResults
|
|
1030
|
+
.filter(result => result.status === 'rejected')
|
|
1031
|
+
.map(result => result.reason)
|
|
1032
|
+
if (rollbackFailures.length > 0) {
|
|
1033
|
+
throw new AggregateError(
|
|
1034
|
+
[error, ...rollbackFailures],
|
|
1035
|
+
'Global User Bindings disablement and rollback failed',
|
|
1036
|
+
{ cause: error },
|
|
1037
|
+
)
|
|
1038
|
+
}
|
|
1039
|
+
throw error
|
|
1040
|
+
}
|
|
1041
|
+
},
|
|
1042
|
+
async dispose() {
|
|
1043
|
+
disposed = true
|
|
1044
|
+
const ownerResults = await Promise.allSettled([candidateOwner.dispose(), codeConsole.dispose()])
|
|
1045
|
+
const failures = []
|
|
1046
|
+
for (const result of ownerResults) {
|
|
1047
|
+
if (result.status === 'rejected') failures.push(result.reason)
|
|
1048
|
+
else failures.push(...result.value)
|
|
1049
|
+
}
|
|
1050
|
+
enabled = false
|
|
1051
|
+
lifecycleGeneration += 1
|
|
1052
|
+
for (const draft of [...drafts.values()]) removeDraft(draft)
|
|
1053
|
+
reviewsByCapability.clear()
|
|
1054
|
+
const unmountResults = await Promise.allSettled([unmountRpc(), unmountAuthoring()])
|
|
1055
|
+
for (const result of unmountResults) {
|
|
1056
|
+
if (result.status === 'rejected') failures.push(result.reason)
|
|
1057
|
+
}
|
|
1058
|
+
if (failures.length > 0) {
|
|
1059
|
+
throw new AggregateError(failures, 'Global User Bindings disposal failed')
|
|
1060
|
+
}
|
|
1061
|
+
},
|
|
1062
|
+
})
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
export { RPC_CONTRACT as USER_BINDINGS_RPC_CONTRACT }
|