[Bug] Desktop multi-session send never completes (outgoing_message.rs:563 missing turn/completed event)

Open 💬 2 comments Opened Jun 29, 2026 by liaoyouzhi38-blip
💡 Likely answer: A maintainer (github-actions[bot], contributor) responded on this thread — see the highlighted reply below.

Codex Desktop — multi-session send 永不完成 Bug 报告

Reporter: user (Codex Desktop 26.623.8305.0 / Codex CLI 0.142.4 on Windows 11)
Repro platform: Windows 11 Pro 10.0.26200
Repro date: 2026-06-30
Severity: High — 单会话能工作,多会话/新会话完全卡死

---

TL;DR

Codex Desktop 在新建(第二个及之后)会话后,用户点发送,模型实际回复正常,但 Desktop UI 永远停留在 "Working..." 状态,前端 watchdog 数秒后兜底重置 loading,UI 表现即"消息凭空消失 + loading 自动消失"。根因是 Rust app-server 端 codex_app_server::outgoing_message 层在 turn 状态机完成后未向 Desktop stdio 发 turn/completed 终态事件,导致 Renderer 永远收不到 stream end,UI 卡在 loading。

---

Reproduction Steps

  1. 启动 Codex Desktop(AppX 26.623.8305.0)
  2. 等待 onboarding 完成
  3. 在会话 A 中发送一条消息 → 正常收到 AI 回复(A 工作正常)
  4. 不关闭 A,通过 Ctrl+N 或 sidebar "New chat" 创建会话 B
  5. 在会话 B 输入任意文字,点发送
  6. 观察:
  • 按钮进入 loading 状态 ✅
  • 输入框文字已清空 ✅
  • B 对话流未出现 user message ❌
  • A 对话流无变化(消息未串会话)
  • 数秒后 loading 自动消失(前端 watchdog 兜底)
  • 整段等待期间无 AI 回复,但模型其实已经回完
  1. 刷新 Codex Desktop / 退出重启 → 恢复(仅本次新建会话能工作,再次新建第二个会话又卡)

---

Expected vs Actual

| | Expected | Actual |
|---|---|---|
| B 点发送后 | user message 立即出现在 B 流 | 流始终空 |
| AI 回复 | 在合理时间内到达 B 流 | 永远不到达(但模型侧已生成) |
| loading 熄灭 | 收到完整回复后 | 数秒后被前端 watchdog 强制重置 |
| 多会话隔离 | A/B 互不影响 | B 完全无法发送,必须刷新 |

---

Root Cause(基于 logs_2.sqlite 真实证据,非症状匹配)

Module: codex_app_server::outgoing_message
File: app-server/src/outgoing_message.rs:563
Failure mode: turn 状态机完成事件未被序列化发送至 Desktop stdio

证据 1 — outgoing_message 单点 hot line

SELECT DISTINCT file, line, COUNT(*) FROM logs
WHERE module_path='codex_app_server::outgoing_message'
GROUP BY file, line ORDER BY 3 DESC LIMIT 5;
app-server\src\outgoing_message.rs | 563 | 7520

outgoing_message.rs:563 在所有 7520 条该模块日志中被唯一命中 → 序列化路径单点。

证据 2 — 全部 TRACE,零 ERROR/WARN

SELECT level, COUNT(*) FROM logs
WHERE module_path='codex_app_server::outgoing_message'
GROUP BY level;
TRACE | 7520
ERROR | 0
WARN  | 0

→ 没有"完成事件发送失败"错误,因为根本没尝试发完成事件。

证据 3 — turn 内核侧正常完成

SELECT MAX(ts), MIN(ts), COUNT(*) FROM logs
WHERE module_path='codex_core::session::turn';
1782757488 | 1782219802 | 892

core/src/session/turn.rs:308 / 1949(共 463 + 425 + 4 = 892 条)按 thread_id 正常分布:

019f0962-... | 76
019ef4d5-... | 67
019f0a15-... | 65
...(每个 thread 33-76 条)

→ 内核侧 turn 状态机走完完整生命周期。问题不在 turn 状态机本身。

证据 4 — outgoing_message 缺 thread_id 维度

SELECT COUNT(*) FROM logs
WHERE module_path='codex_app_server::outgoing_message'
  AND thread_id IS NOT NULL;
0

→ outgoing_message 层向桌面发事件时不带 thread_id,意味着事件无法被桌面正确路由到目标会话的 stream 收尾。

证据 5 — session_index 当天零更新

cat ~/.codex/session_index.jsonl | jq -r '.updated_at' | sort -u | tail -5

全部 thread 的 updated_at 停在 2026-06-29 —— 2026-06-30 当天 Desktop 一次都没成功写入新 thread,直接坐实 outgoing_message 缺完成事件。

证据 6 — Desktop global state unread 列表为空

.codex-global-state.json 中:

"unread-thread-ids-by-host-v1": {"local": []}

按设计,卡住的会话会进 unread 等用户重试。空列表说明桌面根本没意识到有未完成的回复 —— 终态事件缺失的直接表征。

---

Why "Refresh Fixes It"

刷新杀掉 Renderer 进程 → loading 状态被进程销毁清掉 → 下次打开是新进程 + 干净状态。但这是伪恢复:

  • 第一个会话(重启后建的第一个新会话)能用,因为 Renderer 走"新建会话"路径
  • 第二个会话又卡,因为 Renderer 走了"已有会话切到新会话"路径,触发 outgoing_message 缺完成事件的 bug

---

Suggested Fix(对上游)

在 app-server/src/outgoing_message.rs:563 之后,确保 turn 状态机走到 Completed 时向 Desktop stdio 发 turn/completed 事件(带 thread_id + turn_id),让 Renderer 能正确收尾 stream end、熄灭 loading。

参考已有中间事件序列化路径(line 563)补一个终态分支即可。

---

Workaround(用户侧)

  1. 只用第一个会话(每次启动后只开一个新会话,不切不新建)
  2. 用 CLI/TUI 替代 Desktop:

``
D:\codex-cli\codex.exe
``

TUI 不走 app_server 层,绕过该 bug。

  1. 每次新会话前先关闭 Codex Desktop 再重启(仅当前会话能用)

---

Environment

Codex Desktop: 26.623.8305.0 (AppX, winget: No newer package versions available)
Codex CLI:     0.142.4 (latest, 已 dismiss 0.142.3 升级提示)
OS:            Windows 11 Pro 10.0.26200
shell:         bash (Git Bash)
Process count: 11 Codex.exe 同时在跑(潜在资源/句柄占用问题)
tmp residue:   ~/.codex/tmp/arg0/codex-arg0r2u6kQ/.lock + apply_patch.bat 残留
               app-server-smoke-err.log: "WARNING: failed to clean up stale arg0 temp dirs: 拒绝访问"

---

Closing Notes

  • 该 bug 在用户机器上已稳定复现,6 月 30 日全天无任何 Desktop 会话成功完成
  • 用户已在 ~/.codex/.codex-global-state.json 的 prompt-history 中留下相关查询痕迹("Console / Network 面板这些怎么打开?"),说明用户本人也在调查
  • v8 多会话排障 Agent 已验证症状模式匹配(Dispatcher global isSending 假设)不成立,真实根因在 Rust 端 outgoing_message 层

View original on GitHub ↗

2 Comments

github-actions[bot] contributor · 1 month ago

English translation:

Codex Desktop — Multi-Session Send Never Completes Bug Report

Reporter: user (Codex Desktop 26.623.8305.0 / Codex CLI 0.142.4 on Windows 11)
Repro platform: Windows 11 Pro 10.0.26200
Repro date: 2026-06-30
Severity: High — single sessions work, but multi-session/new-session usage completely hangs

---

TL;DR

After Codex Desktop creates a new session (the second and subsequent sessions), when the user clicks send, the model actually responds normally, but the Desktop UI stays in the "Working..." state forever. After a few seconds, the frontend watchdog falls back to resetting loading, and the UI appears as "the message disappears into thin air + loading disappears automatically." The root cause is that after the turn state machine completes, the Rust app-server layer codex_app_server::outgoing_message does not send the terminal turn/completed event to Desktop stdio, causing the Renderer to never receive stream end and the UI to remain stuck in loading.

---

Reproduction Steps

  1. Start Codex Desktop (AppX 26.623.8305.0)
  2. Wait for onboarding to complete
  3. Send a message in session A → receives an AI reply normally (A works normally)
  4. Without closing A, create session B via Ctrl+N or sidebar "New chat"
  5. Enter any text in session B and click send
  6. Observe:
  • The button enters loading state ✅
  • The input text has been cleared ✅
  • The B conversation stream does not show the user message ❌
  • The A conversation stream does not change (the message was not routed to the wrong session)
  • After a few seconds, loading disappears automatically (frontend watchdog fallback)
  • During the entire wait, there is no AI reply, but the model has actually already finished responding
  1. Refresh Codex Desktop / quit and restart → recovers (only the newly created session for that run works; creating a second session again gets stuck)

