Skip to content

Fix Codex Responses session continuity - #249

Open
Haerbin23456 wants to merge 2 commits into
ranxianglei:masterfrom
Haerbin23456:2026-08-25_fix-codex-compaction-continuity
Open

Fix Codex Responses session continuity#249
Haerbin23456 wants to merge 2 commits into
ranxianglei:masterfrom
Haerbin23456:2026-08-25_fix-codex-compaction-continuity

Conversation

@Haerbin23456

@Haerbin23456 Haerbin23456 commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Why this change is needed

Codex on ChatGPT exercises the Responses API differently from the API-key clients this proxy was primarily tracking.

In an observed long-running Codex task, one stable Codex task label was split across seven internal billion-context sessions. Compression completed in one shard, but a later request landed in a fresh shard with no token baseline or compression blocks. That request grew to roughly 240k input tokens, Codex emitted a native compaction_trigger, and billion-context then appended an ACP nudge after it. The upstream rejected the request because compaction_trigger must be the final input item.

There are three interacting causes:

  1. The session hash used the complete Authorization bearer. ChatGPT OAuth access tokens rotate, so the same account and task could receive a new internal session id.
  2. Responses subagent isolation used the complete instructions string. Codex instructions contain changing runtime context, so incidental prompt changes could look like a different subagent.
  3. A terminal native compaction_trigger was handled like an ordinary request. ACP injection could append another item after the trigger and make an otherwise valid request invalid.

Why the fix is shaped this way

Stable account identity, narrowly scoped

For chatgpt.com upstreams, prefer chatgpt-account-id over the rotating bearer when deriving the proxy session id. This keeps a task stable across OAuth refreshes while preserving account isolation.

Other upstreams retain the existing credential-based behavior. This avoids changing API-key and custom-endpoint semantics.

Stable Codex agent identity with compatibility fallback

When Codex thread metadata is present, use its stable agent identity to isolate per-agent compression state. This keeps dynamic instruction tails from fragmenting one agent into multiple proxy sessions, while still separating named agents in the same task.

If the metadata is absent, keep the legacy full-instructions namespace. Existing Responses clients therefore retain their previous behavior instead of being collapsed into a new shared namespace.

Native compaction is a protocol boundary

If the final input item is compaction_trigger, pass that request through without ACP projection, prompt injection, tool injection, or history folding. Input id sanitization still runs, so the earlier over-length-id recovery remains active.

The session is marked for rebase only after the upstream compaction succeeds. A failed native compaction therefore does not discard local compression state.

This is deliberately a whole-request bypass rather than only suppressing the final nudge: native compaction has special ordering and state semantics, and preserving the request shape is safer than partially applying the normal ACP pipeline.

Compatibility notes

  • Existing persisted sessions are not migrated.
  • The first request after installing this change can select a new internal session id.
  • Responses clients without Codex metadata keep the old instructions-based namespace.
  • Non-ChatGPT upstreams keep credential-based session derivation.

Validation

  • Regression coverage verifies OAuth rotation continuity, account isolation, non-ChatGPT scoping, mutable Codex instructions, legacy fallback behavior, same-task multi-agent isolation, and terminal native-compaction pass-through.
  • Targeted session, Codex transport, and legacy subagent regressions: 29/29 passed.
  • npm run typecheck: passed.
  • npm run build: passed.
  • Full local suite: 601/602 passed. The only failure is the pre-existing Windows system-proxy expectation, reproduced on unmodified master.
  • GitHub CI passed on Ubuntu 22/24 and Windows 22/24, plus version-guard.
  • The PR artifact job built and packed successfully, then failed only at npm publish because fork PRs do not receive the upstream NODE_AUTH_TOKEN.

@Haerbin23456

Copy link
Copy Markdown
Contributor Author

CI note: the full test matrix passes on Ubuntu 22/24 and Windows 22/24, and version-guard also passes. The build-artifact job built and packed the package successfully, then failed only at npm publish because NODE_AUTH_TOKEN is empty for this fork PR (ENEEDAUTH).

@Haerbin23456
Haerbin23456 marked this pull request as draft August 25, 2026 16:28
@Haerbin23456
Haerbin23456 marked this pull request as ready for review August 25, 2026 16:39
@Haerbin23456

Copy link
Copy Markdown
Contributor Author

