Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions deployment/.env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ PR_ENRICHMENT_MAX_TOTAL_SIZE_BYTES=20971520
# The same bound prevents branch reconciliation from falling back to an
# unbounded sequence of provider file requests when an archive is unavailable.
VCS_FILE_RETRIEVAL_ARCHIVE_THRESHOLD=25
# Repository archives are rejected before indexing if any uncompressed member,
# total uncompressed content, or file count crosses these per-job bounds.
RAG_ARCHIVE_MAX_ENTRY_SIZE_BYTES=268435456
RAG_ARCHIVE_MAX_EXTRACTED_SIZE_BYTES=4294967296
RAG_ARCHIVE_MAX_ENTRIES=500000

# Testing-only: route normal analyses through full prompt capture instead of an
# LLM. Empty project IDs means every project; a comma-separated list scopes it.
Expand Down
2 changes: 1 addition & 1 deletion deployment/config/inference-orchestrator/.env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ SERVICE_SECRET=change-me-to-a-random-secret
# AI_CLIENT_HOST=0.0.0.0
# AI_CLIENT_PORT=8000
# REDIS_URL=redis://localhost:6379/1
# MAX_CONCURRENT_REVIEWS=4
# MAX_CONCURRENT_REVIEWS=20
# ANALYSIS_QUEUE_HEARTBEAT_SECONDS=30
# ANALYSIS_CONSUMER_HEARTBEAT_SECONDS=5
# MAX_CONCURRENT_COMMANDS=10
Expand Down
16 changes: 16 additions & 0 deletions deployment/config/java-shared/application.properties.sample
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,17 @@ logging.level.org.hibernate.orm.jdbc.bind=OFF
#codecrow.rag.api.timeout.read=120
# RAG indexing timeout - 4 hours for large repositories
#codecrow.rag.api.timeout.indexing=14400
# Per-project fan-out for an explicit Refresh all. Two independent branch
# snapshots (for example main and develop) run in parallel without allowing a
# project with many retained branches to occupy all service slots.
#codecrow.rag.branch-build.parallelism=2
# Dedicated service-wide capacity for full branch snapshot builds. This pool is
# separate from PR, branch-analysis, webhook and inference executors. Size it to
# the available RAG replicas and memory; it does not cap ordinary analyses.
#codecrow.rag.branch-build.global-parallelism=4
# Repair interval for readable Qdrant current-branch aliases of active generations.
#codecrow.rag.operator-alias.reconcile-interval-ms=300000
#codecrow.rag.operator-alias.reconcile-initial-delay-ms=15000
# Shared VCS acquisition threshold for incremental RAG and reconciliation
# fallback. The legacy codecrow.rag.incremental.archive-file-threshold property
# remains a fallback when this property is not set.
Expand All @@ -202,6 +213,11 @@ logging.level.org.hibernate.orm.jdbc.bind=OFF
# Lock cleanup interval - how often to clean up expired locks (in milliseconds)
#analysis.lock.cleanup.interval.ms=300000

# Durable webhook execution. The executor has no in-memory queue: accepted work
# beyond max capacity stays QUEUED in PostgreSQL and is recovered later.
#webhook.executor.core-pool-size=8
#webhook.executor.max-pool-size=20

