[macOS] Heartbeat occurrence consumed during DarkWake after thread/resume timeout; no retry on full wake

Open 💬 3 comments Opened Jul 27, 2026 by rpavlovs

What version of the Codex App are you using (From "About Codex" dialog)?

26.721.41059 (build 5848)

What subscription do you have?

Plus

What platform is your computer?

Darwin 25.5.0 arm64 arm
macOS 26.5.2 (25F84)

What issue are you seeing?

A recurring heartbeat automation was due while the MacBook was asleep with its lid closed. Codex attempted thread/resume during a two-second maintenance DarkWake, but it persisted last_run_at and advanced next_run_at before the resume completed. The resume timed out while the Mac continued sleeping. The scheduled occurrence was then permanently consumed: no heartbeat turn or automation run was created, and a later full user wake did not retry it.

This is silent data loss for scheduled attention. The automation remains ACTIVE and looks as if it ran, but its work never happened.

Verified persisted state after the incident:

scheduled occurrence: 2026-07-27 05:00:00 UTC
last_run_at:          2026-07-27 05:06:32 UTC
next_run_at:          2026-07-28 05:01:30 UTC
automation_runs rows: 0
heartbeat turn:       absent from the target thread rollout

The target thread rollout contains no records between 2026-07-25T18:07:38Z and the later full user session on July 27, so no heartbeat turn was submitted at last_run_at.

What steps can reproduce the bug?

  1. Create an active recurring heartbeat automation attached to an existing local thread.
  2. Schedule an occurrence while the MacBook will be asleep with the lid closed.
  3. Leave Codex Desktop running and let macOS enter normal sleep before the occurrence.
  4. Allow a short maintenance DarkWake after the scheduled time.
  5. Keep the Mac asleep long enough for thread/resume to time out across later maintenance wakes.
  6. Fully wake the Mac with lid or HID activity.
  7. Inspect ~/.codex/sqlite/codex-dev.db, the target thread rollout, the Codex Desktop log, and pmset -g log.

The observed timeline was:

05:00:00 UTC  Heartbeat occurrence due
05:06:32 UTC  Two-second DarkWake begins
05:06:32 UTC  automations.last_run_at is persisted and next_run_at advances
05:06:34 UTC  macOS returns to sleep
05:22:42 UTC  Next DarkWake; Codex logs thread/resume timeout
05:40:30 UTC  Later DarkWake; late thread/resume response is orphaned
10:41:29 UTC  Full lid/HID wake
after wake     No retry, no heartbeat turn, no automation_runs row

Relevant sanitized Codex log entries:

2026-07-27T05:22:42.724Z warning [automations] Scheduled run failed automationId=<redacted> errorMessage="Timed out waiting for MCP response to thread/resume"
2026-07-27T05:40:30.696Z warning [AppServerConnection] response_orphaned ... requestId=thread/resume:<redacted>
2026-07-27T05:40:30.697Z error [electron-message-handler] No promise for request ID id=thread/resume:<redacted>
2026-07-27T05:40:30.699Z error [electron-message-handler] Conversation state not found conversationId=<redacted>

Relevant pmset -g log entries, local time UTC+02:00:

2026-07-27 07:06:32 +0200 DarkWake ... rtc/SleepService ... 2 secs
2026-07-27 07:06:34 +0200 Sleep Entering Sleep state due to 'Sleep Service Back to Sleep'
2026-07-27 07:22:42 +0200 DarkWake ... rtc/SleepService ... 2 secs
2026-07-27 07:22:44 +0200 Sleep Entering Sleep state due to 'Sleep Service Back to Sleep'
2026-07-27 12:41:29 +0200 Wake ... lid ... HID Activity

What is the expected behavior?

A scheduled occurrence should not be marked as run or advanced beyond recovery until its heartbeat turn has been durably accepted.

If thread/resume times out or its response arrives after the requesting promise is gone, Codex should do one of the following:

  1. Leave the occurrence due for retry.
  2. Persist a separate failed or pending attempt and retry it on the next meaningful wake.
  3. Roll back the schedule claim when no heartbeat turn exists.

last_run_at should not imply execution when there is no corresponding turn or run record. Once dispatch starts during DarkWake, Codex should also hold an appropriate bounded sleep assertion long enough to settle the attempt.

A regression test should cover sleep between claiming a due heartbeat and durably creating or resuming its target turn.

Additional information

This is related to, but materially different from:

  • #34794, where an already-created automation turn is suspended during DarkWake and later resumes after a full wake.
  • #17893, where heartbeat schedules advance without execution, but the reported last_run_at remains null.

Here, last_run_at was set exactly at DarkWake, next_run_at advanced, thread/resume later timed out, and the missed occurrence was never retried.

Inspection of the shipped Desktop bundle for this build is consistent with the persisted evidence: the scheduler writes lastRunAt and the next scheduled occurrence before it calls the thread resume/start path. A failure in that later path does not restore the due occurrence. I can provide exact local bundle offsets or additional sanitized logs if useful.

View original on GitHub ↗

3 Comments

rpavlovs · 1 month ago

Sanitized diagnostics uploaded via Codex feedback. Tracking ID: no-active-thread-019fa372-2c8d-7963-bbfe-ad94a68b75da.

IgorGanapolsky · 8 days ago

The bug is claiming the occurrence before the turn exists. last_run_at / next_run_at moved at 05:06:32 during a two-second DarkWake, then thread/resume timed out and the later orphaned response had no promise. Full wake correctly found nothing to retry because the schedule already looked done. ACTIVE with zero automation_runs rows is silent data loss.

Fix the ledger first: do not persist last_run_at until a heartbeat turn id (and an automation_runs row) is durable. If thread/resume times out or the response is orphaned, leave the occurrence due or write a failed attempt and retry on the next full wake — not on the next 2s Sleep Service window. DarkWake is not a run; PreventUserIdleSystemSleep will not stop Sleep Service Back to Sleep.

jakubkrcmar · 1 day ago

Additional sanitized terminal-state evidence from bundled CLI 0.149.0-alpha.4.3:

  • pmset recorded a full lid/user-activity wake at 2026-08-26 20:56:26 CEST.
  • Scheduled automation session 01a03f6e-bcf1-7920-a1db-e21e91d16e29 was created 31 seconds later at 20:56:57.
  • It emitted one assistant preamble at 20:57:06, made no tool call, and had neither task_complete nor turn_aborted by 2026-08-27 11:02:02 CEST (more than 14 hours later).

This is not proof that this particular occurrence was claimed during DarkWake, so it may have a different immediate trigger. It does reinforce the terminal-receipt requirement in this issue: every scheduled activation needs one durable final state (completed, failed, cancelled, or requeued) plus a full-wake recovery sweep for activations left nonterminal beyond a bounded lease. A task with only a preamble should not remain silently open indefinitely.