Codex Desktop: background turn reported completed/interrupted while JSONL continues without final_answer or task_complete

Open 💬 5 comments Opened Aug 17, 2026 by Trying-hc
💡 Likely answer: A maintainer (github-actions[bot], contributor) responded on this thread — see the highlighted reply below.

Summary

Codex Desktop/app-server can report a background-created turn as completed/interrupted while the underlying rollout JSONL is still receiving events and has no final_answer or task_complete.

This appears to affect threads created through app-server thread/start + turn/start, where the external orchestrator only receives acceptance, not final completion.

Environment

  • Codex Desktop release observed in logs: 26.810.41047
  • app-server version observed in logs: 0.148.0-alpha.9
  • session cli_version: 0.146.0
  • Host: local macOS
  • Model provider: codex_local_access
  • Timezone used below: Asia/Shanghai, UTC+8

Evidence

Example B, no compaction/handoff case:

  • Thread: 01a00ee4-6014-7632-ae22-0aef4775476d
  • Turn: 01a00ee4-68c7-7492-9f8f-31fcd00750ae
  • Local JSONL: $HOME/.codex/sessions/2026/08/17/rollout-2026-08-17T16-44-02-01a00ee4-6014-7632-ae22-0aef4775476d.jsonl
  • At 2026-08-17 16:52:07 UTC+8, the JSONL had 224 lines, latest event timestamp 2026-08-17T08:52:07.812Z.
  • The JSONL contained no final_answer and no task_complete.
  • read_thread returned the turn as status="interrupted", error=null, completedAt=null.
  • wait_threads(timeoutMs=0/5000) woke with reason="turnCompleted", but the same response still had latestTurn.status="interrupted" and completedAt=null.
  • During polling, the JSONL kept growing, e.g. 206 -> 216 -> 224 lines, showing events were still being appended after the turn was treated as completed/interrupted.
  • Desktop log around 2026-08-17T08:46:50.390Z showed maybe_resume_success ... latestTurnStatus=interrupted ... markedStreaming=true; the JSONL continued appending after that.

Example A, compaction/handoff-adjacent case:

  • Thread: 01a00ec3-e645-7822-91db-1257005fd4c4
  • A phase="final_answer" handoff summary appeared mid-turn at 2026-08-17 16:12:41 UTC+8.
  • The actual same-turn task_complete was later at 2026-08-17 16:17:19 UTC+8.
  • Desktop appeared to treat the turn as completed before the real task_complete.

Expected Behavior

  • wait_threads should not wake with reason="turnCompleted" unless the turn has a real terminal state.
  • read_thread and Desktop should not present a turn as completed/interrupted while the same turn is still receiving JSONL events.
  • A mid-turn phase="final_answer" should not be treated as task completion without task_complete.

Actual Behavior

  • wait_threads can wake as turnCompleted while the latest turn is still interrupted with completedAt=null.
  • Desktop/read model can diverge from the underlying rollout JSONL stream.
  • Users see background Codex tasks as if they stopped or completed halfway.

Impact

External orchestrators that create Codex tasks through app-server cannot reliably tell whether a turn genuinely finished. Desktop UI also makes background tasks look like they stopped or completed halfway.

Suspected Area

Codex Desktop/app-server read model or wait semantics for background-created turns. This is not limited to context compaction: compaction/handoff final_answer is one trigger shape, but the second example reproduces without final_answer or task_complete.

Diagnostics Snippets

Minimal excerpts only; full logs/JSONL may contain local paths, prompts, project names, message content, and tool outputs.

wait_threads contradiction

{
  "wake": {
    "reason": "turnCompleted",
    "turnId": "01a00ee4-68c7-7492-9f8f-31fcd00750ae",
    "threadId": "01a00ee4-6014-7632-ae22-0aef4775476d"
  },
  "latestTurn": {
    "id": "01a00ee4-68c7-7492-9f8f-31fcd00750ae",
    "status": "interrupted",
    "error": null,
    "completedAt": null,
    "durationMs": null
  }
}

JSONL terminal-event check

{
  "line_count": 224,
  "last_timestamp": "2026-08-17T08:52:07.812Z",
  "finalLines": [],
  "completeLines": []
}

Desktop resume log summary

At 2026-08-17T08:46:50.390Z, Desktop logged a successful resume for the same thread with:

maybe_resume_success ... latestTurnId=01a00ee4-68c7-7492-9f8f-31fcd00750ae latestTurnStatus=interrupted markedStreaming=true ... turnCount=1

The rollout JSONL continued appending after this log line.

View original on GitHub ↗

5 Comments

github-actions[bot] contributor · 11 days ago

Potential duplicates detected. Please review them and close your issue if it is a duplicate.

  • #37647

Powered by Codex Action

nos1609 · 2 days ago

A second live reproduction on Codex Desktop 26.820.7780.0 (Windows ARM64, local CLI 0.149.1) shows a stronger form of the same background-turn projection failure when the target runs on a Linux Remote SSH host.

At one instant, the first-party task tools returned these conflicting views for the same target thread and turn:

  • read_thread: thread active, latest turn inProgress, completedAt=null.
  • wait_threads(timeoutMs=0): thread active, the same latest turn interrupted, completedAt=null.
  • The wait snapshot exposed an early commentary message and an early completed tool marker as the latest visible progress.
  • The read projection contained later tool activity from the same turn.
  • Additional send_message_to_thread calls succeeded and their inputs appeared in that existing turn, but no separate new turn was created.

This caused the orchestrating task to conclude that the target thread was corrupted and needed replacement, although the target was still executing and receiving follow-ups. The failure therefore affects both lifecycle control and model decisions, not only the UI.

Minimal reproduction shape:

  1. Start a long background turn on a Remote SSH task.
  2. Send several follow-ups from another task with send_message_to_thread.
  3. While the target is still active, query it with both read_thread and wait_threads(timeoutMs=0).
  4. Observe that the same turn can be inProgress in one projection and interrupted in the other, while the aggregate thread remains active.
  5. Continue sending follow-ups. They are accepted into the existing turn, but the sender cannot reliably determine whether it steered a live turn, resumed an interrupted turn, or started a new one.

Expected behavior:

  • All first-party task APIs must derive one authoritative lifecycle state for a turn.
  • wait_threads must not expose interrupted while later activity from that same turn is visible through read_thread.
  • A successful cross-thread send should make its effect explicit: queued, steered into the active turn, or started as a new turn.
  • Stale progress markers must not override newer persisted activity.

I did not inspect or modify the remote JSONL or database for this reproduction. The evidence above comes only from live, read-only Codex task APIs. No task IDs, prompts, project names, hostnames, addresses, paths, or account data are included.

nos1609 · 1 day ago

Fresh sanitized cross-API snapshot from Codex Desktop 26.820.7780.0 with CLI 0.149.1 and a task on a Linux Remote SSH host.

At the same instant, first-party task APIs returned mutually incompatible lifecycle views:

  • wait_threads(timeoutMs=0): aggregate thread active, revision unchanged, latest turn completed, final response present.
  • read_thread: aggregate thread active, but an older turn projected as inProgress with completedAt=null.
  • Cross-thread follow-up messages were accepted and advanced the thread update timestamp, but no new latest turn appeared and the wait revision did not change.

The orchestrating task interpreted the aggregate active state as evidence that new work had started, then entered repeated 60-second waits. In reality, the only authoritative latest turn exposed by wait_threads was already terminal.

This strengthens the lifecycle impact: the disagreement can cause an orchestrator to make incorrect control decisions and spend additional model turns waiting for work that was never started.

Expected behavior:

  • read_thread, wait_threads, and cross-thread send must share one authoritative latest-turn projection.
  • An accepted follow-up must explicitly report whether it was queued, steered into a live turn, or started a new turn.
  • Aggregate active must not coexist indefinitely with only a terminal latest turn unless the API exposes the distinct activity that keeps the thread active.

No prompts, task IDs, project or repository names, paths, hostnames, addresses, account data, or credentials are included.

nos1609 · 1 day ago

A fresh reproduction adds a recovery/control failure to the lifecycle projection problem.

