agentic.equipment · Infrastructure & platform · 2026

Gear for work nobody is watching. No model in the loop.

Everything hard about running work unattended happens after the interesting part is over: the retry that must not hammer, the second run that must not duplicate the first, the loop that must stop before it costs something, the deletion that must leave proof it happened, and the report from a child that must never sit unread. This host is the rack those pieces hang on.

The equipment is real and it runs inside the Homeroom platform today. The word “agentic” here names the gear, not a model: the model-vendor SDK count in this runtime is zero, and the section below shows the command, the population, the positive control and the junk-string refusal arm that make that zero an absence rather than a shrug.

Four numbers, each with the command that produced it

Every figure on this page is re-derivable. The method is printed under the number.

Measured at one canonical commit with an explicit pathspec. A number without its instrument is a boast; a number with its instrument is evidence somebody else can check.

52job processors on the rack — non-test TypeScript modules under the worker's processors directorygit ls-tree -r --name-only HEAD -- apps/worker/src/processors | non-test *.ts | count
167TypeScript modules in the runtime overall, processors includedgit ls-tree -r --name-only HEAD -- apps/worker/src | *.ts | count
93of those 167 are test files — the runtime carries more test modules than it does processorsgit ls-tree -r --name-only HEAD -- apps/worker/src | *.test.ts | count
0model-vendor SDK references anywhere in the runtime — absence-proven, with an independent positive control and a junk-string refusal arm firing in the same invocationgit grep -E "@anthropic-ai|openai|generativeai|mistralai|cohere-ai" -- apps/worker/src → rc=1

What this equipment does today

A database-backed queue with 52 processors on it, running now.

The runtime claims work from the database, runs it inside a traced span, and writes the outcome back. That is the whole shape. What makes it equipment rather than a script is that each hard property has a module and a co-located test rather than a convention people are asked to remember: the retry policy is a value, the budget ceiling is a typed throw, the idempotency key is scoped to a school, and the tracing has its own test file.

The processor set is not abstract orchestration. It is the actual obligations of a K-12 platform, each one a named module: exports and cover exports, roster synchronisation, enrollment snapshots, picture-day appointment dispatch and reminders, spread deadlines, notification fan-out, edition purge, retention sweeps, agreement expiry, reconciliation, community moderation, takedown handling, and the compliance sweeps described further down. Fifty-two modules, each with a name that says what it owes somebody.

Two of those processors are worth reading before any of the others, because they are the ones that reveal the posture: cross-tenant-probe.ts, a sweep whose passing condition is that it is refused, and unrouted-tip-sla.ts, which exists so that a safety report cannot go quiet.

The rack — six pieces of gear, each with the symbol that proves it

Open the file. That is the whole argument.

Each item below names an exported symbol or a module path. Nothing here rests on a description: the citation is the evidence, and every entry also states what that piece of gear refuses to do, because a capability described without its limit is half a sentence.

The retry policy

apps/worker/src/backoff.ts — backoffDelayMs, isExhausted, nextAttemptAt

A failed job does not vanish and does not hammer. The delay for attempt N is computed by backoffDelayMs against a declared BackoffPolicy; nextAttemptAt turns that delay into a concrete timestamp the runner can sleep against; isExhausted is the boolean that ends the retry chain instead of letting it run forever. Three functions, one policy object, and the policy is a value you can read — not a magic number buried in a catch block.

What it refuses: It does not decide, on its own, that a job is unimportant. Exhaustion is a boolean the caller branches on; it is not a silent drop.

Idempotency, scoped to one school

apps/worker/src/asset-gen-idempotency.ts — GenerationResultCache, toCachedGeneration; apps/worker/src/job-idempotency-school-scope.test.ts

A job that runs twice must not produce two of a thing. The generation cache stores a completed result against its key and hands the same result back on a repeat. The key is scoped to a school, and there is a test file whose entire subject is that scoping — job-idempotency-school-scope.test.ts exists because a key that is not tenant-scoped is a cross-tenant leak wearing the costume of a cache hit.

What it refuses: The cache does not span schools. A hit for one school is not a hit for another, and the test that pins that is a sibling of the module, not a distant integration suite.

The budget guard

apps/worker/src/max-pages-guard.ts — assertBookPageBudget, isOverPageBudget, BookPageBudgetExceededError, WORKER_MAX_BOOK_PAGES_DEFAULT

Unattended work is exactly where a runaway costs the most, because nobody is watching it cost anything. The page budget is a declared default (600) with a declared floor (1), and assertBookPageBudget throws a NAMED error class rather than returning a soft flag. A typed throw is a stop; a boolean somebody forgets to check is a wish.

What it refuses: It does not raise its own ceiling to make a red go away. The ceiling is configuration read through resolveWorkerMaxBookPages, and lowering the alarm is not the same repair as fixing the job.

Tracing and the job span

apps/worker/src/observability.ts — createObservability, withJobContext, withJobSpan, createWorkerMetrics; apps/worker/src/observability.tracing.test.ts

Every job runs inside withJobSpan, so a run has a span with typed attributes (JobSpanAttrs) rather than a log line somebody has to grep for. withJobContext carries the job identity through the call, and createWorkerMetrics registers the counters against a MetricsRegistry. There is a dedicated tracing test file: the instrumentation itself is under test, which is the only way an instrument earns the right to be believed.

What it refuses: It does not treat an absent metric as a healthy one. A registry with no handles is an unmeasured runtime, not a quiet one.

The run loop and the database runner

apps/worker/src/loop.ts — startPollLoop, PollLoop; apps/worker/src/db-runner.ts; apps/worker/src/failure-detail.ts

startPollLoop is the whole heartbeat: a poll interval, a handle you can stop, and a runner underneath it that claims work from the database. When a run fails, failure-detail.ts is the module that keeps WHY — there is a db-runner.failure-detail.test.ts pinning that the detail survives to the row instead of being flattened to a status word.

What it refuses: It does not hold work in process memory as its source of truth. The queue is the database, so a restarted runtime does not lose the queue with it.

Demo mode, as a first-class state

apps/worker/src/demo-mode.test.ts; apps/worker/src/config.ts

The runtime has a demo posture with its own test file, because the dangerous version of a demo is one that is only a comment in a config. A named mode with a named test is a mode you can prove you are in.

What it refuses: It does not decide the posture from the shape of the data in front of it. The mode is configuration, read once, not inferred per job.

What this runtime does not do, in the present tense

No model runs here. That is a measurement, not a position.

The honest way to state an absence is to show that the instrument could have found the thing. The key was a regular expression over the five model-vendor package names, run against the runtime's 167 TypeScript modules with an explicit pathspec. It returned no match. In the same invocation, an independent positive control — a different key, over the same population — fired in three files, proving the search reached real bytes. Also in the same invocation, a junk string was refused, proving the key discriminates rather than matching everything. A zero without both of those arms is a property of the key, not of the code.

Three further absences, each stated as what is true today rather than as a deferred plan. No model, hosted or local, is called from this runtime. No money moves through this page: there is no price, no plan tier, no checkout and no card field anywhere on this host. No mail is sent from this host; the page carries no contact form and no mail link, and the test file asserts the absence of all three of form, input and mail-scheme link in the rendered body.

If any of that changes, the sentence changes with it, in the same commit as the code. A page that keeps a hopeful caption above a capability it does not have is how a product starts lying to its own team.

Who this is for, and what their first week looks like

Fleet operators and the engineers who own the jobs.

The audience is not a procurement committee. It is the person who gets paged when a nightly export half-ran, the engineer adding a scheduled obligation to a platform that already holds student records, and the operator who has to answer whether an unattended job could have crossed a tenant boundary. Everything on this page is written for someone who will check it.

Day one
Open backoff.ts, max-pages-guard.ts and loop.ts in that order. Between them they are under a few hundred lines and they contain the entire operating theory of the runtime: how a failure is delayed, how a run is stopped, and what the heartbeat is.
Day two
Run the runtime's own tests. There are 93 test modules against 167 source modules; the ratio is the point. Break one guard on purpose, watch it go red, restore it byte-for-byte. A green you have never seen turn red is not evidence that the guard works.
Day three
Read cross-tenant-probe.ts and unrouted-tip-sla.ts back to back. One is a sweep that is supposed to fail; the other is a sweep that exists so a child's report cannot sit unread. Those two files say more about the estate's posture than any positioning statement does.
The rest of the week
Write one processor. The convention is visible in every sibling: a module, a co-located test, typed dependencies injected rather than imported, and — where the job destroys something — an audit row that proves the destruction happened.

