dsh-ptc-plus 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +137 -0
  3. package/README.zh.md +137 -0
  4. package/assets/dsh-ptc-plus-banner.webp +0 -0
  5. package/assets/ptc-plus-repair.png +0 -0
  6. package/assets/ptc-plus-settings.png +0 -0
  7. package/client.js +366 -0
  8. package/cordis.patch.yml +5 -0
  9. package/docs/README.md +21 -0
  10. package/docs/adr/0001-delegate-governance-to-dsh.md +3 -0
  11. package/docs/adr/0002-limit-work-map-scope.md +3 -0
  12. package/docs/adr/0003-prefer-native-program-surfaces.md +3 -0
  13. package/docs/adr/0004-declare-program-bindings-at-the-host.md +5 -0
  14. package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +129 -0
  15. package/docs/adr/0006-rewrite-module-syntax-with-an-ast.md +45 -0
  16. package/docs/adr/0007-separate-worker-transport-from-session-semantics.md +39 -0
  17. package/docs/adr/0008-define-agent-operating-contract.md +25 -0
  18. package/docs/adr/0009-bound-expensive-acceptance-default.md +39 -0
  19. package/docs/adr/0010-session-log-derived-recovery-tips.md +26 -0
  20. package/docs/adr/0011-isolate-the-local-recovery-tip-provider.md +20 -0
  21. package/docs/adr/0012-preserve-mixed-destructuring-initialization.md +23 -0
  22. package/docs/adr/0013-isolate-repl-redeclaration-convenience.md +21 -0
  23. package/docs/adr/0014-persist-cell-rewrite-policy.md +25 -0
  24. package/docs/adr/0015-preserve-source-positions-through-rewrites.md +25 -0
  25. package/docs/adr/0016-archive-resolved-review-findings.md +43 -0
  26. package/docs/adr/0017-track-the-latest-dsh-public-surface.md +23 -0
  27. package/docs/adr/0018-use-versioned-fixture-for-ab-benchmark.md +25 -0
  28. package/docs/adr/0019-plugin-settings-and-kill-switch.md +32 -0
  29. package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +36 -0
  30. package/docs/adr/0021-separate-host-bootstrap-from-session-runtime.md +68 -0
  31. package/docs/adr/README.md +18 -0
  32. package/docs/architecture.md +134 -0
  33. package/docs/capability-projection.md +48 -0
  34. package/docs/client-ui.md +47 -0
  35. package/docs/durability-design.md +265 -0
  36. package/docs/evaluation.md +62 -0
  37. package/docs/installation.md +82 -0
  38. package/docs/program-data-plane.md +61 -0
  39. package/docs/publishing.md +37 -0
  40. package/docs/runtime-reference.md +119 -0
  41. package/docs/value-wire.md +61 -0
  42. package/index.js +532 -0
  43. package/internal/ast-traversal.js +20 -0
  44. package/internal/binding-descriptors.js +95 -0
  45. package/internal/binding-pattern.js +54 -0
  46. package/internal/builtin-esm-sync.js +9 -0
  47. package/internal/cell-analysis.js +467 -0
  48. package/internal/cell-rewriter.js +520 -0
  49. package/internal/config-spec.js +179 -0
  50. package/internal/cordis-tools-owner.js +363 -0
  51. package/internal/diagnostic.js +133 -0
  52. package/internal/direct-surface-owner.js +326 -0
  53. package/internal/edit-transport-owner.js +287 -0
  54. package/internal/failure-reporting.js +139 -0
  55. package/internal/kernel-worker.js +484 -0
  56. package/internal/module-policy.js +46 -0
  57. package/internal/program-bindings.js +187 -0
  58. package/internal/record-utils.js +53 -0
  59. package/internal/recovery-tips.js +81 -0
  60. package/internal/rejected-cell-editor.js +328 -0
  61. package/internal/repl-convenience.js +201 -0
  62. package/internal/runtime-bridge-owner.js +417 -0
  63. package/internal/runtime-config.js +77 -0
  64. package/internal/runtime-contexts.js +31 -0
  65. package/internal/sdk-projection.js +88 -0
  66. package/internal/session-cell-executor.js +612 -0
  67. package/internal/session-journal.js +889 -0
  68. package/internal/session-log-view.js +80 -0
  69. package/internal/session-runtime.js +536 -0
  70. package/internal/session-state.js +45 -0
  71. package/internal/source-position-map.js +224 -0
  72. package/internal/tool-call-canonicalizer.js +339 -0
  73. package/internal/value-wire.js +421 -0
  74. package/internal/worker-client.js +218 -0
  75. package/internal/worker-cwd-virtualization.js +155 -0
  76. package/package.json +100 -0
  77. package/src/client.js +165 -0
@@ -0,0 +1,201 @@
1
+ import { bindingNodes, createGeneratedNameAllocator } from './binding-pattern.js'
2
+ import { applySourceEdits, createMappedTextBuilder, identitySourceMap } from './source-position-map.js'
3
+ import { SKIP_AST_CHILDREN, walkAst } from './ast-traversal.js'
4
+
5
+ /**
6
+ * Optional compatibility policy for a persistent REPL. The cell language does
7
+ * not own this policy: callers may replace it when persistence uses separate
8
+ * lexical frames instead of one shared REPL environment.
9
+ */
10
+
11
+ function containsTopLevelAwait(node) {
12
+ let found = false
13
+ walkAst(node, current => {
14
+ if (current.type === 'AwaitExpression') found = true
15
+ if (current.type === 'FunctionDeclaration' || current.type === 'FunctionExpression'
16
+ || current.type === 'ArrowFunctionExpression') return SKIP_AST_CHILDREN
17
+ })
18
+ return found
19
+ }
20
+
21
+ function mixedDeclaratorText(statement, declarator, bindings, code, offset, allocateName) {
22
+ const fresh = bindings.filter(binding => !binding.existing)
23
+ const existing = bindings.filter(binding => binding.existing)
24
+ const valueName = allocateName('mixed_value')
25
+ const commitName = allocateName('mixed_commit')
26
+ const valuesName = allocateName('mixed_values')
27
+ const positions = new Map(bindings.map((binding, index) => [binding.name, index]))
28
+ const asynchronous = containsTopLevelAwait(declarator.id)
29
+ const builder = createMappedTextBuilder(code, offset)
30
+ const appendBindingName = binding => builder.appendMapped(binding.name, binding.start, binding.end)
31
+ builder.append(statement.kind === 'var' ? 'var [' : 'let [')
32
+ fresh.forEach((binding, index) => {
33
+ if (index > 0) builder.append(', ')
34
+ appendBindingName(binding)
35
+ })
36
+ builder.append(`] = ${asynchronous ? 'await ' : ''}(`)
37
+ builder.append(`(${asynchronous ? 'async ' : ''}(${valueName}, ${commitName}) => { ${statement.kind} `)
38
+ builder.appendSource(declarator.id.start, declarator.id.end)
39
+ builder.append(` = ${valueName}; return ${commitName}([`)
40
+ bindings.forEach((binding, index) => {
41
+ if (index > 0) builder.append(', ')
42
+ appendBindingName(binding)
43
+ })
44
+ builder.append(`]); })((`)
45
+ if (declarator.init === null) builder.append('undefined')
46
+ else builder.appendSource(declarator.init.start, declarator.init.end)
47
+ builder.append(`), (${valuesName}) => { `)
48
+ existing.forEach((binding, index) => {
49
+ if (index > 0) builder.append(' ')
50
+ appendBindingName(binding)
51
+ builder.append(` = ${valuesName}[${positions.get(binding.name)}];`)
52
+ })
53
+ builder.append(' return [')
54
+ fresh.forEach((binding, index) => {
55
+ if (index > 0) builder.append(', ')
56
+ builder.append(valuesName)
57
+ builder.append(`[${positions.get(binding.name)}]`)
58
+ })
59
+ builder.append(']; }) );')
60
+ return builder.result()
61
+ }
62
+
63
+ function existingDeclaratorText(declarator, code, offset) {
64
+ const builder = createMappedTextBuilder(code, offset)
65
+ builder.append(';(')
66
+ builder.appendSource(declarator.id.start, declarator.id.end)
67
+ builder.append(' = ')
68
+ if (declarator.init === null) builder.append('undefined')
69
+ else builder.appendSource(declarator.init.start, declarator.init.end)
70
+ builder.append(');')
71
+ return builder.result()
72
+ }
73
+
74
+ function freshDeclaratorText(statement, declarator, code, offset) {
75
+ const builder = createMappedTextBuilder(code, offset)
76
+ builder.append(statement.kind === 'var' ? 'var ' : 'let ')
77
+ builder.appendSource(declarator.start, declarator.end)
78
+ builder.append(';')
79
+ return builder.result()
80
+ }
81
+
82
+ function bindingEntries(pattern, declarationSpan) {
83
+ return bindingNodes(pattern).map(node => ({
84
+ name: node.name,
85
+ start: node.start,
86
+ end: node.end,
87
+ span: declarationSpan(node),
88
+ }))
89
+ }
90
+
91
+ export function rewriteLooseRedeclarations({
92
+ code,
93
+ sourceMap = identitySourceMap(code.length),
94
+ body,
95
+ offset,
96
+ knownBindings,
97
+ declarations,
98
+ declarationSpan,
99
+ collisionFor,
100
+ autoSplitRedeclarations,
101
+ }) {
102
+ const redeclared = []
103
+ const rewrites = []
104
+ if (body === undefined) {
105
+ return {
106
+ executableCode: code,
107
+ executableSourceMap: sourceMap,
108
+ collisions: declarations.filter(declaration => knownBindings.has(declaration.name)).map(collisionFor),
109
+ redeclared,
110
+ rewrites,
111
+ }
112
+ }
113
+ const replacements = []
114
+ const rejected = []
115
+ const allocateName = createGeneratedNameAllocator(body)
116
+ for (const statement of body) {
117
+ if (statement.type !== 'VariableDeclaration') {
118
+ if ((statement.type === 'FunctionDeclaration' || statement.type === 'ClassDeclaration')
119
+ && statement.id !== null && knownBindings.has(statement.id.name)) {
120
+ rejected.push({ name: statement.id.name, span: declarationSpan(statement.id) })
121
+ }
122
+ continue
123
+ }
124
+ const entries = []
125
+ let statementRejected = false
126
+ for (const declarator of statement.declarations) {
127
+ const bindings = bindingEntries(declarator.id, declarationSpan)
128
+ const existing = bindings.filter(binding => knownBindings.has(binding.name))
129
+ if (existing.length > 0 && existing.length < bindings.length) {
130
+ if (autoSplitRedeclarations) entries.push({ declarator, bindings, existing })
131
+ else {
132
+ rejected.push(...existing)
133
+ statementRejected = true
134
+ }
135
+ continue
136
+ }
137
+ entries.push({ declarator, bindings, existing })
138
+ }
139
+ if (statementRejected) continue
140
+ if (!entries.some(entry => entry.existing.length > 0)) {
141
+ if (statement.kind === 'const') {
142
+ replacements.push({
143
+ start: statement.start - offset,
144
+ end: statement.start - offset + statement.kind.length,
145
+ text: 'let',
146
+ })
147
+ }
148
+ continue
149
+ }
150
+ let replacementText = ''
151
+ const replacementMappings = []
152
+ const appendPart = (part) => {
153
+ if (replacementText.length > 0) replacementText += '\n'
154
+ const partOffset = replacementText.length
155
+ replacementText += part.text
156
+ part.mappings.forEach(mapping => replacementMappings.push({
157
+ ...mapping,
158
+ generatedStart: mapping.generatedStart + partOffset,
159
+ generatedEnd: mapping.generatedEnd + partOffset,
160
+ }))
161
+ }
162
+ for (const { declarator, bindings, existing } of entries) {
163
+ if (existing.length > 0 && existing.length < bindings.length) {
164
+ appendPart(mixedDeclaratorText(
165
+ statement,
166
+ declarator,
167
+ bindings.map(binding => ({ ...binding, existing: knownBindings.has(binding.name) })),
168
+ code,
169
+ offset,
170
+ allocateName,
171
+ ))
172
+ redeclared.push(...existing.map(collisionFor))
173
+ rewrites.push({
174
+ kind: 'redeclaration',
175
+ description: 'split a mixed top-level declaration while preserving native pattern initialization',
176
+ source: existing.map(binding => binding.name).join(', '),
177
+ })
178
+ } else if (existing.length === bindings.length && bindings.length > 0) {
179
+ redeclared.push(...existing.map(collisionFor))
180
+ appendPart(existingDeclaratorText(declarator, code, offset))
181
+ } else {
182
+ appendPart(freshDeclaratorText(statement, declarator, code, offset))
183
+ }
184
+ }
185
+ replacements.push({
186
+ start: statement.start - offset,
187
+ end: statement.end - offset,
188
+ text: replacementText,
189
+ mappings: replacementMappings,
190
+ })
191
+ }
192
+ const collisions = rejected.map(collisionFor)
193
+ let executableCode = code
194
+ let executableSourceMap = sourceMap
195
+ if (collisions.length === 0) {
196
+ const applied = applySourceEdits(code, sourceMap, replacements)
197
+ executableCode = applied.code
198
+ executableSourceMap = applied.sourceMap
199
+ }
200
+ return { executableCode, executableSourceMap, collisions, redeclared, rewrites }
201
+ }
@@ -0,0 +1,417 @@
1
+ /** Own the CodeRuntime patch, per-cell lease, journal projection, and session settlement. */
2
+ import { AsyncLocalStorage } from 'node:async_hooks'
3
+ import { assertObjectJsonSchema, validateJsonSchemaValue } from '@deepseek-ai/dsh-tools'
4
+ import { normalizeBindingDescriptors } from './binding-descriptors.js'
5
+ import { SessionRuntime } from './session-runtime.js'
6
+ import {
7
+ JOURNAL_KEY,
8
+ RECOVERY_BOUNDARY_KEY,
9
+ journalsEqual,
10
+ recoveryBoundariesEqual,
11
+ withRecoveryBoundaries,
12
+ withJournal,
13
+ withRewrites,
14
+ } from './session-journal.js'
15
+ import {
16
+ capabilityFind,
17
+ capabilityInspect,
18
+ capabilityTree,
19
+ toolCapabilityMetadata,
20
+ } from './program-bindings.js'
21
+ import { deepFreeze } from './record-utils.js'
22
+
23
+ export const RUN_CODE = 'run_code'
24
+
25
+ const EDIT_RUN_CODE = 'edit_run_code'
26
+ const PLUGIN_PROGRAM_GLOBALS = new Set(['capabilities', 'code', 'repl'])
27
+
28
+ function nestedRunCodeArguments(value) {
29
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
30
+ throw new TypeError('code.run expects an object with code and description strings')
31
+ }
32
+ const keys = Reflect.ownKeys(value)
33
+ if (keys.length !== 2 || !keys.includes('code') || !keys.includes('description')
34
+ || keys.some(key => typeof key !== 'string' || !Object.prototype.propertyIsEnumerable.call(value, key))
35
+ || typeof value.code !== 'string' || typeof value.description !== 'string') {
36
+ throw new TypeError('code.run expects exactly code and description string properties')
37
+ }
38
+ return { code: value.code, description: value.description }
39
+ }
40
+
41
+ function namespace(global, functions, errorName, memberNameProperty) {
42
+ return {
43
+ global,
44
+ functions,
45
+ errorClass: { name: errorName, memberNameProperty },
46
+ }
47
+ }
48
+
49
+ function schemaAcceptsEmptyObject(schema) {
50
+ const parameters = schema?.parameters
51
+ try {
52
+ assertObjectJsonSchema(parameters)
53
+ } catch {
54
+ return false
55
+ }
56
+ return validateJsonSchemaValue(parameters, {}).length === 0
57
+ }
58
+
59
+ const PROGRAM_CAPABILITY_METADATA = deepFreeze([
60
+ {
61
+ namespace: 'repl',
62
+ members: [{
63
+ name: 'state',
64
+ description: 'List, save, restore, or delete named durable REPL states.',
65
+ parameters: {
66
+ type: 'object',
67
+ properties: {
68
+ action: { enum: ['list', 'save', 'restore', 'delete'] },
69
+ name: { type: 'string' },
70
+ },
71
+ oneOf: [
72
+ { required: ['action'], properties: { action: { const: 'list' } } },
73
+ { required: ['action', 'name'], properties: { action: { const: 'save' } } },
74
+ { required: ['action'], properties: { action: { const: 'restore' } } },
75
+ { required: ['action', 'name'], properties: { action: { const: 'delete' } } },
76
+ ],
77
+ },
78
+ returns: { type: 'object' },
79
+ effect: 'ptc-state',
80
+ authority: 'ptc-plus-program-binding',
81
+ completeness: 'complete',
82
+ replay: 'recorded-value',
83
+ }],
84
+ },
85
+ {
86
+ namespace: 'code',
87
+ members: [{
88
+ name: 'run',
89
+ description: 'Run isolated source already held as data through the top-level code.run binding; do not use tools.code.run or wrap work that can execute directly in the current cell.',
90
+ parameters: {
91
+ type: 'object',
92
+ additionalProperties: false,
93
+ properties: {
94
+ code: { type: 'string' },
95
+ description: { type: 'string' },
96
+ },
97
+ required: ['code', 'description'],
98
+ },
99
+ returns: {
100
+ type: 'object',
101
+ additionalProperties: false,
102
+ properties: {
103
+ logs: {
104
+ type: 'array',
105
+ items: { type: 'string' },
106
+ description: 'Child console output in emission order.',
107
+ },
108
+ result: {
109
+ description: 'Child return value; omitted when the child returns undefined.',
110
+ },
111
+ },
112
+ required: ['logs'],
113
+ },
114
+ effect: 'unknown',
115
+ authority: 'ptc-plus-program-binding',
116
+ completeness: 'unknown',
117
+ replay: 'recorded-value',
118
+ }],
119
+ },
120
+ ])
121
+
122
+ export function createRuntimeBridgeOwner({
123
+ ctx,
124
+ sessionConfig,
125
+ maxNestedRunCodeDepth,
126
+ sessionId,
127
+ toolSchemasForAgent,
128
+ }) {
129
+ const scope = new AsyncLocalStorage()
130
+ // AgentRegistry.withInitiator is AsyncLocalStorage-based. The arrow preserves
131
+ // its receiver when a worker callback re-enters the host tool pipeline.
132
+ const withInitiator = ctx.agents === undefined || typeof ctx.agents.withInitiator !== 'function'
133
+ ? undefined
134
+ : (agent, operation) => ctx.agents.withInitiator(agent, operation)
135
+ const sessions = new SessionRuntime(sessionConfig, { withInitiator })
136
+ let currentConfig = sessions.config
137
+ const runtime = ctx.codeRuntime
138
+ const ownRun = Object.getOwnPropertyDescriptor(runtime, 'run')
139
+ const upstreamRun = runtime.run
140
+ const patchedDefinitions = new Map()
141
+ const pending = new WeakMap()
142
+ let active = true
143
+
144
+ const projectBindings = (
145
+ request,
146
+ depth,
147
+ executionToken,
148
+ inheritedTools = undefined,
149
+ cellConfig = currentConfig,
150
+ ) => {
151
+ const lease = { active: true }
152
+ const release = () => { lease.active = false }
153
+ const bindingDescriptors = normalizeBindingDescriptors(request.bindings)
154
+ const conflict = bindingDescriptors.namespaces
155
+ .map(binding => binding.global)
156
+ .find(global => PLUGIN_PROGRAM_GLOBALS.has(global))
157
+ if (conflict !== undefined) {
158
+ throw new Error(`ptc-plus: request binding conflicts with reserved program namespace ${JSON.stringify(conflict)}`)
159
+ }
160
+ const toolsNamespace = bindingDescriptors.namespaces.find(binding => binding.global === 'tools')
161
+ const functions = inheritedTools ?? (
162
+ toolsNamespace === undefined ? Object.create(null) : toolsNamespace.functions
163
+ )
164
+ const ensureLease = () => {
165
+ if (!lease.active) throw new Error('PTC execution lease expired')
166
+ }
167
+ const runCode = async (value) => {
168
+ ensureLease()
169
+ const args = nestedRunCodeArguments(value)
170
+ if (depth >= cellConfig.maxNestedRunCodeDepth) {
171
+ throw new RangeError(`code.run recursion depth exceeds configured maximum ${cellConfig.maxNestedRunCodeDepth}`)
172
+ }
173
+ if (typeof functions[RUN_CODE] === 'function') return functions[RUN_CODE](args)
174
+ const childProjected = projectBindings(
175
+ { ...request, program: args.code }, depth + 1, executionToken, functions, cellConfig,
176
+ )
177
+ let child
178
+ try {
179
+ child = await upstreamRun.call(runtime, childProjected.request)
180
+ } finally {
181
+ childProjected.release()
182
+ }
183
+ if (child.error !== undefined) {
184
+ throw new Error(`nested run_code failed (${child.error.kind}): ${child.error.message}`)
185
+ }
186
+ return { logs: child.logs, ...(child.value === undefined ? {} : { result: child.value }) }
187
+ }
188
+
189
+ const schemas = toolSchemasForAgent(executionToken?.agent)
190
+ .filter(schema => schema?.name === RUN_CODE
191
+ || (schema?.name !== EDIT_RUN_CODE && typeof functions[schema?.name] === 'function'))
192
+ const emptyObjectTools = new Set(schemas.flatMap(schema => (
193
+ typeof schema?.name === 'string' && schemaAcceptsEmptyObject(schema)
194
+ ? [schema.name]
195
+ : []
196
+ )))
197
+ const projected = bindingDescriptors.namespaces.map((binding) => {
198
+ const wrapped = Object.create(null)
199
+ for (const key of binding.members) {
200
+ Object.defineProperty(wrapped, key, {
201
+ enumerable: true,
202
+ value: async (...args) => {
203
+ ensureLease()
204
+ return binding.functions[key](...args)
205
+ },
206
+ })
207
+ }
208
+ const emptyObjectMembers = binding.global === 'tools'
209
+ ? binding.members.filter(member => emptyObjectTools.has(member))
210
+ : []
211
+ return {
212
+ ...binding,
213
+ functions: wrapped,
214
+ ...(binding.global === 'tools' ? { emptyObjectMembers } : {}),
215
+ }
216
+ })
217
+ const annotations = Object.fromEntries(schemas.flatMap(schema => (
218
+ typeof schema?.name === 'string' && schema.name !== RUN_CODE
219
+ ? [[schema.name, { replay: 'recorded-value' }]]
220
+ : []
221
+ )))
222
+ const metadata = Object.freeze([
223
+ ...toolCapabilityMetadata(schemas, annotations),
224
+ ...PROGRAM_CAPABILITY_METADATA,
225
+ ])
226
+ projected.push(namespace('capabilities', {
227
+ tree: async value => {
228
+ if (value !== undefined) throw new TypeError('capabilities.tree does not accept arguments')
229
+ ensureLease()
230
+ return capabilityTree(metadata)
231
+ },
232
+ find: async value => {
233
+ ensureLease()
234
+ if (typeof value !== 'string') throw new TypeError('capabilities.find expects a query string')
235
+ return capabilityFind(metadata, value)
236
+ },
237
+ inspect: async value => {
238
+ ensureLease()
239
+ if (value === undefined) return capabilityInspect(metadata)
240
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
241
+ throw new TypeError('capabilities.inspect expects an object')
242
+ }
243
+ return capabilityInspect(metadata, value.symbols, value.budget)
244
+ },
245
+ }, 'CapabilityExplorationError', 'operation'))
246
+ projected.push(namespace('code', { run: runCode }, 'CodeExecutionError', 'operation'))
247
+ return { request: { ...request, bindings: projected }, release }
248
+ }
249
+
250
+ const patchResultMetadata = (agent) => {
251
+ const definition = ctx.tools.get(RUN_CODE, agent)
252
+ if (definition === undefined) {
253
+ throw new Error('ptc-plus: run_code definition is unavailable for the owning session')
254
+ }
255
+ if (definition.output === undefined) {
256
+ throw new Error('ptc-plus: run_code definition has no output projection')
257
+ }
258
+ if (patchedDefinitions.has(definition)) return
259
+ const output = definition.output
260
+ const original = output.presentationMeta
261
+ const patched = (args, value) => {
262
+ if (!active) return original === undefined ? undefined : original(args, value)
263
+ const base = original === undefined ? undefined : original(args, value)
264
+ const current = scope.getStore()
265
+ const settlement = current?.settlement
266
+ if (settlement === undefined) return base
267
+ let meta = withJournal(base, settlement.journal)
268
+ if (settlement.recoveryBoundaries !== undefined) {
269
+ meta = withRecoveryBoundaries(meta, settlement.recoveryBoundaries)
270
+ }
271
+ return settlement.rewrites === undefined ? meta : withRewrites(meta, settlement.rewrites)
272
+ }
273
+ try {
274
+ Object.defineProperty(output, 'presentationMeta', {
275
+ configurable: true,
276
+ enumerable: true,
277
+ writable: true,
278
+ value: patched,
279
+ })
280
+ patchedDefinitions.set(definition, { output, original, patched })
281
+ } catch (error) {
282
+ throw new Error(`ptc-plus: cannot attach the session journal to run_code results: ${error.message}`)
283
+ }
284
+ }
285
+
286
+ const patchedRun = function (request) {
287
+ if (!active) return upstreamRun.call(runtime, request)
288
+ const current = scope.getStore()
289
+ if (current === undefined) return upstreamRun.call(runtime, request)
290
+ const projected = projectBindings(request, 0, current)
291
+ return sessions.runTentative(current, { ...projected.request, executionToken: current })
292
+ .then((execution) => {
293
+ current.settlement = execution.settlement
294
+ return execution.result
295
+ })
296
+ .finally(projected.release)
297
+ }
298
+
299
+ Object.defineProperty(runtime, 'run', {
300
+ configurable: true,
301
+ writable: true,
302
+ value: patchedRun,
303
+ })
304
+
305
+ return Object.freeze({
306
+ config: sessions.config,
307
+ reconfigure(nextConfig) {
308
+ sessions.reconfigure(nextConfig)
309
+ currentConfig = sessions.config
310
+ },
311
+ // A composite tool's outer result owns the final durability decision.
312
+ async executeTentative(callSeq, operation) {
313
+ const settlement = {
314
+ current: undefined,
315
+ innerConfirmed: false,
316
+ finalized: false,
317
+ }
318
+ const finalize = (outerConfirmed) => {
319
+ if (settlement.finalized) return
320
+ settlement.finalized = true
321
+ if (settlement.current?.settlement !== undefined) {
322
+ sessions.finalize(settlement.current.settlement, outerConfirmed && settlement.innerConfirmed)
323
+ }
324
+ }
325
+ try {
326
+ const result = await scope.run({
327
+ ...(callSeq === undefined ? {} : { persistedCallSeq: callSeq }),
328
+ deferredSettlement: settlement,
329
+ }, operation)
330
+ return Object.freeze({ result, finalize })
331
+ } catch (error) {
332
+ finalize(false)
333
+ throw error
334
+ }
335
+ },
336
+ handleExecute(exec, next) {
337
+ if (exec.parent !== undefined) return scope.run(undefined, next)
338
+ const id = sessionId(exec.agent)
339
+ if (id === undefined) return next()
340
+ patchResultMetadata(exec.agent)
341
+ const inherited = scope.getStore()
342
+ const persistedCallSeq = inherited?.persistedCallSeq
343
+ const current = {
344
+ id,
345
+ callId: String(exec.callId),
346
+ ...(persistedCallSeq === undefined ? {} : { persistedCallSeq }),
347
+ ...(inherited?.deferredSettlement === undefined
348
+ ? {}
349
+ : { deferredSettlement: inherited.deferredSettlement }),
350
+ session: exec.agent?.session,
351
+ agent: exec.agent,
352
+ }
353
+ if (current.deferredSettlement !== undefined) {
354
+ current.deferredSettlement.current = current
355
+ }
356
+ pending.set(exec, current)
357
+ return scope.run(current, async () => {
358
+ const result = await next()
359
+ const settlement = current.settlement
360
+ if (result?.isError === true && settlement !== undefined) {
361
+ let meta = withJournal(result.meta, settlement.journal)
362
+ if (settlement.recoveryBoundaries !== undefined) {
363
+ meta = withRecoveryBoundaries(meta, settlement.recoveryBoundaries)
364
+ }
365
+ if (settlement.rewrites !== undefined) meta = withRewrites(meta, settlement.rewrites)
366
+ return { ...result, meta }
367
+ }
368
+ return result
369
+ })
370
+ },
371
+ handleResult(exec, result) {
372
+ if (exec.parent !== undefined) return
373
+ const id = sessionId(exec.agent)
374
+ if (id === undefined) return
375
+ const current = pending.get(exec)
376
+ pending.delete(exec)
377
+ const settlement = current?.settlement
378
+ if (settlement === undefined) {
379
+ sessions.noteNoop(id, exec.agent?.session, exec.callId)
380
+ return
381
+ }
382
+ const meta = result?.meta
383
+ const confirmed = meta !== null && typeof meta === 'object' && !Array.isArray(meta)
384
+ && Object.hasOwn(meta, JOURNAL_KEY)
385
+ && journalsEqual(meta[JOURNAL_KEY], settlement.journal)
386
+ && recoveryBoundariesEqual(
387
+ Object.hasOwn(meta, RECOVERY_BOUNDARY_KEY) ? meta[RECOVERY_BOUNDARY_KEY] : undefined,
388
+ settlement.recoveryBoundaries,
389
+ )
390
+ if (current.deferredSettlement !== undefined) {
391
+ current.deferredSettlement.innerConfirmed = confirmed
392
+ return
393
+ }
394
+ sessions.finalize(settlement, confirmed)
395
+ },
396
+ disposeAgent(agent) {
397
+ return sessions.disposeSession(sessionId(agent) ?? String(agent.id))
398
+ },
399
+ disposeSession(session) {
400
+ return sessions.disposeSession(String(session.id))
401
+ },
402
+ async dispose() {
403
+ active = false
404
+ if (runtime.run === patchedRun) {
405
+ if (ownRun === undefined) delete runtime.run
406
+ else Object.defineProperty(runtime, 'run', ownRun)
407
+ }
408
+ for (const { output, original, patched } of patchedDefinitions.values()) {
409
+ if (output.presentationMeta !== patched) continue
410
+ if (original === undefined) delete output.presentationMeta
411
+ else output.presentationMeta = original
412
+ }
413
+ patchedDefinitions.clear()
414
+ await sessions.dispose()
415
+ },
416
+ })
417
+ }
@@ -0,0 +1,77 @@
1
+ import {
2
+ CONFIG_DEFAULTS,
3
+ MAX_TIMER_DELAY_MS,
4
+ } from './config-spec.js'
5
+
6
+ export { MAX_TIMER_DELAY_MS }
7
+
8
+ /** Positive-safe-integer limits applied to runtime budget fields. */
9
+ const BUDGET_KEYS = Object.freeze([
10
+ 'computeMs',
11
+ 'maxOutputBytes',
12
+ 'maxOldGenerationSizeMb',
13
+ 'maxValueNodes',
14
+ 'maxValueEdges',
15
+ 'maxValueArrayLength',
16
+ 'maxValueBigIntDigits',
17
+ ])
18
+
19
+ /** Boolean runtime behavior switches. */
20
+ const BOOLEAN_KEYS = Object.freeze([
21
+ 'cordisToolsEnabled',
22
+ 'canonicalizeToolCalls',
23
+ 'looseTopLevelRedeclarations',
24
+ 'durableReplay',
25
+ 'autoRewriteImports',
26
+ 'autoStripExports',
27
+ 'autoSplitRedeclarations',
28
+ 'tipsEnabled',
29
+ ])
30
+
31
+ /** Positive-safe-integer field groups that validate like budgets. */
32
+ const POSITIVE_KEYS = Object.freeze([
33
+ 'maxNestedRunCodeDepth',
34
+ 'tipCooldownMessages',
35
+ 'tipEscalationFailures',
36
+ ])
37
+
38
+ /**
39
+ * Resolve plugin/runtime config from raw input, applying the shared defaults
40
+ * and the same validation the Host Config schema exposes.
41
+ * @param config - raw config object; unknown keys are preserved for forwards compatibility.
42
+ * @returns a resolved config object.
43
+ */
44
+ export function resolveConfig(config = {}) {
45
+ const resolved = { ...CONFIG_DEFAULTS, ...config }
46
+ if (typeof resolved.enabled !== 'boolean') {
47
+ throw new TypeError('ptc-plus: enabled must be a boolean')
48
+ }
49
+ for (const key of [...BUDGET_KEYS, ...POSITIVE_KEYS]) {
50
+ const value = resolved[key]
51
+ if (!Number.isSafeInteger(value) || value < 1) {
52
+ throw new TypeError(`ptc-plus: ${key} must be a positive safe integer`)
53
+ }
54
+ }
55
+ validateMaxWallMs(resolved.maxWallMs)
56
+ for (const key of BOOLEAN_KEYS) {
57
+ if (typeof resolved[key] !== 'boolean') {
58
+ throw new TypeError(`ptc-plus: ${key} must be a boolean`)
59
+ }
60
+ }
61
+ return resolved
62
+ }
63
+
64
+ /**
65
+ * Validate one wall-clock ceiling against the platform timer limit.
66
+ * @param value - the configured maximum wall time in milliseconds.
67
+ * @returns the validated value.
68
+ */
69
+ export function validateMaxWallMs(value) {
70
+ if (!Number.isSafeInteger(value) || value < 1) {
71
+ throw new TypeError('ptc-plus: maxWallMs must be a positive safe integer')
72
+ }
73
+ if (value > MAX_TIMER_DELAY_MS) {
74
+ throw new TypeError(`ptc-plus: maxWallMs must not exceed ${MAX_TIMER_DELAY_MS}`)
75
+ }
76
+ return value
77
+ }