openai / openai/codex

Codex Desktop side conversation cannot rename parent: set_thread_title stalls in thread/read host resolution

Open
#44,759 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

app app-server bug tool-calls
Dominant language
Rust
Stars
125k
Forks
19.4k
PR merge metrics
PR metrics pending

Description

Summary

In Codex Desktop, a side conversation cannot reliably rename its parent task with the first-class set_thread_title tool. The parent exists, is visible in the sidebar, and remains active, but the tool stalls after a thread/read host-resolution preflight returns -32600 thread not loaded. No structured tool result is returned and the title remains unchanged.

This was reproduced three times from the same side conversation, across two renderer web contents.

Environment

  • Codex Desktop: 26.903.71938 (8576)
  • Bundled app-server: 0.153.4
  • macOS: 26.6 (25G72), Apple Silicon
  • Installed app: Developer ID signed by OpenAI, hardened runtime, stapled notarization ticket
  • app.asar SHA-256: 58fef82480b9064e209b5b2fd934992e8d71515aea8084482369cfeaff1b8ee0

Minimal reproduction

  1. Start a normal local Codex Desktop task (<parent-thread-id>).

  2. Open a Desktop side conversation attached to that task (<side-thread-id>).

  3. From the side conversation, invoke:

    {"threadId":"<parent-thread-id>","title":"Renamed parent"}
    

    with the dynamic set_thread_title tool.

  4. Observe that the parent remains present and usable in the sidebar, but the rename call never completes and its title does not change.

Expected behavior

Renaming should update persisted task metadata through thread/name/set, independently of whether the target conversation is currently loaded in a particular renderer/app-server runtime. If routing is genuinely ambiguous or unavailable, the dynamic tool should return one bounded structured error.

Actual behavior and sanitized evidence

Each attempt begins with dynamic_app_tool_renderer_execution_started tool=set_thread_title. It then issues multiple thread/read requests for <parent-thread-id>:

Attempt Renderer First read Second read Observed tool result
A 1 success in 1 ms -32600 thread not loaded in 864 ms none
B 5 success in 2 ms -32600 thread not loaded in 646 ms none
C 1 success in 1 ms -32600 thread not loaded in 632 ms none

The failure records report timeoutMs=0. For all three call IDs, the log contains the execution-start record but no corresponding Sending server response record. There is also no thread/name/set for these attempts. The parent title remained unchanged when read back through the user-visible task API.

Near attempt B, renderer 5 also logged Received item/completed for unknown conversation for the parent. That supports a renderer-hydration aspect, but the rename should not depend on renderer conversation state.

Shipped-code path

I inspected the read-only extracted JavaScript from the signed installed bundle. The minified names below are included only to make the chain falsifiable:

  1. The set_thread_title handler selects arguments.threadId ?? sourceThreadId and calls the rename helper.
  2. The rename helper first calls a host resolver with only scope and threadId; it does not pass sourceHostId/preferredHostId.
  3. With no preferred host, that resolver probes candidate host managers with Promise.all(...), and each probe calls readThread(threadId, ...).
  4. Only after resolution does it call setThreadTitle(..., {requireAcknowledgement: true}), which ultimately sends thread/name/set.
  5. The dynamic-tool dispatcher awaits the handler without an outer deadline and sends the final dynamic-tool response only after that await settles.

This explains both symptoms: an unnecessary live-read requirement blocks a persisted-metadata operation, and one unresolved candidate-host probe can prevent the handler's own catch from returning a structured error. The logs do not identify which candidate host remained outstanding, so the exact outstanding host is an inference; the unbounded fan-out and missing final response are directly observable.

The app-server rename implementation itself does not need a loaded runtime. It normalizes the ID/name and updates thread metadata directly:

https://github.com/openai/codex/blob/eaa8b6d91701d6cabe464141facc677e5915fbfc/codex-rs/app-server/src/request_processors/thread_processor.rs#L1832-L1864

Current public dynamic-tool plumbing also has unbounded response awaits (receiver.await / rx_response.await.ok()), compounding the missing-completion failure mode.

Related issues / duplicate search

I searched open and closed issues and PRs for set_thread_title, thread/name/set, thread/read, thread not loaded, side-chat rename/title, and unknown conversation. I did not find an exact report of a side conversation failing to rename its parent.

  • #34295 is the closest liveness relative: a local Desktop dynamic-tool call stalls while an unrelated remote SSH host reconnects. It suggests this cross-host fan-out problem predates the current build.
  • #42509 and #42286 describe side-chat/subagent renderer hydration failures (unknown conversation), but not title mutation.
  • #37668 reports thread-management tools hanging before reaching AppServerConnection; here the rename does reach it and stalls in preliminary thread/read routing.
  • #40777 describes a related missing-completion pattern in nested tool dispatch, but not this API path.
  • #40094 and #43492 concern different title-attribution/shortening behavior.

Proposed fix

  1. Preserve target-host provenance in set_thread_title:
    • accept an explicit hostId when provided;
    • use sourceHostId for self/parent operations when the side-conversation provenance establishes that host;
    • otherwise resolve from stable sidebar/catalog summary metadata rather than requiring a live thread/read.
  2. Once the host is known, send thread/name/set directly. Do not require the target conversation to be loaded or hydrated.
  3. If multiple hosts truly match, return a bounded, explicit ambiguity error.
  4. Put per-host deadlines around discovery and use early-success / settled-result handling so one unavailable host cannot block a local match.
  5. Add a dispatcher-level timeout/cancellation path that always emits exactly one structured DynamicToolCallResponse and removes pending-call state.

Regression tests

  1. An ephemeral side conversation renames an unloaded-but-persisted parent on the same local host; a thread/read stub may return thread not loaded, but exactly one thread/name/set is sent and the rename succeeds.
  2. An unavailable/reconnecting remote host is registered while the local parent is targeted; the local rename completes without waiting for the remote host.
  3. Omitted threadId self-rename uses sourceHostId and does not fan out.
  4. Parent events are unknown in the side renderer; persisted rename still succeeds and sidebar/header converge when hydrated.
  5. Genuine multi-host ambiguity returns a bounded structured error naming the ambiguity.
  6. An injected host-read or handler hang produces a timeout response and cleans up the pending dynamic call exactly once.

Workarounds

  • Safest: rename manually from the parent task's sidebar/context menu.
  • Often effective: navigate to the parent task, let it hydrate in the current window, and invoke the rename from the parent task itself.
  • Retrying from the side conversation after merely opening the parent is best-effort only because the side renderer can still perform cross-host discovery.

No SQLite state, installed app files, or application processes were modified during this investigation.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with codex-rs/app-server/src/request_processors/thread_processor.rs#L1832-L1864, then trace the set_thread_title handler, host resolver, and dynamic-tool dispatcher described in the report. Add coverage for unloaded-parent renames, unavailable hosts, ambiguity, and hung calls, verifying bounded responses, cleanup, and exactly one thread/name/set request.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, rust
Domain
backend-api-design, desktop, devtools
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.