Skip to content

(app-server and Desktop) thread/list treats omitted modelProviders as current-provider filter instead of all providers #24648

Open

Description

@joshrad-dev

What issue are you seeing?

The app-server thread/list API currently treats an omitted/null modelProviders field as an implicit filter for the currently configured model provider. If a third party app doesn't include the modelProviders field, then not all threads are shown. This issue is especially annoying when trying to continue a thread with a different model provider, as many apps, including Codex Desktop, don't see the thread with the new model provider. While this issue could be fixed both on the desktop and app server side, I think this is more of an app server issue than a desktop one, since an omitted filter shouldn't be filtering by default.

What steps can reproduce the bug?

App server

  1. Create local threads under at least two model providers, for example openai and lcpp.
  2. Configure Codex so the active provider is one of them, for example:
  model_provider = "lcpp"

  [model_providers.lcpp]
  name = "Llama.cpp"
  base_url = "http://localhost:8080/v1"
  wire_api = "responses"
  1. Call app-server thread/list with modelProviders omitted or set to null.
  2. Observe that only threads for the active provider are returned.
  3. Call thread/list again with:
  {
    "modelProviders": []
  }
  1. Observe that threads from all providers are returned.

Desktop

  1. Open Codex with default OpenAI config
  2. Send message in a folder
  3. Quit Codex Desktop
  4. Change config to use another model and provider
  5. Reopen Codex
  6. See that threads previously used are missing, only threads that used that provider previously are visible.

This is also inconsistent with the CLI since the CLI still lists threads that were started with other model providers.

Here is a screenshot with my OpenAI login, using the default OpenAI provider and model=gpt-5.5 in the config:
Image

After quitting Codex, changing my config to the following, this is what I see:

model_provider = "lcpp"
model = "qwen3.6-27b-mtp"
Image

What is the expected behavior?

Current behavior appears to be:

  • modelProviders: ["openai"] returns only openai threads.
  • modelProviders: [] returns all providers.
  • modelProviders: null / omitted returns only threads matching the current configured provider.

What it should be:

  • modelProviders: ["lcpp"] means only lcpp.
  • modelProviders: [] means all providers.
  • modelProviders: null / omitted means also all providers.

On the desktop side, I expect that my threads will still show up after switching providers.

Additional information

I pointed Codex to the desktop app.asar, and it confirmed that the desktop app currently sends listAllThreads({ modelProviders: null }) which is why threads go missing in the desktop app after switching model providers. If the app server is behaving as intended and you would rather fix it on the desktop app side, then switch model providers to be modelProviders: [] and it will include all threads from all model providers. Again this is inconsistent with the CLI since in the CLI you can resume across different model providers. This issue is the root cause of

Relevant code is here in main:

None => Some(vec![self.config.model_provider_id.clone()]),

I also had drafted a fix but can't open a PR since I don't have permission: https://github.com/joshrad-dev/codex/tree/fix-thread-list-null-model-provider.

This code is the cause and fix for

the following issues:

#20184
#20004
#19088

After getting Codex to patch the ASAR to use [] instead of null for the modelProviders send, I'm now able to see all my threads across providers.
Image

Activity

  1. added
    appIssues related to the Codex desktop app
    app-serverIssues involving app server protocol or interfaces
    custom-modelIssues related to custom model providers (including local models)
    on May 26, 2026
  2. yui-stingray commented on May 27, 2026

    @yui-stingray

    I checked current origin/main, and the behavior described here matches the app-server boundary conversion.

    ThreadListFilters.model_providers is an Option<Vec<String>>. In list_threads_common, a non-empty list stays filtered, [] is converted to no provider filter, but null / omitted is converted to Some(vec![self.config.model_provider_id.clone()]) before calling thread_store.list_threads(...). So the server is not merely passing the caller's omitted filter through; it is rewriting it into an active-provider filter.

    That narrows the fix to the contract decision: if the app-server protocol should treat omitted/null as all providers, the conversion in list_threads_common is the right boundary to change. If omitted/null is intended to mean current provider, then Desktop should send [] when it wants cross-provider history. A focused regression test should cover two stored threads with different model_provider_id values and assert the chosen behavior for omitted/null, [], and a non-empty provider list.

  3. WAcry commented on Jun 15, 2026

    @WAcry

    This also makes the current README misleading.

    codex-rs/app-server/README.md documents thread/list.modelProviders as:
    “unset, null, or an empty array will include all providers.”

    But current behavior appears to be:

    • omitted / null: current provider, except parentThreadId-filtered requests
    • []: all providers

    So either the README should distinguish ordinary thread/list from parentThreadId-filtered requests, or the app-server behavior should be changed to match the documented contract.

  4. rclevenger-hm commented on Jun 27, 2026

    @rclevenger-hm

    I reproduced this on Codex Desktop for macOS while testing an OpenAI-compatible local proxy.

    Observed behavior:

    1. Existing chats briefly appear when Codex Desktop starts.
    2. The chats disappear after the app-server thread list response arrives.
    3. The corresponding state_5.sqlite rows and rollout JSONL files remain intact.
    4. Restoring the original routing configuration makes the catalog visible again.

    The behavior appears consistent with the current conversion in list_threads_common:

    • modelProviders: [] becomes no provider filter.
    • An omitted/null modelProviders becomes Some(vec![self.config.model_provider_id.clone()]).

    Because Desktop omits this field when loading history, the app server implicitly filters the catalog to the currently configured provider.

    A possible fix is to treat the optional provider filter as a filter only when it is non-empty:

    let model_provider_filter = model_providers
        .and_then(|providers| (!providers.is_empty()).then_some(providers));

    That would establish these semantics:

    • omitted/null modelProviders: all providers
    • modelProviders: []: all providers
    • non-empty modelProviders: only the requested providers

    Suggested regression coverage:

    • Create one openai thread and one other_provider thread.
    • Verify omitted and empty filters return both threads.
    • Preserve the existing test proving a non-empty filter returns only the requested provider.

    The protocol documentation should also state that omitted and empty provider filters include all providers.

  5. j178 commented on Jul 10, 2026

    @j178

    Additional macOS reproduction after upgrading from the standalone Codex app to the unified ChatGPT desktop app.

    Environment:

    • ChatGPT desktop: 26.707.31428
    • Bundled Codex CLI: 0.144.0-alpha.4
    • macOS arm64
    • Current provider: custom openai-https
    • Previous sessions: built-in openai

    After the upgrade/restart, the task list shows only sessions whose stored provider matches the currently configured openai-https provider.

    This matches the reported thread/list behavior: the desktop history request sends modelProviders: null, which App Server converts into a filter for the currently configured provider. Passing modelProviders: [] would return threads from all providers.

    The upgrade appears to have triggered a cold history reload; it did not delete or migrate the sessions.

    Expected behavior: the default local history view should remain visible across providers, or Desktop should explicitly pass modelProviders: [] when loading the general task history.

  6. bbrzoska commented on Jul 29, 2026

    @bbrzoska

    Fresh macOS reproduction confirming this remains present in the current unified ChatGPT desktop app.

    Environment:

    • ChatGPT desktop: 26.721.41059
    • Bundled Codex CLI/app-server: 0.146.0-alpha.3.1
    • macOS 26.5.2, arm64
    • Signed in with ChatGPT; custom OpenAI-compatible provider ID: custom

    Observed:

    • With model_provider omitted/commented, the effective provider is openai; the project shows no previous tasks.
    • Setting model_provider = "custom" makes the previous tasks appear immediately.
    • ~/.codex/state_5.sqlite contains 72 rows for that project cwd, all with model_provider = "custom"; the rollout data is intact.

    Directly querying the bundled app-server with the same cwd reproduces the boundary behavior:

    modelProviders: null  -> data: []
    modelProviders: []    -> the persisted Zendesk tasks are returned
    

    The installed desktop renderer still sends modelProviders: null from its recent-thread listing path. Current main also still converts omitted/null providers to Some(vec![self.config.model_provider_id.clone()]), while an empty array removes the provider filter.

    This confirms the bug is not data loss or project-assignment corruption. It is provider-based history filtering, and it persists in the latest app build available to me. Desktop sending modelProviders: [] for general local history would resolve this case.

  7. Riki-Du commented on Aug 4, 2026

    @Riki-Du

    issue still exists

  8. Jensen-Liu76 commented on Aug 11, 2026

    @Jensen-Liu76

    I can confirm this issue still affects the current unified ChatGPT desktop
    app on macOS.

    Environment:

    • ChatGPT desktop: 26.803.61601, build 6396
    • Bundled Codex app-server: 0.147.0-alpha.6.5
    • macOS 26.3, arm64
    • Current default provider: openai
    • Historical threads: custom and modeldock_http

    Observed behavior for one affected project containing 7 valid, unarchived
    threads:

    • modelProviders: null -> 0 threads
    • modelProviders omitted -> 0 threads
    • modelProviders: ["custom"] -> all 7 threads
    • modelProviders: [] -> all 7 threads
    • thread/read by ID succeeds for every affected thread

    The corresponding rows remain present in state_5.sqlite, and all rollout
    JSONL files remain intact. Across the local database, 32 top-level CLI/VS
    Code threads were hidden by the same provider filter.

    As a reversible diagnostic confirmation, changing only the SQLite listing
    metadata from the historical provider ID to openai made the threads
    immediately appear through the default modelProviders: null request. The
    original JSONL provider metadata remained unchanged and thread/read
    continued to report the original provider. I do not recommend this database
    workaround as the product fix, but it confirms that the problem is limited to
    provider-based listing.

    This matches the documented contract that unset, null, and an empty array
    should include all providers.

    A focused fix appears to be changing the app-server boundary so that only a
    non-empty provider list applies a filter, with regression coverage for:

    1. omitted/null -> all providers
    2. empty array -> all providers
    3. non-empty array -> requested providers only

    If the maintainers agree with this approach, I would be happy to implement
    the fix and regression tests. Please let me know if you would like to invite
    a PR.

  9. hobbescalvin414-tech commented on Aug 23, 2026

    @hobbescalvin414-tech

    I prepared a focused fix for this issue, but the repository currently limits opening pull requests to collaborators.

    Patch: hobbescalvin414-tech@d25e00c
    Branch: https://github.com/hobbescalvin414-tech/codex/tree/fix/thread-list-null-model-providers

    The change makes omitted/null and empty modelProviders disable provider filtering, while preserving explicit filtering for non-empty arrays. It also extends the app-server test to cover omitted/null and empty-array behavior across multiple providers.

    Verification: just fmt and git diff --check passed. The app-server test suite was not completed because dependency downloading was stopped before compilation.

    A collaborator is welcome to cherry-pick commit d25e00c88091e1beebd45e2998ee639a420e3e3a.

  10. claell commented on Aug 23, 2026

    @claell

    Confirming this from working on a small local provider-manager tool for custom [model_providers.*] setups.

    Omitted modelProviders behaving as a current-provider filter matches what we see in practice and what was reported in the closed #15494, plus #31625, #31627, and #17316. Sessions remain intact in rollouts and SQLite; only discovery narrows to the active provider bucket.

    Defaulting local discovery to all providers with an explicit filter opt-in (as proposed here) would remove the main footgun of switching between the built-in OpenAI provider and custom ones. Happy to help validate once the default changes.

  11. TheNewMikeMusic commented on Sep 5, 2026

    @TheNewMikeMusic

    Additional reproduction on the official bundled CLI 0.153.4 / macOS Desktop 26.901.41600 (build 7982), with a specifically Android Remote pinned-section symptom.

    The configured provider is an HTTP/SSE configuration named openai_http, using the official OpenAI backend. Existing tasks include both openai and openai_http metadata. Five tasks are pinned: two openai_http, three openai.

    Observed on Android:

    • Only the two openai_http tasks appear in the pinned section.
    • A missing openai task is still visible and accessible inside its project. This is a pinned-list visibility failure, not deletion of that task.
    • Refreshing Remote does not restore the missing pins.
    • The native desktop pin API returns all five pins.

    A read-only control using the same real pinned section in the official app-server reproduces the exact set shown on the phone:

    {
      "method": "thread/list",
      "id": 1,
      "params": {
        "sectionId": "<pinned section ID from threadSection/list>",
        "limit": 100,
        "sourceKinds": [],
        "archived": false,
        "useStateDbOnly": true,
        "modelProviders": null
      }
    }
    Only changed field Result Median local query time, 5 samples
    modelProviders: null 2 pinned tasks, both openai_http 0.74 ms
    modelProviders: [] All 5 pinned tasks 0.90 ms

    The installed desktop's listHostSectionThreadIds implementation already supplies modelProviders: [], explaining why its native pin list can be correct while a default-filtered caller loses three pins.

    We did not capture Android's raw list parameters, so this is an exact-result reproduction of the server behavior rather than proof of the mobile request payload. Please include pinned sections in the cross-provider client/server regression coverage, alongside recent and archived history.

    These measurements were made with the official bundled executable. No native task database or rollout content was edited. Private titles, task IDs, host identifiers, account details, and raw logs are omitted. Related mobile report: #24780.

  12. wyqmath commented on Oct 5, 2026

    @wyqmath

    +!

  13. ShedlonA commented on Oct 10, 2026

    @ShedlonA

    Additional Windows / Android Remote reproduction, checked on 10 October 2026.

    A valid, non-archived chat is missing from the assistant-facing Desktop listing and from Android Remote. It is still present in the local desktop catalog, and read_thread by ID succeeds.

    Environment:

    • Windows x64; MSIX package 26.1007.2314.0.
    • Installed application package.json: 26.1007.21434, build 13901.
    • Installed backend reports codex-cli 0.162.0-alpha.17.2.
    • Official ChatGPT Android app connected to that Windows host; Android version was not recorded.
    • The host's configured provider is a custom provider, called custom below. The missing chat is recorded under openai.

    Read-only comparison of the local state database with list_threads(limit=50) found 119 non-archived local vscode rows:

    Stored provider Local rows Returned in ordinary local results Returned separately as pins
    Current custom provider 22 19 3
    openai 97 0 2

    The 19 ordinary local results exactly equal the current-provider rows minus their pins. There were no additional exclusions in that group. Of the 97 openai rows, 95 were absent from the entire captured result.

    These counts describe the assistant's listing tool, not the native desktop sidebar. The complete tool result contained 50 non-pinned records across local Codex, remote Codex and ChatGPT, plus 7 pins; it reported no unavailable hosts or sources.

    Inspection of the installed assistant wrapper shows that its thread/list request includes modelProviders: null, cursor: null and useStateDbOnly: true. This matches the provider-filter conversion in the current public source: for an ordinary listing, null/omitted selects the configured provider, while an empty array removes the provider filter. We have not established that the installed binary was built from that exact commit.

    On Android, after selecting the Windows host, refreshing and searching, the older openai chat remains missing. A current-provider control chat does appear. The missing chat uses an ordinary project folder, not a managed worktree.

    Android limitation: we have not captured the phone's actual request parameters or response. The two phone checks are consistent with provider filtering, but do not prove Android uses the same request path. We also have not run a live null-versus-empty-array API comparison on this host.

    The separate 50-result limit in #41249 is also present in the assistant wrapper: it discards the backend continuation cursor. That is an additional discovery limitation; increasing the limit alone would not address the provider filter.

    Please include general history discovery and Android Remote in the cross-provider regression coverage. A client intending to list all providers should request modelProviders: [] under the current server behavior, with pagination retained.

    No configuration, application bundle or history metadata was changed during this investigation. Private chat titles, IDs, local paths and provider endpoints are omitted.

  14. nostitos commented on Oct 14, 2026

    @nostitos

    Another reproduction on Codex Desktop 26.1007.30912, app-server 0.162.0-alpha.17.2 (macOS host, ChatGPT for Android Remote). For one project, thread/list returns 2 threads with modelProviders omitted and 46 with [], and the Android list shows the same 2. On this host the Android client also saves the threads it starts under openai instead of the custom default, so threads started from the phone vanish from the phone once they go idle. Write-up: #53848.

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

    appIssues related to the Codex desktop appapp-serverIssues involving app server protocol or interfacesbugSomething isn't workingcustom-modelIssues related to custom model providers (including local models)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions