fix(web): return every service a host row injects, and run the browser lane

The browser e2e lane had been failing wholesale since this stack moved the
agent plane into presets, and nothing caught it: 34 of 48 files. Two of the
causes are product defects, not test breakage.

`bashEnv` goes back to the host plane. `apps/cli/src/web.ts` injects it to
publish `DSH_WEB_URL`/`DSH_WEB_MODE`, so the earlier note that "nothing outside
the agent plane injects bashEnv" was simply wrong — behind a preset's `shell`
realm those variables reached no shell at all, and a `dsh web` agent could not
find the address of its own interface. This is the same criterion that returned
`subagents`: a host row that injects a service resolves before any session
exists and has no agent to key by, so the service is host-plane. `tool-bash`
consumes the host registry from inside the preset, which works because an
agent context chains to the host; only the reverse is invisible.

`tool-subagent-report` goes back with it. It is not a tool this agent calls: it
registers a continuable SETUP on the host `subagents` singleton, and that list
is not scope-aware. One copy per mounted preset meant every child was handed
`report` once per live session, so the second registration threw and a cold
subagent resume failed with `subagent-not-resumable` — a diagnostic three
layers removed from the cause.

The lane's own composition facts follow. Skill roots resolve inside a preset
now, a subtree include patches cannot reach, so the scaffold pins the roots'
documented environment fallback for its whole lifetime rather than for the boot
— presets mount per session. Without it the developer's real `~/.dsh/skills`
enters replay requests and goldens while CI sees none. The `apps/cli`
composition test pins `storage-json` for the same reason: unpinned it wrote,
and then read back, the developer's own `~/.dsh/storages/`.

Three tests now address through an agent what they used to read off the root
context, because that is where the thing lives: the tool catalog, the skill
registry, and the token meter. The seeded-history projection baseline asserts
the opposite of what it did — a detached session yields a preset-plane
projection only from a durable checkpoint written while it was live, and this
seed was written straight to persistence and never ran.

Goldens re-recorded for the hero's preset chip and the settings nav entry.
This commit is contained in:
Yichen Jiang
2026-08-06 19:14:13 +08:00
parent b8bc89c2f0
commit d247c50c6f
28 changed files with 225 additions and 127 deletions

View File

