fix: address ds-review-bot round — insert-aliasing clones, settlement gates, closure module fallback

- Clone patch lists per generation (boot + composeLive): the include pushes
  insert rows by reference and mutates them in place, so a reused object
  baked user overrides into bundle rows and removal could not revert; the
  built-bin hot-reload e2e now asserts an override AND its removal reverting.
- The headless runner awaits Loader settlement before prompting (its inject
  gate covers only apiProxy/httpServer) and abandons cleanly when the tree
  died during the wait.
- healProfilesModuleFallback walks the app's full dependency+peer closure:
  out-of-tree plugins import seam packages (dsh-compact, dsh-subprocess, ...)
  that only implementations reach, and peers are how seams are declared.
- Profile init writes pnpm-workspace.yaml (nodeLinker: hoisted), not .npmrc
  — pnpm >=10 reads settings from the workspace manifest.
- Web dumps reject boot-only flags instead of printing a tree that differs
  from the same invocation's boot; --port validates at the flag;
  --dump-default-config no longer parses the (possibly broken) user layer;
  trustedHosts flag derivation merges over the composed value instead of
  replacing it; web-runtime gains surfaceContext (headless disables the GUI
  prompt/bash-vars the old -p never mounted); 'node_modules' is a reserved
  profile name; plugin-warning names the recovery step; client AGENTS.md
  registration surfaces point at the web-app bundle.
- Ship session-reference/tmux-context/tool-ask-user as app dependencies for
  terminal front-door patch layers (turtle-ui), same stance as mcp-client.
This commit is contained in:
Turtle
2026-08-06 09:27:44 +08:00
parent 273f27260d
commit 925daf141b
20 changed files with 240 additions and 83 deletions

View File

@@ -1,6 +1,7 @@
# The dsh-headless bundle patch: one-shot task mode over dsh-base +
# dsh-web-app. The web composition stays mounted (the session is observable
# in a browser while it runs); this layer silences the URL line, moves the
# in a browser while it runs); this layer silences the URL line and the
# GUI-orientation surface context (this user is not in the GUI), moves the
# webserver to an OS-assigned port so parallel headless runs never collide,
# and mounts the one-shot runner. The launcher patches the runner's `task`.
@@ -13,6 +14,7 @@
config:
mode: production
printUrl: false
surfaceContext: false
- insert:
- id: headless-runner

View File

