The GitHub Copilot app’s support for visualizing active pull request sessions, the status of each, and easily switching between them made it ideal for managing lots of concurrent work inherent to the port. But one of the things that really made it shine was its ability for sessions to interact with other sessions.
A session can create other sessions, and it can message other sessions while they run. Each session, parent or child, gets its own worktree, its own branch, and its own agent loop; it’s separate from the session that spawned it rather than something running inside it. That’s different from a subagent, which runs inside the parent’s own workspace and hands its answer back into the parent’s context. Both are useful constructs for different things.
As an example of how a session might create other sessions, one of the hardest ports was for the session.ts file. This file had grown organically to be ~30,000 lines of TypeScript. It represented the backbone of a session and effectively spanned horizontally across the whole runtime, touching and being touched by practically every component, sitting at the center of state, events, tools, models, hooks, persistence, and entrypoint access. As a result, I left it for close to the end of the porting process, working up from the bottom of the stack across all the verticals until they all dead ended at session.ts. The porting session that took it on did not start by just diving in and writing Rust. It spent its first fifty-six minutes reading, with 122 tool calls before it created anything, building a picture of what the file actually owned and where the seams were. Only then did it start delegating, logically splitting up the file and delegating slices to subsessions. Across the whole 25-hour run, it made 222 shell calls, 205 file views, and 197 ripgrep searches of its own, on top of everything its child sessions did.
That’s 15 child sessions, each one a separate branch with its own worktree and a separate agent, all created implicitly by the parent session at the top. The parent session created them in seven waves over about three hours: the first wave created five, the second another two about twenty minutes later, then another pair twenty minutes after that, then singles and pairs spread out over the next two hours.
Model choice was made per slice: 10 of the 15 ran on GPT-5.6 Sol and five on Claude Opus 4.8. All 15 were started in GitHub Copilot’s autopilot mode, which lets a session pursue an objective without stopping for approval at each step. The median kickoff prompt was about 1,100 characters, long enough to carry the ownership boundary and the constraints, but short enough that the child session had to work out the approach itself. I prompted the parent agent, and then the parent agent, not a human, wrote those kickoff prompts to each child session.
Alongside those 15 subsessions, the same parent session also employed five subagents: three explore agents fired in parallel with the first wave, one code-review, and one rubber-duck. These subagents explored questions, feeding back to the parent answers it needed in its context before it could decide what to do next. The subagents allowed the parent to get deeply thought out answers without needing to spend its own context window on deriving them.
In contrast, child sessions went to work on the actual porting, the work that produced a diff and needed isolation from the other parallel porters. The child sessions’ work touched 140 distinct files in the repo, 120 of which were touched by exactly one session. The 20 contended files were all hubs, such as session.ts itself. But each session was working in its own worktree, and so was able to proceed undisturbed by its siblings. The parent, of course, paid for that in coordination. It spent a good deal of effort communicating with its child sessions, acting as an information broker, polling their state 60 times and sending 89 coordination messages. When the child sessions each announced their completion, the parent cherry-picked their commits into its own branch and resolved the conflicts. These were not particularly clean merges, either, and the parent agent spent a decent amount of time reconciling the edits.
We can see that on a timeline, which shows the parent session and most of its child sessions.
Note those large gaps. I was traveling while working on this port, and I had to close my laptop at various points. (I subsequently changed my workflow to incorporate cloud-based virtual machines I could remote into.)
These parallel child sessions had a significant impact on that laptop. For a while, the concurrent porting was going swimmingly. Then all 15 concurrent agents on one machine each tried to build and test, and my poor laptop ground to a halt. I prompted to the parent, asking it to relay to its child sessions that they must all stop building and testing. The parent relayed that constraint outward, and they thankfully killed their builds and proceeded to work with minimal CPU activity. I subsequently updated my standing instructions that subagents and subsessions should avoid large builds and test runs while porting, instead deferring that to be done only by the parent agent.
Later I took that one step further and made an otherwise ordinary chat session into a build scheduler for eight independent porting sessions. The prompt was embarrassingly simple: send every open session a policy to avoid CPU-intensive building and testing where possible, require it to ask this session for permission when a build was necessary, and act as a gate, handing out the ability for one session at a time to build. Basically I turned the chat session into an agentic mutex. The gate kept an explicit owner and queue and granted one lease at a time through the same cross-session messaging mechanism the sessions already used to coordinate code. Sessions that asked for and were denied the lease often waited by doing other work in the meantime, like picking off things from its todo list.
This session.ts port was also involved in one of the coolest, saddest, and certainly most unexpected interactions I witnessed during the whole runtime port. As I mentioned, we did the port primarily bottom-up, which is why session.ts that effectively sits on top of every other component was one of the last components ported. The only thing consistently above session.ts are all of the entrypoints into the runtime, namely the public functions that are exposed from the SDK and that show up in the previously discussed dispatch table. There are hundreds of these. And while I know that many of them immediately call into session.ts, I wanted to get a jump on the porting, and so after launching the session.ts session, I launched a session to port all of the entrypoints. I told it to stop at the session.ts boundary. I figured there may be a bit of throwaway work and some amount of effort or number of tokens needed in a rebase, but that it would accelerate the overall porting. Then I went to bed. And then… they found each other.
My kickoff prompt for the entrypoints session did tell it that the session port and six component ports were running concurrently, as I wanted it to know its boundaries and what it should avoid porting to avoid as many conflicts as possible. Apparently my prompting had the opposite effect. Just over four minutes in, having inventoried the ingress paths and presumably formed a view of how much they overlapped, it invoked an app built-in orchestrate skill, whose purpose is coordinating work across sessions. From there:
The entrypoints session enumerated every active session and sent messages to the ones with perceived overlap. The session.tssession answered with a 2,001 character inventory titled “Concrete overlap onstephentoub-port-session-to-rust“ The entrypoints session read the session.tssession’s worktree to confirm what it had just been told (trust but verify, I guess). The entrypoints session asked the session.tssession whether it was ready to reconcile its 760 file diff. The session.tssession basically told it to get lost: “Not ready to commit/integrate.” The entrypoints session proceeded to ask the same question three more times, and each time it got back the same answer from the session.tssession. At which point the entrypoints session decided it didn’t care what the session.tssession thought and simply reached into its worktree and grabbed all of the other session’s changes and merged them into its own. Then both sessions went on their merry way.
A few things I took from this interaction:
It’s important to be explicit about intent. The kickoff prompt named the other running sessions so this one would know what to leave alone. But I didn’t make that “leave alone” part explicit, so instead of blocking the agent from doing something, I ended up encouraging it to do it. I needed to be much more explicit in my intent and guidance. Whatever you make available is something an agent may decide applies. The orchestrateskill ships in the GitHub Copilot app and describes itself as being for running independent workstreams in parallel. Nothing in my prompt mentioned it. The model discovered its own situation, matched it against that description, and loaded it. The set of capabilities you expose is the set of behaviors you might get, including in situations you never pictured. Peers need a tiebreaker. Neither session could compel the other. When the session.tssession said it was not ready to integrate four separate times, that refusal carried no weight, so the session willing to act unilaterally won by default. Parallel sessions over adjacent code need a designated coordinator, or they need a human, and these had neither. “Run autonomously” needs an exception for decisions that reach outside your own branch. I really meant “don’t wake me up over design details.” It heard (not unreasonably) that annexing a peer was in scope. Again, I should have been more explicit in my guidance. The root cause here was me. I partitioned this work top-down and bottom-up at the same time, and the two directions met in the middle at the single most connected file in the codebase. I was too greedy to make forward progress. Everything above follows from that.
This whole interaction was, thankfully, an interesting outlier. Across the whole runtime porting effort, most of the leaf component ports were straightforward single-session tasks. The larger subsystems often involved multiple subsessions and subagents. How those pieces participated over the course of a port varied significantly, though.
The port of model orchestration, the layer that actually talks to the providers, provides a good example of one pattern. Its main session ran 42 wall-clock hours and started 126 subagents. At its busiest, 22 were working at the same time. However, the majority of the time, it was only the main agent, and then now and again it would spawn a significant number of subagents for a window of time.
Three things in that picture stand out. First, effectively all of the code generation was done in the first 12 hours; the next day of work after that is all validation. Second, the color of the bottom row shifts left to right, from predominantly blue and green (reading, building) to predominantly blue and orange (reading, reviewing); that makes logical sense, but it’s neat to see it in practice. Third, that example, and more generally this pattern, has very clean separation between phases. The extension-runtime port is a counter-example.
It took 88 hours instead of 42 and had very different structure:
Writing and reviewing overlap significantly. Whereas in the previous example, the work was very waterfall (first code generation, then review), here reviewing starts long before writing stops, and both proceed and overlap for most of the duration of the effort. The bottom row is all over the place color-wise. Where model orchestration shifts from green to orange as it moves from writing to checking, this one is the same mixture of reading, building, and reviewing from beginning to end. The middle half of the reading calls is spread across a 49-hour span, the writing across 33 hours, and the reviewing across 27, inside an 88-hour session. Every category is spread across most of the run. Idling is deferred to the end. The fleet works nearly continuously for the first 56 hours. The proportions still match. Writing Rust is 2% of tool calls here against 1% there, reading 44% against 57%, reviewing 23% against 27%. The two sessions agree on what the work to be done is, just not on when it happens.
Those blank slices at the end are also a visual representation of a problem becoming more and more common in this agentic coding era: waiting for approval. Someone on the team and/or an agent reviews the code and leaves feedback, there’s a brief period of activity where the agent addresses the feedback and drives CI to green again, and then more waiting, rinse and repeat, until eventually we get the dopamine-inducing stamp of approval.
These two examples each represent a dominant pattern. About a fourth of the sessions look more like the model orchestration session, while three quarters look like the extensions runtime one. The clean progression through phases was the exception; the common case was the agent planning, writing, and reviewing all the way through.