@@ -50,51 +50,51 @@ Source: [`packages/core/agent-loop/src/index.ts:277`](../../packages/core/agent-
Registry over the deployment's agent presets.
Discovery is unmemoized: `list()` and `resolve()` re-read the roots on every call so a profile authored while the process runs is visible immediately, and a profile deleted underneath a picker disappears from the next read.
Discovery is unmemoized: `list()` and `resolve()` re-read the roots on every call so a preset authored while the process runs is visible immediately, and a preset deleted underneath a picker disappears from the next read.
```ts cordis-catalog
/**
* Every profile the configured roots currently supply.
* @returns the profiles, first-root-wins per id.
* Every preset the configured roots currently supply.
* @returns the presets, first-root-wins per id.
*/
async list(): Promise<AgentPreset[]>
/**
* Resolve one profile by id.
* @param id - the profile id, or `undefined` for {@link defaultId}.
* @returns the resolved profile.
* Resolve one preset by id.
* @param id - the preset id, or `undefined` for {@link defaultId}.
* @returns the resolved preset.
* @throws when no configured root supplies that id.
*/
async resolve(id?: string): Promise<AgentPreset>
/**
* Compose one agent from a profile, installing it under that agent alone.
* Compose one agent from a preset, installing it under that agent alone.
*
* Call from the agent factory's `setup(agentCtx)`; a rejection there rolls
* the agent creation back, so a broken profile never yields a half-composed
* the agent creation back, so a broken preset never yields a half-composed
* session.
* @param agentCtx - the agent's scope context.
* @param id - the profile id, or `undefined` for {@link defaultId}.
* @returns the profile that was mounted, for the caller to record.
* @throws when the profile is unknown or its composition is unusable.
* @param id - the preset id, or `undefined` for {@link defaultId}.
* @returns the preset that was mounted, for the caller to record.
* @throws when the preset is unknown or its composition is unusable.
*/
async mount(agentCtx: Context, id?: string): Promise<AgentPreset>
/**
* Read one profile's composition text.
* @param id - the profile id.
* Read one preset's composition text.
* @param id - the preset id.
* @returns the composition exactly as stored.
* @throws when no configured root supplies that id.
*/
async read(id: string): Promise<string>
/**
* Create or replace a locally authored profile.
* Create or replace a locally authored preset.
*
* The text is shape-checked before it lands, so a save cannot leave a file no
* session could load; it is NOT mounted, so a composition that parses but
* names a missing plugin still fails at the next session that selects it.
* @param id - the profile id, which becomes its directory name.
* @param id - the preset id, which becomes its directory name.
* @param content - the composition text.
* @param metadata - display name and description; clearing both removes the file.
* @throws when the id is unusable, the text is not an entry list, or the
@@ -103,16 +103,16 @@ async read(id: string): Promise<string>
async write(id: string, content: string, metadata: PresetMetadata = {}): Promise<void>
/**
* Delete a locally authored profile.
* @param id - the profile id.
* @throws when the profile is unknown or ships with the deployment.
* Delete a locally authored preset.
* @param id - the preset id.
* @throws when the preset is unknown or ships with the deployment.
*/
async remove(id: string): Promise<void>
/**
* One agent's instance of a service its profile mounted.
* One agent's instance of a service its preset mounted.
*
* A profile publishes services behind `isolate` realms, which are invisible
* A preset publishes services behind `isolate` realms, which are invisible
* outside the group that declares them — including to the host. This is how a
* caller holding the agent reads one anyway: a request that is ABOUT a
* session but arrives from outside it, which is every browser RPC.
@@ -121,8 +121,8 @@ async remove(id: string): Promise<void>
* because injection resolves before any session exists and has no agent to
* key by; such a service belongs on the host plane instead.
* @param agent - the agent whose composition to look inside.
* @param name - the service name as the profile's rows resolve it.
* @returns the agent's instance, or undefined when its profile mounts none.
* @param name - the service name as the preset's rows resolve it.
* @returns the agent's instance, or undefined when its preset mounts none.
*/
serviceFor<K extends string & keyof Context>(agent: { ctx: Context }, name: K): Context[K] | undefined
@@ -138,9 +138,9 @@ serviceFor<K extends string & keyof Context>(agent: { ctx: Context }, name: K):
* therefore restores the previous composition rather than leaving the agent
* with nothing.
* @param agentCtx - the agent's scope context.
* @param id - the profile to compose the agent from instead.
* @returns the profile now installed.
* @throws when the profile is unknown or its composition is unusable; the
* @param id - the preset to compose the agent from instead.
* @returns the preset now installed.
* @throws when the preset is unknown or its composition is unusable; the
* previous composition is restored first.
*/
async recompose(agentCtx: Context, id: string): Promise<AgentPreset>

View File

@@ -485,6 +485,7 @@ flowchart TD
pkg_lsp_local --> pkg_lsp
pkg_lsp_local --> pkg_subprocess
pkg_lsp_local --> pkg_timeout
pkg_agent_presets --> pkg_atomic_write
pkg_agent_presets --> pkg_invariants
pkg_agent_presets --> pkg_paths
pkg_agent_presets --> pkg_scope
@@ -850,7 +851,9 @@ flowchart TD
pkg_client_ui_agent_preset --> pkg_client_runtime
pkg_client_ui_agent_preset --> pkg_client_ui_conversation
pkg_client_ui_agent_preset --> pkg_client_ui_primitives
pkg_client_ui_agent_preset --> pkg_client_ui_settings
pkg_client_ui_agent_preset --> pkg_client_ui_slots
pkg_client_ui_agent_preset --> pkg_client_web_react
pkg_client_ui_agent_preset --> pkg_invariants
pkg_client_ui_command --> pkg_client_connection
pkg_client_ui_command --> pkg_client_locale
@@ -1191,7 +1194,7 @@ flowchart TD
| [`host-directory-picker-browse`](../packages/host/directory-picker-browse) | `host` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/support/invariants) |
| [`host-directory-picker-native`](../packages/host/directory-picker-native) | `host` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/support/invariants) |
| [`lsp-local`](../packages/lsp/lsp-local) | `lsp` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
| [`agent-presets`](../packages/preset/agent-presets) | `preset` | [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`settings`](../packages/settings/settings) |
| [`agent-presets`](../packages/preset/agent-presets) | `preset` | [`atomic-write`](../packages/util/atomic-write), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`settings`](../packages/settings/settings) |
| [`persona`](../packages/preset/persona) | `preset` | [`invariants`](../packages/support/invariants), [`system-prompt`](../packages/core/system-prompt) |
| [`sandbox-local`](../packages/sandbox/sandbox-local) | `sandbox` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox) |
| [`session-projection`](../packages/session-projection/session-projection) | `session-projection` | [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) |
@@ -1264,7 +1267,7 @@ flowchart TD
| [`session-title-first-message-llm`](../packages/session-title/session-title-first-message-llm) | `session-title` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session-title/session-title), [`session-title-llm`](../packages/session-title/session-title-llm) |
| [`agent-loop-testkit`](../packages/support/agent-loop-testkit) | `support` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`tool-ask-user`](../packages/ui/tool-ask-user) | `ui` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`tools`](../packages/core/tools), [`user-interaction`](../packages/ui/user-interaction) |
| [`client-ui-agent-preset`](../packages/client/ui-agent-preset) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
| [`client-ui-agent-preset`](../packages/client/ui-agent-preset) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`invariants`](../packages/support/invariants) |
| [`client-ui-command`](../packages/client/ui-command) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
| [`client-ui-goal`](../packages/client/ui-goal) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`goal`](../packages/goal/goal), [`invariants`](../packages/support/invariants) |
| [`session-reference`](../packages/context/session-reference) | `context` | [`agent`](../packages/core/agent), [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`retention`](../packages/util/retention), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query) |