Native goal is silently deleted from thread_goals on thread resume / after Voice Live ends

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

Bug description

A native goal that is created and correctly persisted gets silently deleted when the thread is resumed/rehydrated. After resuming a thread (or after a Voice Live session ends, which triggers the same rehydration path via transcript_tail_flush), get_goal returns null and update_goal(status="complete") fails with:

cannot update goal because this thread has no goal

The app-server emits thread/goal/cleared during the resume, and the row is physically removed from the thread_goals table in ~/.codex/goals_1.sqlite.

Reproduction steps (headless, no voice needed)

  1. Create a thread and a goal:

``
codex exec "Call create_goal with objective 'canary goal', then call get_goal and print the raw result."
``

  • get_goal returns status: "active".
  • Confirmed in goals_1.sqlite: a row for the new thread_id exists in thread_goals (status active).
  1. Resume the same thread:

``
codex exec resume <session-id> "Call get_goal and print the raw result."
``

  1. While the resumed session is running (before any model turn even completes), the runtime emits:

``
app-server event: thread/goal/cleared targeted_connections=0
`
and the row for that
thread_id is **deleted** from thread_goals`. The six pre-existing rows for older threads were untouched.

  1. get_goal on the resumed thread returns null.

Observed timeline (2026-08-16, local time)

  • 10:51:34 — goal row present in thread_goals (snapshot of the sqlite db).
  • 10:52 — codex exec resume started.
  • 11:03:37 and 11:08:37 — two thread/goal/cleared events in the runtime logs during the resume.
  • 11:14 — goal row gone from thread_goals (second snapshot).

Voice Live variant (original sighting)

When a Voice (realtime) conversation ends, the injected transcript_tail_flush message ("Realtime conversation ended...") is followed by the same goal clearing: goal active before the voice session, get_goal = null right after. This makes goal mode unusable across any voice session boundary.

Additional inconsistency

On the affected thread, create_goal is refused with:

cannot create a new goal because this thread has an unfinished goal; complete the existing goal first

while get_goal returns null — so the "unfinished goal" check used by create_goal reads different state than get_goal/update_goal.

Environment

  • codex-cli 0.145.0 (darwin arm64)
  • VS Code extension + CLI (codex exec)
  • macOS

Expected behavior

Resuming a thread (or ending a Voice Live session) should preserve the thread's active goal, or at minimum surface why it was cleared. Silent deletion makes native goals unusable for any multi-session or voice-interleaved workflow.

View original on GitHub ↗

2 Comments

github-actions[bot] contributor · 12 days ago

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

  • #38833

Powered by Codex Action

jdcodes1 · 11 days ago

Partial analysis from reading the goal persistence layer on main @ 1f41cc5d92 — I didn't find the final culprit, but the code rules out several suspects and turns your "additional inconsistency" into a strong constraint on where the bug can live.

1. Your create/get inconsistency is impossible at the SQL layer — which localizes it to session wiring. get_goal is a plain SELECT … WHERE thread_id = ? with no filters (state/src/runtime/goals.rs#L41-L66). create_goal's "unfinished goal" error is produced when the upsert's ON CONFLICT(thread_id) … WHERE thread_goals.status = 'complete' clause declines to replace an existing non-complete row for that same thread_id (goals.rs#L213-L246, surfaced at ext/goal/src/tool.rs#L207-L213). Against one database, "get → null" and "create → conflict" for the same key cannot both be true. So on the affected resumed thread, the two tool calls must be executing with different thread identities or different state-DB handles — i.e., the rehydration path wires at least one goal tool instance to a stale thread id (or a stale StateRuntime). That's a much smaller search space than "goals are flaky."

2. What can delete the row. thread_goals rows are removed on exactly two kinds of path:

  • explicit clears — the thread/goal/delete RPC and TUI action, both user-driven (ext/goal/src/api.rs#L288-L329);
  • thread deletion cascadedelete_threads_strict removes the goal row along with queue/memories/logs whenever a thread row is deleted (state/src/runtime/threads.rs#L1090-L1107).

Notably, the resume-restore hook itself only reads: restore_after_resume fetches the goal and updates in-memory accounting, never the DB (ext/goal/src/runtime.rs#L338-L360). And the thread/goal/cleared you captured is emitted by the resume snapshot after a DB read finds nothing (app-server/src/request_processors/thread_lifecycle.rs#L788-L820) — it reports the loss, it doesn't cause it. Combined with your observation that the row physically disappears during resume, the cascade path is the prime suspect: something in exec-resume's thread-row reconciliation (delete + re-insert of the thread row, e.g. id/session migration or rollout↔state-db reconciliation) would take the goal with it silently, and re-inserting the thread row does not restore the goal.

3. A discriminating experiment (cheap, no voice needed): snapshot goals_1.sqlite and the threads table before and after codex exec resume. If the thread row's rowid/created_at changes across resume (same thread_id, fresh row), the cascade theory is confirmed and the fix is to make that reconciliation preserve (or re-attach) dependent rows — goals, queue, memories — rather than cascade-deleting them. If the thread row is untouched, the remaining suspects shrink to the explicit-clear RPC being invoked by the resume path itself, which would show up as a thread/goal/delete request in verbose app-server logs.

Happy to be corrected by whoever owns this area, but at minimum: (1) means the create/get divergence is a resume-wiring bug independent of the deletion, and both are worth regression tests — "goal survives codex exec resume" and "goal tools agree on thread identity after rehydration".