# Hard PR-wide spending limits are supplied to pipeline-agent through deployment/.env:
# ANALYSIS_MAX_FILES=150
# ANALYSIS_MAX_FILE_SIZE_BYTES=5242880
Expand Down
11 changes: 10 additions & 1 deletion deployment/config/rag-pipeline/.env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,19 @@ SERVICE_SECRET=change-me-to-a-random-secret
# === Queue and Server Runtime ===
# REDIS_URL=redis://redis:6379/1
# MAX_CONCURRENT_RAG_JOBS=2
# UVICORN_WORKERS=4
# Keep one API process by default: every worker loads its own embedding/indexing state.
# Increase only when the host has enough memory for another complete runtime.
# UVICORN_WORKERS=1
# Project mutation coordination is correctness-critical in multi-worker setups.
# RAG_MUTATION_LEASE_SECONDS=300
# RAG_MUTATION_ACQUIRE_TIMEOUT_SECONDS=5
# Exact-generation integrity scans are streamed, cached per immutable physical
# collection, and single-flight. This cap applies only to cold verification in
# one RAG service process; it does not cap indexing or application-wide reviews.
# RAG_REVISION_PREFLIGHT_CACHE_ENTRIES=512
# 0 retains immutable positive receipts until LRU eviction or process restart.
# RAG_REVISION_PREFLIGHT_CACHE_TTL_SECONDS=0
# RAG_REVISION_PREFLIGHT_MAX_CONCURRENCY=2
# Expired pending collections are retained for six hours by default.
# RAG_PENDING_COLLECTION_MAX_AGE_SECONDS=21600
# RAG_PENDING_JANITOR_INTERVAL_SECONDS=3600
Expand Down
3 changes: 3 additions & 0 deletions deployment/docker-compose.prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,9 @@ services:
PR_ENRICHMENT_MAX_FILE_SIZE_BYTES: ${PR_ENRICHMENT_MAX_FILE_SIZE_BYTES:-5242880}
PR_ENRICHMENT_MAX_TOTAL_SIZE_BYTES: ${PR_ENRICHMENT_MAX_TOTAL_SIZE_BYTES:-20971520}
CODECROW_VCS_FILE_RETRIEVAL_ARCHIVE_THRESHOLD: ${VCS_FILE_RETRIEVAL_ARCHIVE_THRESHOLD:-25}
RAG_ARCHIVE_MAX_ENTRY_SIZE_BYTES: ${RAG_ARCHIVE_MAX_ENTRY_SIZE_BYTES:-268435456}
RAG_ARCHIVE_MAX_EXTRACTED_SIZE_BYTES: ${RAG_ARCHIVE_MAX_EXTRACTED_SIZE_BYTES:-4294967296}
RAG_ARCHIVE_MAX_ENTRIES: ${RAG_ARCHIVE_MAX_ENTRIES:-500000}
ANALYSIS_QUEUE_INACTIVITY_TIMEOUT_MINUTES: ${ANALYSIS_QUEUE_INACTIVITY_TIMEOUT_MINUTES:-15}
ANALYSIS_QUEUE_ADMISSION_TIMEOUT_MINUTES: ${ANALYSIS_QUEUE_ADMISSION_TIMEOUT_MINUTES:-5}
ANALYSIS_PROMPT_DRY_RUN_ENABLED: ${ANALYSIS_PROMPT_DRY_RUN_ENABLED:-false}
Expand Down
5 changes: 4 additions & 1 deletion deployment/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,9 @@ services:
PR_ENRICHMENT_MAX_FILE_SIZE_BYTES: ${PR_ENRICHMENT_MAX_FILE_SIZE_BYTES:-5242880}
PR_ENRICHMENT_MAX_TOTAL_SIZE_BYTES: ${PR_ENRICHMENT_MAX_TOTAL_SIZE_BYTES:-20971520}
CODECROW_VCS_FILE_RETRIEVAL_ARCHIVE_THRESHOLD: ${VCS_FILE_RETRIEVAL_ARCHIVE_THRESHOLD:-25}
RAG_ARCHIVE_MAX_ENTRY_SIZE_BYTES: ${RAG_ARCHIVE_MAX_ENTRY_SIZE_BYTES:-268435456}
RAG_ARCHIVE_MAX_EXTRACTED_SIZE_BYTES: ${RAG_ARCHIVE_MAX_EXTRACTED_SIZE_BYTES:-4294967296}
RAG_ARCHIVE_MAX_ENTRIES: ${RAG_ARCHIVE_MAX_ENTRIES:-500000}
ANALYSIS_QUEUE_INACTIVITY_TIMEOUT_MINUTES: ${ANALYSIS_QUEUE_INACTIVITY_TIMEOUT_MINUTES:-15}
ANALYSIS_QUEUE_ADMISSION_TIMEOUT_MINUTES: ${ANALYSIS_QUEUE_ADMISSION_TIMEOUT_MINUTES:-5}
ANALYSIS_PROMPT_DRY_RUN_ENABLED: ${ANALYSIS_PROMPT_DRY_RUN_ENABLED:-false}
Expand Down Expand Up @@ -278,7 +281,7 @@ services:
SERVICE_SECRET: ${INTERNAL_API_SECRET:?INTERNAL_API_SECRET must be set in .env}
QDRANT_API_KEY: ${QDRANT_API_KEY:?QDRANT_API_KEY must be set in .env}
REDIS_URL: redis://redis:6379/1
#UVICORN_WORKERS: 1
UVICORN_WORKERS: ${RAG_UVICORN_WORKERS:-1}
ports:
- "127.0.0.1:${RAG_PIPELINE_HOST_PORT:-8004}:8001"
#- "127.0.0.1:5678:5678"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,20 @@ default boolean shouldHaveBranchIndex(Project project, String branchName) {
: null;
return config.ragConfig().shouldHaveBranchIndex(branchName, branchPushPatterns);
}

