dsh-ptc-plus 0.4.2 → 0.4.8

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 (89) hide show
  1. package/README.en.md +170 -76
  2. package/README.md +171 -77
  3. package/assets/dsh-ptc-plus-banner-en.webp +0 -0
  4. package/assets/dsh-ptc-plus-banner-zh.webp +0 -0
  5. package/assets/ptc-plus-icon.svg +5 -0
  6. package/client.js +599 -310
  7. package/compiler-core.cjs +162 -160
  8. package/compiler-core.cjs.map +3 -3
  9. package/docs/README.md +4 -1
  10. package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +49 -11
  11. package/docs/adr/0008-define-agent-operating-contract.md +2 -2
  12. package/docs/adr/0010-session-log-derived-recovery-tips.md +7 -0
  13. package/docs/adr/0016-archive-resolved-review-findings.md +3 -3
  14. package/docs/adr/0017-track-the-latest-dsh-public-surface.md +5 -1
  15. package/docs/adr/0019-plugin-settings-and-kill-switch.md +3 -3
  16. package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +3 -1
  17. package/docs/adr/0021-separate-host-bootstrap-from-session-runtime.md +14 -3
  18. package/docs/adr/0023-global-user-bindings.md +1 -1
  19. package/docs/adr/0026-persist-the-delivery-verdict-against-the-verified-candidate.md +23 -7
  20. package/docs/adr/0027-run-the-session-kernel-in-a-killable-helper-process.md +4 -0
  21. package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +26 -6
  22. package/docs/adr/0030-serve-both-typert-codec-generations-from-one-codec.md +2 -2
  23. package/docs/adr/0031-close-semantic-obligations-by-owner.md +29 -0
  24. package/docs/adr/README.md +4 -4
  25. package/docs/architecture.md +8 -5
  26. package/docs/binding-review-placement-design.md +1 -1
  27. package/docs/client-ui.md +8 -4
  28. package/docs/durability-design.md +7 -7
  29. package/docs/installation.md +48 -12
  30. package/docs/publishing.md +4 -4
  31. package/docs/release-notes.md +109 -0
  32. package/docs/runtime-reference.md +25 -12
  33. package/docs/semantic-validation.md +7 -3
  34. package/docs/user-bindings.en.md +7 -6
  35. package/docs/user-bindings.md +6 -5
  36. package/docs/value-wire.md +1 -1
  37. package/docs/verification.md +43 -2
  38. package/index.js +39 -34
  39. package/internal/async-local-storage-intrinsics.js +12 -7
  40. package/internal/callable-source-facts.js +37 -1
  41. package/internal/cell-analysis.js +64 -9
  42. package/internal/config-spec.js +1 -1
  43. package/internal/cordis-tools-owner.js +81 -15
  44. package/internal/direct-surface-owner.js +331 -7
  45. package/internal/edit-transport-owner.js +80 -20
  46. package/internal/execution-seam-compat.js +192 -30
  47. package/internal/kernel-worker.js +162 -72
  48. package/internal/message-sources.js +14 -0
  49. package/internal/module-compilation.js +2 -1
  50. package/internal/rejected-cell-editor.js +11 -21
  51. package/internal/repl-memory-projection.js +10 -0
  52. package/internal/repl-scope-normalizer.js +72 -35
  53. package/internal/repl-value-observer.js +141 -45
  54. package/internal/runtime-config.js +62 -2
  55. package/internal/runtime-intrinsics.js +3 -0
  56. package/internal/runtime-messages.js +6 -4
  57. package/internal/session-cell-executor.js +24 -1
  58. package/internal/session-journal-recovery.js +267 -67
  59. package/internal/session-journal.js +466 -40
  60. package/internal/session-log-view.js +5 -4
  61. package/internal/session-runtime.js +45 -21
  62. package/internal/session-state.js +7 -29
  63. package/internal/settings-compat.js +66 -1
  64. package/internal/source-position-map.js +19 -0
  65. package/internal/user-binding-console-worker.js +23 -9
  66. package/internal/user-binding-draft-projection.js +3 -2
  67. package/internal/user-bindings-owner.js +2 -1
  68. package/internal/user-bindings.js +6 -1
  69. package/internal/validated-parse-repair.js +6 -2
  70. package/internal/value-wire.js +95 -30
  71. package/internal/worker-client.js +99 -8
  72. package/internal/worker-cwd-virtualization.js +10 -9
  73. package/internal/worker-output-capture.js +187 -0
  74. package/internal/worker-output-fence.js +26 -0
  75. package/internal/worker-realm-surfaces.js +197 -0
  76. package/internal/worker-repl-realm.js +10 -4
  77. package/package.json +42 -4
  78. package/src/client-activity.js +4 -1
  79. package/src/client-authoring-view.js +121 -62
  80. package/src/client-console.js +4 -2
  81. package/src/client-copy.js +4 -2
  82. package/src/client-feature-gates.js +7 -9
  83. package/src/client-host-compat.js +104 -27
  84. package/src/client-repl-view.js +3 -1
  85. package/src/client-styles.js +12 -1
  86. package/src/client-tool-view.js +3 -1
  87. package/src/client-transport.js +2 -2
  88. package/src/client-workbench.js +10 -7
  89. package/src/client.js +56 -17
