Conversation
Sessions spawned by other subagent extensions carry only `parentSession`, the same field a fork writes, so the sidebar showed them as unrelated top-level sessions and the Agents panel could not list them. The parent's own session file proves the relationship: pi-subagents persists a `subagents:record` entry per finished run "for cross-extension history reconstruction", and its spawn tool result carries `details.agentId` while the agent is still running. Matching those run ids against the child name convention (`<profile>#<run id, first 8 chars>`) identifies the child without guessing from its name. The scanner already streams every session file, so the runs are collected there and cached in the persisted index (version bumped to 2). A match becomes `relation.kind: "subagent"` with `source: "external"`, which makes sidebar nesting, the Agents panel, and subagent-specific behavior work unchanged. The delete cascade deliberately skips `source: "external"`: those transcripts belong to another runtime, so they are re-parented like any other child instead of being deleted with the parent.
elrond298
force-pushed
the
fix/nest-parent-sessions
branch
from
September 13, 2026 09:53
eae1e4c to
25a4ef3
Compare
elrond298
marked this pull request as ready for review
September 13, 2026 09:56
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this PR does
Two commits, so the second can be reviewed (or kept) on its own:
fix: nest parent-linked sessions in sidebar— every session with a resolvableparentSessionbecomes a nested, collapsible sidebar row again, instead of only sessions carrying
pi-web:subagentmetadata.feat(subagents): group third-party subagent sessions under their parent— identifies sessionsspawned by other subagent extensions and gives them a real
relation.kind: "subagent", so theyalso appear in the
Agentspanel with their profile, description and status.Commit 2 alone would already list third-party runs in the
Agentspanel (and aggregate their stateonto the parent row); commit 1 is the sidebar-rows change, which is a UX decision you may want to
drop.
Problem
Sessions created by a third-party subagent extension appear as independent top-level sessions, with
no nesting and no collapse control, and never show up in the
Agentspanel.Reproduce:
@tintinweb/pi-subagents, keep Pi Web's built-in subagents off.<agent>#<8 hex>) is a top-level row next to its parent.0.8.9 and earlier nested it correctly, so this is a regression introduced in 0.8.10.
What changed, and why it broke third-party children
buildSessionTree()— nests any session with a resolvableparentSessionlistSessionFamilies()— groups onlyrelation.kind === "subagent"e4b4743"feat: add subagent session switcher" (2026-08-20), merged tomainasf106531, first released in v0.8.10 (git merge-base --is-ancestor e4b4743 v0.8.9isfalse, which pins the boundary).
components/AgentSessionPanel.tsxand introducedlib/session-family.ts, keyed on Pi Web's ownpi-web:subagentmetadata. Forks and third-partychildren are outside that key.
lib/session-tree.tsis still in the tree but is imported only by its own test — deadcode.
The new rule is defensible: the
Agentspanel needs a profile, a description and a status, and asession labelled "subagent" without those would be a lie. The problem is that the only thing a
third-party child has is
parentSession— the exact field a fork writes. Looking at the childalone, Pi Web cannot tell "agent" from "user fork".
The missing link lives in the parent
The parent session already records what the child cannot prove. From a real parent file:
{"type": "custom", "customType": "subagents:record", "data": {"id": "35a50c91-c27b-473", "type": "general-purpose", "description": "Wait one minute", "status": "steered", "result": "succeed", "startedAt": 1787999060817, "completedAt": 1787999132437}}{"type": "message", "message": {"role": "toolResult", "details": {"subagentType": "Explore", "description": "…", "status": "background", "agentId": "aa11bb22-cc33"}}}and the child's own header/naming:
{"type": "session", "parentSession": "…/01a04d0c-…jsonl"} {"type": "session_info", "name": "general-purpose#35a50c91"}subagents:recordis written per finished run — the extension's own comment says it exists "forcross-extension history reconstruction".
details.agentIdbefore the run finishes, so arunning agent is identifiable too.
<profile>#<first 8 chars of the run id>— the extension's naming contract.Matching a child's name suffix against ids the parent itself recorded is not guessing from a
name shape: a fork could only be misread if the parent recorded an id whose first 8 hex characters
equal the fork's
#…suffix.Implementation
lib/subagents.tsexternalSubagentRunFromEntry()reads a run identity from a record entry or a tool result;matchExternalSubagentRun()matches it against the child name. Status map coversqueued/running/background/completed/steered/aborted/stopped/error.lib/session-list-scanner.tsINDEX_FORMAT_VERSION→ 2 (a one-time rescan on upgrade); a malformed persisted field drops that index entry so the file is rescanned instead of serving wrong relations.lib/session-reader.tsloadAllSessions()maps a match torelation.kind: "subagent"withsource: "external", only when the parent session resolves. An orphaned child stays a visible top-level row instead of disappearing (Pi Web's own subagent sessions are hidden in that case).lib/types.tssource?: "external"on the subagent relation; absent means Pi Web's own metadata.app/api/sessions/[id]/route.tslib/session-family.tsforkorsubagent) and exposechildren;subagentsis still the metadata-confirmed subset.components/SessionSidebar.tsxEverything downstream then works unchanged: nesting, collapse, the
Agentspanel listing, thesubagent icon, and the "silent subagent completion" behaviour.
Not included: the
Open sub-agent sessionbutton onAgenttool-call cards — it is driven by PiWeb's own tool-call → session mapping, which third-party runs do not have.
Decisions for review
the intent, commit 1 can be dropped and the third-party case still works.
app/api/sessions/[id]/route.tsdeletes everysubagent below the deleted session (for Bug: deleting a parent session detaches its subagent sessions as standalone sessions #762). Runs marked
source: "external"are excluded andtake the re-parent path instead, because they belong to another runtime and the delete
confirmation never mentions child sessions. Removing the
source === "external"guard makes themcascade like Pi Web's own subagents — a product decision, not a technical one.
Verification
Real sessions on disk, after the change:
Browser: with the external subagent present, selecting the parent session shows the
Agentsbuttonwith badge
1; the panel listsMain agentplusWait one minute / general-purpose · 15 days ago / ✓ Completed. The child row is indented under its parent, and the parent showsCollapse subagents.Tests:
lib/subagents.test.mjs— extraction from both entry shapes, rejection of entries without a runid, matching (hit, unrecorded id, truncated suffix, name without
#, no runs).lib/session-list-scanner.test.mjs— end-to-end with real files: a completed run, a still runningbackground run, and a plain fork under the same parent; classification survives an index restart;
two malformed-index cases for the new field.
npm test→ 1022/1024 pass (the two failures are an unrelated, local work-in-progress).tsc --noEmitandeslintare clean for the touched files.Remaining limits
identified; a runner that persists nothing on the parent side stays a flat row.
in the parent file.
Follow-up (not a blocker)
A stable, documented marker in the child session would remove the inference entirely:
{"type": "custom", "customType": "pi-subagents:session", "data": {"version": 1, "parentSessionId": "…", "agent": "Explore", "runId": "…"}}The exact schema does not matter; what matters is that a session browser can distinguish "agent"
from "fork" without parsing names or reading the parent's history.