Skip to content

Codex App resume silently drops prior context after the 1-hour replay-state TTL expires #702

Closed

Description

@xmanrui

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

  1. Start OpenCodex 2.7.42 with the built-in openai provider using Codex-login forward authentication.
  2. Open Codex App, create a thread, and complete enough turns that the next request depends on earlier context.
  3. Leave the thread idle for more than one hour. The proxy may remain running throughout.
  4. Resume the same thread in Codex App and send a short instruction such as continue or a question referring to prior work.
  5. 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:

Together, these behaviors turn an expired local replay entry into a successful request containing only the current-turn delta.

Suggested fixes:

  1. Make the continuation-state TTL configurable and/or increase the default substantially while retaining the existing count, memory, and snapshot-size caps.
  2. In Codex-login forward mode, fail closed with an explicit continuation-state-expired error when previous_response_id cannot be expanded. Do not silently forward a delta known to be missing prior turns.
  3. 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 request

Sanitized 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

  1. github-actions commented on Jul 29, 2026

    @github-actions
    Contributor

    Automated translation bookkeeping — detected language: English.

  2. LeoWang331 commented on Jul 30, 2026

    @LeoWang331
    Contributor

    I reproduced this on the current dev branch (0666b416) with a server-level integration test.
    The test exercises the real /v1/responses path with the canonical openai provider using openai-responses and authMode: forward:

    • the first response is recorded in local replay state;
    • the replay entry is advanced beyond the one-hour TTL using a controlled clock;
    • a continuation request is sent with previous_response_id;
    • the expired replay entry cannot be expanded;
    • previous_response_id is removed;
    • the mock upstream still receives a second request containing only the current user delta;
    • the server still returns HTTP 200.

    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.

  3. added a commit that references this issue on Jul 30, 2026
  4. lidge-jun commented on Jul 30, 2026

    @lidge-jun
    Owner

    Fixed on dev by 6be54f588 (PR #749).

    src/server/responses/core.ts now rejects a canonical OpenAI forward continuation whose local replay state is gone, returning HTTP 400 invalid_request_error before authentication, adapter construction, or any upstream I/O. The previous path logged the replay miss, stripped previous_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:

    • expired forward state fails closed with no upstream call
    • fresh state still expands the full history and succeeds
    • an ordinary request without previous_response_id is untouched
    • API-key Responses providers keep native previous_response_id support

    Falsification 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_id outright.

    Closing.

  5. added 2 commits that reference this issue on Sep 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions