Repository navigation
fix(#377): re-voice mid-stream acp_summary as user messages (user-carrier) - #428
Conversation
…rier) Replace hoistMidSystemMessages (hoisted mid-stream system messages to the head as separate system messages) with systemToUser (converts them to role "user", leaving them at their anchor position). Why: strict OpenAI-compatible backends (SGLang) require exactly ONE system message at index 0 and reject a mid-conversation or second system message with 400 "System message must be at the beginning" (#377). The kernel renders each compressed block's summary as role "system" anchored mid-conversation; in proxy mode (plain client) the compress tool call is ephemeral and preflight blocks have none, so the acp_summary is the only carrier. Re-voicing it as a "user" message satisfies SGLang's one-system rule AND keeps the head system message (the prefix-cache anchor) byte-stable across compress turns — a new block no longer invalidates the whole-conversation prefix. In plugin/launcher mode the summary carrier is the compress tool call (in the agent's own re-sent history), so the kernel's acp_summary is stripped by stripKernelSummaries and systemToUser is a no-op there. - src/util.ts: hoistMidSystemMessages -> systemToUser - src/server.ts, src/loop/adapter-openai.ts: call sites + two-mode comments - README.md: new "Two compression modes" section (carriers, why they differ, why user not system/tool-call, coexistence determination) - AGENTS.md: Key Design Decisions item 6 (two modes) + Before Making Changes item 4 (every wire-touching change must be reasoned in BOTH modes) - tests: new system-to-user.test.ts (7 tests), updated e2e-mid-system.test.ts (summaries must ride on user messages, not leak into system), deleted hoist-mid-system.test.ts
📦 Built Package ArtifactBranch: Option A — Install from npm PR tag (recommended)npm install -g billion-context@pr-428Each push to this PR publishes a new version under the Option B — Download artifact
tar xzf billion-context-pr428.tgz
npm install -g packageThis comment is automatically updated on each push. |
Reviewed PR #428 (branch Verification (clean
The two "pre-existing" problems the issue body calls out no longer reproduce: the lockfile pins Code review — logic is sound:
One follow-up (human decision): PR #379 ( LGTM — ready to merge (merge is human-only per AGENTS.md). |
The nudge (a trailing user message prompting compression) was not gated by !pluginMode, so in plugin mode the proxy could inject a redundant nudge even though the agent owns compression (its own overflow self-heal). Gate all three nudge injection sites (anthropic/openai/responses) by !pluginMode, matching the injectTools gating, so plugin mode is fully clean: no tool injection, no nudge injection, summary carrier = the agent's own compress tool call.
README "Two compression modes" table: add a row for proxy-injected tools + nudge (plugin mode: none; proxy mode: the 4 context tools + nudge). AGENTS.md item 6: "suppresses tool injection" -> "suppresses tool + nudge injection".
|
确认是否有回归问题 |
已确认,无回归问题。我做了 master 基线对比 + 全量测试(clean 全量测试
−1 个测试是有意为之,不是漏掉回归
本 bug 的专属回归测试通过
无残留引用
一个说明(非回归,是本沙箱的覆盖限制)
结论:无回归。 行为变更(hoist-to-head → 原地改写为 user)是有意设计且覆盖完整;唯一在本沙箱没跑到的只有需要本地 upstream 的真实客户端 e2e。 |
|
新方案发到原issue |
[bot] 🏷 汇总本轮在 #428 上完成的工作:
结论:PR #428 LGTM,无回归,可合并(合并为人工操作)。 |
Problem
Strict OpenAI-compatible backends (SGLang, Qwen3-family "system-first" chat template) require exactly one
systemmessage at index 0 and reject a mid-conversation or second system message with400 {"message":"System message must be at the beginning"}(#377).acp-kernel renders each compressed block's summary as
role:"system"anchored mid-conversation. In proxy mode (plain client) thecompresstool call is ephemeral (server-side loop, never in the client's re-sent history) and preflight blocks have no tool call at all — so theacp_summarysystem message is the only carrier, and ≥2 active blocks produce ≥2 system messages → 400.Fix
Replace
hoistMidSystemMessages(which hoisted mid-stream system messages to the head as separate system messages) withsystemToUser— it converts the mid-streamacp_summarysystem/developer messages torole:"user", leaving them at their anchor position.This satisfies both constraints at once:
injectOpenaiSystem); summaries ride on user messages, which are allowed anywhere.In plugin/launcher mode the summary carrier is the
compresstool call (in the agent's own re-sent history), so the kernel'sacp_summaryis stripped bystripKernelSummariesandsystemToUseris a no-op there.Why
user, not a forged tool call: a forgedcompresstool call would be the "pure" carrier, but in proxy mode it requires fabricating an assistanttool_calls+usertool_resultpair by id, declaring the tool in the request, and handling preflight blocks that have no authentic call — far more invasive than re-voicing a standalone note. A summary is a stand-in for the folded history; re-voicing it as a user turn (clearly marked[Compressed conversation section]) is the accepted trade-off.Changes
src/util.ts:hoistMidSystemMessages→systemToUsersrc/server.ts,src/loop/adapter-openai.ts: call sites + two-mode commentsREADME.md: new "Two compression modes" section (carriers, why they differ, why user not system/tool-call, coexistence determination)AGENTS.md: Key Design Decisions item 6 (two modes, different carriers) + Before Making Changes item 4 (every wire-touching change must be reasoned in BOTH modes)system-to-user.test.ts(7 tests), updatede2e-mid-system.test.ts(summaries must ride on user messages, not leak into system), deletedhoist-mid-system.test.tsVerification
system-to-user.test.ts: 7/7 passe2e-mid-system.test.ts: pass (realstartServervs mock upstream; asserts zero system/developer after the leading prefix + summaries on user messages)npm run build: successnpm test: 891/892 (1 pre-existing failurecompress-reject-reasons.test.ts[bug] 0.1.63 起 Windows persist EPERM 风暴:会话持久化静默丢数据(4.9k 次/117 会话)+ compress no-valid-ranges 拒绝 #362 — installed acp-kernel lacksinvalidReasons; fails onorigin/mastertoo, unrelated)npm run typecheck: 6 pre-existing errors insrc/compress-tool.ts/src/stream.ts(invalidReasons), none in changed files; identical onorigin/masterSupersedes the abandoned
2026-09-01_merge-system-messages-377branch (merge-to-system approach, rejected because it breaks prefix-cache stability).