Parent Issue
#11790 — TenantRepoSyncService scheduler lane MVP. PR #11940 shipped the load-bearing surface (service + scheduling trigger + Orchestrator.poll() wiring + deployment-aware cloud-toggle + CLI operator path). This sub-ticket tracks the Contract Ledger residuals that #11940 explicitly deferred for post-MVP delivery.
Scope (Contract Ledger residuals from #11790)
1. Per-repo cadence + deterministic jitter/backoff (Contract Ledger row 5)
Replace the current single global tenantRepoSyncIntervalMs with per-repo cadence + deterministic jitter:
- Pure scheduling function computes next-due state from
lastRunAttemptAt, per-repo configured cadence (default = global interval), deterministic repoSlug + tenantId salt jitter (to prevent thundering-herd on first bootstrap), and failure backoff (exponential growing on consecutive failures, resetting on success).
- Per-repo failure backoff state persists alongside
lastIngestedRev in the existing tenant-repo-sync-revisions.json.
- Jitter must be deterministic per repoSlug/tenantId so test runs and operator diagnosis are reproducible.
2. Concurrency-limit gate (Contract Ledger row 7)
Reactive concurrencyLimit config on TenantRepoSyncService caps simultaneous git/ingest work:
- Default conservative limit (e.g., 2) to avoid multi-tenant CPU/network exhaustion.
concurrencyLimit: 1 serializes (current MVP shape).
- Per-repo work acquires a semaphore slot before
cloneIfMissing/fetch/buildIngestEnvelope/ingestSourceFiles; releases after completion (success or failure).
- Stable error code
KB_TENANT_REPO_SYNC_CONCURRENCY_GATE_TIMEOUT for slot-acquisition timeout.
3. Stable KB_TENANT_REPO_SYNC_* error codes (Contract Ledger row 9)
Replace ad-hoc error throws with stable constants per the #11787/#11788 redaction-path discipline:
KB_TENANT_REPO_SYNC_TENANT_NOT_FOUND — manual CLI path with unknown tenantId
KB_TENANT_REPO_SYNC_REPO_NOT_CONFIGURED — manual CLI path with unknown repoSlug
KB_TENANT_REPO_SYNC_SYNC_FAILED — per-repo sync failure (wraps the underlying GitMirror/envelope/ingest error with redacted message)
KB_TENANT_REPO_SYNC_MANIFEST_UPDATE_FAILED — tenant-repo-sync-revisions.json write failure
KB_TENANT_REPO_SYNC_CONCURRENCY_GATE_TIMEOUT — (from sub-item 2 above)
- All surfaced errors route via the #11787/#11788 redaction path for credential-bearing hints or git stderr before logging/returning.
- Callers branch on
error.code, not message prose.
Acceptance Criteria
Dependencies
- Parent: #11790 (MVP scheduler lane already shipped in PR #11940)
- Substrate: #11787 (TenantRepoAccessContract redaction path), #11788 (GitMirror error surfaces)
Out of Scope
- Webhook-driven triggering — deferred per Discussion #11782 OQ2 (different lane).
- Cross-deployment KB-collection conflict resolution — out of scope; tenant/repo scoping handles isolation.
- Per-repo health dashboard / operator-visible telemetry — owned by #11791.
Why deferred (PR #11940 cycle-1 scope decision)
Per @neo-gpt #11940 cycle-1 review: the MVP poll-wiring + cloud-toggle gap was the load-bearing blocker for the 40h cloud trial. Jitter/backoff and concurrency-gate are sophistication beyond MVP — useful for production scale but not gating trial start (small tenant count, low concurrent traffic). Stable error codes were similarly deferred to keep PR #11940 scoped to load-bearing integration. This ticket tracks the residuals so the original #11790 Contract Ledger commitments are honored as follow-up work.
Authored by: [Claude Opus 4.7] (Claude Code)
Parent Issue
#11790 — TenantRepoSyncService scheduler lane MVP. PR #11940 shipped the load-bearing surface (service + scheduling trigger + Orchestrator.poll() wiring + deployment-aware cloud-toggle + CLI operator path). This sub-ticket tracks the Contract Ledger residuals that #11940 explicitly deferred for post-MVP delivery.
Scope (Contract Ledger residuals from #11790)
1. Per-repo cadence + deterministic jitter/backoff (Contract Ledger row 5)
Replace the current single global
tenantRepoSyncIntervalMswith per-repo cadence + deterministic jitter:lastRunAttemptAt, per-repo configured cadence (default = global interval), deterministicrepoSlug + tenantIdsalt jitter (to prevent thundering-herd on first bootstrap), and failure backoff (exponential growing on consecutive failures, resetting on success).lastIngestedRevin the existingtenant-repo-sync-revisions.json.2. Concurrency-limit gate (Contract Ledger row 7)
Reactive
concurrencyLimitconfig onTenantRepoSyncServicecaps simultaneous git/ingest work:concurrencyLimit: 1serializes (current MVP shape).cloneIfMissing/fetch/buildIngestEnvelope/ingestSourceFiles; releases after completion (success or failure).KB_TENANT_REPO_SYNC_CONCURRENCY_GATE_TIMEOUTfor slot-acquisition timeout.3. Stable
KB_TENANT_REPO_SYNC_*error codes (Contract Ledger row 9)Replace ad-hoc error throws with stable constants per the #11787/#11788 redaction-path discipline:
KB_TENANT_REPO_SYNC_TENANT_NOT_FOUND— manual CLI path with unknown tenantIdKB_TENANT_REPO_SYNC_REPO_NOT_CONFIGURED— manual CLI path with unknown repoSlugKB_TENANT_REPO_SYNC_SYNC_FAILED— per-repo sync failure (wraps the underlying GitMirror/envelope/ingest error with redacted message)KB_TENANT_REPO_SYNC_MANIFEST_UPDATE_FAILED—tenant-repo-sync-revisions.jsonwrite failureKB_TENANT_REPO_SYNC_CONCURRENCY_GATE_TIMEOUT— (from sub-item 2 above)error.code, not message prose.Acceptance Criteria
tenant-repo-sync-revisions.jsonalongsidelastIngestedRev; deterministic jitter viarepoSlug + tenantIdhash; jitter window ≤ 20% of cadence; backoff doubles on consecutive failures and resets on success.concurrencyLimitreactive config onTenantRepoSyncService; semaphore implementation; CONCURRENCY_GATE_TIMEOUT error code; tests covering serialized (1), bounded-parallel (2), and gate-timeout paths.TenantRepoSyncService.runTaskerror paths return{status: 'failed', details: {code: 'KB_TENANT_REPO_SYNC_*'}}per the enumerated taxonomy. Manual CLI surface returns matching exit codes. Tests for each error path including secret-redaction.runTaskto document the error-code taxonomy as part of the contract.Dependencies
Out of Scope
Why deferred (PR #11940 cycle-1 scope decision)
Per @neo-gpt #11940 cycle-1 review: the MVP poll-wiring + cloud-toggle gap was the load-bearing blocker for the 40h cloud trial. Jitter/backoff and concurrency-gate are sophistication beyond MVP — useful for production scale but not gating trial start (small tenant count, low concurrent traffic). Stable error codes were similarly deferred to keep PR #11940 scoped to load-bearing integration. This ticket tracks the residuals so the original #11790 Contract Ledger commitments are honored as follow-up work.
Authored by: [Claude Opus 4.7] (Claude Code)