dsh-ptc-plus 0.5.6 → 0.5.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.
@@ -148,17 +148,17 @@ Missing-description diagnostics append a complete DSH user message to `additiona
148
148
  tipEscalationFailures: 2
149
149
  ```
150
150
 
151
- The five legacy binding and module switches remain migration inputs. Complete enabled/protected choices map to the corresponding new policy; mixed old choices retain their effective controls until the user explicitly selects the new switch. The Host schema preserves an omitted `bindingUpdates` value until migration, and the settings card resolves the same policy. Selecting the switch also clears the compatibility marker.
151
+ The five legacy binding and module switches remain migration inputs. Complete enabled/protected choices map to the corresponding new policy; mixed old choices retain their effective controls until the user explicitly selects a policy. The Host schema preserves an omitted `bindingUpdates` value until migration, and the settings card resolves the same policy. The **Binding update semantics** selector displays **Legacy settings (not migrated)** until a policy is selected, then shows **Allow redeclarations and overrides** (`stateful`) or **Protect names** (`protected`). Selecting a policy also clears the compatibility marker.
152
152
 
153
153
  `enabled: false` is the settings-based kill switch: the Host keeps the settings namespace, client card, and passive cleanup that can withdraw old PTC declarations at the next permitted accepted step. It removes execution hooks, prompt sections, and tool surfaces without reading binding storage. Settings are applied live when their owner can reconcile them, except the capability inputs frozen per session above and the activation-only seam wait `executionSeamTimeoutMs`, which is read when the plugin activates or reloads. A submitted cell retains one configuration snapshot for its budgets and policies through preflight, execution, binding calls, completion validation, and diagnostics; an update accepted while it runs applies to cells submitted afterward. Worker lifecycle bounds are not cell budgets: `maxOldGenerationSizeMb` is fixed at creation, and `workerReclaimMs` and `startupProbeMs` are read from the current configuration when a worker is created or closed. Node fixes a worker's V8 old-generation limit at creation, so changing `maxOldGenerationSizeMb` while a session worker is active or a submitted cell is preparing to execute is rejected and rolled back rather than reported as applied. Other non-`enabled` updates reconfigure existing owners without replacing session-bound bindings. The settings card renders in whichever settings seat the installed DSH declares — the sidebar **Plugins → Installed** page per-bundle configuration (`plugins.row.config`), or the preceding Host's **Settings → Plugin configuration** card (`settings.plugin.item`) ([ADR 0019](adr/0019-plugin-settings-and-kill-switch.md)) — places the main switch beside the PTC Plus title and disables every other control while the plugin is off. The card does not register in General settings; the sparkle menu opens the same complete form in an application dialog. If a live enable or reconfiguration cannot install a complete runtime, PTC Plus unwinds or restores every owner and persists the last applied settings; a failed compensating settings write is surfaced as an activation diagnostic. The TypeScript language check is deferred until runtime activation, so a non-TypeScript host can load the plugin in disabled mode but cannot enable it.
154
154
 
155
155
  `startupProbeMs` bounds the worker startup conformance probe, which must settle before the kernel reports ready. The worker reads the value from its startup data and falls back to 5000 ms when it is absent; a healthy probe settles inside the worker's own microtasks, so the bound can only fire when startup itself yields or blocks, and the failure is reported as a startup diagnostic without executing any cell. That worker-side read is verified by inspection rather than by a timer test, because a healthy probe never reaches the bound.
156
156
 
157
- `officialReplPriority` defaults to `false` and is evaluated against the live provider capability rather than a release number: a provider that declares `retainsState: true` keeps a program environment per state owner, and a provider that predates that declaration answers through the one retained source form (`programFormat: 'repl'`). With the setting enabled and such a provider live, this host attachment installs no session runtime, so `run_code` and its descriptors — `programFormat`, `retainsState`, `executionInstructions`, `sandboxMode` and `timeout` — stay with the official provider, and the plugin's REPL guidance, session-state semantics, recovery, editor tool, capability SDK, REPL observation and binding workbench are suspended until ownership changes back. The settings card, enablement and stored global bindings remain available so the switch can be reversed. Changing the setting or the host mode re-evaluates ownership as an installation change: the retired attachment disposes its live kernels before the other shape installs, so no committed cell mixes owners. Calls executed under the official mode produce no plugin journal records and are never recovery evidence. The plugin's provider surface forwards `programFormat` to the live original, answers `retainsState` for itself (an installed session kernel retains a per-session environment; otherwise the original declaration decides) and retains weak owner-to-original dispatch provenance for `release(owner)` — captured Agent cleanup reaches the original instances that served that owner even after adapter removal or source replacement, and a failed cleanup remains available for explicit retry. Owner-keyed retained environments and the executed program format belong to the provider that actually runs the program, and non-session consumers such as the workflow adapter keep executing through it — while `executionInstructions`, `sandboxMode` and `timeout` stay withheld while PTC Plus owns session execution. The plugin's own session kernel state is keyed by session and released through the plugin lifecycle, not by `release(owner)`.
157
+ `officialReplPriority` defaults to `false` and is evaluated against the live provider capability rather than a release number: a provider that declares `retainsState: true` keeps a program environment per state owner, and a provider that predates that declaration answers through the one retained source form (`programFormat: 'repl'`). With the setting enabled and such a provider live, this host attachment installs no session runtime, so `run_code` and its descriptors — `programFormat`, `retainsState`, `executionInstructions`, `sandboxMode` and `timeout` — stay with the official provider, and the plugin's REPL guidance, session-state semantics, recovery, editor tool, capability SDK, REPL observation and binding workbench are suspended until ownership changes back. The settings card, enablement and stored global bindings remain available so the switch can be reversed. Changing the setting or the host mode re-evaluates ownership as an installation change: the retired attachment disposes its live kernels before the other shape installs, so no committed cell mixes owners. Calls executed under the official mode produce no plugin journal records and are never recovery evidence. The plugin's provider surface forwards `programFormat` to the live original, answers `retainsState` for itself (an installed session kernel retains a per-session environment; otherwise the original declaration decides) and retains weak owner-to-original dispatch provenance for `release(owner)` — captured Agent cleanup reaches the original instances that served that owner even after adapter removal or source replacement, and a failed cleanup remains available for explicit retry. Owner-keyed retained environments and the executed program format belong to the provider that actually runs the program, and non-session consumers such as the workflow adapter keep executing through it — while `executionInstructions`, `sandboxMode` and `timeout` stay withheld while PTC Plus owns session execution. The plugin's own kernel is keyed by session. For a live session Agent, `release(owner)` implements the Host's explicit reset: it releases the kernel and appends a durable reset boundary, clearing earlier bindings, named checkpoints and edit targets from subsequent computation. For an Agent scope whose public Cordis fiber uid is already null, release is lifecycle cleanup and preserves proven cold recovery. Both routes also clean up original-provider state; failures propagate for Host-managed retry. Non-session owners retain original-provider delegation.
158
158
 