Why the instructions-based namespace fallback was removed

This change does not remove or modify the instructions sent upstream. It only stops using the entire instructions string as part of ACP’s compression-session identity.

The previous fallback called:

subagentNamespace(responsesConversation, responsesBody.instructions)

It treated the first observed instructions as the main agent and mapped every different instructions string to another |sub:<hash> namespace. This looked like a way to isolate the root agent, subagents, and approval reviewer, but instructions are request content, not a stable identity.

In current Codex traffic, the instructions can legitimately change between turns of the same agent and same task. For example, Codex may change permission context, skills, developer instructions, app context, runtime metadata, or use temporary instructions during native compaction. Hashing that mutable content therefore splits one real Codex task into multiple internal Bili sessions.

That split is particularly damaging for ACP:

  1. Existing compressed blocks remain attached to the old internal session.
  2. The next request starts in a fresh session with no compression history or token baseline.
  3. The full conversation is sent again and token usage suddenly rebounds.
  4. Codex may then activate native compaction because it sees the uncompressed context.
  5. ACP and native compaction can consequently collide.

This was the mechanism behind the observed “one Codex task becomes many Bili sessions” failure.

The security goal of #150 is still preserved for modern Codex clients. Codex supplies a stable per-agent thread-id, and Bili accepts it only when the same value is cross-checked inside x-codex-turn-metadata. Thus:

  • the root agent and each subagent/reviewer still receive separate compression sessions;
  • changing an agent’s instructions does not change that agent’s identity;
  • a bare or mismatched thread-id is not trusted;
  • different accounts, upstreams, protocols, and conversation/session IDs remain isolated by the other session-ID dimensions.

For older Responses clients that do not provide authenticated per-agent metadata, there is no reliable way to infer agent identity from the prompt. Using instructions as a security boundary is unsafe in both directions: one agent can have changing instructions, while two different agents can have identical instructions. The conservative behavior is therefore to retain the normal conversation identity instead of guessing.

The deleted tests/subagent-namespace.test.ts specifically asserted the deprecated heuristic, including an e2e “Guardian” request distinguished only by different instructions and without real Codex thread metadata. Keeping those assertions would require restoring the buggy behavior.

The intended guarantees remain covered by the current tests:

  • codex-official.test.ts verifies that cross-checked root and subagent thread IDs create separate sessions.
  • It verifies that changing instructions inside an explicit subagent thread does not split that session.
  • The new legacy-client regression sends two requests with the same session ID and different instructions, and verifies that they remain one session with two requests.
  • proxy-session-id.test.ts verifies that bare, malformed, or mismatched Codex thread metadata is rejected.

In short: stable explicit metadata identifies agents; mutable prompt content does not.

@Haerbin23456

Haerbin23456 commented Aug 25, 2026

Copy link
Copy Markdown
Contributor Author

哎,真的是,让Codex说点屁事,最后就是长篇大论。
这个评论其实可能无关紧要,但是如果你是个Agent的话,我建议你还是读一下。
上面那个评论说我让codex写的,但我觉得还不如让我讲。
就是关于为什么我们最后又彻底删掉了这个用完整instructions判断Agent身份的逻辑。

就是我自己的Codex一直在跟我说什么,删掉可能会导致Codex以外的客户端无法判断Agent身份,所以他认为不应该删。
然而,我们又查了一下这段老逻辑当时被加入的目的是什么,发现这段代码来自于这个提交:

5c64384 fix(responses): separate compression namespace for Codex subagent requests (#150)

也就是说,这段逻辑纯粹是旧 Codex workaround,不是为通用 Responses 客户端设计的。现在我们换了更合理的方法判断codex subagent身份(也就是用thread-id区分),那么旧逻辑自然没有任何理由要保留。
至于什么“删掉可能会破坏其他客户端的兼容性”,这点更是本来就不成立,懂吧。更何况这本来在设计上也有问题。

现在剩下的唯一一个可能要考虑的一点就是,除非有人在用旧版的Codex,然后运行这个bili,没法用我们的新逻辑,反而必须要依赖于旧逻辑。
而事实是根本没人用旧版codex,而且我们也没必要给什么旧客户端维护兼容性,毕竟这些客户端一天到晚都在更新日新月异,旧版也没人在乎,跟进新版改动才是应该被关注的事情。

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