Environment:

  • Codex Desktop package 26.820.7780.0 on Windows ARM64.
  • Target task runs through Linux Remote SSH with codex-cli 0.149.1.
  • Evidence comes only from the first-party task APIs. No rollout or database was inspected.

Observed behavior:

  1. The target remained aggregate active with the same turn reported inProgress, completedAt=null, for more than 24 hours. The latest visible tool marker was already completed, and no newer assistant progress was exposed.
  2. A cross-thread send_message_to_thread call returned success and advanced the task's updatedAt.
  3. Despite that accepted send, the wait revision remained 1, the same old turn remained latest, and no new turn appeared.
  4. wait_threads(timeoutMs=0) and read_thread continued to expose the same stale commentary/tool boundary. Neither API indicated whether the accepted message was queued, steered into the ghost turn, or discarded.
  5. A full Desktop process restart restored the Remote SSH transport and native task reads, but did not recover or terminate the ghost turn.
  6. The first-party task surface exposes no bounded interrupt/cancel operation for an ordinary task. The only reliable recovery is a manual UI Stop. handoff_thread is not a safe substitute because it also moves execution placement and associated Git state.

Impact:

  • An orchestrator cannot recover a stale remote turn without user interaction.
  • Successful message acceptance is not proof that work started.
  • Repeated waits or scheduled checks can consume model turns while the target revision and visible work remain unchanged.
  • Using handoff as an interrupt risks unrelated checkout/worktree side effects.

Expected behavior:

  • Expose a first-party interrupt/cancel operation keyed by host and task identity, without migration or Git side effects.
  • Make send_message_to_thread return an explicit disposition: queued, steered into a live turn, started a new turn, or rejected.
  • Advance the revision when accepted work becomes observable.
  • Detect and recover an aggregate active task whose latest turn has made no observable progress after its last completed tool.
  • Keep read_thread, wait_threads, aggregate status, and send disposition on one authoritative lifecycle state.

This appears to be the same underlying projection defect as the issue, with an additional missing recovery primitive rather than a separate duplicate.

nos1609 · 22 hours ago

A fresh Remote SSH reproduction adds a four-way lifecycle disagreement plus a missing paginated-history projection for a structurally valid, actively growing thread.

Environment:

  • Codex Desktop: 26.820.9563.0 on Windows ARM64.
  • Remote Codex CLI/app-server: 0.149.1 on Linux.
  • Affected thread ID: 019ee6ac-9514-7a21-b8fe-913884f7b98d.

At the same time, first-party surfaces reported incompatible states for the same thread and current turn:

  • Native wait_threads: aggregate thread active, current turn inProgress, with fresh progress.
  • A fresh app-server thread/turns/list: the same current turn was interrupted.
  • A fresh app-server metadata-only thread/read: thread status notLoaded and a stale updatedAt.
  • Desktop task inventory: thread active.

The fresh thread/turns/list request returned in about 2.6 seconds, so this snapshot was not a generic transport timeout.

Storage evidence:

  • The exact rollout existed, continued to grow, and a full snapshot contained about 380 MB and 98,239 valid JSONL records with zero parse failures.
  • The remote thread-store row contained the correct rollout reference and current metadata.
  • The paginated history database had no projection state row, zero projected turns, and zero projected items for this thread.
  • The active writer retained the rollout and continued normal work.

User-visible impact:

  • Desktop first showed a false no rollout found error, then remained on “Loading task…”.
  • VS Code refused the same thread as open in another application.
  • The native task APIs could still observe the live turn, so orchestration and UI clients made decisions from mutually incompatible projections.

Expected behavior:

  1. thread/read, thread/turns/list, Desktop inventory, and native task APIs must use one authoritative latest-turn lifecycle.
  2. A live turn must not simultaneously project as inProgress, interrupted, notLoaded, and aggregate active.
  3. If the paginated projection is absent while the rollout and thread-store row are valid, app-server should rebuild it or fall back to bounded rollout reading.
  4. The renderer should surface a specific projection/ownership error instead of presenting missing history or spinning indefinitely.

No repository name, project title, username, hostname, address, filesystem path, prompt content, or raw log is included.