Brainy Brainy
Docs Brainy

The maintenance hold

In this section

A brain does more than answer calls. Behind its doors it runs jobs: it merges files that pile up, counts, checks, repacks old history, measures what it holds and keeps its indexes tidy. Each one is bounded and yields to the calls in front of it, and on one brain that is invisible. On a process that holds many brains open at once, every brain's jobs run in the same process, and a busy stretch can keep the process's main thread near full for a long time — folder listings and reads then wait their turn behind work that could have waited.

The maintenance hold is the operator's switch for that. While it is up, the maintenance jobs stand down. Nothing is lost and nothing is cancelled for good: what a job was owed stays owed, and the release gives it back.

What a hold does

  • A maintenance job that is running stops at its next natural pause. Its progress marker stays where its last checkpoint left it, nothing is counted against it as a failure, and no retry is armed.

  • A maintenance job that is asked for while the hold is up (an open, a cadence, a call) is deferred, never dropped. The hold remembers it, once per job, with the delay it was asked for.

  • A brain that is opened during the hold starts held too. A process started with the environment switch starts held, so even its first open is held.

  • The release replays what was deferred, exactly once per job. A job a hold stopped mid-run resumes from its marker when it keeps one, and starts over when it does not; a job that was only waiting is started, or armed again at the delay it was asked for.

What a hold never holds

Some jobs are not tidying. A serving job is work a door waits on: while it is owed, some call answers nothing, or one field refuses by name. Holding it would turn a hold into an outage, so a hold never touches one. The engine ranks them in one table, and anything not in it is maintenance.

Every job the engine runs, and what the hold does with it:

job

class

under a hold

what it is

mapper-consistency-sweep

serving

never held

the id table's consistency check that every family's calls read through

metadata-index-verify

serving

never held

the metadata index's deferred integrity check, paid off the main thread

metadata-generation-rebuild

serving

never held

rebuilds the metadata index from the stored records when it cannot be trusted

graph-generation-rebuild

serving

never held

the same for the graph index

vector-generation-rebuild

serving

never held

the same for the vector index

time-index-build

serving

never held

the date index every date call waits on

metadata-column-segment-load

serving

never held

loads a field's column files so a field that refuses by name can answer

metadata-fact-replay

maintenance (serving while a catch-up the calls wait on is owed)

held, unless a catch-up the calls wait on is owed

replays recent changes into the metadata index after an unclean stop

metadata-strand-heal

maintenance

held

repairs strands the index shows

tokens-backfill

maintenance

held

the re-index ceremony an operator starts

pending-embed-fold

maintenance

held

finds writes whose embedding is still owed

metadata-mapper-maintenance

maintenance

durability — never held

compacts the metadata side of the id table

metadata-unindexed-retirement

maintenance

held

retires the stored columns of a field declared unindexed

metadata-column-segment-compaction

maintenance

held

merges column files so they stop piling up

metadata-posted-verb-tally

maintenance

held

seeds the per-relationship-type tally

metadata-lazy-counts

maintenance

held

seeds the per-type counts

metadata-posted-count-rollup

maintenance

held

seeds the posted-count rollup

metadata-lsm-compaction

maintenance

held

merges the metadata index's tables so reads do not slow as flushes accumulate

metadata-lsm-reclaim

maintenance

held

frees the tables a merge has retired

metadata-column-reclaim

maintenance

held

frees the column files a merge has retired

metadata-artifact-sizes

maintenance

held

measures the metadata files for the health report

metadata-index-scrub

maintenance

held

refreshes the integrity records the index keeps

metadata-field-index-compaction

maintenance

held

folds each field's value-index change records into a fresh base, so a save keeps writing only what changed

graph-flush-cadence

maintenance

held

the graph's periodic flush

graph-membership-rebuild

maintenance

held

re-derives which relationships exist when no trusted roster was restored

graph-tree-compaction

maintenance

held

compacts the graph's trees

vector-l0-flush

maintenance

durability — never held

moves recent vectors from the in-memory delta into a segment

vector-consolidation

maintenance

durability — never held

folds the vector segments into a fresh base

vector-canonical-fill

maintenance

held

adds stored records the vector index does not hold

vector-coverage-heal

maintenance

held

re-arms the fill after a recount names a shortfall

vector-artifact-sizes

maintenance

held

measures the vector files for the health report

vector-mapper-maintenance

maintenance

durability — never held

compacts the shared id table

memory-citation-rank

maintenance

held

Brainy Memory's citation ranking

memory-decay

maintenance

held

Brainy Memory's decay bands

memory-episodes

maintenance

held

Brainy Memory's episode grouping

memory-consolidate

maintenance

held

Brainy Memory's consolidation

memory-retirement

maintenance

held

Brainy Memory's retirement pass

memory-projections

maintenance

held

the memory projections an agent reads

memory-absorption-backfill

maintenance

held

Brainy Memory's backfill of the absorption field from the abstractedInto edges

language-classify-write

maintenance

held

the labels a write asked a language model for

language-summarize-cluster

maintenance

held

the summary behind brain.language.summarize

history-byte-census

maintenance

held

weighs the committed history

history-repack

maintenance

held

folds cold history into sealed segments, in 50 ms installments; each fold takes the event loop back from inside its work (about 2 MB or 20 ms per stretch)

generation-census-verify

maintenance

held