/**
* Whether an eligible PR target that is not retained may receive a temporary,
* revision-pinned branch snapshot. This never makes branch pushes retain data.
*/
default boolean shouldCreateTransientBranchIndex(Project project, String branchName) {
var config = project.getConfiguration();
if (config == null || config.ragConfig() == null || branchName == null) {
return false;
}
return config.ragConfig().isTransientBranchIndexesEnabled()
&& !branchName.equals(getBaseBranch(project))
&& !shouldHaveBranchIndex(project, branchName);
}

/**
* Get the authoritative base branch for RAG indexing.
Expand Down Expand Up @@ -203,11 +217,9 @@ default void createOrUpdateBranchIndex(
}

/**
* Update branch index by calculating diff between base branch and target branch.
*
* This method always recalculates the full diff between the base branch (e.g., "master")
* and the target branch (e.g., "release/1.0"), then indexes all changed files with
* the target branch in their metadata.
* Update an already retained branch from its completed checkpoint. A first
* legacy branch seed may still compare it with the primary branch; exact
* generation implementations replace that seed path with a complete snapshot.
*
* Use this when a push happens to a non-main branch and you need to update
* the RAG index to reflect the current state of that branch.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.rostilos.codecrow.queue.RedisQueueService;
import org.rostilos.codecrow.core.model.rag.RagBranchIndexGenerationStatus;
import org.rostilos.codecrow.core.persistence.repository.rag.RagBranchIndexGenerationRepository;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

Expand All @@ -35,6 +37,9 @@ public class AiAnalysisClient {
private final RedisQueueService queueService;
private final ObjectMapper objectMapper;

@Autowired(required = false)
private RagBranchIndexGenerationRepository branchGenerationRepository;

static final String INACTIVITY_TIMEOUT_MINUTES_KEY =
"ANALYSIS_QUEUE_INACTIVITY_TIMEOUT_MINUTES";
static final String ADMISSION_TIMEOUT_MINUTES_KEY =
Expand Down Expand Up @@ -348,6 +353,25 @@ private Map<String, Object> buildSerializableRequestPayload(AiAnalysisRequest re
payload.put("previousCommitHash", request.getPreviousCommitHash());
payload.put("currentCommitHash", request.getCurrentCommitHash());
payload.put("baseCommitHash", request.getBaseCommitHash());
if (branchGenerationRepository != null
&& request.getProjectId() != null
&& request.getTargetBranchName() != null
&& request.getBaseCommitHash() != null) {
branchGenerationRepository.findAvailableExactGeneration(
request.getProjectId(),
request.getTargetBranchName(),
request.getBaseCommitHash(),
List.of(
RagBranchIndexGenerationStatus.ACTIVE,
RagBranchIndexGenerationStatus.SUPERSEDED))
.stream()
.findFirst()
.ifPresent(generation -> {
payload.put("ragCollectionTarget", generation.getCollectionName());
payload.put("ragBaseGenerationManifestSha256",
generation.getManifestDigest());
});
}
payload.put("previousCodeAnalysisIssues", request.getPreviousCodeAnalysisIssues());
payload.put("reconciliationFileContents", request.getReconciliationFileContents());
payload.put("projectCapabilities", request.getProjectCapabilities());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,13 +153,33 @@ public BranchAnalysisProcessor(
public Map<String, Object> process(
BranchProcessRequest request,
Consumer<Map<String, Object>> consumer) throws IOException {
return process(request, consumer, false);
}

/**
* Execute a branch job whose persisted cross-analysis dependencies were
* already resolved by the dispatcher.
*/
public Map<String, Object> processAfterDependencyGate(
BranchProcessRequest request,
Consumer<Map<String, Object>> consumer) throws IOException {
return process(request, consumer, true);
}