---

Expected vs Actual

| | Expected | Actual |
|---|---|---|
| After clicking send in B | user message immediately appears in the B stream | stream remains empty |
| AI reply | arrives in the B stream within a reasonable time | never arrives (but the model side has generated it) |
| loading stops | after receiving the complete reply | forcibly reset by the frontend watchdog after a few seconds |
| multi-session isolation | A/B do not affect each other | B cannot send at all and must be refreshed |

---

Root Cause (based on real evidence from logs_2.sqlite, not symptom matching)

Module: codex_app_server::outgoing_message
File: app-server/src/outgoing_message.rs:563
Failure mode: the turn state machine completion event is not serialized and sent to Desktop stdio

Evidence 1 — outgoing_message single hot line

SELECT DISTINCT file, line, COUNT(*) FROM logs
WHERE module_path='codex_app_server::outgoing_message'
GROUP BY file, line ORDER BY 3 DESC LIMIT 5;
app-server\src\outgoing_message.rs | 563 | 7520

outgoing_message.rs:563 is the only hit among all 7520 logs for this module → single point on the serialization path.

Evidence 2 — all TRACE, zero ERROR/WARN

SELECT level, COUNT(*) FROM logs
WHERE module_path='codex_app_server::outgoing_message'
GROUP BY level;
TRACE | 7520
ERROR | 0
WARN  | 0

→ There is no "failed to send completion event" error, because no attempt was made to send the completion event at all.

Evidence 3 — turn completes normally on the core side

SELECT MAX(ts), MIN(ts), COUNT(*) FROM logs
WHERE module_path='codex_core::session::turn';
1782757488 | 1782219802 | 892

core/src/session/turn.rs:308 / 1949 (463 + 425 + 4 = 892 entries total) are distributed normally by thread_id:

019f0962-... | 76
019ef4d5-... | 67
019f0a15-... | 65
... (33-76 entries per thread)

→ The turn state machine on the core side goes through its full lifecycle. The problem is not in the turn state machine itself.

Evidence 4 — outgoing_message lacks the thread_id dimension

SELECT COUNT(*) FROM logs
WHERE module_path='codex_app_server::outgoing_message'
  AND thread_id IS NOT NULL;
0

→ When the outgoing_message layer sends events to Desktop, it does not include thread_id, which means Desktop cannot correctly route events to close the target session's stream.

Evidence 5 — session_index had zero updates that day

cat ~/.codex/session_index.jsonl | jq -r '.updated_at' | sort -u | tail -5

All threads' updated_at values are stuck on 2026-06-29 — on 2026-06-30, Desktop did not successfully write any new thread even once, directly confirming that outgoing_message is missing the completion event.

Evidence 6 — Desktop global state unread list is empty

In .codex-global-state.json:

"unread-thread-ids-by-host-v1": {"local": []}

By design, stuck sessions should enter unread and wait for the user to retry. The empty list shows that Desktop is not aware there is an unfinished reply at all — a direct sign that the terminal event is missing.

---

Why "Refresh Fixes It"

Refreshing kills the Renderer process → the loading state is destroyed and cleared with the process → the next open uses a new process + clean state. But this is a false recovery:

  • The first session (the first new session created after restart) works because Renderer follows the "new session" path
  • The second session gets stuck again because Renderer follows the "existing session switched to new session" path, triggering the missing completion event bug in outgoing_message

---

Suggested Fix (for upstream)

After app-server/src/outgoing_message.rs:563, ensure that when the turn state machine reaches Completed, a turn/completed event (with thread_id + turn_id) is sent to Desktop stdio, so the Renderer can correctly close stream end and stop loading.

Use the existing intermediate-event serialization path (line 563) as reference and add a terminal-state branch.

---

Workaround (user side)

  1. Use only the first session (after each launch, open only one new session; do not switch or create another)
  2. Use CLI/TUI instead of Desktop:

``
D:\codex-cli\codex.exe
``

TUI does not go through the app_server layer, bypassing this bug.

  1. Close and restart Codex Desktop before each new session (only the current session works)

---

Environment

Codex Desktop: 26.623.8305.0 (AppX, winget: No newer package versions available)
Codex CLI:     0.142.4 (latest, dismissed the 0.142.3 upgrade prompt)
OS:            Windows 11 Pro 10.0.26200
shell:         bash (Git Bash)
Process count: 11 Codex.exe processes running simultaneously (potential resource/handle usage issue)
tmp residue:   ~/.codex/tmp/arg0/codex-arg0r2u6kQ/.lock + apply_patch.bat residue
               app-server-smoke-err.log: "WARNING: failed to clean up stale arg0 temp dirs: Access denied"

---

Closing Notes

  • This bug has been stably reproduced on the user's machine, and no Desktop session successfully completed at any point on June 30
  • The user left related query traces in the prompt-history of ~/.codex/.codex-global-state.json ("How do I open the Console / Network panels?"), indicating the user was also investigating
  • The v8 multi-session troubleshooting Agent has verified that symptom-pattern matching (the Dispatcher global isSending hypothesis) does not hold; the real root cause is in the Rust-side outgoing_message layer

<!-- codex-issue-translator -->

liaoyouzhi38-blip · 1 month ago

Amendment after fresh diagnostic (2026-06-30 04:05 UTC+8)

After running additional traces against the active 4 sessions created during Desktop restart tests with multi_agent = true + followUpQueueMode = "steer", I have corrected evidence that changes the root-cause attribution.

What I observed

SELECT module_path, COUNT(*)
FROM logs
WHERE thread_id IN (
  '019f14fb-f420-7350-90f4-447dd36844aa', -- A (working)
  '019f14fc-4bd3-7d20-9fa0-d7a3a65764f5', -- active session
  '019f14f3-eb04-7260-a474-33bc716f8e35', -- B (stuck)
  '019f14f5-423a-7241-9973-06b87870544b'  -- C (stuck)
)
GROUP BY module_path
ORDER BY 2 DESC;

Result:

codex_core::stream_events_utils         76
                                       52  (empty module_path)
hyper_util::client::legacy::connect::http  39
hyper_util::client::legacy::client     39
hyper_util::client::legacy::pool       28
codex_core::session::turn              24
codex_feedback                         13
codex_client::transport                13
codex_client::default_client           13
codex_client::custom_ca                13
codex_core::spawn                      11
codex_core::session::handlers           6
codex_core::context_manager::normalize  6
codex_models_manager::manager           4
codex_core::tools::router               3
codex_core::shell_snapshot              2
codex_models_manager::cache             1
codex_app_server::request_processors::thread_lifecycle  1

What this means

ALL sessions — including the stuck B/C sessions — flow through codex_client::transport and never touch codex_app_server::* (except a single thread_lifecycle event).

The earlier attribution to app-server/src/outgoing_message.rs:563 was wrong. The outgoing_message.rs:563 TRACE lines (7878 of them) are unrelated activity (ambient suggestions, remote control sync, etc.) — none of them carry a thread_id and none of them correspond to the user's send clicks.

Real picture

| Stage | Module | Status |
|---|---|---|
| E1 UI click | (Electron renderer) | ✅ |
| E2 dispatch | codex_core::session::turn (line 1949) | ✅ |
| E3 transport | codex_client::transport → hyper → HTTPS to relay | ✅ |
| E4 stream events | codex_core::stream_events_utils (line 524, 4-46 events) | ✅ — model did reply |
| E5 turn close | codex_core::session::turn (line 308, TRACE) | ✅ — turn state machine ended |
| E6 notify Renderer | MISSING | ❌ |

For session B/C, turn lifecycle ends cleanly but no completion signal reaches the Electron Renderer, so loading spins forever until the frontend watchdog resets it.

Revised suggested fix area

Instead of patching app-server/src/outgoing_message.rs:563 (the wrong layer), the bug is likely in the bridge between codex_core::session::turn completion and the Renderer-facing IPC stream.

Candidates to investigate upstream:

  • codex_core::session::turn → completion event fan-out
  • The Electron-side consumer of codex_client::transport stream end
  • Possibly a session-id/routing bug in the Renderer-to-Core IPC bridge that drops the completion event for sessions created after the first session in a given Desktop process

Reproduction reliability

Confirmed reproducible after every Desktop cold start: the very first send in the first session works; creating any second session (Ctrl+N) and sending in it always hangs.

Sorry for the earlier wrong attribution

The earlier root-cause attribution to outgoing_message.rs:563 was based on a static baseline that included unrelated CLI/sync activity. This amendment supersedes it.