# Understanding the Source-Activation-Drain Mechanism in MCP Server Lifecycle Management

> Learn how the source-activation-drain mechanism prevents race conditions in MCP server lifecycle management. Ensure tool results are never orphaned with this essential technique.

- Repository: [Craft Ai Agents/craft-agents-oss](https://github.com/craft-ai-agents/craft-agents-oss)
- Tags: deep-dive
- Published: 2026-07-06

---

**The source-activation-drain mechanism prevents race conditions by deferring session restarts until a safe boundary is reached, ensuring parallel tool results are never orphaned in the session journal.**

The `craft-ai-agents/craft-agents-oss` repository implements a sophisticated turn-based lifecycle for its MCP (Multi-Channel Processor) server. When session-scoped tools activate new sources mid-turn, the source-activation-drain mechanism ensures the agent can safely end the current assistant turn without losing sibling tool results or corrupting the session history.

## Why Mid-Turn Source Activation Creates Race Conditions

When a tool like `mcp__session__source_test` activates a new source during a turn, the Agent must immediately end the current assistant turn. This allows the renderer to resend the original user message with the newly-available tools included in the context.

The naive approach—aborting the turn as soon as the first `tool_result` appears—drops any sibling `tool_result` events belonging to the same parallel-tool batch. These orphaned `tool_use` IDs persist in the session journal and block all subsequent sends, effectively deadlocking the session. The source-activation-drain mechanism solves this by introducing a controlled **drain boundary** that defers the abort until all sibling events are safely ignored.

## How the SourceActivationDrainController Works

The `SourceActivationDrainController` class in [`packages/shared/src/agent/source-activation-drain.ts`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/packages/shared/src/agent/source-activation-drain.ts) manages the deferred restart lifecycle. It operates as a state machine that captures activations, drains subsequent events, and fires a synthetic `source_activated` event at the appropriate boundary.

### Capturing the Pending Activation

The controller monitors every yielded `AgentEvent` through its `observe()` method. When it encounters a `tool_result` whose tool reported a pending source activation (via `consumePending()`), it stores a `PendingActivationRestart` record containing the `sourceSlug` and `userMessage`.

```typescript
// Conceptual flow inside the controller
if (event.type === 'tool_result' && pendingRestart) {
  this.capturedSlug = pendingRestart.sourceSlug;
  this.userMessage = pendingRestart.userMessage;
  // Entering drain mode...
}

```

### Draining Subsequent Events

While a pending activation is captured, the controller returns `true` from `observe()`, signaling the caller to **skip normal per-event handling**. This includes bypassing inactive-source detection and compaction reset operations. The drain continues until the boundary policy determines it is safe to fire, ensuring the rest of the parallel-tool batch is safely discarded without journal corruption.

### Boundary Policies and Firing Points

The controller supports two distinct firing policies selected at construction time:

**`batch-boundary`** (Claude backend): Used when the adapted event array contains synthetic events like `task_backgrounded`, `shell_backgrounded`, or `shell_killed`. The controller waits for the end of the batch via `shouldFireAtBoundary()` before emitting the `source_activated` event.

**`fire-on-non-tool-result`** (Pi backend): Used when the adapter is 1:1. The first non-`tool_result` event after capture marks the start of the next assistant turn. The `shouldFireBeforeEvent()` method checks each incoming event to determine if the boundary has been reached.

When the boundary is reached, `takeFire()` produces a `SourceActivatedEvent` (`type: 'source_activated'`) that the caller yields **before** any further events. The controller toggles its `fired` flag to ensure the operation is idempotent.

## Implementation in Pi and Claude Agents

The controller is instantiated with policy-specific configurations in the two main agent implementations:

**Pi Agent** uses the `fire-on-non-tool-result` policy at line 2086 of [`packages/shared/src/agent/pi-agent.ts`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/packages/shared/src/agent/pi-agent.ts):

```typescript
const drain = new SourceActivationDrainController('fire-on-non-tool-result');

```

**Claude Agent** uses the `batch-boundary` policy at line 1455 of [`packages/shared/src/agent/claude-agent.ts`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/packages/shared/src/agent/claude-agent.ts):

```typescript
const drain = new SourceActivationDrainController('batch-boundary');

```

Both implementations follow the same event-loop pattern, integrating the controller into their async generators to manage the lifecycle safely.

## Code Implementation Examples

The following pattern appears in both [`pi-agent.ts`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/pi-agent.ts) and [`claude-agent.ts`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/claude-agent.ts), demonstrating the integration of the drain controller into the agent's event generation loop:

```typescript
import { SourceActivationDrainController } from './source-activation-drain.ts';

async function* runAgent(drain: SourceActivationDrainController) {
  for await (const ev of agentEvents) {
    // 1. Give the controller a chance to fire before we yield
    const pre = drain.shouldFireBeforeEvent(ev);
    if (pre) {
      yield pre;              // emit source_activated
      break;                  // abort current turn
    }

    // 2. Let the controller observe the event (may capture pending restart)
    const shouldDrain = drain.observe(ev, () => pendingRestart);
    if (shouldDrain) continue;  // skip normal handling while draining

    // 3. Normal per-event processing
    yield ev;
  }

  // 4. At the end of the batch/stream, fire if still pending
  const final = drain.shouldFireAtBoundary();
  if (final) yield final;
}

```

You can inspect the controller state for debugging purposes:

```typescript
if (drain.capturedSlug) {
  console.log('Captured source:', drain.capturedSlug);
}
if (drain.hasFired) {
  console.log('Source activation already emitted');
}

```

## Summary

- The source-activation-drain mechanism prevents session journal corruption by deferring source restarts until safe boundaries are reached.
- The `SourceActivationDrainController` in [`packages/shared/src/agent/source-activation-drain.ts`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/packages/shared/src/agent/source-activation-drain.ts) manages capture, drain, and firing phases through `observe()`, `shouldFireBeforeEvent()`, and `shouldFireAtBoundary()`.
- Two policies handle different backend architectures: `batch-boundary` for Claude (synthetic event arrays) and `fire-on-non-tool-result` for Pi (1:1 event mapping).
- The controller guarantees exactly one `source_activated` event per activation and ensures idempotency via the `fired` flag.

## Frequently Asked Questions

### What happens if the controller fires before processing all parallel tool results?

If the controller fires prematurely, sibling `tool_result` events would be orphaned in the session journal with unacknowledged `tool_use` IDs. This would block all subsequent sends from the MCP server. The drain mechanism specifically prevents this by returning `true` from `observe()` to skip normal handling until the boundary is reached.

### How does the controller distinguish between the two boundary policies?

The constructor accepts a policy string: `'batch-boundary'` or `'fire-on-non-tool-result'`. The `batch-boundary` policy relies on `shouldFireAtBoundary()` called at the end of event batches, while `fire-on-non-tool-result` uses `shouldFireBeforeEvent()` checked before every event to detect the first non-tool result.

### Can multiple source activations occur within a single turn?

The controller is designed to handle one activation at a time. Once `takeFire()` executes and sets the `fired` flag, the controller becomes inactive until explicitly reset. This prevents duplicate `source_activated` events and ensures the session journal maintains a consistent 1:1 relationship between activations and restarts.

### Where can I find the test coverage for this mechanism?

Comprehensive unit tests covering both policies are located in [`packages/shared/src/agent/__tests__/source-activation-drain.test.ts`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/packages/shared/src/agent/__tests__/source-activation-drain.test.ts). These tests verify boundary detection, idempotent firing, and proper handling of mixed event sequences across both Claude and Pi backend configurations.