Git storage compaction
Git quietly accumulates junk until commands get slow and the store eats tens of gigabytes. The Git storage card lists every repository Omniscio knows about with its real size, then compacts the ones you pick — and it refuses rather than guess.
What it is
Where: Settings → Diagnostics → Git storage
Git quietly accumulates junk. Every fetch, every branch, every worktree leaves loose objects and extra pack files behind, and nothing removes them on its own. On a busy repo this grows until git itself gets slow — every command has to scan the pile — and it can eat tens of gigabytes.
The Git storage card lists every git repository Omniscio knows about with its real size on disk, then compacts the ones you pick. On a badly-bloated store this routinely reclaims several gigabytes and makes every git operation noticeably faster.
Where to find it
How to use it
- Open Settings → Diagnostics and find Git storage.
- Tick the repositories you want to compact. Each row shows its size on disk, pack-file count, and loose-object count, so you can see which ones are actually bloated.
- You often need not do anything here at all: a store that has grown bloated is compacted at the
next start on its own (see What it does by itself below). If you want to force it anyway,
choose one of:
- Compact on next restart — nothing happens now; it runs the next time you start Omniscio.
- Restart & compact now — Omniscio closes and does it immediately.
- On the next launch the startup screen shows live progress. When it finishes, the card reports what it reclaimed.
A scheduled compaction shows a Scheduled for the next restart line with a Cancel button, so a pending request is never invisible.
How it behaves
Why it needs a restart
A full-strength compaction rewrites the repository's object store. It cannot safely run while anything else is using that repository — and Omniscio itself is constantly running git in the background for its sessions, worktrees, and branch tracking.
So instead of trying to hunt down and kill those processes, Omniscio does the compaction during its own startup, before it opens its window and before any session resumes. At that exact moment it has no git activity of its own, which is the quiet window the compaction needs. Nothing gets killed, and nothing races.
What to expect
- How long depends on the repository. A small one is done in seconds; a large, long-neglected one can take 5 to 30 minutes. The startup screen names the repository and which of the three steps is running out of how many, and because a single step can run quietly for many minutes it keeps a quieter line underneath saying it is still working — so a long wait never looks stalled.
- The time the startup screen quotes is the time that run really has. A compaction Omniscio starts by itself is capped much shorter than one you ask for from the card, and the screen states the ceiling of whichever of the two is actually running rather than one figure for both.
- A long compaction is never reported as a failed start. Omniscio's "opened to a blank screen" check does not count the time it spends compacting, so a slow run sends no crash report and puts no "blank screen" card in your inbox. If the dashboard really does stay blank afterwards, that is still reported.
- You can stop it at any time. The startup screen shows a Skip and open now button for as long as the compaction runs, with a line under it explaining what the tidy-up is and that stopping costs nothing. Pressing it stops the work immediately and opens Omniscio — whatever had already finished is kept. The button comes down with the run, so it never shows up on a normal startup and never lingers once the compaction has ended.
- Your sessions are safe. They are suspended when Omniscio closes and resume automatically afterwards.
- Force-quitting during it is also safe, though Skip is the better exit. Git writes the new data to a temporary file and only swaps it in at the very end, so an interrupted compaction changes nothing.
- It never deletes your work. It reorganizes how git stores what it has, and a compaction you schedule also removes data nothing uses any more (next section) — never anything a branch, recent history or open worktree still uses.
- If it stops early, the card says why — whether you skipped it or it ran out of time — and any repository it never reached is listed rather than quietly dropped.
A compaction you schedule also removes data nothing uses
Git keeps everything it ever stored, including the leftovers of branches and work nothing points at any more. A compaction you schedule from the card also clears those out:
- What it keeps: everything any branch, recent history entry or open worktree still uses — including local copies of files the remote (GitHub) also holds — and anything that was in use during the last two weeks.
- Two weeks unused, then gone. A run deletes data nothing has used for two weeks, and sets aside newer unused data with its own date; a later compaction you schedule deletes it once it has gone unused for two weeks. Git dates unused data by the file it is packed in, and a busy repository's files are re-dated every day, so there the first run mostly sets data aside and frees little. The card's result line says how much a run set aside.
- Nothing is removed until the new copies are fully written, so Skip, a crash or an error removes nothing — the card then says why this part was skipped.
- Only when you schedule it. A request that arrives without your mark — a headless write of the list alone, or a legacy entry — never deletes anything.
- It needs about twice the repository's size free, plus a gigabyte, while it works; with less, only this part is skipped and the rest of the compaction still runs.
When it refuses (and says so)
Each of these is reported in plain English, and nothing is changed:
- The folder has been moved or deleted.
- The folder is not a git repository.
- The folder is a linked worktree rather than a main repository — compacting it would act on the parent repository instead, which is not what you asked for.
- The drive does not have enough free space. A compaction writes the new consolidated data before deleting the old, so it temporarily needs roughly one and a half times the repository's current size, plus a gigabyte of margin.
On a git older than 2.53
Git 2.43 through 2.52 cannot combine the pack files of a partial clone (a repository that keeps some of its files only on the server, as Omniscio's own checkout does). On such a git the compaction skips that one step instead of failing: it still tidies references, removes unused data when you scheduled it, and cleans up old worktree records, and the Git storage card says "Pack files were not combined … Update git to 2.53 or newer." Update git, then schedule the compaction again. A full clone is unaffected on any git, and so is any git 2.53 or newer. The background maintenance skips the same step on such a git, and the app-closed recovery script stops before it changes anything.
Does this replace the automatic maintenance?
No — it complements it. Omniscio already repacks git storage automatically in the background, but that automatic version deliberately backs off whenever the machine is busy. On a heavily-loaded machine it can therefore go days without getting a turn, and the store bloats anyway. This card is the deliberate, user-driven version for exactly that situation: when you can see the store is large and you want it dealt with now.
One thing did change on the automatic side: a background clean-up that starts and is then stopped because the machine got busy — so it combined nothing at all — no longer counts as the machine's turn for that store. It gets another try shortly after instead of waiting out its full next interval. A clean-up the machine never started changes nothing; it still backs off exactly as before.
What it does by itself
When a store has grown genuinely bloated — past the same pack-count line the everyday background clean-up uses — Omniscio compacts it during the next start, with nothing asked of you. A store that is already tidy is skipped before any work begins, so a start whose stores are fine pays nothing for this and starts no git process at all.
The check is a plain folder listing, and it refuses without asking for three things: a repository
whose .git belongs to a linked worktree (compacting it would act on the parent repo), a store the
loss-protection watch believes is missing, and anything that is not a repository. A run started this
way takes its own time budget rather than one you chose, never deletes unused data, and can be turned
off outright with AMC_DISABLE_GIT_BOOT_AUTO_COMPACT.
It also leaves alone a store it can tell is too big for that shorter budget. A tidy-up of a very large store takes longer than a start is allowed to spend, so running it would hold your startup for the full budget and then change nothing — every single time you started. Omniscio therefore measures the store's size as well as its pack count, and a store measured as too large is not tidied at startup. Instead you get a card naming it, and the card takes you to the Git storage card, where Restart & compact now runs the full-length version on purpose. A store whose size cannot be read is never skipped on that basis: the app only declines a run it has actually measured.
That card clears itself. Once a start measures your stores and finds none too large — which is what happens the moment after you run the full tidy-up — the notice is withdrawn, so you never keep reading a warning about a problem you have already fixed.
An earlier version of this was removed, and the difference matters. In September 2026 Omniscio escalated on its own whenever a store looked untidy, and it was taken out because it could not change what the background clean-up actually did — it watched loose objects while the fold acts on packs, so it promised more than it delivered. What replaced it measures the one thing the fold acts on, through the same threshold the background clean-up reads, which is what lets it act in exactly the case that clean-up is blocked.
What you DO get automatically is that an automatic request is CAPPED rather than unbounded: a request the card did not make — a legacy entry, or one written headlessly without the user flag — runs on a one-minute budget instead of the long one, and the splash is released at that budget plus a minute. A big store cannot be compacted in that minute, so the run is cut there and reported on the card as having run out of time. The long run is the one you ask for on the card.
For agents
- A content read on a partial clone fails like this:
fatal: Cannot read blob <oid> for path <path>, orwarning: lazy fetching disabledthenfatal: could not fetch <oid> from promisor remote. This checkout is ablob:nonepartial clone: the commits and trees are local, the BLOBS are not, and every one an inspection command reads either comes over the network or fails. Agent work runs underGIT_NO_LAZY_FETCH=1, which makes it FAIL rather than fetch — deliberately, because the fetch it would otherwise do is not batched. A single-snapshot read collects everything it needs and fetches once (git show <rev>:<path>,git diff,git blame -L,git grep <rev>,git merge-tree— one pack each, measured 2026-10-04); a history WALK does not, because it discovers each commit's blobs as it goes and fetches them one at a time —git log -p,git log -Sandgit log --followmint one promisor pack per commit walked (19 packs for a 20-commit history). DO NOT delete the guard to make the read work. That is what took this box's store from 32 to 344 pack files in 35 minutes, at ~13 packs/minute. Hydrate the objects ONCE instead, then read with the guard on and watch it mint nothing:
Name every tip in ONE call. The candidates for all of them are unioned before the single fetch, so a 20-tip sweep costs ceil(total/128) packs once rather than one pack per pair; measured on this repo, 20 tips need 5,299 objects — 42 packs as one union. It names the objects the read will need from the TREES alone (npm run git:hydrate -- <base> <tip> # the pair a diff / merge-tree reads npm run git:hydrate -- <base> <t1> <t2> <t3> # a whole sweep — one fetch, not one per pair npm run git:hydrate -- <oldest> <tip> --history # ALSO the range a log -p / -S / --follow walksgit diff --rawandgit log --raw, neither of which reads a blob, so asking can never itself stall), fetches them in bounded batches, and tells you exactly which of five things happened:hydrated,partial,already-complete(nothing was missing, and a re-run mints nothing),nothing-to-read, ornot-a-partial-clone.partialis not success: a sweep larger than the 4,096-object cap hydrates what fits, reports how many are still absent, and exits NON-ZERO — re-run the same command for the next batch. It never refuses the whole set, because refusing would hydrate nothing and hand every read back to the one-pack-per-object path. WHAT IT DOES NOT COVER. The pair and--historysets are what a DIFF, a MERGE, alog -p, alog -Sand alog --followread. Agit blame <rev> -- <path>walks one FILE's history and agit show <rev>:<path>reads one unchanged blob, so neither is named by a plain pair: give blame the file's whole life as the range (--historyfrom the file's first commit to the rev you blamed), and treatshow/grepas still needing their own object. Implementation:scripts/git-hydrate.mjsover the shared primitivescripts/lib/promisor-diff-hydrate.mjs, whosehydrateRevsis the entry point other in-repo callers should use. - The automatic path needs no call from you. At boot, before the window opens, the app measures
each project's store — pack count from a plain folder listing, never a git process — and compacts
any repo at or above
packConsolidateThreshold(), the accessor the live consolidation tier itself reads. It skips a tidy store before any work, skips a linked worktree and any store the loss fence refuses, de-duplicates by store, and never throws. It writes NOTHING, so it can never be mistaken for a scheduled request. Implementation:src/main/services/maintenance/git-repack-auto-trigger.ts; kill switchAMC_DISABLE_GIT_BOOT_AUTO_COMPACT(registry rowgit-store-boot-auto-compact). - A store too large for the automatic run's budget is declined, not attempted. The trigger also
sizes the store (
readPackedBytes— a directory listing plus onestatper pack, again no git process, and only ever asked about a store already over the pack threshold), andfoldFitsBootBudget(packedBytes, budgetMs)refuses a fold that cannot finish inside the SAME budget the run would be given. The rate is LEARNED from folds this machine has actually completed — the median of their (bytes, ms) samples — floored at the constant and capped, so evidence can only ever make the app MORE willing to tidy up, never less. With no samples yet it is exactly the constant.AMC_GIT_FOLD_BYTES_PER_SECoverrides both. A refused store lands in the plan'stooLarge(with the byte total the decision used) instead ofrepoPaths, and the boot sequence raisesGIT_STORE_FOLD_SKIPPED_DEDUP_KEY's card pointing at the Git storage card.readPackedBytesfails open to 0 andfoldFitsBootBudgetanswers "fits" for it, so a store whose size cannot be read is queued exactly as before — the bound only ever removes a run that was measured not to fit. - The screen's ceiling is pushed, not compiled in. The long-wait reassurance rung carries its
minutes as a token (
SPLASH_CEILING_TOKEN); the boot sequence fills it from the budget of the path actually running viasetSplashRepackCeilingMinutes, and a page that has been pushed nothing falls back toSPLASH_CEILING_MINUTES_DEFAULT(the user-scheduled budget, pinned to it by a test). The step line reports each step's position fromREPACK_STEPSitself, so a step added or reordered there reaches the screen with no second place to update. - If the store was declined, the boot owes the user an account of it.
raiseGitStoreFoldSkippedAlert(git-store-fold-skipped-alert.ts) raises one deduped card naming the store, and its one-click destination is declared against that key insrc/shared/alert-actions/diagnostics.ts. - Schedule headlessly by writing the setting
repackReposOnNextStartup(an array of absolute repo paths) viaPATCH /settings, then restart the app. Read the outcome fromlastGitRepackResults, dated bylastGitRepackResultsAt(written on every run);lastGitRepackAtis the separate "actually compacted something" clock, and a run that changed nothing leaves it untouched rather than clearing it. A request written this way reads as AUTOMATIC and gets the one-minute pre-window budget — and that is the ceiling for a headless ask.repackOnNextStartupRequestedByUseris REFUSED to every CLI caller (it sits on the settings-patch denylist): the mark means "the USER asked to wait for this", and it buys the 45-minute budget AND the dead-data step (src/main/services/maintenance/git-dead-object-drop.ts). Only the user arms it — from the Git Storage card, or by running the Desktop repack script, which does the same work outside the app. An agent that finds a bloated store points them at that. Measured 2026-10-03: an agent-set mark cost the owner's next start 488,655 ms (8m9s) behind the splash for a deleting compaction he never asked for. Boot clears both keys together; a skipped or finished dead-data step leaves its note in the repo'slastGitRepackResultsentry (reason, also set on acompactedresult). - A pending request is always one that was asked for: nothing in the app queues a run on its own initiative, so it never queues a compaction you did not ask for. A request that arrives without the user mark — a legacy entry, or a headless write of the list alone — still gets the shorter budget above; a headless write can no longer supply that mark at all. (A list you scheduled and then carried across in a settings export, a backup or a restore keeps the LONG budget: it travels with its mark, when YOU import it — a CLI settings import whose diff touches the mark is refused in full, because an agent may not restore a consent it cannot grant.) (An automatic escalation used to escalate the git-store maintenance tier instead; it was removed on review, because it could not change what the tier did.)
- On a partial clone with git older than 2.53 the fold is SKIPPED, not failed: the result is
compactedwith the update-git note inreason(src/main/services/maintenance/geometric-fold-support.tsdecides, fromgit --versionandgit config --list). - Read the per-repo stats over IPC via the
git:store-reposchannel. - Engine:
git-store-repack.ts. Boot consume:git-repack-on-startup.ts. - Rules that must hold: only the promisor-safe command set is ever used (never
git gcor a fullrepack -a -d, which are the wrong tools on a partial clone), and the pending request is cleared before any work starts so a failure can never cause a restart loop. These are invariants R0–R7 of the repo's owngit-store-maintenance-contract.md— a contributor-only file that ships nowhere. § The full-strength compaction — most importantly: only the promisor-safe command set (nevergit gcor a fullrepack -a -d, which are the wrong tools on a partial clone), and the pending request is cleared before any work starts so a failure can never cause a restart loop.
Related
What Omniscio's own maintenance jobs do, and when they run, is covered by Dev Pipeline Maintenance.
Last verified 2026-10-04