fix(agent-presets,web): broken presets are roster rows, not gaps
A hand-damaged preset was silent until the worst moment. An unparsable composition listed as an ordinary selectable row and failed only at the next session start — set as default, every new session failed. A directory whose composition file was deleted vanished from the roster while still occupying its id: copy answered "delete the existing preset first" while remove answered "not found", a dead end. Discovery now owns health: every id-shaped directory is a roster slot, broken when its composition is missing or unloadable, checked with the loader's own entryListSchema dialect (!!js included) so health never rejects what the loader accepts. `broken` rides AgentPreset, the agentPreset.list entry, and the UI row; mount/recompose/standingKeyFor refuse broken up front with the discovery-reported reason, while resolve/read/remove still answer. The section renders marked red cards — unselectable, uncopyable, deletable, location kept on custom rows — and both pickers drop broken rows entirely. The cordis preset's persona now forbids editing the shipped install (corrupting cordis would disable the mode itself) and points authoring at $DSH_HOME/.agent-presets; its skill teaches preset.yml metadata, the copy-first workflow, the one-escalation sandbox reality, and honest verification. Exercised live: asked to edit the shipped composition the composed agent refuses citing both rules; asked for real presets (simple and complex) it lands them under the user root with one approved escalation each and self-checks with the loader dialect.
This commit is contained in:
@@ -153,6 +153,10 @@ export class AgentPresets extends Service {
|
||||
|
||||
/**
|
||||
* Resolve one preset by id.
|
||||
*
|
||||
* A broken preset resolves — deleting one, reading one, and reporting one
|
||||
* all need the row — and the mounting paths refuse it AFTER resolution
|
||||
* through {@link resolveMountable}.
|
||||
* @param id - the preset id, or `undefined` for {@link defaultId}.
|
||||
* @returns the resolved preset.
|
||||
* @throws when no configured root supplies that id.
|
||||
@@ -167,6 +171,24 @@ export class AgentPresets extends Service {
|
||||
return found
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve one preset that is about to compose an agent, refusing a broken
|
||||
* one with its discovery-reported reason. Failing here rather than inside
|
||||
* the loader keeps the answer the same for every unloadable shape — ghost
|
||||
* directory, unparsable YAML, rowless list — and spends no mount attempt
|
||||
* on a composition discovery already read as unusable.
|
||||
* @param id - the preset id, or `undefined` for {@link defaultId}.
|
||||
* @returns the resolved, mountable preset.
|
||||
* @throws when the preset is unknown or discovery reports it broken.
|
||||
*/
|
||||
private async resolveMountable(id?: string): Promise<AgentPreset> {
|
||||
const preset = await this.resolve(id)
|
||||
if (preset.broken !== undefined) {
|
||||
throw new PresetMountError(preset.id, preset.broken)
|
||||
}
|
||||
return preset
|
||||
}
|
||||
|
||||
/**
|
||||
* Standing mounts by preset id, single-flight so two agents racing the
|
||||
* first use of one preset share one composition. A settled failure is
|
||||
@@ -198,7 +220,7 @@ export class AgentPresets extends Service {
|
||||
if (agentKey === undefined) {
|
||||
throw new Error('agent-presets: refusing to compose an unscoped context; the scope key is what joins an agent to its preset')
|
||||
}
|
||||
const preset = await this.resolve(id)
|
||||
const preset = await this.resolveMountable(id)
|
||||
const standing = await this.ensureStanding(preset)
|
||||
setScopeParent(agentKey, standing.key)
|
||||
return preset
|
||||
@@ -314,7 +336,7 @@ export class AgentPresets extends Service {
|
||||
if (agentKey === undefined) {
|
||||
throw new Error('agent-presets: refusing to recompose an unscoped context')
|
||||
}
|
||||
const preset = await this.resolve(id)
|
||||
const preset = await this.resolveMountable(id)
|
||||
const standing = await this.ensureStanding(preset)
|
||||
setScopeParent(agentKey, standing.key)
|
||||
return preset
|
||||
@@ -332,7 +354,7 @@ export class AgentPresets extends Service {
|
||||
* @throws when the preset is unknown or its composition is unusable.
|
||||
*/
|
||||
async standingKeyFor(id?: string): Promise<ScopeKey> {
|
||||
const preset = await this.resolve(id)
|
||||
const preset = await this.resolveMountable(id)
|
||||
return (await this.ensureStanding(preset)).key
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user