the listing check an attached open skipped

aggregate-backfill

maintenance

held

builds a new aggregate from existing records

policy-mirror-rebuild

maintenance

durability — never held

rebuilds the policy mirror

media-describe

maintenance

held

describes pictures through the describe service, and once a day walks every picture for ones never described or changed since

graph-verb-store-maintenance

maintenance (native body)

not held (cannot be)

compacts the graph's id tables on the engine's own worker threads, off the main thread; Rust re-arms it, so no switch in the library reaches it

touch-index-backfill

maintenance

held

the per-record history index backfill

metadata-id-orphan-retraction

maintenance

held

the operator's orphaned-id retraction

Durability, never held. A hold may let files pile up, but a write must still be accepted inside its normal time for as long as the hold lasts. Five jobs are classed as maintenance by the engine and are still never held, because without them a write waits or is refused:

  • vector-l0-flush — held, the in-memory vector delta would grow to its hard cap and the next write would wait for a whole flush.

  • vector-consolidation — held, vector segments pile up to their cap and every vector write is then refused.

  • vector-mapper-maintenance, metadata-mapper-maintenance — held, the shared id table's debt crosses its high-water mark; the vector write path then waits for a compaction pass, and past the ceiling it refuses.

  • policy-mirror-rebuild — held, a store with no policy mirror refuses every write while its metadata index is dark, because the rule check cannot be answered.

The engine's own class for each is unchanged (status() still says maintenance); the exemption lives in the hold, by name. The other write paths were read and need no exemption: the graph flushes its in-memory trees inline when they fill, the metadata index's memory gate is drained by the engine's own flusher rather than a job, and everything else held only costs reads or disk. An acknowledged write is durable in the canonical log, never in a derived index, so a held flush cannot lose one.

Calls that need a held job refuse at once. Three calls can only be answered by a maintenance job: the first query of an aggregate that has never been built (aggregate-backfill; a warm aggregate answers from its current state and is never refused), brain.language.summarize (language-summarize-cluster), and whenServing({ families: ['pending-embeds'] }) (pending-embed-fold). Under a hold each throws MaintenanceHeldError immediately, naming the job and the hold's reason, instead of waiting for a release that may be hours away. Nothing is queued by the refused call; release the hold and call again. The engine's own table decides the class, not this page: status() reports each job's class, and a test keeps this table in step with the engine's.

Three ways to reach it

The environment. Start the process with BRAINY_MAINTENANCE_HOLD=1. The variable is read once, when the library loads, so the first brain the process opens is held. 0 (or unset) leaves maintenance running; any other value is refused by name when the library loads, so a typo cannot leave maintenance running quietly.

In the process. The library exports three names:

import { holdMaintenance, releaseMaintenance, maintenanceHoldStatus } from '@soulcraft/brainy'

const hold = holdMaintenance('migrating the venue brains')   // a reason is required
maintenanceHoldStatus()   // { held, reason, since, holds, heldRunners, deferredJobs, deferredCount }
hold.release()            // drops this hold's reference; returns whether the process is still held
releaseMaintenance()      // drops the most recent hold, for a caller with no handleTS

Holds are counted. Two holds and one release is still held; the process is free only when every hold has been released. Releasing a hold twice, or releasing a free process, changes nothing. maintenanceDebt() carries the same state under held, so a debt that is growing because of a hold reads differently from one that is growing because a job is stuck.

On the host. brainy host answers three doors on the service key (a principal's key is refused by name), outside the contract's route table because brainy serve runs no background jobs of its own:

request

answer

GET /v1/maintenance

the state: held, reason, since, holds, heldRunners, deferredJobs, deferredCount

POST /v1/maintenance/hold with {"reason": "…"}

raises one hold and answers the state; a missing reason is refused by name

POST /v1/maintenance/release

drops the most recent hold and answers the state; releasing a free process is an answer, not an error

The host's doors and the process's are one switch: a hold raised in the process shows in the GET, and one raised over the wire shows in maintenanceHoldStatus().

What holding for a day costs

Nothing is skipped, so everything that was deferred is debt that grows while the hold is up and is paid when it is released. What that looks like, by kind, without a measured figure for any of it (none has been taken for a day-long hold):

  • Merges (metadata-lsm-compaction, metadata-column-segment-compaction, graph-tree-compaction): a writer keeps adding files and nothing merges them, so reads have more files to look through the longer the hold lasts.

  • Reclaims (metadata-lsm-reclaim, metadata-column-reclaim): files that earlier merges retired stay on disk until the release.

  • The value index's change records (metadata-field-index-compaction): each save that touches a field adds one small record to that field's chain, and an open reads the chain behind the field's base, so opens read a little more the longer the hold lasts. A field has room for 64 records; when a hold has kept the job away that long, the save that would add the 65th writes the field's base itself instead (once per 64 saves to that field) and says so in its line.

  • Small deferred seeds and measurements (the counts, tallies and size measurements): the numbers they feed are late or approximate until they run.

  • Memory and language jobs: derived results stop updating.

The release does not run all of it in one breath: the deferred jobs come back through the same scheduler as ever, one claim at a time, yielding to the calls in front of them. On a process with many brains, release the hold when the load is low, and expect the catch-up to take a while.

A hold is a tool for getting a struggling process to breathe, not a setting to leave on. Read held and deferredJobs while it is up, and release it.