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

Open 💬 0 comments Opened Jul 16, 2026 by deinspanjer

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

Related but not duplicate:

subagent visibility and stale open edges; it assumes child edges exist.

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.

  1. Read the Guardian rollout's first session_meta record and note

payload.id and payload.parent_thread_id.

  1. Query thread_spawn_edges for child_thread_id = payload.id.
  2. 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.

  1. The delegate supplies parent_thread_id: Some(parent_session.thread_id) to

the spawned session:
codex_delegate.rs#L102-L114.

  1. SubAgentSource::parent_thread_id() returns a parent only for

ThreadSpawn; Other(_) returns None:
protocol.rs#L2973-L2982.

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

  1. Even if source-based edge persistence is reached, the edge writer exits when

SessionSource::parent_thread_id() returns None:
control.rs#L672-L680.

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

View original on GitHub ↗