@@ -41,6 +41,7 @@
"cordis": "^4.0.0-rc.7"
},
"devDependencies": {
"@cordisjs/plugin-loader": "workspace:^",
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-host-apiproxy": "workspace:^",
"@deepseek-ai/dsh-host-webserver": "workspace:^",

View File

@@ -17,6 +17,8 @@ import { InProcessApiClient, toFetchHandler } from '@deepseek-ai/dsh-host-apipro
// Empty type imports carry the httpServer and agent/status Context merges used below.
import type {} from '@deepseek-ai/dsh-host-webserver'
import type {} from '@deepseek-ai/dsh-agent'
// Empty type import carries the loader Context merge for the settlement await.
import type {} from '@cordisjs/plugin-loader'
import type { MuxFrame } from '@deepseek-ai/dsh-host-apiproxy/api'
import type { RpcRequest, RpcResponse } from '@deepseek-ai/dsh-host-apiproxy/api/rpc'
import type { SessionId } from '@deepseek-ai/dsh-session'
@@ -136,13 +138,20 @@ export function apply(ctx: Context, config: Config): void {
// Fire-and-forget by design: the run outlives plugin activation, and every
// failure path inside ends in io.exit, not a rejection.
void (async () => {
// The Loader mounts sibling rows concurrently and this plugin's inject
// gate covers only apiProxy/httpServer; prompting before the agent loop,
// adapters, and tools settle would fail the turn on a half-mounted tree.
// The old launcher ran strictly after settled boot — preserve that.
// A tree disposed mid-settlement (early SIGTERM) has nothing to run.
await ctx.get('loader')?.await()
if (ctx.get('httpServer') === undefined) return
// The headless session is web-observable while it runs (same composition).
io.stderr.write(`dsh: observing at http://127.0.0.1:${String(ctx.httpServer.port)}\n`)
const api = new InProcessApiClient(toFetchHandler(ctx.apiProxy))
const created = await unwrap(await api.sessions.create({}), io)
// Open the stream before prompting so no frame is lost — kept in this
// order even though in-process delivery has no race, so the code survives
// a move to a remote HTTP carrier unchanged.
// Open the stream before prompting so no frame is lost. The quiescence
// anchor below is an in-process ctx subscription, so a remote-carrier
// port of this runner must replace it with a wire-visible idle signal.
const abort = new AbortController()
const frames = api.events.mux({}, abort.signal)
const idle = new Promise<void>((resolve) => {

View File

@@ -174,6 +174,36 @@ describe('headless runner', () => {
await ctx.fiber.dispose()
})
it('waits for Loader settlement and abandons the run when the tree died during it', async () => {
const ctx = new Context()
let err = ''
let exited = false
ctx.provide('headlessIo', {
stdout: { write: () => true },
stderr: { write: (chunk: string) => { err += chunk; return true } },
exit: () => { exited = true },
} satisfies HeadlessIo)
ctx.provide('apiProxy', scriptedApi([]) as never)
// The webserver is provided by a child fiber whose disposal (early
// SIGTERM during the boot window) removes the service; settlement
// resolves only afterwards, and the runner must abandon rather than
// crash on the torn-down port read.
const webserverFiber = ctx.plugin((childCtx: Context) => {
childCtx.provide('httpServer', { port: 1 } as never)
})
await webserverFiber
let release: () => void
const settlement = new Promise<void>((resolve) => { release = resolve })
ctx.provide('loader', { await: () => settlement } as never)
apply(ctx, { task: 't' })
await webserverFiber.dispose()
release!()
await new Promise(resolve => setTimeout(resolve, 10))
expect(err).toBe('')
expect(exited).toBe(false)
await ctx.fiber.dispose()
})
it('fails loud without the launcher-owned headlessIo seam', () => {
const ctx = new Context()
ctx.provide('apiProxy', scriptedApi([]) as never)

View File

@@ -11,6 +11,9 @@
{
"path": "../../../vendor/cordis"
},
{
"path": "../../../vendor/loader"
},
{
"path": "../../../vendor/schemastery"
},

View File

@@ -34,6 +34,13 @@ export interface Config {
mode: WebMode
/** Print the URL line on activation; a headless layer over this bundle turns it off. */
printUrl: boolean
/**
* Register the model-visible surface context (the `app:web-surface` prompt
* section and the `DSH_WEB_URL`/`DSH_WEB_MODE` bash variables). A one-shot
* layer turns it off: its user is not interacting through the GUI, so the
* orientation text would be false.
*/
surfaceContext: boolean
/**
* LAN IPv4 addresses sampled once by the launcher when the effective bind
* is all-interfaces — the exact snapshot the /api trust fence was
@@ -46,6 +53,7 @@ export interface Config {
export const Config: z<Config> = z.object({
mode: z.union([z.const('production'), z.const('development')]).default('production'),
printUrl: z.boolean().default(true),
surfaceContext: z.boolean().default(true),
lanAddresses: z.array(String).default([]),
})
@@ -104,23 +112,25 @@ export const internals: { resolveDistIndex: () => string } = { resolveDistIndex
*/
export function apply(ctx: Context, config: Config): void {
ctx.plugin(FrontendStatic, { distIndex: internals.resolveDistIndex() })
ctx.inject(['systemPrompt'], (promptCtx) => {
promptCtx.systemPrompt.section({
name: 'app:web-surface',
order: -98,
text: () => webSurfacePrompt(localWebUrl(promptCtx), config.mode),
if (config.surfaceContext) {
ctx.inject(['systemPrompt'], (promptCtx) => {
promptCtx.systemPrompt.section({
name: 'app:web-surface',
order: -98,
text: () => webSurfacePrompt(localWebUrl(promptCtx), config.mode),
})
})
})
ctx.inject(['bashEnv'], (runtimeCtx) => {
runtimeCtx.bashEnv.register({
name: 'web-runtime',
variables: {
[DSH_WEB_URL]: { description: 'Canonical local URL of the DeepSeek Harness Web GUI serving this session.' },
[DSH_WEB_MODE]: { description: 'Web runtime mode: production, or development when the client-plugin HMR receiver is active.' },
},
resolve: () => ({ [DSH_WEB_URL]: localWebUrl(runtimeCtx), [DSH_WEB_MODE]: config.mode }),
ctx.inject(['bashEnv'], (runtimeCtx) => {
runtimeCtx.bashEnv.register({
name: 'web-runtime',
variables: {
[DSH_WEB_URL]: { description: 'Canonical local URL of the DeepSeek Harness Web GUI serving this session.' },
[DSH_WEB_MODE]: { description: 'Web runtime mode: production, or development when the client-plugin HMR receiver is active.' },
},
resolve: () => ({ [DSH_WEB_URL]: localWebUrl(runtimeCtx), [DSH_WEB_MODE]: config.mode }),
})
})
})
}
if (config.printUrl) {
// The URL line is a readiness signal: supervisors (and the keyless CLI
// smoke) RPC as soon as they observe it, so it must not print while

View File

@@ -69,7 +69,7 @@ describe('web-app runtime glue', () => {
},
} as never)
const log = vi.spyOn(console, 'log').mockImplementation(() => {})
apply(ctx, new Config({ mode: 'development', printUrl: true, lanAddresses: ['192.168.1.5'] }))
apply(ctx, new Config({ mode: 'development', printUrl: true, surfaceContext: true, lanAddresses: ['192.168.1.5'] }))
await ctx.plugin(SystemPrompt, { persona: '' })
// Settle the injected registrations.
await new Promise(resolve => setTimeout(resolve, 0))
@@ -90,7 +90,7 @@ describe('web-app runtime glue', () => {
const ctx = new Context()
ctx.provide('httpServer', fakeHttpServer().server)
const log = vi.spyOn(console, 'log').mockImplementation(() => {})
apply(ctx, new Config({ mode: 'production', printUrl: false, lanAddresses: [] }))
apply(ctx, new Config({ mode: 'production', printUrl: false, surfaceContext: true, lanAddresses: [] }))
await ctx.plugin(SystemPrompt, { persona: '' })
await new Promise(resolve => setTimeout(resolve, 0))
expect(log).not.toHaveBeenCalled()
@@ -100,12 +100,32 @@ describe('web-app runtime glue', () => {
await ctx.fiber.dispose()
})
it('skips the surface context when disabled (the one-shot layer): no prompt section, no bash variables', async () => {
stageDist()
const ctx = new Context()
ctx.provide('httpServer', fakeHttpServer().server)
const contributions: BashContribution[] = []
ctx.provide('bashEnv', {
register: (contribution: BashContribution) => {
contributions.push(contribution)
return () => {}
},
} as never)
apply(ctx, new Config({ mode: 'production', printUrl: false, surfaceContext: false, lanAddresses: [] }))
await ctx.plugin(SystemPrompt, { persona: '' })
await new Promise(resolve => setTimeout(resolve, 0))
const assembly = await ctx.systemPrompt.assemble()
expect(assembly.sections.some(entry => entry.name === 'app:web-surface')).toBe(false)
expect(contributions).toEqual([])
await ctx.fiber.dispose()
})
it('prints the loopback-only URL line when no LAN snapshot exists', async () => {
stageDist()
const ctx = new Context()
ctx.provide('httpServer', fakeHttpServer().server)
const log = vi.spyOn(console, 'log').mockImplementation(() => {})
apply(ctx, new Config({ mode: 'production', printUrl: true, lanAddresses: [] }))
apply(ctx, new Config({ mode: 'production', printUrl: true, surfaceContext: true, lanAddresses: [] }))
await new Promise(resolve => setTimeout(resolve, 0))
expect(log).toHaveBeenCalledWith('dsh web: http://127.0.0.1:4567')
await ctx.fiber.dispose()
@@ -121,7 +141,7 @@ describe('web-app runtime glue', () => {
const settlement = new Promise<void>((resolve) => { release = resolve })
settled.provide('loader', { await: () => settlement } as never)
const log = vi.spyOn(console, 'log').mockImplementation(() => {})
apply(settled, new Config({ mode: 'production', printUrl: true, lanAddresses: [] }))
apply(settled, new Config({ mode: 'production', printUrl: true, surfaceContext: true, lanAddresses: [] }))
await new Promise(resolve => setTimeout(resolve, 0))
expect(log).not.toHaveBeenCalled()
release!()
@@ -140,7 +160,7 @@ describe('web-app runtime glue', () => {
let releaseTorn: () => void
const tornSettlement = new Promise<void>((resolve) => { releaseTorn = resolve })
torn.provide('loader', { await: () => tornSettlement } as never)
apply(torn, new Config({ mode: 'production', printUrl: true, lanAddresses: [] }))
apply(torn, new Config({ mode: 'production', printUrl: true, surfaceContext: true, lanAddresses: [] }))
await child.dispose() // the httpServer service goes away
releaseTorn!()
await new Promise(resolve => setTimeout(resolve, 0))
@@ -156,7 +176,7 @@ describe('web-app runtime glue', () => {
const { server } = fakeHttpServer()
Object.defineProperty(server, 'port', { get: () => undefined })
ctx.provide('httpServer', server)
apply(ctx, new Config({ mode: 'production', printUrl: false, lanAddresses: [] }))
apply(ctx, new Config({ mode: 'production', printUrl: false, surfaceContext: true, lanAddresses: [] }))
await ctx.plugin(SystemPrompt, { persona: '' })
await new Promise(resolve => setTimeout(resolve, 0))
await expect(ctx.systemPrompt.assemble()).rejects.toThrow('httpServer service missing')