# How Child Session Spawning and Coordination Works in Background-Agents

> Learn how the background agents library manages child session spawning and coordination using a three layer architecture. Discover its hierarchical limits and state propagation techniques.

- Repository: [Cole Murray/background-agents](https://github.com/ColeMurray/background-agents)
- Tags: internals
- Published: 2026-07-13

---

**Child session spawning and coordination in Background-Agents relies on a three-layer architecture spanning the control plane, Cloudflare Durable Objects, and web client, enforcing hierarchical limits through `parentSessionId` and `spawnDepth` tracking while propagating state changes via WebSocket events.**

Background-Agents architect every interactive coding job as a session backed by a Cloudflare Durable Object (DO) and persisted in a D1 SQLite table. When a session needs to delegate work to a sandboxed sub-task, it triggers child session spawning and coordination through a strictly enforced pipeline that maintains provenance, depth limits, and real-time UI synchronization.

## The Three-Layer Architecture

The system decomposes child session management into distinct backend, compute, and frontend layers, each with specific responsibilities for creation, execution, and observation.

### Control Plane (Backend)

The control plane handles the authoritative creation and validation of child sessions. In `packages/control-plane/src/routes/sessions/[parentId]/children.ts`, the **POST `/sessions/:parentId/children`** endpoint validates the request, checks the parent's current child count, and inserts a new row into the `sessions` table.

The database schema, defined in [`terraform/d1/migrations/0012_add_parent_session.sql`](https://github.com/ColeMurray/background-agents/blob/main/terraform/d1/migrations/0012_add_parent_session.sql), stores `parentSessionId`, `spawnDepth`, `spawnSource`, and copies the parent's `environmentId` to ensure environmental provenance. The endpoint enforces hard limits: **429 Too Many Requests** is returned if a parent already has **≥ 5 active children** or **≥ 15 total children**. The handler also propagates `userId` from parent to child, preserving `null` values when the parent lacks an authenticated user.

After database insertion, the control plane instantiates the child DO using `env.SESSION.idFromName(newSessionId)` to obtain a stub, then calls `/internal/initialize` to seed the sandbox authentication token and link the child to its parent.

### Durable Object (Session DO)

Each session runs inside a Cloudflare Durable Object defined in [`packages/control-plane/src/durableObjects/session.ts`](https://github.com/ColeMurray/background-agents/blob/main/packages/control-plane/src/durableObjects/session.ts). The DO maintains its own internal state at `/internal/state` and tracks its hierarchical position via the `parentSessionId` and `spawnDepth` properties.

When the child's lifecycle changes (e.g., spawned, running, completed), the DO emits a server-message with `type: "child_session_update"` to notify connected clients. This message conforms to the shared schema used across the platform, ensuring consistent state representation between the sandbox and the control plane.

### Web Client (Frontend)

The frontend coordinates UI updates through WebSocket listeners and SWR cache revalidation. In [`packages/web/src/lib/session-socket/reducer.ts`](https://github.com/ColeMurray/background-agents/blob/main/packages/web/src/lib/session-socket/reducer.ts), incoming messages are routed based on their type. When a `child_session_update` arrives, the reducer invokes the revalidation logic in [`packages/web/src/lib/session-socket/swr-revalidation.ts`](https://github.com/ColeMurray/background-agents/blob/main/packages/web/src/lib/session-socket/swr-revalidation.ts).

This layer triggers immediate refetching of two critical cache keys: the parent's child list (`/api/sessions/:id/children`) and the global unarchived-session list. The `useChildSessions` React hook in the frontend consumes these endpoints via `useSWR`, ensuring the hierarchy displays real-time state without manual refresh.

## The Child Session Lifecycle

The spawning workflow follows a strict sequence to maintain consistency across the distributed system:

1. **API Invocation** – A client or parent session POSTs to `/sessions/:parentId/children` with a JSON body containing `title`, `prompt`, and `model` parameters.
2. **Validation & Persistence** – The control plane validates the request against concurrency limits, increments `spawnDepth` (parent depth + 1), and inserts the child record with inherited `environmentId` and `userId`.
3. **DO Instantiation** – The control plane generates a fresh sandbox auth token, resolves the child DO stub via `idFromName`, and invokes `/internal/initialize` to activate the sandbox.
4. **State Publication** – The child DO publishes `child_session_update` messages as its status transitions, including metadata about its parent relationship.
5. **Client Revalidation** – The web client receives the WebSocket event, triggers SWR revalidation of the child list and global sessions, and renders the updated hierarchy.

## Concurrency Limits and Safety Guarantees

The implementation enforces several invariants to prevent resource exhaustion and maintain data integrity:

- **Depth Tracking** – The `spawnDepth` column records generational distance from the root session; tests verify grandchildren receive `spawnDepth = 2`.
- **Environment Provenance** – Children immutable inherit the parent's `environmentId`, ensuring sandboxed tools execute in the same context as their orchestrator.
- **Identity Propagation** – Authentication state flows downward; if a parent has `userId: null`, the child explicitly receives `userId: null` rather than defaulting to a system value.
- **Rate Limiting** – Hard caps of 5 active and 15 total children per parent prevent runaway spawning, returning HTTP 429 when exceeded.

## Implementation Examples

### Spawning a Child Session via API

```bash
curl -X POST https://api.example.com/sessions/parent-abc/children \
  -H "Authorization: Bearer $PARENT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
        "title": "Run Linter",
        "prompt": "lint the repo",
        "model": "gpt-4o-mini"
      }'

# Response: { "sessionId": "child-xyz" }

```

### Listening for Child Updates via WebSocket

```typescript
socket.onmessage = (msg) => {
  const data = JSON.parse(msg.data);
  if (data.type === "child_session_update") {
    // Triggers SWR revalidation of child list and global sessions
    swrRevalidate(data.sessionId);
  }
};

```

### Fetching Child Sessions in React

```typescript
import useSWR from "swr";

export const useChildSessions = (parentId: string) =>
  useSWR(`/api/sessions/${parentId}/children`, fetcher);

```

## Summary

- **Child session spawning and coordination** operates across three layers: the control plane (`packages/control-plane/src/routes/sessions/[parentId]/children.ts`), the Durable Object ([`packages/control-plane/src/durableObjects/session.ts`](https://github.com/ColeMurray/background-agents/blob/main/packages/control-plane/src/durableObjects/session.ts)), and the web client ([`packages/web/src/lib/session-socket/reducer.ts`](https://github.com/ColeMurray/background-agents/blob/main/packages/web/src/lib/session-socket/reducer.ts)).
- The system enforces hierarchical limits of **5 active** and **15 total children** per parent, returning HTTP 429 when thresholds are exceeded.
- State propagation relies on WebSocket messages with type `child_session_update`, triggering SWR revalidation of `/api/sessions/:id/children` and the global session list.
- Children inherit `environmentId` and `userId` from parents, with `spawnDepth` tracking generational distance in the SQLite schema defined in [`terraform/d1/migrations/0012_add_parent_session.sql`](https://github.com/ColeMurray/background-agents/blob/main/terraform/d1/migrations/0012_add_parent_session.sql).

## Frequently Asked Questions

### What are the limits on child session spawning?

Background-Agents enforces two hard limits: **5 active children** and **15 total children** per parent session. When a POST request to `/sessions/:parentId/children` would violate these constraints, the control plane returns HTTP 429 without creating a database record or Durable Object instance.

### How does environment inheritance work across parent-child relationships?

When a child session spawns, the control plane copies the parent's `environmentId` into the child's record during the INSERT operation in `packages/control-plane/src/routes/sessions/[parentId]/children.ts`. This ensures the child executes within the same sandboxed environment as its parent, maintaining consistent tool access and filesystem context.

### What happens when a child session's status changes?

The child Durable Object emits a WebSocket message with `type: "child_session_update"` whenever its lifecycle state changes (e.g., from "spawning" to "running" or "completed"). The client's [`reducer.ts`](https://github.com/ColeMurray/background-agents/blob/main/reducer.ts) catches this event and triggers SWR revalidation of the parent's child list and the global session cache, updating the UI without manual refresh.

### How is the parent-child hierarchy tracked in the database?

The `sessions` table includes `parentSessionId` (foreign key reference), `spawnDepth` (integer representing generational distance from root), and `spawnSource` (describing the origin tool or action). These columns, added in [`terraform/d1/migrations/0012_add_parent_session.sql`](https://github.com/ColeMurray/background-agents/blob/main/terraform/d1/migrations/0012_add_parent_session.sql), enable recursive queries and enforce depth-based constraints while preserving the full ancestry chain.