fix(preset): keep the token meter host-plane and name unjoined agents

Moving the agent plane behind presets left two readers on the wrong side of
the host/agent line.

`dsh-token-meter` was disabled on the host and mounted inside each preset's
`compaction` realm, but its three projection units register into the
process-wide `sessionProjections` table. A unit registered from one preset
answers for every session, so whether a `minimal` session showed a context
meter depended on whether some other session had mounted `standard` since
boot, and a process that only ever ran `minimal` showed none. The meter takes
no configuration, keys every fold by Session, and registers no tool or prompt
section, so it returns to the host composition and leaves the presets'
`isolate` map; the realm and `compact-basic` stay, because what a preset
chooses is whether its agent compacts, not whether its tokens are counted.

Nothing named an agent that joined no preset. The join is a scope-parent link,
and without it the tools, prompt-section, and skill views resolve the empty
global layer: the agent publishes, the turn runs, and the model receives
nothing. `AgentPresets` now logs one warning per such agent while a roster is
configured, and the invariant companion fails outright — at
`system-prompt/assemble` rather than at publication, because an unjoined agent
is legal until it addresses a model and `recompose` binds exactly such an
agent. The warning stays advisory: a synchronous `agent/created` throw vetoes
publication, and the ACP bridge, SDK server, and headless bundle all create an
unjoined agent today.

Three limits are recorded rather than fixed: projection key presence is not a
per-session capability signal, a superseded standing generation is never
reclaimed, and a `cordis_mount` temporary plugin belongs to the composition
rather than the session that mounted it.

Fixes #2203
This commit is contained in:
Yichen Jiang
2026-08-10 22:36:06 +08:00
parent cd226191a8
commit 1cfbafab6a
49 changed files with 398 additions and 114 deletions

View File

@@ -106,6 +106,13 @@ async function run(ctx: Context, task: string, io: HeadlessIo): Promise<void> {
if (agents === undefined || defaultModel === undefined || sessions === undefined) return
const selection = defaultModel.currentSelection()
// This bundle composes no preset roster, so the model-facing rows sit in the
// host plane and the agent reads them from the global registry layer. A
// deployment that DOES configure `dsh-agent-presets` has to join one here
// first: registration visibility inherits only along the `dsh-scope` parent
// chain, and `AgentPresets.mount()` is the only thing that links an agent to
// it, so an agent created without that link reads an empty global layer and
// reaches the model with no tools at all.
const { agent } = await agents.create({
sessionId: SessionId(`session-${randomUUID()}`),
meta: { cwd: process.cwd() },

View File

@@ -270,8 +270,17 @@
- id: plan-mode
disabled: true
- id: token-meter
disabled: true
# The token METER stays on the host plane; only the compaction backend that
# reads it moves. It takes no configuration at all, keys every fold by Session,
# and registers no tool or prompt section, so one host instance serves every
# preset exactly as before. It also owns the `tokenUsage`/`contextPressure`/
# `contextBreakdown` projection units, and the projection registry is
# process-wide rather than scope-layered: registered from inside a preset, the
# browser's context meter would appear for a session whose own composition
# mounts no meter as soon as some OTHER session mounted one, and disappear
# entirely in a process that only ever ran `minimal`. That is the same criterion
# `tasks` and `goals` are read by — a Service the host and every preset can see
# belongs to the plane both can see.
- id: compact-basic
disabled: true