Repository navigation
Guardian threads retain their parent in rollouts but are absent from thread_spawn_edges #33670
Description
Verified against: upstream/main commit
71448a29e7343b9613eaea620fcdbd196aed2af0 from 2026-07-16.
Related but not duplicate:
- #25341 covers ordinary
subagent visibility and staleopenedges; it assumes child edges exist. - #32035 shows Guardian
parent_thread_idmetadata while reporting resume and UI failures; it does
not report the missingthread_spawn_edgesrow.
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?
- Run Codex with Guardian review enabled and trigger an action that creates a
persisted Guardian review thread. - Read the Guardian rollout's first
session_metarecord and note
payload.idandpayload.parent_thread_id. - Query
thread_spawn_edgesforchild_thread_id = payload.id. - 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:
- Guardian spawning receives the reviewed parent and uses
SubAgentSource::Other("guardian"):
review_session.rs#L656-L681. - The delegate supplies
parent_thread_id: Some(parent_session.thread_id)to
the spawned session:
codex_delegate.rs#L102-L114. SubAgentSource::parent_thread_id()returns a parent only for
ThreadSpawn;Other(_)returnsNone:
protocol.rs#L2973-L2982.- 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. - Even if source-based edge persistence is reached, the edge writer exits when
SessionSource::parent_thread_id()returnsNone:
control.rs#L672-L680. - The state backfill also derives the parent only from serialized
source, so
it cannot recover Guardian edges fromsession_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.
Activity
starriet9 commentedon Aug 1, 2026
I reproduced an additional user-visible consequence of this missing persisted relationship on current
main(ee0247f95a6fe2b094ba2253d82cae2a2b4c2dff).After a restart,
thread/deletediscovers persisted descendants through the spawned-agent graph. Because Guardian threads are absent from that graph, deleting a parent thread can leave the Guardian rollout and SQLite thread row behind.This implements the alternative already suggested in this issue: PR #26662 confirms that Guardian threads are intentionally outside the spawned-agent lifecycle, so the prototype persists a separate ownership relation instead of adding Guardians to
thread_spawn_edges. Adding them directly to that graph would also change unrelated lifecycle andthread/list parentThreadIdsemantics.The prototype:
session_meta.parent_thread_idas a separate ownership relation in thread metadata;session_meta;The integration regression test creates an existing Guardian rollout whose parent is known only from
session_meta, deletes the parent throughthread/delete, and verifies that both rollout files and both SQLite rows are removed.Verification completed locally:
codex-statetests;codex-rolloutandcodex-thread-storetests;thread_deleteintegration tests;Prototype branch:
https://github.com/starriet9/codex/tree/fix/guardian-parent-spawn-edges
Commit:
starriet9@6514af2
I have not opened a pull request because external code contributions are by invitation only. If this approach matches the intended ownership model, I would be happy to submit it as an invited PR.