@@ -43,6 +43,16 @@ function callableSourceStart(source, node) {
43
43
  return start + tokens.getToken().start
44
44
  }
45
45
 
46
+ function removeCallableSourceMarks(source, prefix, suffix, sources, options) {
47
+ const tree = parse(prefix + source + suffix, options)
48
+ const markers = new Set(sources.entries.map(entry => entry.marker))
49
+ const offset = prefix.length
50
+ const edits = tree.comments.filter(comment => markers.has(comment.value)
51
+ && comment.start >= offset && comment.end <= offset + source.length)
52
+ .map(comment => ({ start: comment.start - offset, end: comment.end - offset, text: '' }))
53
+ return edits.length === 0 ? source : applySourceEdits(source, identitySourceMap(source.length), edits).code
54
+ }
55
+
46
56
  export function callableMarkerPosition(node, source, comments) {
47
57
  if (node.type !== 'ArrowFunctionExpression') return node.body.start + 1
48
58
  let start = source.lastIndexOf('=>', node.body.start - 1)
@@ -129,6 +139,7 @@ export function markCallableSources(source, sourceMap = identitySourceMap(source
129
139
  const nativeFunctions = typedRecords.map(record => markedNodes.get(record.marker))
130
140
  .filter(node => t.isFunction(node) && !isMethod(node)).sort((left, right) => left.start - right.start)
131
141
  const nativeRecipes = new Map()
142
+ const aliasMarkers = []
132
143
  const nativeFunctionIndex = start => {
133
144
  let low = 0, high = nativeFunctions.length
134
145
  while (low < high) {
@@ -175,7 +186,17 @@ export function markCallableSources(source, sourceMap = identitySourceMap(source
175
186
  original = lowered.source
176
187
  const entries = [...originals].map(([key, text]) => [key, key === marker ? original : text])
177
188
  for (const [node, fact] of markedCallableOwners(lowered.emitted, entries)) {
178
- sources.aliases.push([lowered.emittedSource.slice(node.start, node.end), fact.original])
189
+ const generated = lowered.emittedSource.slice(node.start, node.end)
190
+ aliasMarkers.push([generated, fact.marker])
191
+ const generatedMethod = isMethod(node)
192
+ const markerFree = removeCallableSourceMarks(
193
+ generated,
194
+ generatedMethod ? 'class Source {' : '(',
195
+ generatedMethod ? '}' : ')',
196
+ sources,
197
+ options,
198
+ )
199
+ if (markerFree !== generated) aliasMarkers.push([markerFree, fact.marker])
179
200
  }
180
201
  }
181
202
  // Prefer source-preserving erasure. Runtime TypeScript constructs such as
@@ -202,6 +223,7 @@ export function markCallableSources(source, sourceMap = identitySourceMap(source
202
223
  } catch {
203
224
  lowerCallable()
204
225
  }
226
+ original = removeCallableSourceMarks(original, prefix, suffix, sources, options)
205
227
  entry.buffer = sources.buffers.length
206
228
  sources.buffers.push(original)
207
229
  entry.start = 0
@@ -209,6 +231,8 @@ export function markCallableSources(source, sourceMap = identitySourceMap(source
209
231
  originals.set(marker, original)
210
232
  if (!method && t.isFunction(node)) nativeRecipes.set(node, { node, start, end: node.end, text: original })
211
233
  }
234
+ const finalFacts = factMap(sources)
235
+ for (const [generated, marker] of aliasMarkers) sources.aliases.push([generated, originalText(finalFacts.get(marker))])
212
236
  return { ...marked, callableSources: sources, nativeJavaScript }
213
237
  }
214
238
 
@@ -273,6 +297,18 @@ export function collectCallableSources(code, sources, parserOptions = {}, transf
273
297
  }).concat(sources.aliases ?? [])
274
298
  }
275
299
 
300
+ /** Restore only callable marks whose transformed starts have exact source-map ownership. */
301
+ export function restoreMappedCallableSourceMarks(mapped, input, transformation, sources, parserOptions = {}) {
302
+ if (sources === undefined || sources.length === 0) return mapped
303
+ const { tree, owners } = callableSourceOwners(mapped.code, sources, parserOptions,
304
+ { code: input, map: transformation.map })
305
+ const edits = [...owners].map(([node, fact]) => {
306
+ const start = callableMarkerPosition(node, mapped.code, tree.comments)
307
+ return { start, end: start, text: `/*${fact.marker}*/` }
308
+ })
309
+ return edits.length === 0 ? mapped : applySourceEdits(mapped.code, mapped.sourceMap, edits)
310
+ }
311
+
276
312
  /** Emission ranges come from typed nodes and exact generator mappings. */
277
313
  export function regionCallableRanges(input, generated, sourceRanges, parserOptions) {
278
314
  if (!sourceRanges?.length) return []
@@ -20,7 +20,7 @@ import {
20
20
  import { applySourceEdits, mapSourcePosition, mapSourceSpan, sourceRangeHasOriginalText } from './source-position-map.js'
21
21
  import { SKIP_AST_CHILDREN, walkAst } from './ast-traversal.js'
22
22
  import { compileStatefulRoot } from './stateful-root-compiler.js'
23
- import { CELL_PARSER_PLUGINS, bindCompilerIntrinsics, lowerStatefulDecorators, lowerStatefulResources, lowerNativeLanguageSource, normalizeStatefulScopes } from './repl-scope-normalizer.js'
23
+ import { CELL_PARSER_PLUGINS, bindCompilerIntrinsics, lowerStatefulDecorators, lowerStatefulResources, lowerNativeLanguageSource, normalizationFailureInput, normalizeStatefulScopes } from './repl-scope-normalizer.js'
24
24
  import { markCallableSources, collectCallableSources } from './callable-source-facts.js'
25
25
  import { LegacyPreflightError, prepareLegacyProgram } from './legacy-cell-analysis.js'
26
26
  import { adaptLegacyModuleImports } from './managed-module-operations.js'
@@ -220,6 +220,49 @@ function isFunction(node) {
220
220
  || node.type === 'ArrowFunctionExpression'
221
221
  }
222
222
 
223
+ function lowerCellImportMeta(result, importMeta, originalSource) {
224
+ if (!result.code.includes('import')) return result
225
+ const tree = parseSource(result.code, {
226
+ sourceType: 'unambiguous',
227
+ errorRecovery: true,
228
+ allowAwaitOutsideFunction: true,
229
+ allowImportExportEverywhere: true,
230
+ allowReturnOutsideFunction: true,
231
+ allowUndeclaredExports: true,
232
+ plugins: CELL_PARSER_PLUGINS,
233
+ }).program
234
+ const references = []
235
+ walkAst(tree, (node) => {
236
+ if (node.type === 'MetaProperty' && node.meta?.name === 'import' && node.property?.name === 'meta') {
237
+ references.push(node)
238
+ }
239
+ })
240
+ if (references.length === 0) return result
241
+ if (importMeta === null || typeof importMeta !== 'object'
242
+ || typeof importMeta.url !== 'string' || typeof importMeta.filename !== 'string'
243
+ || typeof importMeta.dirname !== 'string') {
244
+ const node = references[0]
245
+ throw new ModuleRewriteError('import.meta requires the session module base', mapSourcePosition({
246
+ line: node.loc.start.line,
247
+ column: node.loc.start.column + 1,
248
+ }, result.code, originalSource, result.sourceMap))
249
+ }
250
+ const binding = createGeneratedNameAllocator(tree)('import_meta')
251
+ const directiveEnd = tree.directives.at(-1)?.end ?? tree.interpreter?.end ?? 0
252
+ const declaration = `\nconst ${binding}={__proto__:null,url:${JSON.stringify(importMeta.url)},filename:${JSON.stringify(importMeta.filename)},dirname:${JSON.stringify(importMeta.dirname)}};\n`
253
+ const edits = [
254
+ { start: directiveEnd, end: directiveEnd, text: declaration },
255
+ ...references.map(node => ({
256
+ start: node.start,
257
+ end: node.end,
258
+ text: binding,
259
+ mappings: [{ generatedStart: 0, generatedEnd: binding.length,
260
+ originalStart: node.start, originalEnd: node.end }],
261
+ })),
262
+ ]
263
+ return { ...result, ...applySourceEdits(result.code, result.sourceMap, edits), importMetaBinding: binding }
264
+ }
265
+
223
266
 
224
267
  function staticModuleClassification(moduleLoads) {
225
268
  const reasons = new Map()
@@ -282,14 +325,16 @@ export function classifyDurability(code, knownBindings = new Set(), {
282
325
  const reasons = new Map()
283
326
  const addReason = reason => reasons.set(renderDurabilityReason(reason), reason)
284
327
  for (const reason of staticModuleClassification(moduleLoads)) addReason(reason)
285
- const classifyModule = (source) => {
328
+ const classifyModule = (source, form) => {
286
329
  if (source?.type !== 'Literal' || typeof source.value !== 'string') {
287
330
  addReason(DYNAMIC_MODULE_REASON)
288
331
  return
289
332
  }
290
333
  const classification = classifyModuleSource(source.value)
291
334
  if (classification.status === 'forbidden') {
292
- throw new PreflightError(`cell import of ${source.value} is forbidden because it exposes kernel control`, source)
335
+ const span = sourceMap === undefined || originalSource === undefined ? declarationSpan(source)
336
+ : mapSourceSpan(declarationSpan(source), code, originalSource, sourceMap)
337
+ throw new PreflightError(`cell ${form} of ${source.value} is forbidden because it exposes kernel control`, undefined, span)
293
338
  }
294
339
  if (classification.reason !== undefined) addReason(classification.reason)
295
340
  }
@@ -327,7 +372,7 @@ export function classifyDurability(code, knownBindings = new Set(), {
327
372
  nestedScopes = [...scopes, loopBindings(node)]
328
373
  }
329
374
  if (node.type === 'ImportExpression') {
330
- classifyModule(node.source)
375
+ classifyModule(node.source, 'import')
331
376
  }
332
377
  const sourceOwnedThis = sourceMap === undefined
333
378
  || sourceRangeHasOriginalText(sourceMap, code, originalSource, node.start, node.end)
@@ -336,7 +381,7 @@ export function classifyDurability(code, knownBindings = new Set(), {
336
381
  }
337
382
  if (node.type === 'CallExpression' && node.callee?.type === 'Identifier' && node.callee.name === 'require'
338
383
  && !isBound('require', nestedScopes)) {
339
- classifyModule(node.arguments[0])
384
+ classifyModule(node.arguments[0], 'require')
340
385
  }
341
386
  if (node.type === 'Identifier' && isReferenceIdentifier(node, parent, parentKey)
342
387
  && !isBound(node.name, nestedScopes) && AMBIENT_GLOBALS.has(node.name)
@@ -462,21 +507,31 @@ export function prepareProgram(program, options = {}) {
462
507
  let normalized
463
508
  let prepared
464
509
  let callableSources
510
+ let diagnosticInput
465
511
  const intrinsicContext = { bindings: new Set() }
466
512
  try {
467
513
  const marked = markCallableSources(program, undefined, { plugins: CELL_PARSER_PLUGINS, allowImportExportEverywhere: true, allowUndeclaredExports: true },
468
514
  { lowerNativeSource: lowerNativeLanguageSource, nativeUsing: options.nativeUsing })
515
+ const moduleMetadata = lowerCellImportMeta(marked, options.importMeta, program)
516
+ diagnosticInput = moduleMetadata
469
517
  callableSources = marked.callableSources
470
- normalized = normalizeStatefulScopes(marked.code, marked.sourceMap, { mode: options.languageSemantics,
471
- nativeJavaScript: marked.nativeJavaScript, deferDecorators: true, deferResources: true, intrinsicContext })
518
+ normalized = normalizeStatefulScopes(moduleMetadata.code, moduleMetadata.sourceMap, { mode: options.languageSemantics,
519
+ callableSources, nativeJavaScript: marked.nativeJavaScript,
520
+ deferDecorators: true, deferResources: true, intrinsicContext })
521
+ if (moduleMetadata.importMetaBinding !== undefined) {
522
+ normalized.internalBindings = new Set(normalized.internalBindings)
523
+ normalized.internalBindings.add(moduleMetadata.importMetaBinding)
524
+ }
525
+ diagnosticInput = normalized
472
526
  prepared = compileStatefulRoot(normalized.code, { ...options, sourceMap: normalized.sourceMap,
473
527
  internalBindings: normalized.internalBindings, dynamicBindings: normalized.dynamicBindings,
474
528
  privateBindings: normalized.privateBindings, originalSource: program })
475
529
  } catch (error) {
476
530
  if (error instanceof ModuleRewriteError) throw error
477
531
  const position = error.loc === undefined ? undefined : { line: error.loc.line, column: error.loc.column + 1 }
478
- throw new ModuleRewriteError(error.message, normalized === undefined ? position
479
- : mapSourcePosition(position, normalized.code, program, normalized.sourceMap))
532
+ const failureInput = normalizationFailureInput(error) ?? diagnosticInput
533
+ throw new ModuleRewriteError(error.message, failureInput === undefined ? position
534
+ : mapSourcePosition(position, failureInput.code, program, failureInput.sourceMap))
480
535
  }
481
536
  prepared = { ...prepared, deferredHelpers: normalized.deferredHelpers }
482
537
  intrinsicContext.expression = `this[${JSON.stringify(prepared.rootRuntimeName)}].intrinsics`
@@ -96,7 +96,7 @@ export const CONFIG_FIELDS = Object.freeze([
96
96
  {
97
97
  key: 'userBindingsEnabled',
98
98
  type: 'boolean',
99
- default: false,
99
+ default: true,
100
100
  label: '启用全局用户 Binding',
101
101
  labelEn: 'Enable Global User Bindings',
102
102
  description: '管理 TypeScript helper,并将已启用条目作为跨会话的默认 REPL 绑定。',
@@ -1,5 +1,7 @@
1
1
  import * as CordisTools from '@deepseek-ai/dsh-tool-cordis'
2
2
  import * as CordisSkillFilesystem from '@deepseek-ai/dsh-skill-filesystem'
3
+ import { existsSync } from 'node:fs'
4
+ import { createRequire } from 'node:module'
3
5
  import { dirname, isAbsolute, join } from 'node:path'
4
6
  import { isDeepStrictEqual } from 'node:util'
5
7
  import { RUN_CODE } from './runtime-bridge-owner.js'
@@ -8,6 +10,16 @@ const CORDIS_PRESET_ID = 'cordis'
8
10
  const CORDIS_SKILL_NAME = 'cordis-plugin-development'
9
11
  const CORDIS_SKILL_PROVIDER = 'ptc-plus-cordis'
10
12
  const COMPANION_SKILL_TOOL = 'skill'
13
+ const COMPANION_PACKAGE_LAYOUTS = Object.freeze([
14
+ Object.freeze({
15
+ name: '@deepseek-ai/dsh-agent-preset',
16
+ skillRoot: directory => join(directory, 'skills'),
17
+ }),
18
+ Object.freeze({
19
+ name: '@deepseek-ai/dsh-agent-presets',
20
+ skillRoot: directory => join(directory, 'presets', CORDIS_PRESET_ID, 'skills'),
21
+ }),
22
+ ])
11
23
 
12
24
  function exactCompanionCandidates(observation) {
13
25
  if (Array.isArray(observation)) {
@@ -131,15 +143,51 @@ function requireCompanionServices(agent) {
131
143
  return { agentPresets, skills }
132
144
  }
133
145
 
134
- async function companionSkillDirectory(agentPresets) {
146
+ function containsCompanionSkill(directory, fileExists) {
147
+ return fileExists(join(directory, CORDIS_SKILL_NAME, 'SKILL.md'))
148
+ }
149
+
150
+ /** Resolve the official Cordis companion Skill across DSH preset generations. */
151
+ export async function resolveCompanionSkillDirectory(agentPresets, options = {}) {
152
+ const fileExists = options.fileExists ?? existsSync
135
153
  const preset = await agentPresets.resolve(CORDIS_PRESET_ID)
136
- if (typeof preset?.path !== 'string' || !isAbsolute(preset.path)) {
137
- throw new Error('ptc-plus: DSH cordis preset did not expose an absolute composition path')
138
- }
139
- if (typeof preset.broken === 'string' && preset.broken.length > 0) {
154
+ if (typeof preset?.broken === 'string' && preset.broken.length > 0) {
140
155
  throw new Error(`ptc-plus: DSH cordis preset is unavailable: ${preset.broken}`)
141
156
  }
142
- return join(dirname(preset.path), 'skills')
157
+ if (preset?.path !== undefined) {
158
+ if (typeof preset.path !== 'string' || !isAbsolute(preset.path)) {
159
+ throw new Error('ptc-plus: DSH cordis preset did not expose an absolute composition path')
160
+ }
161
+ const directory = join(dirname(preset.path), 'skills')
162
+ if (!containsCompanionSkill(directory, fileExists)) {
163
+ throw new Error(`ptc-plus: DSH cordis preset does not contain ${CORDIS_SKILL_NAME}/SKILL.md beside its composition`)
164
+ }
165
+ return directory
166
+ }
167
+
168
+ for (const layout of COMPANION_PACKAGE_LAYOUTS) {
169
+ const pkg = options.packageOf?.(layout.name, options.packageBaseUrl)
170
+ if (pkg === undefined) continue
171
+ if (pkg?.name !== layout.name || typeof pkg.dir !== 'string' || !isAbsolute(pkg.dir)) {
172
+ throw new Error(`ptc-plus: DSH pluginPackages returned an invalid ${layout.name} package resource`)
173
+ }
174
+ const directory = layout.skillRoot(pkg.dir)
175
+ if (containsCompanionSkill(directory, fileExists)) return directory
176
+ }
177
+
178
+ if (options.packageOf === undefined && options.resolveLegacyPackageManifest !== undefined) {
179
+ let manifest
180
+ try {
181
+ manifest = options.resolveLegacyPackageManifest('@deepseek-ai/dsh-agent-presets/package.json')
182
+ } catch (error) {
183
+ if (error?.code !== 'MODULE_NOT_FOUND' && error?.code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED') throw error
184
+ }
185
+ if (manifest !== undefined) {
186
+ const directory = join(dirname(manifest), 'presets', CORDIS_PRESET_ID, 'skills')
187
+ if (containsCompanionSkill(directory, fileExists)) return directory
188
+ }
189
+ }
190
+ throw new Error(`ptc-plus: the active DSH installation does not publish a loadable ${CORDIS_SKILL_NAME} companion Skill through its package-resolution surface`)
143
191
  }
144
192
 
145
193
  async function requireCompanionSkill(agent, skills) {
@@ -260,6 +308,7 @@ export function createCordisToolsOwner(
260
308
  ctx,
261
309
  cordisPlugin = CordisTools,
262
310
  skillFilesystemPlugin = CordisSkillFilesystem,
311
+ companionResolution = undefined,
263
312
  ) {
264
313
  if (typeof ctx?.agents?.list !== 'function') {
265
314
  throw new Error('ptc-plus: cordisToolsEnabled requires the DSH agents.list API')
@@ -271,6 +320,22 @@ export function createCordisToolsOwner(
271
320
  const cordisInspectLeases = createCordisInspectLeases(ctx)
272
321
  const scopedCordisPlugin = cordisInspectLeases.plugin(cordisPlugin)
273
322
  const scopedSkillPlugin = exactCompanionSkillPlugin(skillFilesystemPlugin)
323
+ const pluginPackages = ctx.get?.('pluginPackages')
324
+ const packageBaseUrl = ctx.baseUrl
325
+ const skillResolution = companionResolution ?? {
326
+ packageOf: pluginPackages === undefined
327
+ ? undefined
328
+ : (name, parentURL) => {
329
+ if (typeof pluginPackages?.packageOf !== 'function') {
330
+ throw new Error('ptc-plus: cordisToolsEnabled requires the DSH pluginPackages.packageOf API')
331
+ }
332
+ return pluginPackages.packageOf(name, parentURL)
333
+ },
334
+ packageBaseUrl,
335
+ resolveLegacyPackageManifest: pluginPackages === undefined && typeof packageBaseUrl === 'string'
336
+ ? specifier => createRequire(packageBaseUrl).resolve(specifier)
337
+ : undefined,
338
+ }
274
339
  let disposed = false
275
340
 
276
341
  const leafErrors = error => error instanceof AggregateError
@@ -329,6 +394,7 @@ export function createCordisToolsOwner(
329
394
 
330
395
  const installAgent = (agent) => {
331
396
  if (disposed) return Promise.resolve()
397
+ if (withdrawn.has(agent)) return Promise.resolve()
332
398
  const mounted = mounts.get(agent)
333
399
  if (mounted !== undefined) {
334
400
  return mounted.withdrawn === true ? Promise.resolve() : mounted.activation
@@ -353,13 +419,14 @@ export function createCordisToolsOwner(
353
419
  mounts.set(agent, mount)
354
420
  mount.activation = Promise.resolve().then(async () => {
355
421
  const { agentPresets, skills } = requireCompanionServices(agent)
356
- const skillDirectory = await companionSkillDirectory(agentPresets)
422
+ const skillDirectory = await resolveCompanionSkillDirectory(agentPresets, skillResolution)
357
423
  if (mount.disposed) return
358
424
 
359
425
  const skillFiber = agent.ctx.plugin(scopedSkillPlugin, {
360
426
  providerName: CORDIS_SKILL_PROVIDER,
361
427
  includeDefaultRoots: false,
362
- customSkillDirs: [skillDirectory],
428
+ bundledSkillDir: skillDirectory,
429
+ watch: false,
363
430
  })
364
431
  if (typeof skillFiber?.dispose !== 'function') {
365
432
  throw new Error('ptc-plus: DSH Context.plugin did not return a disposable Cordis Skill fiber')
@@ -416,11 +483,11 @@ export function createCordisToolsOwner(
416
483
  /**
417
484
  * Install for a call the host makes on its own behalf.
418
485
  *
419
- * DSH 0.1.6 announces `agent/created` serially and rejects `agents.create()`
420
- * when a listener rejects, so a scope this owner cannot serve must withdraw
421
- * instead of failing an operation another plugin asked for. Install-time
422
- * enumeration stays strict: there the plugin speaks for what it claims to
423
- * have published, and `ready` reports the same failure.
486
+ * The public host lifecycle announces `agent/created` serially and rejects
487
+ * `agents.create()` when a listener rejects, so a scope this owner cannot
488
+ * serve must withdraw instead of failing an operation another plugin asked
489
+ * for. Install-time enumeration stays strict: there the plugin speaks for
490
+ * what it claims to have published, and `ready` reports the same failure.
424
491
  */
425
492
  const installForHost = (agent) => {
426
493
  let activation
@@ -451,8 +518,7 @@ export function createCordisToolsOwner(
451
518
  if (disposed) return
452
519
  for (const agent of [...pending]) {
453
520
  void Promise.resolve().then(() => installAgent(agent)).catch(error => {
454
- pending.delete(agent)
455
- ctx.logger?.warn?.('ptc-plus: deferred Cordis activation failed', error)
521
+ withdraw(agent, error)
456
522
  })
457
523
  }
458
524
  })