# How Background Agents Resolve Multi-Repository Session Targets: A Complete Technical Guide

> Learn how background agents resolve multi-repository session targets using a two-phase validation pipeline. Ensure workspace integrity with all-or-nothing semantics.

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

---

**Background agents resolve multi-repository session targets through a two-phase validation pipeline that supports both environment-based and ad-hoc repository lists, enforcing all-or-nothing semantics to ensure complete workspace integrity before session creation.**

The ColeMurray/background-agents control plane handles multi-repository sessions by validating every target against GitHub's API before instantiation. This resolution system guarantees that sessions only launch with fully accessible, canonical repository data, preventing partial workspace failures.

## Understanding the Two Resolution Modes

The system accepts multi-repository targets through two distinct input methods, both converging on the same validation pipeline.

### Environment-Based Target Resolution

When a session specifies an `environmentId`, the system retrieves a static list of member repositories from the database. The `resolveEnvironmentTarget()` function in [`packages/control-plane/src/repos/resolve.ts`](https://github.com/ColeMurray/background-agents/blob/main/packages/control-plane/src/repos/resolve.ts) validates the environment exists, fetches its repositories via `EnvironmentStore.getRepositoriesForEnvironment()`, and maps each database row to the `SessionRepositoryResolutionInput` shape.

This mode centralizes repository management, allowing teams to define consistent workspace compositions that persist across sessions.

### Ad-Hoc Repository Lists

For dynamic workspace requirements, clients can provide a `repositories` array directly in the request payload. Each entry must specify `repoOwner`, `repoName`, and optionally `baseBranch`. The `resolveSessionRepositories()` function processes these inputs concurrently, validating each against the SCM provider without requiring a pre-configured environment.

## The Session-Repository Resolution Pipeline

Both resolution modes feed into a unified four-phase pipeline that guarantees data integrity and access control.

### Phase 1: Fetch Raw Inputs

The pipeline begins by collecting repository identifiers. For environment-based sessions, `resolveEnvironmentTarget()` queries the environment table. For ad-hoc requests, the system uses the provided array directly. This phase produces a list of `SessionRepositoryResolutionInput` objects containing owner, name, and optional branch specifications.

### Phase 2: Validate Access and Canonical Data

The system initializes a `SourceControlProvider` (GitHub App) and calls `checkRepositoryAccess` for every repository concurrently. This validation confirms the GitHub App is installed on the target repository, retrieves the repository's canonical `repoId`, and captures the default branch.

### Phase 3: Error Handling and Deduplication

The resolver implements all-or-nothing semantics: if any repository fails access checks or throws an exception, the entire request fails with a detailed error message. After successful validation, the system deduplicates repositories by both identity (`owner/name`) and checkout path (`repoName`) to prevent conflicts during cloning.

### Phase 4: Return Repository References

The pipeline returns a `RepositoryRef[]` array, where each entry contains `repoOwner`, `repoName`, `repoId`, and the resolved `baseBranch`. This canonical data passes to the session creation flow in `handleCreateSession` ([`packages/control-plane/src/routes/session-create.ts`](https://github.com/ColeMurray/background-agents/blob/main/packages/control-plane/src/routes/session-create.ts)).

## Implementation Deep Dive

The core resolution logic resides in [`packages/control-plane/src/repos/resolve.ts`](https://github.com/ColeMurray/background-agents/blob/main/packages/control-plane/src/repos/resolve.ts), with orchestration handled by the session creation route.

### resolveEnvironmentTarget

This function bridges environment storage to the resolution pipeline:

```typescript
export async function resolveEnvironmentTarget(
  store: EnvironmentStore,
  environmentId: string
): Promise<SessionRepositoryResolutionInput[]> {
  const environment = await store.getById(environmentId);
  if (!environment) throw new HttpError(`Environment not found: ${environmentId}`, 404);
  const repositories = await store.getRepositoriesForEnvironment(environmentId);
  if (repositories.length === 0) throw new HttpError(`Environment has no repositories: ${environmentId}`, 500);
  return repositories.map(repo => ({
    repoOwner: repo.repo_owner,
    repoName: repo.repo_name,
    baseBranch: repo.base_branch,
  }));
}

```

*(Source: [resolve.ts](https://github.com/ColeMurray/background-agents/blob/main/packages/control-plane/src/repos/resolve.ts#L26-L46))*

### resolveSessionRepositories

This function performs the heavy lifting of access validation and reference construction:

```typescript
export async function resolveSessionRepositories(
  env: Env,
  inputs: SessionRepositoryResolutionInput[],
  ctx: RequestContext,
  logger: Logger,
  sourceControlProvider?: SourceControlProvider
): Promise<RepositoryRef[]> {
  const provider = sourceControlProvider ?? createRouteSourceControlProvider(env);
  const outcomes = await Promise.all(
    inputs.map(async (input) => {
      try {
        const access = await provider.checkRepositoryAccess({ owner: input.repoOwner, name: input.repoName });
        if (!access) return { input, ref: null, reason: "not installed for the GitHub App", errored: false };
        return {
          input,
          ref: {
            repoOwner: access.repoOwner,
            repoName: access.repoName,
            repoId: access.repoId,
            baseBranch: input.baseBranch?.trim() || access.defaultBranch || "main",
          },
          reason: null,
          errored: false,
        };
      } catch (e) {
        logger.error("Failed to resolve session repository", { ... });
        return { input, ref: null, reason: "resolution failed", errored: true };
      }
    })
  );
  // Error aggregation, duplicate detection, and final return omitted for brevity
}

```

*(Source: [resolve.ts](https://github.com/ColeMurray/background-agents/blob/main/packages/control-plane/src/repos/resolve.ts#L56-L45))*

### Session Creation Integration

The `handleCreateSession` function in [`packages/control-plane/src/routes/session-create.ts`](https://github.com/ColeMurray/background-agents/blob/main/packages/control-plane/src/routes/session-create.ts) orchestrates the resolution flow:

```typescript
if (body.environmentId) {
  const envInputs = await resolveEnvironmentTarget(new EnvironmentStore(env.DB), body.environmentId);
  repositories = await resolveSessionRepositories(env, envInputs, ctx, logger);
  environmentId = body.environmentId;
} else if (body.repositories) {
  repositories = await resolveSessionRepositories(env, body.repositories, ctx, logger);
}

```

*(Source: [session-create.ts](https://github.com/ColeMurray/background-agents/blob/main/packages/control-plane/src/routes/session-create.ts#L80-L90))*

## Code Examples

### Resolving Environment-Based Multi-Repo Sessions

To resolve repositories from a predefined environment:

```typescript
import { resolveEnvironmentTarget, resolveSessionRepositories } from "@open-inspect/control-plane/src/repos/resolve";
import { EnvironmentStore } from "@open-inspect/control-plane/src/db/environments";
import { createLogger } from "@open-inspect/control-plane/src/logger";

const logger = createLogger("demo");
const envStore = new EnvironmentStore(d1Database);
const envId = "env_123";

// 1️⃣ Get raw repo inputs from the environment
const rawInputs = await resolveEnvironmentTarget(envStore, envId);

// 2️⃣ Resolve each repo against GitHub (access check, canonical IDs)
const repoRefs = await resolveSessionRepositories(
  env,               // the Env object with bindings
  rawInputs,
  requestContext,   // from the HTTP route
  logger
);

// repoRefs is an array of RepositoryRef ready for cloning
console.log(repoRefs);

```

### Resolving Ad-Hoc Repository Lists

For dynamic repository configurations:

```typescript
import { resolveSessionRepositories } from "@open-inspect/control-plane/src/repos/resolve";

const inputs = [
  { repoOwner: "open‑inspect", repoName: "web", baseBranch: "main" },
  { repoOwner: "open‑inspect", repoName: "control‑plane", baseBranch: "dev" },
];

const repoRefs = await resolveSessionRepositories(env, inputs, ctx, logger);
console.log(repoRefs);

```

Both examples throw an `HttpError` (400 or 500) if any repository fails to resolve, maintaining the all-or-nothing integrity guarantee.

## Summary

- **Background agents resolve multi-repository session targets** through a unified pipeline supporting both environment-based and ad-hoc repository lists.
- **All-or-nothing semantics** ensure sessions only create when every repository passes access validation, preventing partial workspace failures.
- **Two-phase resolution** separates input fetching (`resolveEnvironmentTarget`) from SCM validation (`resolveSessionRepositories`).
- **Deduplication logic** prevents conflicts by checking for duplicate repository identities and checkout paths before session initialization.
- **Canonical data** including `repoId` and resolved `baseBranch` flows to `handleCreateSession` for storage and subsequent cloning operations.

## Frequently Asked Questions

### What happens if one repository in a multi-repo session fails to resolve?

The entire session creation request fails with an `HttpError`. The `resolveSessionRepositories` function aggregates all repository access outcomes, and if any entry returns an error or inaccessible status, it throws an exception that prevents session initialization. This guarantees atomic workspace creation.

### Can I mix environment-based and ad-hoc repository targets in the same session?

No. The resolution logic in `handleCreateSession` implements an exclusive OR pattern: it checks for `environmentId` first, and only processes the `repositories` array if no environment is specified. You must choose either a predefined environment or a custom list, not both.

### How does deduplication work for multi-repository sessions?

After successful access validation, the resolver checks for duplicate repository identities (`owner/name` combinations) and duplicate checkout paths (`repoName`). If duplicates exist, the request fails with an error indicating the conflict, preventing file system collisions during the cloning phase.

### Where is the resolved repository data stored after validation?

The resolved `RepositoryRef` array passes to `handleCreateSession`, which stores the first repository's details in scalar columns (`repoOwner`, `repoName`) for backward compatibility, while the complete list feeds into the session's repository configuration for cloning and automation execution. The `environmentId` is stored on the session row for provenance tracking when applicable.