Skip to content

fix(proxy): treat self-contained client replays as resume-not-applicable#585

Merged
icebear0828 merged 1 commit into
devfrom
fix/skip-implicit-resume-on-full-replay
May 17, 2026
Merged

fix(proxy): treat self-contained client replays as resume-not-applicable#585
icebear0828 merged 1 commit into
devfrom
fix/skip-implicit-resume-on-full-replay

Conversation

@icebear0828
Copy link
Copy Markdown
Owner

Summary

修根因。配合上一个 PR #584 的 guard 阈值放宽,这个 PR 让 proxy 一开始就不会把 client 自包含 full replay 误判成 missing_tool_calls

背景

Codex CLI 在 /compact 或 error recovery 时会发自包含 full replay:input 里同时有历史 function_call 和它们配对的 function_call_output。proxy 之前的逻辑:

  1. session-affinity 里有最近的 response_id(同一 conv),所以 implicit-resume 被激活
  2. 检查 input 里的 function_call_output call_ids → 必须在 storedFunctionCallIds 里
  3. 找不到(这些 call_id 是 input 自带的历史,不是 proxy 之前 track 的某个 upstream response 的)
  4. reason: "missing_tool_calls" → payload guard 413 → 客户端连续重试都被拦,对话冻死

guard 阈值放宽(#584)已经把"卡死"问题降级到"偶发烧 token"。本 PR 修上游:让 evaluator 一开始就识别出这是合法的 client replay。

Fix

evaluateImplicitResume 新增 inlineFunctionCallIds 入参,由 buildProxySessionContext 通过新增的 helper getInlineFunctionCallIds(codexRequest.input) 在调用前收集 input 里所有 function_call 项的 call_id。

新增判定(在 missing_tool_calls 检查之前):

if (
  requiredFunctionCallOutputIds.length > 0 &&
  requiredFunctionCallOutputIds.every(id => inlineFunctionCallIds.has(id))
) {
  return { active: false, reason: "self_contained_replay" };
}

self_contained_replay 不在 payload guard 拦截的 reason 列表里,请求正常透传给上游。混合场景(部分 inline、部分既不 inline 也不 stored)仍然判 missing_tool_calls,留 guard 挡真 runaway。

Changes

  • src/routes/shared/proxy-session-helpers.ts — 新增 getInlineFunctionCallIds / isSelfContainedReplay 辅助;ImplicitResumeOptsinlineFunctionCallIds? 字段;evaluateImplicitResume 加 self-contained-replay 前置检查。
  • src/routes/shared/proxy-session-context.tsbuildProxySessionContext 在 implicit-prev 路径上收集 inlineFunctionCallIds,并把它塞进 resumeEvaluationInput
  • tests/unit/routes/shared/proxy-handler-implicit-resume.test.ts — 新增 7 个测试覆盖 self-contained replay / 混合 / inline 优先 / helper 行为。
  • tests/unit/routes/shared/proxy-session-context.test.ts — 现有 resumeEvaluationInput 期望值加 inlineFunctionCallIds

Test Plan

  • npx vitest run tests/unit/routes/shared/proxy-handler-implicit-resume.test.ts — 18 pass
  • npx vitest run — 2273 pass / 1 skipped / 0 fail (230 files)
  • npx tsc --noEmit — clean
  • Pre-push hook validates the branch
  • 本地 rebuild + restart,原来 vh=a2ade151d7aa conv=019e317e 的 308KB/102 items replay 现在应该走 self_contained_replay 分支正常透传(具体效果等真请求触发后查 log 验证)

Notes

#584 是父子关系:#584 是 hotfix(拉宽阈值),本 PR 是根因修复。两个都合上后:guard 仍在,但只对真正的 runaway loop 触发;正常 client 重放走透传不被打扰。

…ble, not missing_tool_calls

When Codex CLI sends a self-contained full-history replay — e.g. after
/compact or error recovery — the input contains both the historical
function_call entries AND their paired function_call_output entries.
The proxy was treating this as an implicit-resume candidate because
session-affinity still had a recent response_id for the conversation,
then bouncing it as missing_tool_calls because the function_call_output
call_ids weren't in stored functionCallIds (they live in the input
itself, not in any prior upstream response we tracked). The payload
guard then 413'd subsequent retries, freezing the conversation.

evaluateImplicitResume now accepts an inlineFunctionCallIds argument
populated by buildProxySessionContext via getInlineFunctionCallIds.
When every function_call_output call_id is satisfied by an inline
function_call in the same request, the evaluator returns
reason="self_contained_replay" (active=false) and the request flows
through to upstream normally. Genuine missing-tool-call cases — where
neither inline nor stored ids cover the outputs — still resolve to
missing_tool_calls so the payload guard can still catch runaway loops.

Adds 4 unit tests covering pure-inline replay, mixed
inline+truly-missing, self-contained precedence over missing, plus
helper-level tests for getInlineFunctionCallIds and
isSelfContainedReplay. Updates the existing buildProxySessionContext
expectation to include the new inlineFunctionCallIds field.
@icebear0828 icebear0828 merged commit 7efc5e2 into dev May 17, 2026
2 checks passed
@icebear0828 icebear0828 deleted the fix/skip-implicit-resume-on-full-replay branch May 17, 2026 18:58
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.

1 participant