DSH / Atlas
2026-10-11implementedbug-fix

Process-specific checkout path and Web URL move to runtime context

进程相关的 checkout 路径与 Web URL 改走运行时上下文

`dsh web` wrote two process-specific values directly into the system prompt: the implementation checkout path in the `harness:source` section (four directories above the web-app module) and the Web GUI URL in the `app:web-surface` section (the bound port or `publicUrl`). These operations change them without the user perceiving a system-prompt change: | Operation | Changed value | |---|---| | Every Desktop launch (`--

English

Problem

dsh web wrote two process-specific values directly into the system prompt: the implementation checkout path in the harness:source section (four directories above the web-app module) and the Web GUI URL in the app:web-surface section (the bound port or publicUrl). These operations change them without the user perceiving a system-prompt change:

OperationChanged value
Every Desktop launch (--port 0 picks a random port)Web URL
A different --port, or alternating A/B checkoutsWeb URL, checkout path
npm and Desktop installations sharing one DSH_HOMECheckout path

On resume the loop renders the prompt again. On a route without systemPromptUpdate: 'in-history', such as a hand-declared pi-ai anthropic-messages route, the changed prompt rewrites the surface head node. Claude binds historical thinking-block signatures to the conversation prefix, system prompt included, so the upstream answers 400 (Invalid signature … bound to a different conversation) and the Session cannot continue until the environment is restored. In-history routes do not fail, but every change appends a complete prompt.

Decision

Both values become runtime context and the system prompt keeps only value-free guidance; a Session whose log already holds the earlier format keeps its logged paragraphs.

Ownership

PackageResponsibility
@deepseek-ai/dsh-harness-dev-context (packages/context/harness-dev-context)Both sections, both contexts, and the earlier-format projection; all module-private, exporting only the plugin protocol (name, inject, apply)
@deepseek-ai/dsh-host-webserverWebServer.getOrigin() (the bind address's URL form with protocol and bound port) and bindUrlHost(host)
@deepseek-ai/dsh-web-appThe web-runtime row provides webApp, whose getAdvertisedUrl() returns the configured publicUrl, otherwise the listener origin, the same value as DSH_WEB_URL and the startup URL; the bundle patch mounts the harness-dev-context row
@deepseek-ai/dsh-system-promptCONTEXT_ORDERS.HARNESS_SOURCE = 200 and WEB_SURFACE = 210

The plugin injects systemPrompt, sessionProjections, and webApp and reads those services in code; its patch row carries no configuration expression. The checkout path is four directories above this package's source or built entry, the same depth as the former web-app location, so source, npm, and Desktop installations resolve the same root. The URL is webApp.getAdvertisedUrl(), so a profile that overrides the web-runtime row's publicUrl reaches the model as well. The plugin reaches the webApp type through import type {} from '@deepseek-ai/dsh-web-app'; the import is erased at runtime, so the web-app bundle's dependency on this package remains the only runtime edge.

New Sessions

RegistrationContent
Section harness:source (the HARNESS_SOURCE slot)The runtime context names the DeepSeek Harness implementation checkout. …, the remaining guidance unchanged
Context harness:source (CONTEXT_ORDERS.HARNESS_SOURCE = 200)Current DeepSeek Harness implementation checkout: "<path>".
Section app:web-surface (the WEB_SURFACE slot)You are interacting with the user through the DeepSeek Harness Web GUI whose URL the runtime context names. …, the remaining guidance unchanged
Context app:web-surface (CONTEXT_ORDERS.WEB_SURFACE = 210)Current DeepSeek Harness Web GUI URL: "<url>".

Both contexts are optional (no required). The only complete persona, web-app's minimal preset, also disables optional runtime context, so it still sees neither value. Runtime context follows the existing mechanism: an unchanged value emits no snapshot, a change appends a complete snapshot before the step's user message, and resume compares against the latest logged snapshot, never rewriting a committed message.

Earlier-format Sessions

The plugin registers the host-only session projection harnessDevLegacyPrompt, folding every nonempty system/message in event order into { source, web }: the paragraphs of that prompt that match an earlier template verbatim, or null.

Earlier templateMatching rule
harness:sourceStarts with The DeepSeek Harness implementation checkout is at , ends with the earlier fixed guidance, no line break between
app:web-surfaceStarts with You are interacting with the user through the DeepSeek Harness Web GUI at , ends with . plus the guidance as earlier releases rendered it, frozen as a literal, no line break between

Both section text providers read the projection state of AssembleContext.agent.session: with an earlier paragraph they return it verbatim (interpolate: false), otherwise the current guidance. Diagnostic assemblies without an agent use the current guidance. sessionProjections is a required injection and the plugin registers its own unit, so the state is readable whenever an agent exists.

An earlier-format Session therefore satisfies both requirements:

ScenarioSystem promptRuntime context
First resume after upgrading, same portByte-identical to the log; the loop emits no system/messageAppends a snapshot with both current values
Port, slot, or installation change after upgradingStill the logged earlier paragraphsAppends a snapshot with the new values

The projection computes only on each committed system/message, adding no synchronous read of historical events; its state persists with projection checkpoints. When compaction or a new request series rewrites the head, the rendering still contains the same earlier paragraphs, so the projection state does not change.

Alternatives considered

Move both values to runtime context without keeping earlier paragraphs. The upgrade itself changes the prompt text, so every existing Claude Session rewrites its head and fails with 400 on its first turn after the upgrade.

Offer supportsMidConvoSystemMessages on hand-declared Anthropic routes. The upstream accepts mid-conversation system messages in a live probe, but the pi-ai catalog already supplies that capability for named Anthropic routes and hand-declared routes withhold catalog fields by convention; in-history routes would still append a complete prompt on every value change.

Ask the upstream to drop mismatched thinking blocks (prefix_mismatch_behavior: "drop_block"). It silently loses historical reasoning and covers one vendor.

Freeze the whole system prompt per Session. Legitimate prompt changes such as plan mode, skills, and tool guidance would also be frozen and never reach the model.

Keep the implementation in web-app and app-boot. Development-facing context and earlier-format compatibility would enlarge the public API of two core packages; app-boot's addHarnessSourceSection had web-app as its only caller.

Read webStartup.publicUrl. It carries only the --public-url flag, so a profile override of the web-runtime row's publicUrl would reach DSH_WEB_URL but not the model; the effective value exists only inside web-app, which now provides it as webApp.

Read the head node text from Session history directly. The synchronous arbitrary-history read APIs are deprecated and new calls are prohibited; a projection is the established way to obtain that state after resume.

Consequences

  • A new Session's system prompt no longer changes with the port, slot, or installation; both current values arrive as runtime-context snapshots, and a change appends a few lines.
  • An earlier-format Session keeps the old values of its earlier paragraphs for its whole lifetime; the later Current … runtime context in the same request gives the current values.
  • Only these two paragraphs are protected. When other prompt text changes between releases, routes without in-history updates still rewrite the head.
  • A release without this fix rewrites the head of a new-format Session with the earlier template on resume; both A/B checkouts need this fix before they alternate.
  • app-boot no longer exports addHarnessSourceSection or HARNESS_SOURCE_SECTION; web-app's surfaceContext controls only DSH_WEB_URL, and disabling the harness-dev-context row turns both contexts off.
  • Runtime context in recorded Web e2e Sessions gains two lines; the test scaffold normalizes the URL and checkout path to placeholders.

Testing

  • packages/context/harness-dev-context/tests/harness-dev-context.spec.ts: on a production AgentLoop, an earlier-format Session resumed on another port emits no system/message, the request's system prompt is byte-identical to the log, and runtime context carries the current URL; a second port change keeps this; replace-only and in-history routes each run. When a series consolidation only empties a later version, the next turn keeps the earlier paragraphs. Similar text without the fixed ending or broken across lines receives the current guidance. A new Session keeps its system prompt across a port change and only appends a runtime-context snapshot. Sections carry no values, the context carries the advertised URL, and disposing the plugin fiber removes every registration.
  • packages/host/webserver/tests/webserver.spec.ts: bindUrlHost across address forms; getOrigin() on HTTP and HTTPS listeners, and its errors before binding and for a zoned non-loopback address.
  • packages/bundle/web-app/tests/web-app.spec.ts: webApp.getAdvertisedUrl() returns the listener origin, or the row's configured publicUrl.
  • packages/core/system-prompt/tests/system-prompt.spec.ts: the added context positions are unique integers.

中文

问题

dsh web 曾把两项随进程变化的值直接写进 system prompt:harness:source 段里的实现 checkout 路径(web-app 模块位置上溯四层),以及 app:web-surface 段里的 Web GUI URL(实际监听端口或 publicUrl)。下列操作都会改变它们,而用户感知不到 system prompt 变了:

操作变化的值
Desktop 每次启动(--port 0 取随机端口)Web URL
换 --port、A/B 两个 checkout 交替运行Web URL、checkout 路径
npm 安装与 Desktop 安装共用同一个 DSH_HOMEcheckout 路径

会话恢复时 loop 重新渲染 prompt。对不声明 systemPromptUpdate: 'in-history' 的路由(例如手写的 pi-ai anthropic-messages 路由),变化的 prompt 会改写 surface 头节点。Claude 的历史 thinking 块签名绑定包括 system prompt 在内的对话前缀,前缀一变上游返回 400(Invalid signature … bound to a different conversation),会话在环境恢复原样之前无法继续。声明 in-history 的路由不报错,但每次变化都追加一份完整 prompt。

决定

两项值改为运行时上下文,system prompt 只保留不含值的说明;已经按旧格式写入日志的会话继续沿用日志里的旧段落。

归属

包职责
@deepseek-ai/dsh-harness-dev-context(packages/context/harness-dev-context)两个 section、两个 context、旧格式投影;全部模块私有,只导出插件协议(name、inject、apply)
@deepseek-ai/dsh-host-webserverWebServer.getOrigin()(绑定地址的 URL 形式加协议与实际端口)与 bindUrlHost(host)
@deepseek-ai/dsh-web-appweb-runtime 行提供 webApp,其 getAdvertisedUrl() 返回配置的 publicUrl,否则返回监听器 origin,与 DSH_WEB_URL 和启动 URL 同值;bundle patch 挂载 harness-dev-context 行
@deepseek-ai/dsh-system-promptCONTEXT_ORDERS.HARNESS_SOURCE = 200、WEB_SURFACE = 210

插件 inject 为 systemPrompt、sessionProjections、webApp,在代码里直接读取这些服务;patch 行不写任何配置表达式。checkout 路径取本包源码或构建入口上溯四层,与原 web-app 位置同深度,源码、npm 与 Desktop 安装下结果不变。URL 取 webApp.getAdvertisedUrl(),因此 profile 覆盖 web-runtime 行的 publicUrl 时模型也能看到。插件通过 import type {} from '@deepseek-ai/dsh-web-app' 取得 webApp 类型;该导入在运行时被擦除,因此 web-app bundle 对本包的依赖仍是唯一的运行时依赖。

新会话

注册内容
section harness:source(HARNESS_SOURCE 位)The runtime context names the DeepSeek Harness implementation checkout. …,其余说明不变
context harness:source(CONTEXT_ORDERS.HARNESS_SOURCE = 200)Current DeepSeek Harness implementation checkout: "<path>".
section app:web-surface(WEB_SURFACE 位)You are interacting with the user through the DeepSeek Harness Web GUI whose URL the runtime context names. …,其余说明不变
context app:web-surface(CONTEXT_ORDERS.WEB_SURFACE = 210)Current DeepSeek Harness Web GUI URL: "<url>".

两个 context 都是可选项(不设 required)。唯一的 complete persona(web-app 的 minimal 预设)同时关闭了可选运行时上下文,因此它仍然看不到这两项。运行时上下文按既有机制工作:值不变不发新快照,变化时在本轮 user 消息前追加完整快照,恢复时从会话日志取上一份快照比较,不改写任何已提交消息。

旧格式会话

插件注册仅限宿主的会话投影 harnessDevLegacyPrompt,按事件顺序折叠每个非空 system/message,状态为 { source, web }:该 prompt 中按旧模板逐字匹配到的段落,未匹配为 null。

旧模板匹配规则
harness:sourceThe DeepSeek Harness implementation checkout is at 开头、旧版固定说明结尾、中间不含换行
app:web-surfaceYou are interacting with the user through the DeepSeek Harness Web GUI at 开头、. 加早期版本渲染的说明结尾(冻结为字面量)、中间不含换行

两个 section 的文本提供函数读取 AssembleContext.agent.session 的投影状态:有旧段落时原样返回该段落(interpolate: false),否则返回新说明。没有 agent 的诊断性组装使用新说明。sessionProjections 是插件的必需注入,投影单元由插件自己注册,因此存在 agent 时状态必然可读。

旧会话因此满足两点:

场景system prompt运行时上下文
升级后首次恢复,端口不变与日志逐字相同,loop 不产生 system/message追加含两项当前值的快照
升级后换端口、slot 或安装位置仍是日志里的旧段落追加新值快照

投影只在每次提交的 system/message 上计算,不新增同步读取历史事件的调用;状态随投影检查点持久化。压缩或新请求序列重写头节点时,渲染结果仍含同一旧段落,投影状态不变。

备选方案

只把两项值移到运行时上下文,不保留旧段落。 升级本身会改变 prompt 文本,所有既有 Claude 会话在升级后第一轮改写头节点并报 400。

为手写 Anthropic 路由开放 supportsMidConvoSystemMessages。 上游实测接受对话中途的 system 消息,但 pi-ai 目录已为具名 Anthropic 路由提供该能力,手写路由按既有约定不开放目录字段;而且 in-history 路由每次值变化仍追加一份完整 prompt。

请求上游丢弃不匹配的 thinking 块(prefix_mismatch_behavior: "drop_block")。 会静默丢失历史推理,且只覆盖一个供应商。

会话内冻结整份 system prompt。 plan 模式、技能和工具说明等合法的 prompt 变化也会被冻结,无法到达模型。

留在 web-app 与 app-boot 中实现。 面向开发的上下文与旧格式兼容逻辑会扩大两个核心包的公开 API;app-boot 的 addHarnessSourceSection 当时只有 web-app 一个调用方。

读取 webStartup.publicUrl。 它只携带 --public-url 参数,profile 覆盖 web-runtime 行的 publicUrl 时只会影响 DSH_WEB_URL,到不了模型;生效值只存在于 web-app 内部,现在由它以 webApp 提供。

直接读取会话历史中的头节点文本。 同步读取任意历史事件的 API 已弃用,新调用被禁止;投影是恢复后取得该状态的既定方式。

后果

  • 新会话的 system prompt 不再随端口、slot 或安装位置变化;两项当前值以运行时上下文快照送达,变化时只追加几行。
  • 旧会话在整个生命周期内保留旧段落中的旧值;同一请求里更晚的 Current … 运行时上下文给出当前值。
  • 只保护这两段。其他提示文本在版本之间变化时,不支持 in-history 的路由仍会改写头节点。
  • 不含本修复的旧版本恢复新格式会话时会按旧模板改写头节点;A/B 两个 checkout 都需包含本修复后再交替运行。
  • app-boot 不再导出 addHarnessSourceSection 与 HARNESS_SOURCE_SECTION;web-app 的 surfaceContext 只控制 DSH_WEB_URL,关闭这两项上下文改为禁用 harness-dev-context 行。
  • Web e2e 录制会话里的运行时上下文多出两行;测试脚手架把 URL 与 checkout 路径规范化为占位符。

测试

  • packages/context/harness-dev-context/tests/harness-dev-context.spec.ts:真实 AgentLoop 上,旧格式会话在另一端口恢复时不产生 system/message、请求中的 system prompt 与日志逐字相同、运行时上下文携带当前 URL;再换一次端口仍成立;不支持与支持 in-history 的路由各跑一遍。series 合并只清空后续版本时,下一轮仍沿用旧段落。缺少固定结尾或跨行的相似文本得到新说明。新会话换端口后 system prompt 不变、只追加运行时上下文快照。section 不含值、context 携带公告 URL,销毁插件 fiber 后全部注册消失。
  • packages/host/webserver/tests/webserver.spec.ts:bindUrlHost 各地址形式;getOrigin() 在 HTTP 与 HTTPS 监听上的值,绑定前与带 zone 的非回环地址抛错。
  • packages/bundle/web-app/tests/web-app.spec.ts:webApp.getAdvertisedUrl() 返回监听器 origin,或该行配置的 publicUrl。
  • packages/core/system-prompt/tests/system-prompt.spec.ts:新增的 context 位置唯一且为整数。