Summary
A child session created with sys_session_create is live, drivable and readable by id — but absent from both views of sys_session_list. The returned conversation_id is the only handle that ever reaches the orchestrator, and nothing can re-derive it. If the parent loses that string (context compaction, restart, a summarizing harness), the child becomes permanently unreachable while still holding a runner, a native CLI process and a workspace.
Two independent defects cause this, plus a third that corrupts the rows which do survive:
- Free-form titles are silently dropped from the
sub_agents view. sys_session_create writes the caller's title verbatim, but every consumer requires the "<agent>:<title>" convention and skips rows without a ":".
- The global
sessions view never contains sub-agent children at all. The runner calls GET /v1/sessions without a kind param; the server defaults to kind="default", which is top-level-only by definition.
- Titles that do contain a colon are mis-attributed. The prefix before the first
":" is reported as the agent, so an arbitrary user string is presented to the LLM as an agent name.
Observed on omnigent 0.6.0, harness claude-native, local server + local runner.
Reproduction
Two children created from the same parent, differing only in title:
Child (1) then completed a full turn successfully — it is unambiguously alive:
Now list:
Expected: both children present.
Actual:
656fe0e7… (free-form title) — absent from both lists. Reachable only via the literal id.
c148565b… (colon title) — present, but reported as agent: "probe". Its real agent is claude-native-ui (public name Claude); "probe" is just the first half of a user-chosen label.
- Neither child appears under
sessions, despite that view being documented as "a global view of every session the caller can access".
sys_agent_list shows the same hole — its session_agents block lists the parent only.
Root cause
Defect 1 — the ":" filter. sys_session_create forces parent_session_id and passes title through untouched, producing a kind="sub_agent" row whose title does not follow the named-spawn convention:
runner/tool_dispatch.py:2059 _build_session_create_body — if isinstance(title, str) and title: body["title"] = title, no "<agent>:" prefix.
Every reader then discards it:
runner/tool_dispatch.py:4254 (in _child_rows_to_entries, REST path) — if not title or ":" not in title or is_session_closed(...): continue
tools/builtins/spawn.py:578 (in-process path) — if child.title is None or ":" not in child.title: continue
The convention is an explicit, documented framework invariant — and sys_session_create is the thing violating it. From tools/builtins/spawn.py:1129-1140:
:raises RuntimeError: If the title is missing or doesn't contain a ":" separator — both indicate a framework invariant broken upstream (sub-agent conversations are always created with "<agent>:<title>").
The in-process sub_agents reader comments even claim the case cannot arise ("Phase-3 anonymous spawns left None titles, but those have NULL parent_conversation_id and won't appear in this query at all") — sys_session_create rows do have a non-NULL parent, so they reach the filter and get silently dropped rather than raising.
Defect 2 — the missing kind. The global view is fetched without a kind filter:
runner/tool_dispatch.py:4187 _collect_global_sessions → params = {"limit": _AGENT_LIST_PAGE_LIMIT, "order": "desc"} (line 4207) — no kind.
server/routes/sessions.py:15310 → kind: str = Query(default="default", pattern="^(default|sub_agent|any)$"), documented as "default (the default) returns only top-level user-initiated sessions — the sidebar's view".
So the "global" list is structurally incapable of returning any sub-agent session. Notably, the same docstring already names the fix and the exact motivating case:
"any" returns both; this lets the new-session agent picker discover agents that are only bound to sub-agent sessions (e.g. ones uploaded via sys_session_create).
Defect 3 — the bogus agent field. _child_rows_to_entries (tool_dispatch.py:4238) reuses the server-parsed tool/session_name split of the title, so for a plain child the split is performed on a user string rather than an agent name. _split_agent_title (spawn.py:1141) does the same partition(":"). A title like "fix: retry logic" yields agent: "fix".
This also collides with the closed-session encoding "<agent>:<title>:closed:<conv_id>" (spawn.py:1122) — a user title containing ":closed:" is indistinguishable from a tombstone.
Impact
Suggested fix
_collect_global_sessions should request kind="any" (the server already supports it and the docstring anticipates this caller). One-line change at tool_dispatch.py:4207.
- Stop keying identity off the title string. Either:
- have
sys_session_create write a conforming "<agent_name>:<title>" title, and carry the real agent name in a structured field so consumers stop parsing labels; or
- make the readers fall back to
agent_name from the row and emit the raw title when there is no ":", instead of continue.
The second is strictly better for the mis-attribution defect: agent should come from the row's actual agent binding, never from the title prefix.
- Given
_split_agent_title documents this state as a broken invariant, the silent continue in the two readers should at minimum log — a dropped child is currently indistinguishable from no child.
- Move the closed-marker out of the title (a label/column) so user titles can contain
":" safely.
Related
Summary
A child session created with
sys_session_createis live, drivable and readable by id — but absent from both views ofsys_session_list. The returnedconversation_idis the only handle that ever reaches the orchestrator, and nothing can re-derive it. If the parent loses that string (context compaction, restart, a summarizing harness), the child becomes permanently unreachable while still holding a runner, a native CLI process and a workspace.Two independent defects cause this, plus a third that corrupts the rows which do survive:
sub_agentsview.sys_session_createwrites the caller'stitleverbatim, but every consumer requires the"<agent>:<title>"convention and skips rows without a":".sessionsview never contains sub-agent children at all. The runner callsGET /v1/sessionswithout akindparam; the server defaults tokind="default", which is top-level-only by definition.":"is reported as theagent, so an arbitrary user string is presented to the LLM as an agent name.Observed on omnigent 0.6.0, harness
claude-native, local server + local runner.Reproduction
Two children created from the same parent, differing only in title:
Child (1) then completed a full turn successfully — it is unambiguously alive:
Now list:
sys_session_list() => {"sub_agents": [ {"agent": "probe", "title": "colon-title", "conversation_id": "c148565b…"} ], "sessions": [ {"session_id": "ebd98484…", "agent_name": "Claude", "status": "running", "parent_session_id": null} // the parent, and only the parent ]}Expected: both children present.
Actual:
656fe0e7…(free-form title) — absent from both lists. Reachable only via the literal id.c148565b…(colon title) — present, but reported asagent: "probe". Its real agent isclaude-native-ui(public nameClaude);"probe"is just the first half of a user-chosen label.sessions, despite that view being documented as "a global view of every session the caller can access".sys_agent_listshows the same hole — itssession_agentsblock lists the parent only.Root cause
Defect 1 — the
":"filter.sys_session_createforcesparent_session_idand passestitlethrough untouched, producing akind="sub_agent"row whose title does not follow the named-spawn convention:runner/tool_dispatch.py:2059_build_session_create_body—if isinstance(title, str) and title: body["title"] = title, no"<agent>:"prefix.Every reader then discards it:
runner/tool_dispatch.py:4254(in_child_rows_to_entries, REST path) —if not title or ":" not in title or is_session_closed(...): continuetools/builtins/spawn.py:578(in-process path) —if child.title is None or ":" not in child.title: continueThe convention is an explicit, documented framework invariant — and
sys_session_createis the thing violating it. Fromtools/builtins/spawn.py:1129-1140:The in-process
sub_agentsreader comments even claim the case cannot arise ("Phase-3 anonymous spawns left None titles, but those have NULL parent_conversation_id and won't appear in this query at all") —sys_session_createrows do have a non-NULL parent, so they reach the filter and get silently dropped rather than raising.Defect 2 — the missing
kind. The global view is fetched without a kind filter:runner/tool_dispatch.py:4187_collect_global_sessions→params = {"limit": _AGENT_LIST_PAGE_LIMIT, "order": "desc"}(line 4207) — nokind.server/routes/sessions.py:15310→kind: str = Query(default="default", pattern="^(default|sub_agent|any)$"), documented as "default(the default) returns only top-level user-initiated sessions — the sidebar's view".So the "global" list is structurally incapable of returning any sub-agent session. Notably, the same docstring already names the fix and the exact motivating case:
Defect 3 — the bogus
agentfield._child_rows_to_entries(tool_dispatch.py:4238) reuses the server-parsedtool/session_namesplit of the title, so for a plain child the split is performed on a user string rather than an agent name._split_agent_title(spawn.py:1141) does the samepartition(":"). A title like"fix: retry logic"yieldsagent: "fix".This also collides with the closed-session encoding
"<agent>:<title>:closed:<conv_id>"(spawn.py:1122) — a user title containing":closed:"is indistinguishable from a tombstone.Impact
sys_session_closeneeds theconversation_id. Once the id is out of the parent's context there is no listing that returns it, so the child cannot be closed and its runner/native-CLI process/workspace are never released. This is a plausible contributor to the orphan-accumulation reported in [Bug] Unbounded ~/.omnigent growth: per-session native-harness dirs are never garbage-collected (28 GB / 273 codex-native dirs observed on one host) #2454 and [Bug] codex app-server + MCP bridge children leak on ANY unclean runner death — orphans re-parent to the host daemon and persist (trigger-agnostic superset of #1898) #2421.agentvalues. The LLM is shown fabricated agent names and may route work using them.sessionsis "a global list of every session you can access"; it never lists sub-agents.Suggested fix
_collect_global_sessionsshould requestkind="any"(the server already supports it and the docstring anticipates this caller). One-line change attool_dispatch.py:4207.sys_session_createwrite a conforming"<agent_name>:<title>"title, and carry the real agent name in a structured field so consumers stop parsing labels; oragent_namefrom the row and emit the raw title when there is no":", instead ofcontinue.The second is strictly better for the mis-attribution defect:
agentshould come from the row's actual agent binding, never from the title prefix._split_agent_titledocuments this state as a broken invariant, the silentcontinuein the two readers should at minimum log — a dropped child is currently indistinguishable from no child.":"safely.Related
sys_session_closefor "plain-titled children" and alignssys_session_*contracts, but does not restore them to eithersys_session_listview.-native-uiname in session tools viapublic_agent_name(); that normalization is applied in_collect_global_sessionsbut not to the title-derivedagentin_child_rows_to_entries.