Skip to content

fix(compaction): send the session id as the summary prompt cache key - #1165

Merged
vastsa merged 2 commits into
vastsa:mainfrom
MM8866-stock:fix/compaction-summary-conversation-key
Sep 28, 2026
Merged

vastsa merged 2 commits into
vastsa:mainfrom
MM8866-stock:fix/compaction-summary-conversation-key

Conversation

@MM8866-stock

@MM8866-stock MM8866-stock commented Sep 28, 2026 •

Copy link
Copy Markdown

概要

在对接 Codex 后端的网关上,/compact 与自动上下文压缩失败(本机在 anyrouter 复现:api_style=responses、模型 gpt-6-astra),而同一会话、同一提供商、同一模型的普通回合正常:

Summarization failed: 8ea56b43-… API error (400):
{"message":"invalid codex request","type":"new_api_error","code":"invalid_responses_request"}

摘要请求不经过 agent 的 streamFn,因此是会话里唯一不携带对话身份的请求。本 PR 让它带上该身份,其他行为不变。

根因

compact 自行组装流选项并交给 Models.completeSimple,其中要求 cacheRetention: "none";pi-ai 的 Responses 适配器因此不发送该字段:

// pi-ai/dist/api/openai-responses.js
prompt_cache_key: cacheRetention === "none" ? undefined : clampOpenAIPromptCacheKey(promptCacheKey)

普通回合通过 streamFn 传入 sessionId: this.sessionId,总是带 prompt_cache_key。单变量探针(同一端点、提供商、模型与凭据,每次只改一个字段):

实际发出的摘要体 结果
PI 当前发出的原始载荷 400 invalid codex request
同一份 + prompt_cache_key 200
同一份 + tools / tool_choice 400
同一份 + prompt_cache_key + instructions 200
精简为 {model, input: "ping", stream: false} 400

说明拒绝与摘要的提示形状、developer 角色、max_output_tokens 或缺少 Codex 信封无关:缺该 key 的请求一律被拒。

改动

withCompactionRequestHeaders(已经给这一个请求套上会话标头的接缝)为两个 Responses 形状的 API 补回对话 key:

const SUMMARY_CONVERSATION_APIS = new Set(["openai-responses", "openai-codex-responses"]);
  • 添加在浅拷贝上:适配器载荷对象与已有 onPayload 钩子的返回值都保持原对象,未改动时原样返回;
  • 在已有钩子之后执行,调用方自己设置的 key 优先;
  • 用 pi-ai 自身的 clampOpenAIPromptCacheKey(适配器 64 字符上限),且仅在尚未设置时添加;
  • 其他线协议的载荷逐字节不变;cacheRetention、Anthropic cache_control / prompt_cache_retention 与 Codex 会话标头均不受影响。

文档

docs/spec/03-runtime/02-agent-runtime.md、docs/spec/06-delivery/04-e2e-test-plan.md(均含 zh-CN)与 docs/project/unreleased.md。不涉及 ADR:检查点载荷与 details.failureReason 词表未变。

验证

  • pnpm -r --if-present test — 全工作区通过;agent-runtime 75 文件 / 1127 测试,含 key 注入、长度截断、已存在 key、既有钩子语义,以及通过真实 Responses 适配器断言出站摘要体携带会话 id。
  • agent-runtime 与 desktop typecheck、check-architecture.mjs(base 取 origin/main)、docs:check、docs:build、check:agent-policy、check:release-docs、check:pr-base — 全部通过(即仓库 CI 会跑的门禁)。
  • 冒烟 E2E 23/23(2 项 live-model 因无 API key 跳过);开发构建上对真实网关执行 /compact:成功。
  • 同一 commit 在本 fork 上的同名 workflow 运行:CI(js + rust 两个 job)与 Docs check 均通过 — CI run、Docs check run。上游 PR 上的运行需维护者批准首次贡献者后才会开始。

兼容性与风险

两个线协议的摘要请求多一个标准字段;无迁移、无 schema 或持久化格式变更,回退该提交即恢复原行为。

The summary of a context checkpoint reaches the same gateway backend as the
conversation it summarizes, but the Responses-shaped adapters attach
`prompt_cache_key` only while the caller keeps some cache retention, and
pi-agent-core asks for `"none"` on a summary. The summary therefore travelled
without the conversation identity every other turn of the session sends, and a
gateway fronting a Codex backend answered 400 `invalid_responses_request` (on
anyrouter: "invalid codex request"), failing `/compact` and automatic
compaction with `Summarization failed`.

`withCompactionRequestHeaders` adds the session id as `prompt_cache_key` for
`openai-responses` and `openai-codex-responses`: on a shallow copy, after an
existing `onPayload` hook, clamped to the adapter's 64-character limit, and
only while no key is set yet. Every other wire API keeps its payload
byte-identical, and an unchanged payload keeps the hook's own return value.

A single-variable probe against the failing gateway showed the same summary
body answered 400 alone and 200 with the key. Unit tests cover the key, the
clamp, an existing key, and existing hook semantics; a runtime test asserts the
outbound summary request carries the session id.

Docs: the compaction seam note carries the change in English and zh-CN, the
/compact scenario expects the conversation identity, and the unreleased
highlights entry records the fix.
@muzimu217

Copy link
Copy Markdown
Contributor

确认根因链成立,这份实现的边界处理是我见得最干净的几处之一:

  • 用 pi-ai 自身的 clampOpenAIPromptCacheKey 做注入前处理(64 字符上限)——我最初担心的"绕过 clamp 直接塞 sessionId"不存在,格式安全由适配器同款函数保证;
  • 仅在 prompt_cache_key 未设置时添加、调用方 onPayload 返回值仍可替换、适配器载荷对象不被 mutate——与既有钩子语义兼容;
  • SUMMARY_CONVERSATION_APIS 白名单精确圈定两个 Responses 形状协议,其他线协议载荷逐字节不变。

与 #1164 的关系值得维护者注意:两者是同一问题的两面——#1164 是"摘要失败时无法知道请求形状"(可观测性),本 PR 是其中被探针定位出的那一个具体根因(缺 prompt_cache_key)。根因修掉后 #1164 的诉求仍然独立成立:其它失败模式(网关换规则、适配器载荷变化、分块失败)依旧会以 summary_provider 这种封闭词表失败,事后依旧无从下手。建议两个一起看:本 PR 治病,#1164 装监护仪。

验证侧无保留意见:真实网关 /compact 冒烟 + 全工作区测试 + CI 门禁全家,已覆盖我要问的每一层。

@vastsa
vastsa merged commit 4323298 into vastsa:main Sep 28, 2026
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.

3 participants