Skip to content

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

Open

Description

@tex-spec

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).
  2. Resume the same thread:

    codex exec resume <session-id> "Call get_goal and print the raw result."
    
  3. 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.

  4. 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.

Activity

  1. added
    bugSomething isn't working
    CLIIssues related to the Codex CLI
    app-serverIssues involving app server protocol or interfaces
    on Aug 16, 2026
  2. github-actions commented on Aug 16, 2026

    @github-actions
    Contributor

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

    Powered by Codex Action

  3. jdcodes1 commented on Aug 17, 2026

    @jdcodes1

    Partial analysis from reading the goal persistence layer on main @ 1f41cc5 — 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 cascade — delete_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".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    CLIIssues related to the Codex CLIapp-serverIssues involving app server protocol or interfacesbugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions