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.
- package/README.en.md +10 -2
- package/README.md +10 -2
- package/client.cjs +13 -23
- package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +18 -12
- package/docs/adr/0038-freeze-session-capability-plans.md +1 -1
- package/docs/adr/0042-persist-provider-availability-and-handback.md +8 -4
- package/docs/adr/0044-coexist-with-the-official-persistent-repl.md +2 -0
- package/docs/adr/0051-persist-explicit-host-environment-resets.md +29 -0
- package/docs/adr/README.md +1 -0
- package/docs/architecture.md +2 -2
- package/docs/capability-planning-components.md +1 -1
- package/docs/durability-design.md +10 -0
- package/docs/installation.md +2 -2
- package/docs/releases/v0.5.6.md +7 -3
- package/docs/releases/v0.5.7.md +51 -0
- package/docs/runtime-reference.md +4 -4
- package/internal/capability-environment.js +7 -3
- package/internal/config-spec.js +2 -2
- package/internal/cordis-tools-owner.js +32 -28
- package/internal/execution-seam-compat.js +13 -15
- package/internal/runtime-bridge-owner.js +7 -1
- package/internal/runtime-contexts.js +5 -1
- package/internal/runtime-messages.js +8 -0
- package/internal/session-journal-recovery.js +68 -25
- package/internal/session-journal-schema.js +10 -0
- package/internal/session-journal.js +25 -13
- package/internal/session-log-view.js +15 -8
- package/internal/session-runtime.js +13 -0
- package/package.json +1 -1
- package/src/client-activity.js +4 -1
- package/src/client-settings-view.js +3 -10
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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 ||
|
|
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/
|
|
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'
|
package/internal/config-spec.js
CHANGED
|
@@ -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: '
|
|
134
|
-
labelEn: '
|
|
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
|
|
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 ===
|
|
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 ===
|
|
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 !==
|
|
50
|
+
if (candidate?.name !== skillName) return undefined
|
|
51
51
|
const skill = await Reflect.apply(provider.get, provider, [candidate, options])
|
|
52
|
-
return skill?.name ===
|
|
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
|
|
148
|
-
|
|
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
|
|
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
|
-
|
|
165
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 !==
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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:
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|