private Map<String, Object> process(
BranchProcessRequest request,
Consumer<Map<String, Object>> consumer,
boolean dependencyGateSatisfied) throws IOException {
Project project = projectService.getProjectWithConnections(request.getProjectId());

// PR jobs are registered before async processing starts and remain active
// until their source-branch lock is released and analysis is persisted.
branchAnalysisGateService.awaitPrAnalysis(
project.getId(), request.getTargetBranchName(),
request.getSourcePrNumber(), consumer);
if (!dependencyGateSatisfied) {
// Scheduled/direct callers without a durable dispatch job retain the
// broad compatibility barrier. Webhook and pipeline dispatchers use the
// job-id snapshot barrier before calling processAfterDependencyGate().
branchAnalysisGateService.awaitPrAnalysis(
project.getId(), request.getTargetBranchName(),
request.getSourcePrNumber(), consumer);
}
refreshMergedBranchHead(project, request);

Optional<String> lockKey = analysisLockService.acquireLockWithWait(
Expand Down Expand Up @@ -724,8 +744,12 @@ private void performDirectPushAnalysisIfNeeded(
}

// Check commit coverage by open/merged PRs
boolean exactTargetBranchCoverage = project.getConfiguration() != null
&& project.getConfiguration().ragConfig() != null
&& project.getConfiguration().ragConfig().isMultiBranchEnabled();
CommitCoverageService.CoverageResult coverage = commitCoverageService.checkCoverage(
project.getId(), request.getTargetBranchName(), unanalyzedCommits);
project.getId(), request.getTargetBranchName(), unanalyzedCommits,
exactTargetBranchCoverage);

switch (coverage.status()) {
case FULLY_COVERED:
Expand Down Expand Up @@ -858,10 +882,19 @@ private void performIncrementalRagUpdate(BranchProcessRequest request, Project p
return;
}

String targetBranch = request.getTargetBranchName();
String baseBranch = ragOperationsService.getBaseBranch(project);
String targetBranch = request.getTargetBranchName();
String baseBranch = ragOperationsService.getBaseBranch(project);

if (!targetBranch.equals(baseBranch)
&& !ragOperationsService.shouldHaveBranchIndex(project, targetBranch)) {
log.info("Skipping RAG update for non-retained branch: project={}, branch={}",
project.getId(), targetBranch);
EventNotificationEmitter.emitStatus(consumer, "rag_skipped",
"Branch is analyzed but is not configured as a retained RAG branch");
return;
}

// Health check: verify RAG pipeline is reachable before starting
// Health check: verify RAG pipeline is reachable before starting
if (!ragOperationsService.isRagPipelineHealthy()) {
log.warn("RAG pipeline is not reachable — skipping incremental update for project={}",
project.getId());
Expand Down
Loading
Loading