159
- `cordisToolsEnabled: true` atomically mounts the official `@deepseek-ai/dsh-tool-cordis` plugin and exactly the shipped `cordis-plugin-development` companion Skill in agent scopes that expose the `run_code` and `skill` tools plus the public preset and Skill services. PTC Plus resolves the official `cordis` preset through DSH's public preset service, lets the maintained filesystem provider read its Skill root as an immutable bundled Host resource, and filters that provider at the public registration boundary so sibling Skills are never published to the PTC scope. It does not copy the Skill or switch the agent's preset. The first request waits until the Cordis tool fiber, owner guidance, exact Skill provider, and scoped Skill load are all ready. An agent scope that omits one of those surfaces is left for later: the owner publishes nothing there, reports nothing, and retries when the tool surface changes or the next prompt is assembled. A scope whose exposed preset, Skill/tool service, declared Cordis service, inspect manifest, or activation contradicts the host contract fails the setting change at installation and removes every provisional contribution; reached through the host's own `agent/created` or assemble call it withdraws from that scope with one warning instead, so this plugin's unavailability never fails an agent another plugin created ([ADR 0029](adr/0029-keep-plugin-unavailability-out-of-the-host-agent-lifecycle.md)). Disable and agent/runtime disposal remove both fibers. Native agents inherit neither the resulting `tools.*` members nor the Skill, and the code-only direct-tool projection remains `[run_code]`. Cordis can evaluate model-written plugins against the live runtime, so enabling it grants shell-equivalent trust.
159
+ `cordisToolsEnabled: true` atomically mounts the official `@deepseek-ai/dsh-tool-cordis` plugin and exactly the selected official companion Skill in agent scopes that expose the `run_code` and `skill` tools plus the public preset and Skill services. PTC Plus resolves the official `cordis` preset through DSH's public preset service, lets the maintained filesystem provider read its Skill root as an immutable bundled Host resource, and filters that provider at the public registration boundary so sibling Skills are never published to the PTC scope. It does not copy the Skill or switch the agent's preset. The first request waits until the Cordis tool fiber, owner guidance, exact Skill provider, and scoped Skill load are all ready. An agent scope that omits one of those surfaces is left for later: the owner publishes nothing there, reports nothing, and retries when the tool surface changes or the next prompt is assembled. A scope whose exposed preset, Skill/tool service, declared Cordis service, inspect manifest, or activation contradicts the host contract fails the setting change at installation and removes every provisional contribution; reached through the host's own `agent/created` or assemble call it withdraws from that scope with one warning instead, so this plugin's unavailability never fails an agent another plugin created ([ADR 0029](adr/0029-keep-plugin-unavailability-out-of-the-host-agent-lifecycle.md)). Disable and agent/runtime disposal remove both fibers. Native agents inherit neither the resulting `tools.*` members nor the Skill, and the code-only direct-tool projection remains `[run_code]`. Cordis can evaluate model-written plugins against the live runtime, so enabling it grants shell-equivalent trust.
160
160
 
161
- The Skill root uses the preset's absolute composition path when that generation publishes one. A pathless generation is resolved through the active Host's public `pluginPackages.packageOf` service, which returns the official singular or plural preset package from that installation rather than an ambient package reachable from the plugin profile. An older generation with no such service may resolve only its historical plural package relative to the Host's `ctx.baseUrl`; the fallback is disabled whenever `pluginPackages` exists. The exact `cordis-plugin-development/SKILL.md` must exist before the mount begins. An invalid published path remains authoritative and fails instead of falling through to another generation's package layout.
161
+ The Skill root uses the preset's absolute composition path when that generation publishes one. A pathless generation is resolved through the active Host's public `pluginPackages.packageOf` service, which returns the official singular or plural preset package from that installation rather than an ambient package reachable from the plugin profile. An older generation with no such service may resolve only its historical plural package relative to the Host's `ctx.baseUrl`; the fallback is disabled whenever `pluginPackages` exists. The root must contain `dsh-doc/SKILL.md` or the historical `cordis-plugin-development/SKILL.md`; `dsh-doc` takes precedence when both exist. The selected official name and resource base remain intact through discovery, loading and readiness validation. An invalid published path remains authoritative and fails instead of falling through to another generation's package layout.
162
162
 
163
163
  Keep large Cordis host or client source in a top-level binding before calling a Cordis tool. If that call rejects while the worker stays live, an initialized binding can be reused in a short continuation. Decide whether to retry from the operation owner's retry/idempotence contract and available execution facts; a thrown call or a smaller cell does not prove that no effect occurred.
164
164
 
@@ -496,8 +496,11 @@ const callingRevision = capability => capability === undefined || capability.eff
496
496
 
