deepseek-harness/.agents/notes/implemented/architecture/2026-08-29-plugin-inventory-agent-preset-scopes.md
Yichen Jiang 8349cc6c73 fix(agent-presets): live-mount-first inventory, per-runtime mounts, localized shipped preset names
Review round on #3316: the composition inventory answers from a standing
mount before the broken verdict (a file corrupted after mounting no longer
hides the running composition), livePresetMounts filters by the caller's
root fiber so a second Cordis runtime in one process never answers for it,
compositions carry trust and the plugin list resolves shipped preset names
through the shared dsh-agent-presets/display fold over ui-agent-preset's
dictionaries (INLINE_SAFE inline import; no cross-plugin runtime import),
the condition detail label reads Disabled when/禁用条件, and the stale
four-surfaces comment says three.
2026-08-29 20:41:01 +08:00

6.9 KiB

Agent Note: The plugin inventory carries every agent preset's composition

Status: implemented

English | 中文

Problem

Per-session agent presets moved every model-facing row onto the agent plane, and the settings plugin list kept projecting ctx.loader.entries() alone. The surface therefore hid the plugins sessions actually run — a directly-plugged preset subtree never appears in the Loader's entries — and actively misled about the rest: the web overlay's deliberate disabled: true tombstones (tool-bash, tool-fs, plan-mode, …) rendered as two dozen plainly "disabled" rows while the same modules ran in every standard-preset session. Beside it, General settings carried a default-preset dropdown that wrote the same agent-presets.default field as the roster section's own make-default action — two editors for one fact, one of them blind to the roster it was choosing from.

Decision

The inventory speaks for both planes. pluginInventory/list gains an optional agentPresets block — one group per roster preset with id, trust, display name, default marking, health, and flattened composition rows — supplied by the new AgentPresets.compositionInventory(): a preset with a live standing mount — matched within this runtime's own root, so a second Cordis runtime in the same process never answers for it — answers from its newest generation's Loader entries even when its file has since broken (the mount is what sessions run; the broken verdict applies only to a preset nothing composed), and one never composed since boot answers from its composition file. dsh-host-plugin-inventory resolves the roster as an optional peer through ctx.get('agentPresets') (the plugin-package-inventory-deepseek pattern) and only maps root-fiber states onto its public phase vocabulary, so deployments without a roster keep serving Loader entries alone with the field absent.

File answers are evaluated, not guessed, and reading never mounts. !!js disabled gates are platform/environment conditions the Loader itself evaluates at every mount decision, so the file read evaluates them against the Loader context and reports the decision a mount on this host would make; a gate the evaluator refuses stays 'conditional' with its expression text carried for display. The read parses and evaluates only — no import, no compose — so listing every preset's plugins activates none of them, and a regression test pins livePresetMounts() empty after a full inventory read. Building this surface also exposed the reverse leak: EntryTree's constructor files every new tree under the nearest owning Loader entry's subtree slot, so the first standing mount hung the whole preset composition off the roster's own row and root loader.entries() walked it as host entries. PresetTree now reclaims the slot, restoring the standing mount's documented absence from the Loader, and a regression test holds the root entry list identical across a mount.

The list is grouped by scope, with the misleading rows given their own state. The preset group renders first, collapsible and open by default, behind a display-only switcher — the General-settings selector pill over a menu — that opens on the default preset and writes no settings, because inspecting minimal must not change what new sessions run. Preset names resolve through the shared presetDisplayText fold in dsh-agent-presets/display — the groups carry trust for exactly this split, and an inline-safe pure module is the seam that satisfies both the client purity gate (no cross-plugin runtime imports) and the typert client analyzer (no new Context service face) — so shipped presets follow the active locale's dictionaries while user-authored metadata stays untranslated. The global group follows collapsed, failures float first, and a global entry that is disabled while at least one preset row for the same module specifier is actually enabled is marked preset-provided in place, its details naming the enabling presets — a third state instead of the generic "disabled" that started this, and deliberately not a sub-group: the preset group above already shows those plugins as compositions, so a second cluster restating them earned its removal. The status dot appears only for a live root fiber — a file-state row carries its enablement tag alone, so an unmounted preset does not read as a column of grey mystery dots. The provider rule is strict enabled === true: counting conditional declarations would claim per-session provision tool-pwsh never delivers on POSIX. Search spans both groups, forces them open, and points at matches sitting in unselected presets.

The General row is deleted, not relocated. The default keeps two surfaces that can still act on it — the roster section's make-default beside the visible roster, and the new-session chip for the session about to start — so ui-agent-preset drops the row, its menu, and the write/writability half of its settings store, which slims to the display roster the header label reads.

Alternatives considered

Render every preset as its own always-open section. Four shipped presets already put ~100 rows behind the fold; the switcher keeps one composition in view while the per-row provider details and the search pointers preserve the cross-scope answer the all-at-once layout was buying.

Keep file-state gates unevaluated (conditional until first mount). Honest but it re-created the misleading reading this change removes: on a cold host the default preset's tool-bash read as "conditional" and its host row fell back to plain "disabled" until the first session mounted the preset.

A structured composition viewer in the Agent presets section. A second home for the same rows; the section keeps its raw-YAML viewer for authors and the plugin list owns the structured view.

Enable/disable toggles in the same change. Writing a row's disabled back into a custom preset's agent.cordis.yml needs comment-preserving partial YAML edits, applies-to-new-sessions messaging, and a copy-then-edit path for shipped presets — deliberately its own change; this one is read-side truth.

Consequences

Searching "bash" now answers the question that motivated the change in one screen: enabled in the standard preset, provided per session where the global plane disabled it, plainly disabled only where nothing enables it. The wire snapshot's row enablement is the union boolean | 'conditional' with the gate expression beside it, and the settings-chrome goldens pin the grouped layout. ui-agent-preset loses AgentPresetRow and PresetMenu; the settings.agentPreset locale namespace declaration moved to the plugin entry, and the settings-chrome English scenario probes locale resolution through the nav label instead of the deleted row.