dsh-ptc-plus 0.3.2 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/README.en.md +63 -80
  2. package/README.md +63 -80
  3. package/assets/ptc-plus-bindings-en.png +0 -0
  4. package/assets/ptc-plus-bindings-zh.png +0 -0
  5. package/assets/ptc-plus-repair-en.png +0 -0
  6. package/assets/ptc-plus-repair-zh.png +0 -0
  7. package/assets/ptc-plus-repl-workspace-en.png +0 -0
  8. package/assets/ptc-plus-repl-workspace-zh.png +0 -0
  9. package/assets/ptc-plus-settings-en.png +0 -0
  10. package/assets/ptc-plus-settings-zh.png +0 -0
  11. package/client.js +30827 -742
  12. package/compiler-amaro.cjs +5 -0
  13. package/compiler-amaro.cjs.map +6 -0
  14. package/compiler-asset-124f80c1bba92ff5fe79cfec6e8ad518f476de4a42514cae3919538887f510e8.txt +1 -0
  15. package/compiler-assets.json +3 -0
  16. package/compiler-core.cjs +556 -0
  17. package/compiler-core.cjs.map +6 -0
  18. package/compiler-platform.cjs +2 -0
  19. package/compiler-platform.cjs.map +6 -0
  20. package/compiler-typescript.cjs +388 -0
  21. package/compiler-typescript.cjs.map +6 -0
  22. package/docs/README.md +7 -0
  23. package/docs/adr/0004-declare-program-bindings-at-the-host.md +1 -1
  24. package/docs/adr/0006-rewrite-module-syntax-with-an-ast.md +5 -3
  25. package/docs/adr/0007-separate-worker-transport-from-session-semantics.md +1 -1
  26. package/docs/adr/0009-bound-expensive-acceptance-default.md +2 -0
  27. package/docs/adr/0010-session-log-derived-recovery-tips.md +59 -5
  28. package/docs/adr/0011-isolate-the-local-recovery-tip-provider.md +1 -1
  29. package/docs/adr/0012-preserve-mixed-destructuring-initialization.md +3 -1
  30. package/docs/adr/0013-isolate-repl-redeclaration-convenience.md +3 -1
  31. package/docs/adr/0014-persist-cell-rewrite-policy.md +6 -2
  32. package/docs/adr/0015-preserve-source-positions-through-rewrites.md +1 -1
  33. package/docs/adr/0016-archive-resolved-review-findings.md +1 -1
  34. package/docs/adr/0017-track-the-latest-dsh-public-surface.md +49 -0
  35. package/docs/adr/0018-use-versioned-fixture-for-ab-benchmark.md +3 -3
  36. package/docs/adr/0019-plugin-settings-and-kill-switch.md +5 -3
  37. package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +2 -2
  38. package/docs/adr/0021-separate-host-bootstrap-from-session-runtime.md +12 -0
  39. package/docs/adr/0023-global-user-bindings.md +104 -0
  40. package/docs/adr/0024-repl-console-observation.md +33 -0
  41. package/docs/adr/0025-use-versioned-logical-binding-identities.md +119 -0
  42. package/docs/adr/0026-persist-the-delivery-verdict-against-the-verified-candidate.md +21 -0
  43. package/docs/adr/0027-run-the-session-kernel-in-a-killable-helper-process.md +37 -0
  44. package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +103 -0
  45. package/docs/adr/0029-keep-plugin-unavailability-out-of-the-host-agent-lifecycle.md +27 -0
  46. package/docs/adr/0030-serve-both-typert-codec-generations-from-one-codec.md +27 -0
  47. package/docs/architecture.md +77 -21
  48. package/docs/binding-review-placement-design.md +200 -0
  49. package/docs/capability-projection.md +1 -1
  50. package/docs/client-ui.md +95 -19
  51. package/docs/durability-design.md +28 -10
  52. package/docs/evaluation.md +78 -6
  53. package/docs/installation.md +41 -6
  54. package/docs/publishing.md +5 -3
  55. package/docs/runtime-reference.md +76 -25
  56. package/docs/semantic-validation.md +29 -0
  57. package/docs/stateful-binding-updates-proposal.md +759 -0
  58. package/docs/user-bindings.en.md +66 -0
  59. package/docs/user-bindings.md +66 -0
  60. package/docs/value-wire.md +3 -3
  61. package/docs/verification-optimization-work.md +41 -0
  62. package/docs/verification.md +71 -0
  63. package/index.js +369 -92
  64. package/internal/ast-traversal.js +5 -2
  65. package/internal/async-local-storage-intrinsics.js +38 -0
  66. package/internal/binding-declaration-contract.js +8 -0
  67. package/internal/binding-descriptors.js +1 -0
  68. package/internal/binding-pattern-traversal.js +25 -0
  69. package/internal/binding-pattern.js +26 -29
  70. package/internal/binding-source-analysis.js +708 -0
  71. package/internal/binding-update-policy.js +72 -0
  72. package/internal/bounded-worker-round.js +56 -0
  73. package/internal/callable-definition-environment.js +42 -0
  74. package/internal/callable-source-catalog.js +129 -0
  75. package/internal/callable-source-encoding.js +47 -0
  76. package/internal/callable-source-facts.js +459 -0
  77. package/internal/callable-source-format.js +2 -0
  78. package/internal/callable-source-lowering.js +123 -0
  79. package/internal/cell-analysis-contract.js +25 -0
  80. package/internal/cell-analysis.js +253 -290
  81. package/internal/cell-completion.js +15 -0
  82. package/internal/cell-error.js +7 -0
  83. package/internal/cell-parser.js +66 -0
  84. package/internal/cell-rewriter.js +3 -677
  85. package/internal/commonjs-export-evidence.js +63 -0
  86. package/internal/commonjs-source-evidence.js +72 -0
  87. package/internal/compiler-amaro.cjs +2 -0
  88. package/internal/compiler-compression.js +12 -0
  89. package/internal/compiler-data.js +58 -0
  90. package/internal/compiler-descriptors.js +54 -0
  91. package/internal/compiler-dynamic-cache.js +48 -0
  92. package/internal/compiler-entry.js +35 -0
  93. package/internal/compiler-generated-names.js +17 -0
  94. package/internal/compiler-intrinsics.js +50 -0
  95. package/internal/compiler-module-links.js +37 -0
  96. package/internal/compiler-operations.js +61 -0
  97. package/internal/compiler-platform-factory.js +84 -0
  98. package/internal/compiler-platform.js +136 -0
  99. package/internal/compiler-record-roles.js +53 -0
  100. package/internal/compiler-region-output.js +259 -0
  101. package/internal/compiler-resource-lowering.js +103 -0
  102. package/internal/compiler-scope-facts.js +115 -0
  103. package/internal/compiler-service.js +109 -0
  104. package/internal/compiler-source-cache.js +32 -0
  105. package/internal/compiler-source-pieces.js +93 -0
  106. package/internal/compiler-source-regions.js +132 -0
  107. package/internal/compiler-storage-source.js +54 -0
  108. package/internal/compiler-text.js +15 -0
  109. package/internal/compiler-typescript.cjs +2 -0
  110. package/internal/config-spec.js +83 -27
  111. package/internal/cordis-tools-owner.js +130 -21
  112. package/internal/diagnostic.js +27 -1
  113. package/internal/direct-surface-owner.js +76 -23
  114. package/internal/dynamic-binding-evidence.js +16 -0
  115. package/internal/dynamic-environment-compiler.js +779 -0
  116. package/internal/dynamic-environment-integration.js +91 -0
  117. package/internal/dynamic-environment-runtime.js +383 -0
  118. package/internal/dynamic-native-calls.js +153 -0
  119. package/internal/dynamic-native-runtime.js +262 -0
  120. package/internal/dynamic-scope-analysis.js +90 -0
  121. package/internal/edit-transport-owner.js +27 -2
  122. package/internal/execution-seam-compat.js +193 -0
  123. package/internal/failure-reporting.js +136 -28
  124. package/internal/host-rpc.js +66 -0
  125. package/internal/isolated-worker.js +555 -0
  126. package/internal/kernel-child.js +139 -0
  127. package/internal/kernel-worker.js +1179 -233
  128. package/internal/language-semantics.js +14 -0
  129. package/internal/legacy-binding-catalog.js +48 -0
  130. package/internal/legacy-cell-analysis.js +600 -0
  131. package/internal/legacy-cell-parser.js +50 -0
  132. package/internal/legacy-cell-rewriter.js +631 -0
  133. package/internal/legacy-repl-convenience.js +291 -0
  134. package/internal/managed-module-operations.js +170 -0
  135. package/internal/module-compilation.js +322 -0
  136. package/internal/module-runtime-platform.js +19 -0
  137. package/internal/module-transform-contract.js +42 -0
  138. package/internal/native-await-declarations.js +68 -0
  139. package/internal/native-root-compilation.js +54 -0
  140. package/internal/native-root-dynamic.js +67 -0
  141. package/internal/record-utils.js +24 -15
  142. package/internal/recovery-tips.js +27 -15
  143. package/internal/rejected-cell-editor.js +13 -8
  144. package/internal/repl-convenience.js +2 -289
  145. package/internal/repl-memory-projection.js +81 -12
  146. package/internal/repl-observation-interest.js +70 -0
  147. package/internal/repl-rewrite-contract.js +6 -0
  148. package/internal/repl-scope-normalizer.js +2091 -0
  149. package/internal/repl-value-observer.js +117 -0
  150. package/internal/rpc-contract.js +52 -0
  151. package/internal/runtime-bridge-owner.js +172 -28
  152. package/internal/runtime-config.js +48 -52
  153. package/internal/runtime-contexts.js +82 -10
  154. package/internal/runtime-intrinsics.js +101 -0
  155. package/internal/runtime-messages.js +111 -0
  156. package/internal/session-cell-executor.js +402 -87
  157. package/internal/session-journal-recovery.js +684 -0
  158. package/internal/session-journal-schema.js +123 -0
  159. package/internal/session-journal.js +199 -654
  160. package/internal/session-log-view.js +42 -9
  161. package/internal/session-runtime.js +257 -95
  162. package/internal/session-state.js +247 -71
  163. package/internal/source-position-map.js +230 -71
  164. package/internal/stateful-module-compiler.js +154 -0
  165. package/internal/stateful-module-runtime.js +303 -0
  166. package/internal/stateful-root-compiler.js +600 -0
  167. package/internal/stateful-root-runtime.js +340 -0
  168. package/internal/typert-codec-compat.js +28 -0
  169. package/internal/typescript-transform.js +31 -0
  170. package/internal/user-binding-authoring.js +70 -0
  171. package/internal/user-binding-console-worker.js +244 -0
  172. package/internal/user-binding-console.js +148 -0
  173. package/internal/user-binding-draft-projection.js +335 -0
  174. package/internal/user-binding-model-context.js +31 -0
  175. package/internal/user-binding-runner.js +56 -0
  176. package/internal/user-bindings-owner.js +1065 -0
  177. package/internal/user-bindings-store.js +293 -0
  178. package/internal/user-bindings.js +361 -0
  179. package/internal/value-wire-schema.js +28 -0
  180. package/internal/value-wire.js +130 -91
  181. package/internal/worker-client.js +57 -67
  182. package/internal/worker-cwd-virtualization.js +80 -53
  183. package/internal/worker-environment.js +65 -0
  184. package/internal/worker-output-relay.js +29 -0
  185. package/internal/worker-repl-realm.js +134 -0
  186. package/internal/worker-shutdown.js +14 -0
  187. package/package.json +76 -7
  188. package/src/client-activity.js +95 -53
  189. package/src/client-authoring-view.js +580 -0
  190. package/src/client-binding-review.js +230 -0
  191. package/src/client-bindings-data.js +29 -0
  192. package/src/client-catalog.js +127 -0
  193. package/src/client-code-editor.js +84 -0
  194. package/src/client-console.js +112 -0
  195. package/src/client-copy.js +370 -0
  196. package/src/client-feature-gates.js +62 -0
  197. package/src/client-host-compat.js +150 -0
  198. package/src/client-indicator-view.js +267 -0
  199. package/src/client-repl-view.js +292 -0
  200. package/src/client-rpc.js +20 -0
  201. package/src/client-settings-view.js +221 -0
  202. package/src/client-styles.js +106 -0
  203. package/src/client-tool-view.js +108 -0
  204. package/src/client-transport.js +159 -0
  205. package/src/client-workbench.js +521 -0
  206. 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 }