497
497
  function contractUpdateMessage(snapshot, sessionId) {
498
498
  const dependencies = new Map()
499
+ // Recorded generations retain their exact body for restoration and deduplication.
500
+ const grouped = snapshot.schema === 'ptc-capability-contracts/v2'
501
+ const onDemand = grouped ? snapshot.changes.filter(change => change.capability !== null && change.disclosure === 'on-demand') : []
499
502
  const text = ['PTC Plus current calling contracts. These updates replace the earlier contracts for the listed capabilities only; session enablement and disclosure choices are unchanged.',
500
- ...snapshot.changes.map(change => change.capability === null
503
+ ...snapshot.changes.filter(change => !grouped || change.capability === null || change.disclosure === 'full').map(change => change.capability === null
501
504
  ? `### ${change.id}\nThe Host no longer supplies this capability. Its earlier calling contract no longer applies.`
502
505
  : change.disclosure === 'full'
503
506
  ? [...(change.capability.dependencies ?? []).flatMap(item => {
@@ -508,6 +511,7 @@ function contractUpdateMessage(snapshot, sessionId) {
508
511
  return [`### dependency:${item.id}\n${item.text}`]
509
512
  }), `### ${change.id}\n${change.capability.contract}`].join('\n\n')
510
513
  : `### ${change.id}\nThe Host calling contract has changed. Retrieve the current complete contract with capabilities.search(${JSON.stringify({ query: change.id })}) before invoking it.`),
514
+ ...(onDemand.length === 0 ? [] : [`### Updated on-demand contracts\nBefore invoking any capability below, retrieve its current complete contract with capabilities.search({query: "<capability ID>"}):\n${onDemand.map(change => `- ${change.id}`).join('\n')}`]),
511
515
  ...(snapshot.overageBytes > 0 ? [`The currently selected full contracts exceed the configured budget by ${snapshot.overageBytes} UTF-8 bytes.`] : [])].join('\n\n')
512
516
  return createUserMessage({ source: { kind: PTC_MESSAGE_SOURCE_KIND, form: 'capability-contracts', sessionId,
513
517
  digest: digest(snapshot), snapshot }, content: [{ type: 'text', text }] })
@@ -517,7 +521,7 @@ function readContractUpdate(message, planDigest, sessionId) {
517
521
  const source = message?.source
518
522
  const snapshot = source?.snapshot
519
523
  if (source?.kind !== PTC_MESSAGE_SOURCE_KIND || source.form !== 'capability-contracts'
520
- || source.sessionId !== sessionId || snapshot?.schema !== 'ptc-capability-contracts/v1'
524
+ || source.sessionId !== sessionId || !['ptc-capability-contracts/v1', 'ptc-capability-contracts/v2'].includes(snapshot?.schema)
521
525
  || snapshot.planDigest !== planDigest || source.digest !== digest(snapshot)
522
526
  || !Array.isArray(snapshot.views) || !Array.isArray(snapshot.changes)
523
527
  || !Number.isSafeInteger(snapshot.overageBytes) || snapshot.overageBytes < 0) return undefined
@@ -1953,7 +1957,7 @@ export function createCapabilityEnvironment({ ctx, config, toolSchemasForAgent,
1953
1957
  .filter(item => state.snapshot.capabilities.some(original => original.id === item.id)),
1954
1958
  manual: { baseline: [], overrides: Object.fromEntries(state.snapshot.plan.entries.map(entry => [entry.id, entry.selection === 'host' ? 'full' : entry.selection])) },
1955
1959
  navigationText: NAVIGATION, configurationRevision: state.snapshot.plan.configurationRevision })
1956
- additions.push(contractUpdateMessage({ schema: 'ptc-capability-contracts/v1', planDigest, views, changes,
1960
+ additions.push(contractUpdateMessage({ schema: 'ptc-capability-contracts/v2', planDigest, views, changes,
1957
1961
  overageBytes: livePlan.budgets.overageBytes }, sessionId))
1958
1962
  }
1959
1963
  const expandedVisible = new Set([...visible, ...messages].flatMap(message => message.source?.form === 'capability-descriptions'
@@ -130,8 +130,8 @@ export const CONFIG_FIELDS = Object.freeze([
130
130
  type: 'enum',
131
131
  options: Object.freeze(['stateful', 'protected']),
132
132
  default: 'stateful',
133
- label: '跨 cell 声明策略',
134
- labelEn: 'Cross-cell declaration policy',
133
+ label: '绑定更新语义',
134
+ labelEn: 'Binding update semantics',
135
135
  description: '同一作用域的声明和赋值可更新变量、函数、类及导入。选“启用名称保护”时改为检测跨 cell 重声明,并保护新建的 const 和 import;静态 import/export 始终可用。',
136
136
  descriptionEn: 'Declarations and assignments update variables, functions, classes and imports in the same scope. With “Protect names” selected, cross-cell redeclarations are detected instead and newly created const and import bindings are protected; static import/export remains available.',
137
137
  },
@@ -8,7 +8,7 @@ import { RUN_CODE } from './runtime-bridge-owner.js'
8
8
  import { sessionExecutionAvailable } from './host-tool-sdk.js'
9
9
 
10
10
  const CORDIS_PRESET_ID = 'cordis'
11
- const CORDIS_SKILL_NAME = 'cordis-plugin-development'
11
+ const COMPANION_SKILL_NAMES = Object.freeze(['dsh-doc', 'cordis-plugin-development'])
12
12
  const CORDIS_SKILL_PROVIDER = 'ptc-plus-cordis'
13
13
  const COMPANION_SKILL_TOOL = 'skill'
14
14
  const COMPANION_PACKAGE_LAYOUTS = Object.freeze([
@@ -22,19 +22,19 @@ const COMPANION_PACKAGE_LAYOUTS = Object.freeze([
22
22
  }),
23
23
  ])
24
24
 
25
- function exactCompanionCandidates(observation) {
25
+ function exactCompanionCandidates(observation, skillName) {
26
26
  if (Array.isArray(observation)) {
27
- return observation.filter(candidate => candidate?.name === CORDIS_SKILL_NAME)
27
+ return observation.filter(candidate => candidate?.name === skillName)
28
28
  }
29
29
  if (observation === null || typeof observation !== 'object'
30
30
  || !Array.isArray(observation.candidates)) return observation
31
31
  return {
32
32
  ...observation,
33
- candidates: observation.candidates.filter(candidate => candidate?.name === CORDIS_SKILL_NAME),
33
+ candidates: observation.candidates.filter(candidate => candidate?.name === skillName),
34
34
  }
35
35
  }
36
36
 
37
- function exactCompanionProvider(provider) {
37
+ function exactCompanionProvider(provider, skillName) {
38
38
  if (provider === null || typeof provider !== 'object'
39
39
  || typeof provider.name !== 'string'
40
40
  || typeof provider.list !== 'function'
@@ -44,17 +44,17 @@ function exactCompanionProvider(provider) {
44
44
  return {
45
45
  ...provider,
46
46
  async list(options) {
47
- return exactCompanionCandidates(await Reflect.apply(provider.list, provider, [options]))
47
+ return exactCompanionCandidates(await Reflect.apply(provider.list, provider, [options]), skillName)
48
48
  },
49
49
  async get(candidate, options) {
50
- if (candidate?.name !== CORDIS_SKILL_NAME) return undefined
50
+ if (candidate?.name !== skillName) return undefined
51
51
  const skill = await Reflect.apply(provider.get, provider, [candidate, options])
52
- return skill?.name === CORDIS_SKILL_NAME ? skill : undefined
52
+ return skill?.name === skillName ? skill : undefined
53
53
  },
54
54
  }
55
55
  }
56
56
 
57
- function exactCompanionSkillPlugin(skillFilesystemPlugin) {
57
+ function exactCompanionSkillPlugin(skillFilesystemPlugin, skillName) {
58
58
  return {
59
59
  ...skillFilesystemPlugin,
60
60
  apply(pluginCtx, config) {
@@ -68,7 +68,7 @@ function exactCompanionSkillPlugin(skillFilesystemPlugin) {
68
68
  const facade = new Proxy(skills, {
69
69
  get(target, property) {
70
70
  if (property === 'registerProvider') {
71
- return create => target.registerProvider(control => exactCompanionProvider(create(control)))
71
+ return create => target.registerProvider(control => exactCompanionProvider(create(control), skillName))
72
72
  }
73
73
  const value = Reflect.get(target, property, target)
74
74
  return typeof value === 'function' ? value.bind(target) : value
@@ -144,12 +144,13 @@ function requireCompanionServices(agent) {
144
144
  return { agentPresets, skills }
145
145
  }
146
146
 
147
- function containsCompanionSkill(directory, fileExists) {
148
- return fileExists(join(directory, CORDIS_SKILL_NAME, 'SKILL.md'))
147
+ function companionSkill(directory, fileExists) {
148
+ const skillName = COMPANION_SKILL_NAMES.find(name => fileExists(join(directory, name, 'SKILL.md')))
149
+ return skillName === undefined ? undefined : { skillRoot: directory, skillName }
149
150
  }
150
151
 
151
152
  /** Resolve the official Cordis companion Skill across DSH preset generations. */
152
- export async function resolveCompanionSkillDirectory(agentPresets, options = {}) {
153
+ export async function resolveCompanionSkillResource(agentPresets, options = {}) {
153
154
  const preset = await agentPresets.resolve(CORDIS_PRESET_ID)
154
155
  const resolution = typeof options === 'function' ? options() : options
155
156
  const fileExists = resolution.fileExists ?? existsSync
@@ -161,10 +162,11 @@ export async function resolveCompanionSkillDirectory(agentPresets, options = {})
161
162
  throw new Error('ptc-plus: DSH cordis preset did not expose an absolute composition path')
162
163
  }
163
164
  const directory = join(dirname(preset.path), 'skills')
164
- if (!containsCompanionSkill(directory, fileExists)) {
165
- throw new Error(`ptc-plus: DSH cordis preset does not contain ${CORDIS_SKILL_NAME}/SKILL.md beside its composition`)
165
+ const resource = companionSkill(directory, fileExists)
166
+ if (resource === undefined) {
167
+ throw new Error(`ptc-plus: DSH cordis preset does not contain a supported companion SKILL.md beside its composition`)
166
168
  }
167
- return directory
169
+ return resource
168
170
  }
169
171
 
170
172
  for (const layout of COMPANION_PACKAGE_LAYOUTS) {
@@ -174,7 +176,8 @@ export async function resolveCompanionSkillDirectory(agentPresets, options = {})
174
176
  throw new Error(`ptc-plus: DSH pluginPackages returned an invalid ${layout.name} package resource`)
175
177
  }
176
178
  const directory = layout.skillRoot(pkg.dir)
177
- if (containsCompanionSkill(directory, fileExists)) return directory
179
+ const resource = companionSkill(directory, fileExists)
180
+ if (resource !== undefined) return resource
178
181
  }
179
182
 
180
183
  if (resolution.packageOf === undefined && resolution.resolveLegacyPackageManifest !== undefined) {
@@ -186,29 +189,30 @@ export async function resolveCompanionSkillDirectory(agentPresets, options = {})
186
189
  }
187
190
  if (manifest !== undefined) {
188
191
  const directory = join(dirname(manifest), 'presets', CORDIS_PRESET_ID, 'skills')
189
- if (containsCompanionSkill(directory, fileExists)) return directory
192
+ const resource = companionSkill(directory, fileExists)
193
+ if (resource !== undefined) return resource
190
194
  }
191
195
  }
192
- throw new Error(`ptc-plus: the active DSH installation does not publish a loadable ${CORDIS_SKILL_NAME} companion Skill through its package-resolution surface`)
196
+ throw new Error(`ptc-plus: the active DSH installation does not publish a loadable official companion Skill through its package-resolution surface`)
193
197
  }
194
198
 
195
- async function requireCompanionSkill(agent, skills) {
199
+ async function requireCompanionSkill(agent, skills, skillName) {
196
200
  const lookup = {
197
201
  cwd: agent.session?.header?.cwd,
198
202
  scope: agent,
199
203
  }
200
204
  const owned = (await skills.list(lookup)).filter(skill => skill?.provider === CORDIS_SKILL_PROVIDER)
201
205
  const summary = owned[0]
202
- if (owned.length !== 1 || summary?.name !== CORDIS_SKILL_NAME
206
+ if (owned.length !== 1 || summary?.name !== skillName
203
207
  || summary.invocation?.modelInvocable !== true) {
204
- throw new Error(`ptc-plus: official Cordis Skill provider must publish exactly ${JSON.stringify(CORDIS_SKILL_NAME)} as model-invocable`)
208
+ throw new Error(`ptc-plus: official Cordis Skill provider must publish exactly ${JSON.stringify(skillName)} as model-invocable`)
205
209
  }
206
- const skill = await skills.get(CORDIS_SKILL_NAME, lookup)
210
+ const skill = await skills.get(skillName, lookup)
207
211
  if (skill?.provider !== CORDIS_SKILL_PROVIDER
208
212
  || skill.invocation?.modelInvocable !== true
209
213
  || typeof skill.content !== 'string'
210
214
  || skill.content.length === 0) {
211
- throw new Error(`ptc-plus: official Cordis companion Skill ${JSON.stringify(CORDIS_SKILL_NAME)} is not loadable`)
215
+ throw new Error(`ptc-plus: official Cordis companion Skill ${JSON.stringify(skillName)} is not loadable`)
212
216
  }
213
217
  }
214
218
 
@@ -321,7 +325,6 @@ export function createCordisToolsOwner(
321
325
  const withdrawn = new Map()
322
326
  const cordisInspectLeases = createCordisInspectLeases(ctx)
323
327
  const scopedCordisPlugin = cordisInspectLeases.plugin(cordisPlugin)
324
- const scopedSkillPlugin = exactCompanionSkillPlugin(skillFilesystemPlugin)
325
328
  const packageBaseUrl = ctx.baseUrl
326
329
  const skillResolution = companionResolution ?? (() => {
327
330
  const pluginPackages = ctx.get?.('pluginPackages')
@@ -423,13 +426,14 @@ export function createCordisToolsOwner(
423
426
  mounts.set(agent, mount)
424
427
  mount.activation = Promise.resolve().then(async () => {
425
428
  const { agentPresets, skills } = requireCompanionServices(agent)
426
- const skillDirectory = await resolveCompanionSkillDirectory(agentPresets, skillResolution)
429
+ const { skillRoot, skillName } = await resolveCompanionSkillResource(agentPresets, skillResolution)
430
+ const scopedSkillPlugin = exactCompanionSkillPlugin(skillFilesystemPlugin, skillName)
427
431
  if (mount.disposed) return
428
432
 
429
433
  const skillFiber = agent.ctx.plugin(scopedSkillPlugin, {
430
434
  providerName: CORDIS_SKILL_PROVIDER,
431
435
  includeDefaultRoots: false,
432
- bundledSkillDir: skillDirectory,
436
+ bundledSkillDir: skillRoot,
433
437
  watch: false,
434
438
  })
435
439
  if (typeof skillFiber?.dispose !== 'function') {
@@ -449,7 +453,7 @@ export function createCordisToolsOwner(
449
453
  await Promise.resolve(cordisFiber)
450
454
  if (mount.disposed) return
451
455
 
452
- await requireCompanionSkill(agent, skills)
456
+ await requireCompanionSkill(agent, skills, skillName)
453
457
  if (mount.disposed) return
454
458
  mount.activated = true
455
459
  }).catch(async error => {
@@ -94,19 +94,16 @@ export function createExecutionSeam(service, serviceName) {
94
94
  requireCompatible()
95
95
  return installed === undefined ? upstreamRun.call(service, input) : installed.execute(input)
96
96
  },
97
- /**
98
- * Release one owner's retained environment. The resources live in the
99
- * original generation (the session kernel releases its state through its
100
- * own lifecycle). Adapter retirement only closes execution; the original
101
- * still owns cleanup for references retained by its consumers.
102
- */
103
- release(owner) {
104
- if (typeof service.release !== 'function') return Promise.resolve()
105
- try {
106
- return Promise.resolve(service.release.call(service, owner))
107
- } catch (error) {
108
- return Promise.reject(error)
109
- }
97
+ /** Release both the installed environment and any original-provider state. */
98
+ async release(owner) {
99
+ const entry = installed
100
+ const results = await Promise.allSettled([
101
+ Promise.resolve().then(() => entry?.release?.(owner)),
102
+ Promise.resolve().then(() => service.release?.call(service, owner)),
103
+ ])
104
+ const failures = results.filter(result => result.status === 'rejected').map(result => result.reason)
105
+ if (failures.length === 1) throw failures[0]
106
+ if (failures.length > 1) throw new AggregateError(failures, 'ptc-plus: execution environment release failed')
110
107
  },
111
108
  [PLUGIN_EXECUTION_SEAM]() { return seam },
112
109
  })
@@ -143,11 +140,12 @@ export function createExecutionSeam(service, serviceName) {
143
140
  requireCompatible()
144
141
  return upstreamRun.call(service, spec)
145
142
  },
146
- installExecute(execute) {
143
+ installExecute(execute, release) {
147
144
  requireAvailable()
148
145
  if (typeof execute !== 'function') throw new TypeError('ptc-plus: execution entry must be a function')
149
146
  if (installed !== undefined) throw new Error('ptc-plus: execution seam already installed')
150
- const entry = { execute, language: service.language }
147
+ if (release !== undefined && typeof release !== 'function') throw new TypeError('ptc-plus: release entry must be a function')
148
+ const entry = { execute, release, language: service.language }
151
149
  installed = entry
152
150
  const restore = () => {
153
151
  if (installed !== entry) return
@@ -697,7 +697,13 @@ function applyEditableSlots(definition, promptText, slotPatches) {
697
697
  .finally(projected.release)
698
698
  }
699
699
 
700
- const releaseSeam = seam.installExecute(patchedRun)
700
+ const releaseSeam = seam.installExecute(patchedRun, owner => {
701
+ if (owner?.session === undefined || sessionId(owner) === undefined) return
702
+ // Host uses release for explicit reset and for its retained-environment effect.
703
+ // Cordis clears the public Agent scope uid before invoking disposal effects.
704
+ if (owner.ctx?.fiber?.uid === null) return sessions.disposeAgent(sessionId(owner))
705
+ return sessions.resetEnvironment(owner.session)
706
+ })
701
707
 
702
708
  /**
703
709
  * Whether this execution is the derived execution the edit transport submitted.
@@ -23,8 +23,12 @@ function hadPluginComputation(session) {
23
23
  && Object.hasOwn(event.data?.meta ?? {}, JOURNAL_KEY))
24
24
  }
25
25
 
26
- /** Persist actual provider release through public Session and single-writer owners. */
26
+ /** Persist provider-only release through public Session and single-writer owners. */
27
27
  export async function handBackRuntimeSessions(ctx, owners) {
28
+ // Cordis FiberState.UNLOADING: application teardown has no continuing model
29
+ // request to hand back. Archive-wide writes do not belong to process cleanup;
30
+ // a restarted provider reconciles availability at the next accepted step.
31
+ if (ctx.root?.fiber.state === 5) return
28
32
  const service = name => owners ? owners[name] : typeof ctx.get === 'function' ? ctx.get(name) : ctx[name]
29
33
  const sessions = service('sessions')
30
34
  const persistence = service('sessionPersistence')
@@ -1,3 +1,4 @@
1
+ import { ENVIRONMENT_RESET_VERSION } from './session-journal-schema.js'
1
2
  import { createUserMessage } from '@deepseek-ai/dsh-llm'
2
3
  import {
3
4
  CATALOG_PREFIX, EMPTY_CATALOG, LEGACY_STATE_PREFIX, PREVIOUS_EMPTY_CATALOG, PREVIOUS_STATE_PREFIX,
@@ -185,3 +186,10 @@ export function runtimeNoticeMessage(tip) {
185
186
  if (readRuntimeMessage(message) === undefined) throw new Error('invalid PTC recovery notice')
186
187
  return message
187
188
  }
189
+
190
+ /** A persisted computation boundary; the Host separately owns its reset receipt. */
191
+ export function environmentResetMessage() {
192
+ return createUserMessage({ source: { kind: PTC_MESSAGE_SOURCE_KIND, form: 'environment-reset', version: ENVIRONMENT_RESET_VERSION },
193
+ content: [{ type: 'text', text: 'PTC Plus computation reset: earlier session bindings, named checkpoints and edit targets are retired. New computation starts empty with the configured global bindings. Completed external effects remain.' }],
194
+ })
195
+ }