Skip to content

[Bug] sys_session_create children are absent from both sys_session_list views — orchestrator permanently loses the only handle #3270

Description

@shkarupa-alex

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:

  1. 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 ":".
  2. 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.
  3. 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:

// (1) free-form title
sys_session_create(agent_id="58a1bc5b…", model="claude-opus-5", title="omnigent dropout test")
=> {"conversation_id": "656fe0e7…", "kind": "sub_agent",
    "agent_name": "claude-native-ui", "title": "omnigent dropout test", "status": "idle"}

// (2) title that happens to contain a colon
sys_session_create(agent_id="58a1bc5b…", title="probe:colon-title")
=> {"conversation_id": "c148565b…", "kind": "sub_agent",
    "agent_name": "claude-native-ui", "title": "probe:colon-title", "status": "idle"}

Child (1) then completed a full turn successfully — it is unambiguously alive:

sys_session_send(session_id="656fe0e7…", args="")   // -> completed, result drained from sys_read_inbox
sys_session_get_info(session_id="656fe0e7…")
=> {"status": "idle", "title": "omnigent dropout test", "model": "claude-opus-5",
    "runner_id": "runner_token_59b7…", "runner_online": true,
    "parent_session_id": "ebd98484…"}          // correct parent, live runner

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 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_bodyif 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_sessionsparams = {"limit": _AGENT_LIST_PAGE_LIMIT, "order": "desc"} (line 4207) — no kind.
  • server/routes/sessions.py:15310kind: 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

  1. _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.
  2. 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.
  3. 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.
  4. Move the closed-marker out of the title (a label/column) so user titles can contain ":" safely.

Related

Metadata

Metadata

Assignees

Labels

BugSomething isn't workingP1-highPriority: major feature broken, no workaroundcomp:runnerComponent: agent runner, execution enginecomp:serverComponent: server, API, session managementtriagedIssue has been triaged by the botvalidated:reproducedRepro agent validated the issue can be reproduced

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions