Guardian threads retain their parent in rollouts but are absent from thread_spawn_edges

Open
#33,670 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
4/5
Estimated time
3-5 days
Newbie friendliness
45/100
Issue type
Bug
Clarity
Mostly clear
Activity status
Quiet
Tech stack
rust, sqlite
Domain
backend, databases

Research direction

Start with codex-rs/core/src/guardian/review_session.rs, codex_delegate.rs, and agent/control/spawn.rs to trace Guardian creation and edge persistence. Read codex-rs/core/src/agent/control.rs and codex-rs/state/src/runtime/threads.rs, then reproduce the SQLite query against a Guardian rollout. Done means the persisted parent relationship is available in thread_spawn_edges or another canonical state relation, with existing Guardian records backfillable from session_meta.parent_thread_id.

Written by the indexing model from the issue text.

Description

bug CLI session subagent

Verified against: upstream/main commit
71448a29e7343b9613eaea620fcdbd196aed2af0 from 2026-07-16.

Related but not duplicate:

  • #25341 covers ordinary
    subagent visibility and stale open edges; it assumes child edges exist.
  • #32035 shows Guardian
    parent_thread_id metadata while reporting resume and UI failures; it does
    not report the missing thread_spawn_edges row.

The examples below are from a local profile, with thread IDs replaced by
consistent synthetic UUIDs so the SQLite and rollout records can still be
correlated safely.

What issue are you seeing?

Persisted Guardian review threads have an exact parent_thread_id in their
rollout session_meta, but no corresponding row is written to
thread_spawn_edges in ~/.codex/state_5.sqlite.

This makes the canonical SQLite thread graph incomplete. Consumers using
thread_spawn_edges cannot associate Guardian cost, activity, or lifecycle
with the parent thread even though Codex had and persisted the exact parent ID.

Local read-only counts:

Guardian rows in threads:       466
All thread_spawn_edges rows:   1137
Guardian child edge rows:         0

Two example threads records:

id                                    source                                      thread_source  cli_version
11111111-1111-4111-8111-111111111111  {"subagent":{"other":"guardian"}}          subagent       0.144.5
22222222-2222-4222-8222-222222222222  {"subagent":{"other":"guardian"}}          subagent       0.144.5

For both children, this query returns zero rows:

SELECT parent_thread_id, child_thread_id, status
FROM thread_spawn_edges
WHERE child_thread_id IN (
  '11111111-1111-4111-8111-111111111111',
  '22222222-2222-4222-8222-222222222222'
);

Their rollout files contain the missing relationship in the first record:

{"type":"session_meta","payload":{"id":"11111111-1111-4111-8111-111111111111","parent_thread_id":"aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa","source":{"subagent":{"other":"guardian"}},"thread_source":"subagent"}}
{"type":"session_meta","payload":{"id":"22222222-2222-4222-8222-222222222222","parent_thread_id":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb","source":{"subagent":{"other":"guardian"}},"thread_source":"subagent"}}

Each prompt independently records the same parent:

Reviewed Codex session id: aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa
Reviewed Codex session id: bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb

Both referenced parent IDs exist in threads.

What steps can reproduce the bug?

  1. Run Codex with Guardian review enabled and trigger an action that creates a
    persisted Guardian review thread.
  2. Read the Guardian rollout's first session_meta record and note
    payload.id and payload.parent_thread_id.
  3. Query thread_spawn_edges for child_thread_id = payload.id.
  4. Observe that the rollout has an exact parent while the edge query returns no
    row.

Aggregate verification query:

SELECT count(*) AS guardian_edges
FROM thread_spawn_edges AS edge
JOIN threads AS child ON child.id = edge.child_thread_id
WHERE child.source = '{"subagent":{"other":"guardian"}}';

Observed result: 0.

What is the expected behavior?

A persisted Guardian thread with session_meta.parent_thread_id should have a
canonical parent relation in SQLite. Preferably, thread_spawn_edges should
contain the same parent/child pair so existing relation filters and graph
consumers work consistently for all persisted subagent threads.

If Guardians are intentionally excluded from thread_spawn_edges, the state
database should expose another canonical, queryable parent relation rather than
requiring consumers to reopen rollout JSONL files.

Existing Guardian rollouts should be backfillable from
session_meta.parent_thread_id.

Additional information

The current source still has the complete failure path:

  1. Guardian spawning receives the reviewed parent and uses
    SubAgentSource::Other("guardian"):
    review_session.rs#L656-L681.
  2. The delegate supplies parent_thread_id: Some(parent_session.thread_id) to
    the spawned session:
    codex_delegate.rs#L102-L114.
  3. SubAgentSource::parent_thread_id() returns a parent only for
    ThreadSpawn; Other(_) returns None:
    protocol.rs#L2973-L2982.
  4. The edge writer is called from the regular agent spawn and resume paths, but
    the Guardian spawn path above does not call it:
    spawn.rs#L516-L526 and
    spawn.rs#L898-L916.
  5. Even if source-based edge persistence is reached, the edge writer exits when
    SessionSource::parent_thread_id() returns None:
    control.rs#L672-L680.
  6. The state backfill also derives the parent only from serialized source, so
    it cannot recover Guardian edges from session_meta.parent_thread_id:
    threads.rs#L318-L326 and
    threads.rs#L1252-L1255.

The prompt builder also deliberately emits the reviewed session ID:
prompt.rs#L184-L191.

No write or manual repair was performed while collecting this evidence.

Dominant language
Rust
Stars
125k
Forks
19.5k
Avg merge
1m
Merged PRs (30d)
1k

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from openai/codex

All issues in openai/codex

Similar issues

More Rust issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.