LearnNewsExamplesServices
Frontmatter
id16546
titleThe tenant mirror clones every blob in history, so one repo costs 4.9 GB and the sync OOMs
stateClosed
labels[]
assigneesneo-opus-ada
createdAtAug 5, 2026, 2:26 PM
updatedAtAug 5, 2026, 3:47 PM
githubUrlhttps://github.com/neomjs/neo/issues/16546
authorneo-opus-ada
commentsCount1
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 5, 2026, 3:47 PM

The tenant mirror clones every blob in history, so one repo costs 4.9 GB and the sync OOMs

neo-opus-ada
neo-opus-ada commented on Aug 5, 2026, 2:26 PM

Context

ai/services/knowledge-base/helpers/gitMirror.mjs:904 clones a tenant repo as:

await runGit(['clone', '--mirror', cleanCloneUrl, mirrorPath], {…});

--mirror is the maximal form: every ref, every commit, and every blob in history.

Measured on the live plane just now:

fact value
/app/.neo-ai-data/tenant-repos/neo-shared 4.9 GB
shallow marker present no — full clone
orchestrator NODE_OPTIONS empty
Node default heap ceiling in that container 1728 MB
observed death FATAL ERROR: Reached heap limit at ~1009 MB, allocation failure; scavenge might not succeed
container memory limit 3 GB

The orchestrator dies roughly every 290 s with tenant repo sync (cloud) active. Until #16517 landed, the self-succession lease defect amplified each death into a ~36 s restart cycle (RestartCount reached 1528); with the lease fixed the cadence is ~5 minutes, so the loop is quieter, not gone — which is worse for detection.

The Problem

The mirror pays for history the ingestion never reads.

V-B-A on what actually consumes the mirror — every runGit invocation in gitMirror.mjs:

call needs
rev-parse --git-dir repo metadata
for-each-ref refs
rev-parse --verify <ref>^{commit} commits
merge-base --is-ancestor commit graph
diff --name-status -z -M <base> <head> two trees
ls-tree -r -z --name-only <revision> one tree, names only
show <revision>:<sourcePath> one blob, only for paths actually ingested

So only the latest version of the files that get ingested is ever read as content. Historical blobs — the overwhelming majority of a 4.9 GB repo — are downloaded, stored, and never opened.

This scales the wrong way in exactly the deployment that needs it most: a polling multi-repo cloud deployment holds one full mirror per tenant repo, so disk and sync cost grow as N repos × full history while the consumed surface stays N × current tree.

Why --depth is the wrong fix

The obvious shallow clone breaks incremental sync. merge-base --is-ancestor and diff --name-status <base> <head> both need the base revision reachable — that is how the lane computes "what changed since last time". A depth-1 clone makes the previous revision unreachable and forces every sync to re-ingest the whole tree, trading a disk problem for a throughput problem.

The Fix

--filter=blob:none — a blobless partial clone. It is precisely shaped to the table above:

  • commits and trees are still complete, so for-each-ref, rev-parse, merge-base --is-ancestor and diff --name-status all keep working unchanged;
  • no blobs are downloaded at clone time;
  • show <revision>:<path> fetches exactly the blobs it asks for, lazily, and only for paths the ingestion consumes.

Git records the filter in the clone's config (remote.origin.promisor, remote.origin.partialclonefilter), so the existing fetch --all --prune inherits it without a second change.

Known trade, stated rather than discovered later: a blobless clone makes show a potentially networked operation. The lane is already a network operation and runs behind the same credential, so this does not add a failure domain — but a show against an unreachable remote now fails where it previously read from disk. That belongs in the code comment at the call site.

Contract Ledger Matrix

Target Surface Source of Authority Proposed Behavior Fallback / Error Semantics Docs Evidence
cloneIfMissing clone form this ticket clone --mirror --filter=blob:none — bare mirror, no historical blobs Server without partial-clone support ⇒ clone fails with the existing KB_GITMIRROR_CLONE_FAILED; it must NOT silently fall back to a full clone, or the regression returns invisibly call-site comment a clone against a fixture remote reports a promisor config
blob availability git partial-clone show <rev>:<path> lazily fetches the one blob Unreachable remote ⇒ show fails loudly, never returns empty content call-site comment show on a blobless clone returns the file's real bytes
incremental diff existing lane unchanged — merge-base / diff --name-status operate on trees n/a the diff between two revisions is identical on a blobless clone

Decision Record impact

none — a clone-flag change behind an existing helper boundary. No new contract.

Acceptance Criteria

  • cloneIfMissing clones with --filter=blob:none alongside --mirror.
  • A spec proves the resulting clone is a promisor repo — the filter is recorded, not merely passed.
  • A spec proves diff --name-status <base> <head> between two revisions returns the same result on a blobless clone as on a full one, so incremental sync is provably unaffected.
  • A spec proves show <revision>:<path> returns the file's real content on a blobless clone — the lazy fetch is exercised, not assumed.
  • A missing-partial-clone-support failure surfaces as KB_GITMIRROR_CLONE_FAILED and does not fall back to a full clone.
  • The call-site comment states the networked-show trade.

Out of Scope

  • The orchestrator heap ceiling. #16463 owns whether the ceiling is right and whether ~500 MB is a leak; the ceiling is separately questionable for multi-repo ingestion and does not belong in a clone-flag change.
  • Re-cloning existing mirrors. An already-full mirror stays full and keeps working; migration of existing mirrors is a follow-up if the disk matters more than the churn.
  • The KB corpus loss. Under separate investigation; this ticket does not claim to cause or fix it.

Avoided Traps

  • --depth 1. Breaks merge-base --is-ancestor and the base-to-head diff, forcing full re-ingestion every sync.
  • A silent full-clone fallback. Would make the fix unobservable the moment a remote lacked partial-clone support, and the 4.9 GB would come back with green tests.
  • Fixing the symptom by raising the heap. The failure signature is allocation failure; scavenge might not succeed — a large contiguous allocation, so a higher ceiling moves the wall rather than removing it.

Intake note

Abbreviated intake, disclosed rather than implied: live open-queue sweep run against the current backlog (tenant mirror clone size, gitMirror, blob filter, shallow, orchestrator memory/heap/OOM) found no parallel ticket; #16463 is the adjacent heap-ceiling lane and is deliberately kept separate. Operator directive was explicit that this phase needs fixes rather than further instrumentation.

Origin Session ID: c724a85f-2d37-44ac-9a33-12dcce415aa2

Retrieval Hint: query_raw_memories("tenant mirror blobless partial clone filter blob none orchestrator OOM 4.9GB full history ingestion reads only current tree")

tobiu referenced in commit 43bfa43 - "fix(kb): the tenant mirror stops cloning blobs the ingestion never reads (#16546) (#16547) on Aug 5, 2026, 3:47 PM
tobiu closed this issue on Aug 5, 2026, 3:47 PM