Nothing in that week requires an account on this host, because this host does not have accounts. The first week is reading and running, in a repository, against tests that already exist.

The data this equipment touches, and the gates on it

Scheduled sweeps run against records belonging to children.

This is the part of an infrastructure page that usually gets one reassuring line. It gets five modules here instead, because the sweeps below are the ones that actually reach a minor's record, and each of them is a file somebody can open.

Cross-tenant canary probe

apps/worker/src/processors/cross-tenant-probe.ts — runCrossTenantProbe, CANARY_IDS, CROSS_TENANT_PROBE_LOOP_NAME

A named loop whose only job is to try, on a schedule, to read one tenant's canary record from another tenant's context, and to record the outcome. This is the negative arm of tenancy: the sweep is designed to FAIL, and a success is the alarm. A tenancy claim backed only by a passing positive test has never been shown to refuse anything.

Biometric expunge

apps/worker/src/processors/biometric-expunge.ts — ExpungeCandidate, ExpungeAuditRow, SealedTemplateDestroyer, BiometricExpungeRepo

Deletion of a biometric template is modelled as a candidate, a destroyer and an AUDIT ROW — three separate types, because a destroyer that returns void can be reached and do nothing, and only the audit row records that the destruction actually happened. The trigger is typed as scheduled or manual, so an operator-initiated expunge is distinguishable in the record from the routine one.

COPPA posture audit

apps/worker/src/processors/coppa-posture-audit.ts — COPPA_POSTURE_EVALUATOR_VERSION, COPPA_POSTURE_DISCLAIMER, CoppaSchoolPosture, CoppaPostureAuditRow

A versioned evaluator (homeroom.coppa-posture/1) writes a per-school posture row on a schedule. It ships a DISCLAIMER constant as part of its public surface: the audit describes a posture we can observe, and it is not a legal opinion about any jurisdiction. A compliance sweep that shipped without that constant would be claiming more than it measures.

Retention, purge and expiry

apps/worker/src/processors/retention-sweep.ts, edition-purge.ts, agreement-expiry-sweep.ts

Three separate scheduled processors for three separate lifecycles: data past its retention window, a published edition being withdrawn, and an agreement reaching its end date. They are separate modules on purpose — a single 'cleanup' job is a place where three different policies quietly become one policy.

Unrouted safety-tip SLA

apps/worker/src/processors/unrouted-tip-sla.ts — runUnroutedTipSlaSweep, unroutedTipSlaTick, UnroutedSlaBreachRow, buildUnroutedSlaDigest

A safety tip that arrives and is never routed to a human is the worst silent failure in the platform. This sweep exists to make that failure LOUD: it builds a digest of tips past their SLA and writes a breach row. The equipment's most important job is not throughput. It is noticing that something a child sent has been sitting untouched.

The limit, stated plainly: none of the above is a claim of legal sufficiency in any jurisdiction. The COPPA evaluator ships a disclaimer constant as part of its public surface for precisely that reason. Consent posture is enforced upstream in the platform, not invented by a sweep; minors' data is consent-gated and is never public. What this equipment contributes is that deletion leaves an audit row, tenancy is probed adversarially on a schedule, and an unrouted safety tip becomes a breach row instead of silence.

The six questions a sceptical reader actually asks

Objections, with the awkward ones kept in.

The domain says “agentic” and you just told me no model runs. Which is it?

Both, and the order matters. The gear on this rack is what an autonomous system would need underneath it — a durable queue, a retry policy, tenant-scoped idempotency, a hard budget ceiling, traced spans, and sweeps that are designed to fail loudly. None of that requires a model, and today none of it has one: the model-vendor SDK count in this runtime is zero, absence-proven with a positive control and a junk-string refusal arm in the same invocation. Naming the gear honestly is the point of an outfitter. If a model is ever put in this loop, the sentence you are reading is the one that has to change first.

Unattended jobs touching student records is precisely what worries me.

