Skip to content

fix(agent-runtime): send the provider's headers on the compaction request (#398) - #422

Merged
vastsa merged 1 commit into
vastsa:mainfrom
L4XB:fix/398-compaction-provider-headers
Sep 15, 2026
Merged

vastsa merged 1 commit into
vastsa:mainfrom
L4XB:fix/398-compaction-provider-headers

Conversation

@L4XB

@L4XB L4XB commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Closes #398

Summary

Reproduced. Manual /compact on an OpenCode Go provider sends a request with
no outbound headers at all — not just a missing x-opencode-session, which
is why the gateway answers 400. Ordinary turns on the same provider are fine.

The compaction summary is the one provider request the session does not own.
Every other request goes through the agent's streamFn
(packages/agent-runtime/src/runtime.ts:1655), where withProviderHeaders( withOpenCodeSessionHeaders(…)) merges the OpenCode routing headers, the
provider row's own headers map and the Copilot IDE headers. The summary does
not: generateCompaction (packages/agent-runtime/src/runtime.ts:5774) hands
this.models straight to pi-agent-core's compact
(packages/agent-runtime/src/runtime.ts:5780), and compact assembles its own
SimpleStreamOptions and calls Models.completeSimple directly. Nothing in
that path ever reaches streamFn.

The reporter's second observation is the expected half. x-opencode-session is
a reserved header key on both sides — packages/agent-runtime/src/provider-headers.ts:37
and crates/host-core/src/providers/validation.rs:57, which is the
is reserved rejection they saw in the advanced editor. The runtime is meant to
own that header, so adding it by hand is correctly refused; the defect is that
the runtime was not adding it on this one request.

The change

compact takes the model collection as an argument, and that is the only seam
that reaches its request. A new
packages/agent-runtime/src/compaction-request.ts wraps the collection for that
call: completeSimple applies exactly the merge streamFn applies, and every
other member stays the collection's own, so a later pi-agent-core release that
calls something else keeps working unchanged.

One argument changes in runtime.ts. Everything else in the diff is the new
module, its test, the runtime test, and the specs plus their zh-CN mirrors per
AGENTS.md §11.

Two notes on what the wrapped request now sends:

  • The session's conversation id, not the per-call UUID pi-agent-core mints
    when the caller has none (createSummaryRequestOptions uses
    options.sessionId ?? uuidv7(), so a caller-supplied id was always
    preferred). This matches what the session's turns already send, so the
    summary reaches the same gateway backend as the conversation it summarizes,
    and it matches how title generation, prompt enhancement and plugin
    agent.complete already forward sessionId.
  • cacheRetention: "none" is untouched — the harness still isolates the
    summary from the conversation's cache.

Automatic threshold compaction shares generateCompaction, so it is fixed by
the same change; the issue reports the manual path because that is the one a
user triggers.

Evidence

Two tests in packages/agent-runtime/src/runtime.test.ts drive the real
generateCompaction through the real compact, with the model collection
replaced by a recorder. They are the discriminating pair.

Base — origin/main @ 0a1dd80e in a detached worktree, with only this
branch's test block copied in:

FAIL  src/runtime.test.ts > DesktopAgentRuntime compaction request headers >
      sends the session's OpenCode header on the summary request
AssertionError: expected '01a0a579-0c7a-760e-b538-69727ac66e11' to be 'session-1'

FAIL  src/runtime.test.ts > DesktopAgentRuntime compaction request headers >
      sends a provider row's own headers without adding OpenCode's
AssertionError: expected undefined to deeply equal { 'X-Team': 'platform' }

Tests  2 failed | 158 skipped (160)

headers is undefined on the base — that is the 400, at the request
boundary — and the session id is the per-call UUID.

This branch:

✓ src/compaction-request.test.ts > compactionRequestOptions > carries the session id pi-agent-core's per-call fallback replaced
✓ src/compaction-request.test.ts > compactionRequestOptions > keeps a provider row's own headers and adds none for other providers
✓ src/compaction-request.test.ts > withCompactionRequestHeaders > stamps the headers on completeSimple and leaves the collection otherwise intact
✓ src/runtime.test.ts > DesktopAgentRuntime compaction request headers > sends the session's OpenCode header on the summary request
✓ src/runtime.test.ts > DesktopAgentRuntime compaction request headers > sends a provider row's own headers without adding OpenCode's

Test Files  2 passed (2)
     Tests  163 passed (163)

The second runtime test is the control that keeps the change honest: a plain
OpenAI-compatible row gets its own X-Team header and no OpenCode header.

Validation

Check Result
pnpm build:js passed
pnpm --filter @pi-desktop/desktop typecheck passed
pnpm lint passed (biome 55 files, style tokens OK)
pnpm -r --if-present test passed — shared 658, agent-runtime 458 (35 files), plugin-sdk 316, plugin-devkit 48, agent-host 39, i18n 24, desktop 1951 pass / 0 fail
node scripts/check-architecture.mjs (ARCHITECTURE_BASE=origin/main) passed
pnpm docs:check passed — 77 English/Chinese pairs
pnpm test:e2e and the other test:e2e:* suites NOT RUN
cargo fmt / cargo test -p host-core / cargo clippy NOT RUN

pnpm test:e2e stops at host binary missing; tried: …/target/debug/pi-desktop-host-core.
There is no Rust toolchain on the machine this was validated on (cargo: command not found), so pi-desktop-host-core cannot be built here and no e2e suite ran.
The unit coverage above drives the real compaction path end to end at the request
boundary, but it does not replace those runs; the remaining risk sits there and
with the post-integration E2E AGENTS.md §15 requires.

The Rust side is untouched by this diff — validation.rs is cited only as the
source of the "reserved" message, not changed.

File and line references are against main @ 0a1dd80e.

中文摘要

复现了。在 OpenCode Go 供应商上手动 /compact,发出的请求完全没有任何自定义
标头
,不只是缺 x-opencode-session,所以网关直接返回 400;同一供应商的普通
对话回合则一切正常。

原因是压缩摘要是会话唯一不经过自己请求边界的一次调用。其他请求都走 agent 的
streamFn(packages/agent-runtime/src/runtime.ts:1655),在那里
withProviderHeaders(withOpenCodeSessionHeaders(…)) 会合入 OpenCode 路由标头、
供应商行自己的 headers,以及 Copilot 的 IDE 标头。而 generateCompaction
(packages/agent-runtime/src/runtime.ts:5774)把 this.models 直接交给
pi-agent-core 的 compact,后者自行组装 SimpleStreamOptions 并调用
Models.completeSimple,整条路径从不经过 streamFn。

Issue 里说的第二点是预期行为:x-opencode-session 在 TS 与 Rust 两侧都是保留键
(packages/agent-runtime/src/provider-headers.ts:37、
crates/host-core/src/providers/validation.rs:57),高级配置里看到的提示就来自
那里。这个标头本应由运行时负责注入,所以手工添加被拒绝是对的;真正的缺陷是运行时
在这一次请求上没有注入。

compact 把模型集合作为参数传入,这是唯一能触及该请求的接口。新增的
packages/agent-runtime/src/compaction-request.ts 只为这次调用包装集合:
completeSimple 应用与 streamFn 完全相同的合并,其余成员保持原集合不变。
runtime.ts 只改了一个实参。该请求现在携带会话自己的对话 id,而不是 pi-agent-core
在调用方没有 id 时生成的按次 UUID,因此摘要与被压缩的对话落在同一个网关后端;
cacheRetention: "none" 保持不变。阈值触发的自动压缩共用同一函数,同样被修复。

pi-agent-core's `compact` assembles its own stream options and calls
`Models.completeSimple` directly, so the context-compaction summary never
passed the agent's `streamFn`, where every other request of a session picks
up its outbound headers. On an OpenCode Go provider the gateway therefore
answered 400 with a missing `x-opencode-session` on `/compact` while ordinary
turns succeeded, and a provider row's own `headers` map was absent from the
same call. `x-opencode-session` is a reserved key, so a user cannot supply it
from the advanced header editor.

`compact` takes the model collection as an argument, which is the one seam
that reaches its request. Wrap the collection for that call: `completeSimple`
now stamps the same merge `streamFn` applies, and every other member stays the
collection's own. The summary carries the session's conversation id rather than
the per-call id the harness mints when the caller has none, so it reaches the
same gateway backend as the conversation it summarizes.
Copilot AI lite review requested due to automatic review settings September 15, 2026 14:48

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@vastsa
vastsa merged commit d7c93bb into vastsa:main Sep 15, 2026
3 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] 使用opencode供应商手动压缩报错

3 participants