Repository navigation
Codex App resume silently drops prior context after the 1-hour replay-state TTL expires #702
Description
Client or integration
Codex App
Area
Proxy and routing
Summary
Resuming a Codex App thread after it has been idle for more than one hour can silently drop all prior conversation context when using the built-in openai Codex-login forward provider.
The resumed request still succeeds with HTTP 200, but only the newest user message reaches the model. The model therefore behaves as if the existing thread were a brand-new conversation. I expected either the complete thread context to be restored or an explicit continuation-expired error; OpenCodex should not silently continue with a truncated delta.
This occurred without restarting the OpenCodex proxy. The same OpenCodex conversation ID and the same openai / gpt-5.6-sol route were used before and after the idle period.
Reproduction
- Start OpenCodex 2.7.42 with the built-in
openaiprovider using Codex-login forward authentication. - Open Codex App, create a thread, and complete enough turns that the next request depends on earlier context.
- Leave the thread idle for more than one hour. The proxy may remain running throughout.
- Resume the same thread in Codex App and send a short instruction such as
continueor a question referring to prior work. - Observe that the request returns HTTP 200, but the assistant asks for the missing task/configuration or explicitly says it cannot see earlier context.
In the observed thread, the last healthy request at 16:39 contained 85,073 input tokens. The first resumed request at 18:58, approximately 2 hours 19 minutes later, contained only 22 input tokens and zero cached input tokens. Subsequent messages contained only 41, 9, 11, and 15 input tokens. All requests used the same OpenCodex conversation ID, provider=openai, and resolvedModel=gpt-5.6-sol.
The apparent cause is the fixed one-hour continuation-state TTL:
RESPONSE_TTL_MS = 60 * 60 * 1_000- On a replay miss, the passthrough path warns but continues even though earlier turns may be missing:
src/server/responses/core.ts - Forward mode strips
previous_response_idbecause the ChatGPT Codex backend rejects it:src/adapters/openai-responses.ts
Together, these behaviors turn an expired local replay entry into a successful request containing only the current-turn delta.
Suggested fixes:
- Make the continuation-state TTL configurable and/or increase the default substantially while retaining the existing count, memory, and snapshot-size caps.
- In Codex-login forward mode, fail closed with an explicit continuation-state-expired error when
previous_response_idcannot be expanded. Do not silently forward a delta known to be missing prior turns. - Add a regression test that advances time beyond one hour and verifies that a resumed forward-mode request either replays the full history or returns an explicit error.
Version
2.7.42
Operating system
macOS 26.5.2 (25F84)
Codex App 26.721.81911; bundled Codex runtime 0.146.0-alpha.3.1.
Provider and model
openai (Codex-login forward provider) / gpt-5.6-sol
Logs or error output
[responses] previous_response_id <redacted> not found in local replay state (model gpt-5.6-sol); forwarding without it — earlier turns may be missing from this requestSanitized request-log comparison:
16:39 provider=openai resolvedModel=gpt-5.6-sol inputTokens=85073 status=200
18:58 provider=openai resolvedModel=gpt-5.6-sol inputTokens=22 status=200
19:00 provider=openai resolvedModel=gpt-5.6-sol inputTokens=41 status=200
Redacted configuration
{
"defaultProvider": "openai",
"providers": {
"openai": {
"adapter": "openai-responses",
"authMode": "forward"
}
}
}Checks
- I searched existing issues and documentation.
- I removed secrets, tokens, account details, request credentials, and personal data.
Activity
github-actions commentedon Jul 29, 2026
Automated translation bookkeeping — detected language: English.
LeoWang331 commentedon Jul 30, 2026
I reproduced this on the current
devbranch (0666b416) with a server-level integration test.The test exercises the real
/v1/responsespath with the canonicalopenaiprovider usingopenai-responsesandauthMode: forward:previous_response_id;previous_response_idis removed;I also added a fail-closed regression assertion. It currently fails consistently because the upstream is called twice instead of only for the initial request.
I’m preparing a scoped fix that rejects this specific unsafe forward-mode continuation before adapter construction or any upstream request. I’ll keep the change limited to the expired/missing replay-state path and add controls for valid, non-expired continuations and ordinary requests without
previous_response_id.Please let me know if the maintainers prefer a different error-response contract before I finalize the PR.
merge: PR #749 — fail closed on expired forward replay state (#702)
lidge-jun commentedon Jul 30, 2026
Fixed on
devby6be54f588(PR #749).src/server/responses/core.tsnow rejects a canonical OpenAI forward continuation whose local replay state is gone, returning HTTP 400invalid_request_errorbefore authentication, adapter construction, or any upstream I/O. The previous path logged the replay miss, strippedprevious_response_id, and forwarded the continuation delta on its own — which is exactly the silent context loss reported here.Regression coverage in
tests/issue-702-expired-replay-state.test.ts:previous_response_idis untouchedprevious_response_idsupportFalsification check: reverting only the source change turns one of those into a failure, so the test binds the fix rather than passing incidentally. Full suite on the merged tree: 6080 pass / 0 fail.
Scope note: the guard covers every replay miss, not only the 1-hour TTL case — a proxy restart or cache eviction now produces the same 400. The error text says "unavailable or expired" for that reason. If you hit this, start a new session; the old response id cannot be recovered on the canonical ChatGPT backend, which rejects
previous_response_idoutright.Closing.
docs(devlog): measure the Windows suite instead of restating the plan
fix(lab): finalize projection statements so a rebuild can replace its…
fix(lab): finalize projection statements so a rebuild can replace its…
merge: PR lidge-jun#749 — fail closed on expired forward replay state (…
fix(lab): finalize projection statements so a rebuild can replace its…