fix(web): compose a forked session, and give the shell realm its provider

Two consequences of moving the agent plane behind presets, both invisible
until the host plane stopped carrying model-facing rows.

`sessions.fork` built its child with a bare `installTarget` and a `meta`
without `agentPreset`. That was harmless while every tool sat in the host
plane — the child inherited them for free. It now comes up with an EMPTY
tool set. The child composes the parent's preset instead, for the same
reason a resumed session keeps its own: the seeded history was produced
under those tools.

`bashEnv` lives in its own `dsh-bash-env` row rather than inside
`tool-bash`, so a preset that isolates the realm must compose the provider
beside its consumer; the host row is disabled here like every other
model-facing one. Nothing outside the agent plane injects `bashEnv`, so it
stays per-session.
This commit is contained in:
Yichen Jiang
2026-08-05 15:40:32 +08:00
parent 3d68185480
commit 25b6381c84
5 changed files with 53 additions and 1 deletions

View File

@@ -22,6 +22,11 @@
isolate:
bashEnv: true
config:
# The registry and its consumer share the realm: a consumer left outside
# would resolve the host's `bashEnv`, which this plane no longer provides.
- id: bash-env
name: '@deepseek-ai/dsh-bash-env'
- id: tool-bash
name: '@deepseek-ai/dsh-tool-bash'

View File

@@ -40,6 +40,11 @@
isolate:
bashEnv: true
config:
# The registry and its consumer share the realm: a consumer left outside
# would resolve the host's `bashEnv`, which this plane no longer provides.
- id: bash-env
name: '@deepseek-ai/dsh-bash-env'
- id: tool-bash
name: '@deepseek-ai/dsh-tool-bash'

View File

@@ -154,6 +154,36 @@ describe('the shipped Web composition', () => {
})
})
describe('a forked session', () => {
it('inherits the composition its seeded history was produced under', async () => {
const parent = await ctx.agents.create({
sessionId: SessionId('preset-fork-parent'),
meta: { agentPreset: 'core-web' },
setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'core-web').then(() => undefined),
})
const inherited = parent.agent.session.header.agentPreset
const child = await ctx.agents.create({
sessionId: SessionId('preset-fork-child'),
meta: {
parentSession: SessionId('preset-fork-parent'),
seedLength: 0,
...inherited === undefined ? {} : { agentPreset: inherited },
},
setup: agentCtx => ctx.agentPresets.mount(agentCtx, inherited).then(() => undefined),
})
try {
// Composing nothing would leave the child empty: this layer moved every
// model-facing row out of the host plane, so there is nothing to inherit
// for free any more.
expect(toolNames(ctx, child.agent)).toEqual(toolNames(ctx, parent.agent))
expect(toolNames(ctx, child.agent).length).toBeGreaterThan(0)
} finally {
await child.dispose()
await parent.dispose()
}
})
})
describe('a session keeps the preset it was created with', () => {
it('refuses to adopt a live session under a different preset', async () => {
const handle = await ctx.agents.create({

View File

@@ -201,6 +201,9 @@
# absent from a surface overlay would silently reappear the day someone reorders
# the composition.
- id: bash-env
disabled: true
- id: tool-bash
disabled: true

View File

@@ -1898,6 +1898,12 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
})
}
const childId = `session-${randomUUID()}` as SessionId
// The child inherits the parent's composition for the same reason a
// resumed session keeps its own: the seeded history was produced under
// those tools, and composing anything else would strand the tool calls
// it already carries. Now that no model-facing row sits in the host
// plane, composing nothing would leave the child with no tools at all.
const forkComposition = await composeAgent(source.header.agentPreset)
try {
await ctx.agents.create({
sessionId: childId,
@@ -1906,9 +1912,12 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
...source.header.cwd === undefined ? {} : { cwd: source.header.cwd },
parentSession: source.id,
seedLength: cut,
...forkComposition.agentPreset === undefined
? {}
: { agentPreset: forkComposition.agentPreset },
},
agentOptions,
setup: installTarget,
setup: forkComposition.setup,
})
} catch (error: unknown) {
return err(request, {