From 29f293675a5e0b1dc7dadc528fed40f7401b3dc3 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Thu, 23 Jul 2026 20:08:33 +0800 Subject: [PATCH] fix(host): opt in to model titles from web --- apps/cli/README.md | 2 +- apps/cli/src/web.ts | 1 + packages/host/runtime/README.md | 8 +++---- packages/host/runtime/src/boot.ts | 11 ++++++--- .../host/runtime/tests/host-runtime.spec.ts | 23 ++++++++++++++++--- 5 files changed, 34 insertions(+), 11 deletions(-) diff --git a/apps/cli/README.md b/apps/cli/README.md index 87f5e670e3..459fe9f301 100644 --- a/apps/cli/README.md +++ b/apps/cli/README.md @@ -10,7 +10,7 @@ The TUI surface: - tells the agent where its own source lives: after boot it adds a prompt section naming this harness checkout, resolved from the launcher's real path so it holds under a PATH symlink and an arbitrary cwd, so the self-referential `cordis` toolset can read and modify it; - applies the personal overlay from `~/.dsh` (see [app-boot's Personal config](../../packages/ui/app-boot/README.md#personal-config)): `.env` fills environment gaps (ambient > project `.env` > personal `.env`), `config.yaml` patches the booted tree. -The Web surface treats its invoking directory as the default project and loads applicable `AGENTS.md`/`CLAUDE.md` instructions into each agent-loop request prefix with a 65,536-byte render budget. +The Web surface treats its invoking directory as the default project, loads applicable `AGENTS.md`/`CLAUDE.md` instructions into each agent-loop request prefix with a 65,536-byte render budget, and opts into first-message model titles. The headless surface retains deterministic fallback titles without making the auxiliary title-model request. ## Install (developer machine) diff --git a/apps/cli/src/web.ts b/apps/cli/src/web.ts index 66a99bb577..917e4e137b 100644 --- a/apps/cli/src/web.ts +++ b/apps/cli/src/web.ts @@ -40,6 +40,7 @@ export async function runWeb(argv: string[]): Promise { boot: { persistenceRoot: './.sessions', workspaceContext: { maxBytes: 65_536 }, + sessionTitleLlm: true, }, }) diff --git a/packages/host/runtime/README.md b/packages/host/runtime/README.md index edef75ee69..ed006c8bcb 100644 --- a/packages/host/runtime/README.md +++ b/packages/host/runtime/README.md @@ -1,6 +1,6 @@ # @deepseek-ai/dsh-host-runtime -Host runtime assembly for `dsh`: `bootHost` composes the core plugin spine (LLM service + DeepSeek adapter, sessions with JSONL persistence, immediate fallback titles and first-message model summaries, system prompt, tools, agents, agent loop, workspace instructions, local bash), `createApiProxy` implements the [`dsh-host-apiproxy`](../apiproxy/README.md) contract over that composition, and `startHost` is the one-step shell seam returning `{ api, handler, defaults, ctx, dispose }`. +Host runtime assembly for `dsh`: `bootHost` composes the core plugin spine (LLM service + DeepSeek adapter, sessions with JSONL persistence and immediate fallback titles, optional first-message model summaries, system prompt, tools, agents, agent loop, workspace instructions, local bash), `createApiProxy` implements the [`dsh-host-apiproxy`](../apiproxy/README.md) contract over that composition, and `startHost` is the one-step shell seam returning `{ api, handler, defaults, ctx, dispose }`. Which plugins mount and with what defaults is decided only here — shells must not `ctx.plugin` to alter the assembly. `RunningHost.ctx` is a formal seam with exactly two sanctioned uses: mounting protocol front-door plugins (e.g. a future `dsh acp`) and headless session-event subscription; consuming clients must not bypass `api` through it. @@ -14,7 +14,7 @@ Which plugins mount and with what defaults is decided only here — shells must | `model` | `'deepseek-v4-flash'` | Default model id, same single source as `provider`. | | `cwd` | `process.cwd()` | Default project directory for a session whose create request omits `cwd`. | | `sessionTitle` | 5 words / 40 fallback bytes / 80 accepted bytes | Deterministic fallback and accepted-title limits. | -| `sessionTitleLlm` | 5 words / 10 CJK chars / 4,096 input bytes / 64 output tokens / 60 s | First-message model-title policy. An omitted route inherits the logged main-request provider and model. | +| `sessionTitleLlm` | disabled | `true` enables the 5-word / 10-CJK-character, 4,096-input-byte, 64-output-token, 60-second first-message policy; an explicit config overrides it. An omitted route inherits the logged main-request provider and model. | ## ApiProxy implementation notes @@ -22,11 +22,11 @@ Unary methods take the narrow `RpcRequest

` and echo `request.rpcId`; a prompt ## Model Experience -Indirectly, through the non-blocking first-message title request owned by [`dsh-session-title-llm`](../../session-title/session-title-llm/README.md), the provider/model defaults injected into created and resumed agents, and the other model-facing plugins `bootHost` mounts. When `workspaceContext` is enabled, each agent-loop instance freezes the applicable workspace instructions into its logged request prefix; the owning package documents the exact [model-visible framing](../../context/workspace-context/README.md#prompt-shape). +Indirectly, through the non-blocking first-message title request owned by [`dsh-session-title-llm`](../../session-title/session-title-llm/README.md) when `sessionTitleLlm` is enabled, the provider/model defaults injected into created and resumed agents, the other model-facing plugins `bootHost` mounts, and the logged [workspace-instruction prefix](../../context/workspace-context/README.md#prompt-shape) when `workspaceContext` is enabled. #### KV Cache effect -No main-request invalidation; the auxiliary title request has its own cache behavior and the conversation prefix remains unchanged. +No main-request invalidation; when enabled, the auxiliary title request has its own cache behavior and leaves the conversation prefix unchanged. ## Known Limitations and Deferred Work diff --git a/packages/host/runtime/src/boot.ts b/packages/host/runtime/src/boot.ts index 514d1be174..98d31e8dda 100644 --- a/packages/host/runtime/src/boot.ts +++ b/packages/host/runtime/src/boot.ts @@ -70,8 +70,8 @@ export interface BootHostOptions { model?: string /** Deterministic fallback-title limits. */ sessionTitle?: SessionTitleConfig - /** First-message model-title policy; omitted provider/model inherit the session's logged main-request route. */ - sessionTitleLlm?: SessionTitleLlmConfig + /** Opt-in first-message model-title policy; `true` selects host defaults and an explicit config overrides them. */ + sessionTitleLlm?: true | SessionTitleLlmConfig /** * Default project directory for sessions created without an explicit cwd * (defaults to the host process working directory). A session's cwd is its @@ -116,7 +116,12 @@ export async function bootHost(options: BootHostOptions): Promise { await ctx.plugin(LlmService) await ctx.plugin(SessionStore) await ctx.plugin(SessionTitleService, options.sessionTitle ?? DEFAULT_SESSION_TITLE_CONFIG) - await ctx.plugin(SessionTitleFirstMessageLlm, options.sessionTitleLlm ?? DEFAULT_SESSION_TITLE_LLM_CONFIG) + if (options.sessionTitleLlm !== undefined) { + await ctx.plugin( + SessionTitleFirstMessageLlm, + options.sessionTitleLlm === true ? DEFAULT_SESSION_TITLE_LLM_CONFIG : options.sessionTitleLlm, + ) + } await ctx.plugin(SystemPrompt, { persona: '' }) await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) diff --git a/packages/host/runtime/tests/host-runtime.spec.ts b/packages/host/runtime/tests/host-runtime.spec.ts index be6626b852..ae8e889610 100644 --- a/packages/host/runtime/tests/host-runtime.spec.ts +++ b/packages/host/runtime/tests/host-runtime.spec.ts @@ -104,7 +104,7 @@ afterEach(async () => { async function boot( script: (StreamChunk[] | 'hang')[] = [], sessionTitle?: SessionTitleConfig, - sessionTitleLlm?: SessionTitleLlmConfig, + sessionTitleLlm?: true | SessionTitleLlmConfig, ): Promise { host = await startHost({ boot: { @@ -188,6 +188,23 @@ describe('bootHost / startHost', () => { expect(requestText).toContain('Instructions from: AGENTS.md') expect(requestText).toContain('host-workspace-context-probe') }) + + it('keeps model title generation disabled when sessionTitleLlm is omitted', async () => { + const running = await boot([textResponse('pong')]) + const { api, ctx } = running + const { sessionId } = expectOk(await api.sessions.create(request({}))) + const agent = ctx.agents.get(sessionId) as Agent + const idle = waitForIdle(ctx, agent) + expectOk(await api.sessions.prompt(request({ + sessionId, + mode: 'queue' as const, + content: [{ type: 'text' as const, text: 'Explain durable session titles.' }], + }))) + await idle + + expect((await ctx.sessionTitle.refresh(agent.session))?.source).toEqual({ kind: 'fallback' }) + expect(agent.session.events.some(event => event.type === 'session/title-llm-request')).toBe(false) + }) }) describe('host.describe', () => { @@ -218,7 +235,7 @@ describe('sessions.create / list', () => { describe('sessions.prompt / cancel', () => { it.each([ - { name: 'host default', config: undefined, target: '5 words', maxTokens: 64 }, + { name: 'host default', config: true, target: '5 words', maxTokens: 64 }, { name: 'configured policy', config: { @@ -233,7 +250,7 @@ describe('sessions.prompt / cancel', () => { }, ] satisfies { name: string - config: SessionTitleLlmConfig | undefined + config: true | SessionTitleLlmConfig target: string maxTokens: number }[])('replaces the fallback with a model-backed first-message title using the $name', async ({ config, target, maxTokens }) => {