It should, and the design agrees with you. Idempotency keys are scoped to a school and there is a test file whose whole subject is that scoping. A named loop, cross-tenant-probe, actively tries to breach tenancy on a schedule and records the outcome — a sweep whose pass condition is a refusal. Biometric deletion is split into a destroyer and an audit row, because a destroyer that quietly does nothing passes every reachability check. The honest limit: none of that is a claim of legal sufficiency, which is why the COPPA evaluator ships a disclaimer constant rather than a certificate.

What happens when a job dies halfway through?

The queue is in the database, not in the runtime's memory, so a restart does not take the work with it. The retry delay is computed, not guessed, and the chain ends on an explicit isExhausted boolean rather than running forever. The reason for the failure is kept by a dedicated module, failure-detail.ts, with its own test pinning that the detail reaches the row — because a status word is where a diagnosis goes to die.

What stops a loop from running away and costing me?

A typed throw. assertBookPageBudget raises a named BookPageBudgetExceededError against a declared default of 600 pages and a declared floor of 1. It is a stop, not a warning flag somebody has to remember to read. And the honest half of the answer: money is off across this estate today, so the cost a runaway would burn here is compute and attention, not a charge to anyone.

Can I buy this?

No. There is no price on this page, no plan tier, no checkout, no card field, and no form of any kind — the page's own test asserts that no form element, no input element and no mailto link appears in the rendered body, so this is a checked property rather than a promise. As a product, agentic.equipment is concept-stage: the plan of record marks it unbuilt as a sellable thing and names the open question as a scoping decision, not an engineering one. What is real is the runtime described above, which runs inside the platform today.

Why not just use an off-the-shelf agent framework?

For the parts that are genuinely generic, that argument is sound and this page is not going to pretend otherwise. What is specific here is the shape of the work: a picture-day pipeline, an edition purge, a biometric expunge, a safety-tip SLA. Those sweeps are not orchestration in general; they are obligations with names, most of them attached to a minor's record. The gear exists because the obligations do, and it is owned end to end so that the licence on a third-party runtime is never a question in a diligence room.

Where this sits in the family

The infrastructure corner of the software-factory estate.

This property belongs to the estate's industrial family — the side of the house where software is made, extended, distributed and operated, as against the warm editorial voice of the school-facing flagship. Within that family it sits in the infrastructure cluster: the connective tissue rather than the storefront. The family brief describes this member as the outfitter and the agent edge, and gives it hazard amber on graphite specifically so it does not read like the institutional members. That is why this page is not navy: navy is for an audience deciding whether to trust a district vendor, and this one is for an audience deciding whether to trust a runtime.

Family resemblance is a thread, not a uniform. The typographic system is shared with every other property in the estate — Newsreader for headings, Bricolage Grotesque for body, no ornamental script logo anywhere. The wordmark, the accent, the hazard edge on each rack slot and the monospace symbol citations belong to this host alone.

What this property is, and what it is not

The runtime is built. The product is a decision that has not been made.

Two different things share this domain name and it would be dishonest to blur them. The first is the equipment described above: real, running, tested, and doing work inside the platform right now. The second is the idea of agentic.equipment as a product sold to somebody outside this estate — an outfitter kit of tools, credentials, memory and runtime for teams running agents. That second thing is concept-stage. Its plan of record says so in the first line of its build-state section, and records that no render, no route and no product code named this domain at the time it was written.

The open question in that plan is not an engineering question. It is whether an agent-outfitter belongs in a K-12 estate at all, given that it has no classroom tie-in, and that is a founder decision rather than a backlog item. This page describes that intention as an intention and stops there. It does not imply a roadmap it has not been given.

Pricing state, present tense: no price is set for agentic.equipment. There is no plan, no tier, no trial, no checkout and no card field on this host. Money is honest-off across this estate until the founder enables it, and this page has no seam through which a charge could occur.

The next concrete step is the scoping decision named in the plan of record: does this belong in the estate, and if it does, what is the first shippable slice? Until that is answered, the honest use of this domain is exactly what you are reading — a specification surface for equipment that genuinely exists, published so that it can be checked rather than pitched. This is not a sales page and there is nothing here to sign.