Repository navigation
Conversation
On routes whose model reads a later system message as the effective prompt, dsh does not rewrite the first system node when the rendered prompt changes: SystemPromptProjection appends a complete system message after the cached history, and the Messages serializer ships it as a plain-text role:"system" message after the user turn. The newest such message is the prompt in force. ACP folded it like any plain-text message, so one compress range covering it left the model on the stale head prompt until the text changed again. Add a fifth durable identity to the dsh lane classifier, durable/system-prompt (dshSystemPromptPath), under the existing durable/* latest-per-identity default: the newest copy is pinned, superseded copies stay foldable. The criterion is the role (no text marker exists for a prompt): a non-empty plain-text system-role core that is neither bili's own summary carrier nor a structured system. Structured systems keep the kernel's hard pin; the classifier reuses hasUnrecoverableMediaPayload so the two rules cannot overlap and a tool delta arriving after the prompt cannot take its latest slot. The identity is consulted before the twelve marker classifiers. Their role gate only refuses the assistant role, so a system prompt that quoted a carrier marker used to project to that carrier's identity and take the latest slot from the real user-channel instance. Host layer only: no change under kernel/, no config field, no wire or persistence change. Other lanes keep folding plain-text in-history systems. oneShotCarrierPaths (drift accounting) is unchanged.
… limits Follow-up to the durable/system-prompt identity, from review. Tests: - Messages wire end-to-end: assert the role the kept prompt is forwarded with. Once the fold removes the assistant that followed it, the kernel's position heal re-voices a plain system as user, so the text survives and the system role does not. Pinned as a known limit; a kernel that relocates instead of demoting flips the assertion. - Lane wiring: drop the "registry keys are exactly [dsh]" pin (another lane registering a classifier is not this test's business) and assert instead that no other lane projects a plain-text system. - Drift registry: pin that system-role text containing a carrier marker is still accounted by marker there, as before the identity existed. - The chat-completions scenario cites the llm-pi-ai wire test it mirrors. Comments only in src/durable-message-guards.ts: where a developer message carrying text would share the identity (llm-pi-ai splits the text off on chat-completions and responses), and that the drift chain has no system-role step. Devlog: no retroactive unfold for prompts already covered by a block from an earlier build, the token cost of a never-folded copy, and the corrected red count on master (9 of 14).
There was a problem hiding this comment.
Note
Copilot was unable to complete this review at the selected tier due to model policy or plan restrictions.
🟡 Changes recommended
A moderate active issue remains in src/durable-message-guards.ts where using msg.text.trim() as a whitespace check can unnecessarily allocate large strings (e.g., ~45K chars) and create avoidable memory churn in a repeatedly-invoked path, so a non-allocating whitespace test is needed.
1 open issue
What changed in this PR
Fix a dsh-lane correctness bug where the latest effective in-history system prompt could be folded into a summary, causing requests to run with a stale head prompt until the prompt text changes again.
Changes:
- Add a new dsh durable identity
durable/system-promptto preserve the newest plain-text in-history system prompt while keeping superseded copies foldable. - Prioritize this identity ahead of marker-based durable carriers to prevent carrier-slot takeover.
- Add regression coverage for classifier behavior, compression geometry, and real folding on both Messages and chat-completions wires, plus update inline docs/devlogs.
| File | Description |
|---|---|
| tests/dsh-system-prompt-durable.test.ts | Adds a new regression test suite validating that the newest in-history system prompt is preserved through classification and real compression for Messages and chat-completions wires. |
| src/durable-message-guards.ts | Introduces dshSystemPromptPath (durable/system-prompt) and wires it early in the dsh classifier so the latest plain-text system prompt is protected from folding. |
| src/config.ts | Updates configuration documentation comments to include the new durable/system-prompt identity alongside existing dsh durable carriers. |
| devlog/2026-11-12_dsh-durable-system-prompt/WORKLOG.md | Documents the investigation, implementation notes, testing performed, and known limitations for the dsh durable system-prompt change. |
| devlog/2026-11-12_dsh-durable-system-prompt/REQ.md | Captures the requirements, acceptance criteria, and explicit non-goals for preserving the latest in-history system prompt in dsh. |
Review effort: Balanced
Comment on lines
+200
to
+205
| export function dshSystemPromptPath(msg: CoreMessage): string | undefined { | ||
| return msg.role === "system" && msg.contentType === "text" && typeof msg.text === "string" && | ||
| msg.text.trim().length > 0 && !msg.text.startsWith(SUMMARY_HEADER) && !hasUnrecoverableMediaPayload(msg) | ||
| ? "durable/system-prompt" | ||
| : undefined; | ||
| } |
Author
There was a problem hiding this comment.
已改:换成 /\S/.test(msg.text),不再为判空复制整段文本。
dshSystemPromptPath used msg.text.trim().length > 0 only to ask whether the text has any non-whitespace character. trim() allocates a copy of the whole prompt (tens of thousands of characters in the reports that motivated the identity), and the classifier is consulted repeatedly during protection and fold checks. /\S/.test(msg.text) answers the same question (same whitespace set as trim) without the copy. No behavior change; the existing negatives (empty and whitespace-only system nodes) cover it. From review on the PR.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Fixes #6352
问题
在 dsh 车道上,如果 compress 的范围覆盖了最新的历史内系统提示(in-history system prompt),这条提示会被折叠(fold)进摘要(summary)。从那时起,直到提示文本再次变化,模型一直按过期的头部提示运行。没有报错,也没有日志。
这条消息为什么重要:渲染出的系统提示变化时,DeepSeek Harness 并不总是重写第一个 system 节点。在模型把后面的
system消息当作有效提示的路由上(systemPromptUpdate: "in-history"),它会在缓存历史之后追加一条完整的新 system 消息。顶层的system字段保留旧文本,最新的历史内 system 消息才是正在生效的提示。billion-context 把这条消息当作普通历史。
hasUnrecoverableMediaPayload只固定结构化的 system 消息("Plain-text ones … fold like any text message",纯文本的……和普通文本消息一样折叠),而 dsh 的持久(durable)分类器(src/durable-message-guards.ts)认识四种载体(carrier),都在 user 通道上。所以模型发起的 compress 调用和代理(proxy)发起的折叠都可能覆盖到它。这个 PR 恢复了什么,没恢复什么
改动之后,最新提示的文本总会留在转发的请求里。它的角色取决于 wire:
system。healIllegalSystemPositions,[Bug][anthropic] 0.1.205: 400 "role 'system' must precede an 'assistant' message" still occurs with #4396 present (mid-conversation system + tool_addition, cf. #4400) #4457/kernel authorization request: heal must enforce the FOLLOW side of Anthropic's mid-conversation system position rule (#5432) #5438)会把这个位置上的纯文本 system 重新标成user。提示文本能到达模型,但是以user消息的形式。端到端测试固定了这个角色,所以内核一旦改动,这里会有反应。要保持 system 角色需要改内核,本 PR 不做(见「未覆盖的部分」)。这个形态从哪里来(deepseek-harness 源码,MIT,标签
dsh-v0.2.2-rc.2;与 rc.1 相比没有变化)packages/core/agent-loop/src/runtime-context.ts,SystemPromptProjection.project:"A capable continuing series appends changed nonempty text after the cached history."(一个有能力的连续序列会把变化后的非空文本追加在缓存历史之后。)只有在不支持的路由、提示被清空、或者文本不同的新请求序列这三种情况下,才会重写第一个节点。packages/llm/llm-deepseek/src/serialize.ts:在历史内路由上,非开头的 system 消息会变成{ role: "system", content: [{ type: "text", text }] },排在 user / tool-result 轮之后、下一条 assistant 之前;只有位于开头的 system 才会成为顶层的system。它的tests/adapter.spec.ts固定了结果:提示变化后,body.system仍是first prompt,messages里带着{ role: "system", content: [{ type: "text", text: "second prompt" }] };清空再恢复之后,请求里唯一的提示就是那条历史内消息。packages/llm/llm-pi-ai/src/models.ts(conversationUpdates)和context.ts:对声明了supportsMidConvoSystemMessages的目录模型,同样是历史内模式。它的tests/conversation-updates.spec.ts固定了各 wire 的形态:Anthropic Messages 和 chat-completions 用role: "system",responses API 用role: "developer"加字符串内容。SystemPromptProjection(system)和agent.ts里的工具注册表更新(developer;只有tool-addition/tool-removal块,没有文本)。复现
tests/dsh-system-prompt-durable.test.ts,本 PR 新增。在 masterc26c31ac上它的 14 个测试里有 9 个失败(新导出被桩成返回undefined,以便文件能加载);通过的 5 个是分类器的反例,以及对必须保持不变的行为的固定。llm-deepseek形态的 Messages 请求体(头部提示在顶层字段,两次历史内更新,一条 tool-delta system,一个实时请求),经anthropicToCore和applyCompression,使用 dsh 车道配置。在 master 上,最新提示的 id 落在新建的块里面。startServer跑八轮,两次历史内提示更新,经/__bili/plugin/tool做一次范围覆盖这两条的compress,再跑一轮。在 master 上,转发给上游的请求里不再有最新的提示,Messages wire 和 chat-completions wire 都是如此。改动
在 dsh 车道分类器里增加第五种持久身份:
durable/system-prompt(dshSystemPromptPath),归现有的durable/*按身份保留最新一份的默认规则管。最新的一份被固定;被后来的副本顶替的(superseded)副本和普通历史一样折叠。判定条件:
role === "system",内容是文本,非空,并且hasUnrecoverableMediaPayload而不是重新写一遍,所以这条规则和内核对 tool-delta / 仅指令 system 的硬固定在构造上就是不相交的,在提示之后到达的 tool delta 也不可能占掉提示的最新位置;system,KDD feat: bili CLI + debug in config + README rewrite #7)。分类器按角色匹配,而不是按文本标记,因为系统提示没有稳定的标记:它是任意的宿主和插件文字。这里角色是比标记更强的证据。它由宿主的序列化器打上;人的输入总是走 user 通道,模型输出总是走 assistant 通道,所以用户消息和模型回声(#2766)都产生不出它。头部提示在每个编解码器里都会被提到折叠空间之外,因此 dsh 车道里 system 角色的核心消息就是历史内的那种。
新身份在那十二个标记分类器之前被查询。它们的角色门只拒绝
assistant,所以在 master 上,引用了某个载体标记的系统提示(比如一份讲解 skill 目录标签的提示)会被投影成那个载体的身份,并且因为更新,从真正的 user 通道载体手里抢走最新位置。由角色决定的身份排在前面之后,这条提示保有自己的固定,真正的载体也保有自己的位置(测试:"the four durable carriers keep their own latest slot next to the prompt",在 master 上为红)。行为变化,仅限 dsh 车道(旧 → 新)
durable/system-prompt不变的部分
oneShotCarrierPaths/dshCarrierRawMarkers(预期重基线的核算):没有改动;被顶替的提示副本的核算与以前完全一致。那条链仍然没有 system 角色这一步,所以含有载体标记的 system 角色文本,在位置链里算提示,在漂移核算里算那个标记的载体。这是已有的漂移处理方式,源码里有注明,测试文件里也有固定。durable/…模式,仍然没有默认值;durable/system-prompt可以像另外四个一样被点名(已测试)。Config.messagePathOf,内核在每个判定可折叠性的地方都会查它(compress、unit-projection、recommend、absorb),所以模型发起的折叠和代理发起的折叠由同一条规则覆盖。没有改动任何 wire、工具注入或摘要载体的代码。按 wire 的适用范围
规则作用在内核核心的角色上,所以与 wire 无关。各 wire 的证据:
llm-deepseek、llm-pi-ai)和下面的现场测量。编解码层测试和端到端测试。llm-pi-ai;端到端测试用的是它的conversation-updates.spec.ts所固定的位置),以及 Non-leading system messages are demoted to regular history messages, causing the system prompt to accumulate progressive | 非首位的 system message 被降级成普通历史消息,导致 system prompt 逐步累积 #6193 里的生产报告(那里副本排在数组末尾)。编解码层测试和端到端测试。llm-pi-ai发出{ role: "developer", content });我没有流量样本。只有编解码层的分类器行,没有端到端测试。配置面
没有新字段、环境变量或 CLI 标志。只在现有的
durable/虚拟路径命名空间里多了一个名字,可通过现有的protectedTools/protectedLatestTools模式访问。src/config.ts里protectedLatestTools上方的注释列出了这个身份;gen-config-docs generate没有产生漂移。内核
kernel/**下没有改动。分类器导入了两个现有的内核导出(hasUnrecoverableMediaPayload、SUMMARY_HEADER)。现场证据
一个长期运行的 dsh 会话,九天窗口,只统计结构性数量:143 份历史内系统提示副本;其中 18 到 37 份(两种计数方法分别取交集和并集)在仍是最新副本时被一个新建的块覆盖;按交集计数,这些时段跨越 6,206 个模型请求中的 2,919 个。
相关
tool_additionbut keeps the latertool_removalfor the same tool → 400tool_reference_unresolved#4818 和 PR [PR] EN | kernel: relocate misplaced tool-delta systems instead of demoting them (fixes #4818) #5800(open):同一类问题(折叠前后历史内 system 消息会怎样),但消息类别和层次不同。它们针对的是结构化 tool-delta system 和 Messages wire 上内核的位置修复([Bug][anthropic][claude-code] 0.1.208: the system-position heal drops atool_additionbut keeps the latertool_removalfor the same tool → 400tool_reference_unresolved#4818 来自 Claude Code;[PR] EN | kernel: relocate misplaced tool-delta systems instead of demoting them (fixes #4818) #5800 还引用了一份 dsh 现场报告,说只含添加的载体被丢掉)。本 PR 针对的是纯文本提示和宿主的持久分类,并明确把 tool-delta system 留给内核规则。coreToAnthropic把system以user的角色发出。上面说的 Messages wire 上的角色丢失是同一类问题。未覆盖的部分
prune丢弃已覆盖的 id 时不查保护,之后的合并会把已被消耗的块的覆盖范围(range)整个接过来(用一次性探针检查过:旧版本折叠,然后在新配置下渲染并合并;两种情况下提示都保持隐藏)。这样的会话要等 dsh 发出下一次提示更新才能恢复。llm-pi-ai在 chat-completions 和 responses 上就是这样做的,即使该消息同时带有工具块(它的 spec:{ role: "system", tools }后面跟着{ role: "system", content: "…" });在 Messages wire 上只有纯文本的 developer 消息会这样(文本加工具块仍是一条结构化 system)。验证
Node 22.23.3,Windows,隔离的家目录,出站网络被阻断(任何一次运行都没有记录到被阻断的请求)。
tests/dsh-native.test.ts,该文件在未修改的 master 上同样时序不稳(5 次基线运行里有 2 次取消同样的 4 个;该文件不导入被改模块)。第二个提交只改了注释和新测试文件。npm run typecheck、npm run build:干净。check-unused-exports、check-test-import-graph、config-surface-guard、check-architecture-docs:干净。