How Background Agents Resolve Multi-Repository Session Targets: A Complete Technical Guide
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 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).
Implementation Deep Dive
The core resolution logic resides in 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:
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)
resolveSessionRepositories
This function performs the heavy lifting of access validation and reference construction:
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)
Session Creation Integration
The handleCreateSession function in packages/control-plane/src/routes/session-create.ts orchestrates the resolution flow:
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)
Code Examples
Resolving Environment-Based Multi-Repo Sessions
To resolve repositories from a predefined environment:
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:
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
repoIdand resolvedbaseBranchflows tohandleCreateSessionfor 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →