/** * The background delivery-derivation cache — `GET /runs` p99 (perf finding #2). * * The vacuity split (crew#311) put two git probes per would-be-stranded run on the run DTOs' * assembly path. On the LIST route that is a fan-out: ~20 terminal worktrees × (status + log) * with a 10s timeout each, contending with the active worker's own git — measured p99 18.7s / * max 18.9s while p50 stayed 182ms and `GET /runs/:id` (the same actor read) stayed 10ms in the * exact same windows. The TTL memo bounded the cost per window but not the worst case: the * unlucky poll that lands after expiry pays every probe, serially contended. * * This cache inverts the ownership: derivation runs in the BACKGROUND and the request path only * ever reads a map. Concretely — * * - a sweep every {@link DELIVERY_SWEEP_INTERVAL_MS} (= `WORKTREE_CLEAN_TTL_MS`, so labels * stay exactly as fresh as the memo already promised) re-derives every candidate run: * COMPLETED + repo-scoped + not already delivered — the only shape whose full derivation * can differ from the stat-only one; * - a terminal-frame warm ({@link warm}) derives a run ONCE when it reaches terminal state, * so a finished run's label heals in seconds, not at the next tick; * - the request path calls {@link read}: a hit answers the derived state; a miss degrades to * the stat-only tri-state (`deliveryStateOf` — the pre-crew#311 stranded/none label) for at * most one tick and NEVER blocks on (or triggers) a derivation. The list path spawns no git, * ever. * * The sweep pool is capped at {@link MAX_CONCURRENT_DERIVATIONS} with an in-flight guard per * run, so the background layer can never storm git the way the request fan-out did. Probes are * injected (the shared TTL-memoized git probes in production), so the campaigns rollup — which * still derives on-request over the SAME probe memo — rides warm entries the sweeper refreshed. * * FAILURE RULE (PR #435 review): a derivation whose probes could not answer * (`VacuityProbeUnavailable` — git unspawnable, timeout, a worktree racing the engine's * terminal reap) is NEVER recorded — absence of an answer is not a verdict. The entry stays * untouched (miss ⇒ the honest stat-only degrade) and the run re-derives on a short backoff, * so a raced terminal-frame warm heals in seconds even where the sweep is off. */ import type { SessionView } from '../core/types.js'; import { type DeliveryState, type VacuityProbes } from './delivery-index.js'; /** What the cache classifies (crew#481): the whole run view — the deliver unit and the def * (the run's recorded name → `runWorkflowDef`) are two of `runCanDeliver`'s inputs, so candidacy needs * the view, not the session facts alone. */ export type CacheView = SessionView; /** One tick = the trust window the per-probe memo already grants, so moving derivation off the * request path changes WHO pays, never how stale a label may read. */ export declare const DELIVERY_SWEEP_INTERVAL_MS = 30000; /** The background pool cap: at most this many derivations (each up to two git subprocesses) run * at once, so a 50-run backlog drains as a trickle instead of a spike that contends with the * active worker's git — the exact contention the request-path fan-out exhibited. */ export declare const MAX_CONCURRENT_DERIVATIONS = 3; /** First retry delay after a derivation whose probes could not answer; doubles per consecutive * failure up to the sweep cadence. The terminal-frame warm fires at the most turbulent instant * of a run's life (the engine's terminal reap runs concurrently), so a raced first derivation * is EXPECTED — the retry is what heals it in seconds even where no sweep ticks (test-built * daemons keep the interval off). */ export declare const DERIVATION_RETRY_BASE_MS = 1000; export interface DeliveryDerivationCacheDeps { /** The candidate set per sweep — the same ~10ms actor read the routes use. */ listViews: () => Promise; /** The probes behind 'stranded'/'vacuous'. In the daemon these are the SAME TTL-memoized git * probes the campaigns rollup uses; only `worktreeExists` (one stat, no git) is ever invoked * on the request path — the git pair runs exclusively under this cache's pool. */ probes: VacuityProbes; /** A run the `DeliveryIndex` already answers needs no derivation — the read path * short-circuits on the recorded URL before it ever consults this cache. */ isDelivered: (runId: string) => boolean; /** Def-awareness (crew#481 / D-14): `runCanDeliver(view, runWorkflowDef(view, workflows))` in * the daemon — a completed run whose def has no deliver unit and no code-work phase is NOT a * candidate (never derived, never cached; `read` answers `'none'` from the same predicate the * campaigns rollup and the resume 409 apply). Absent ⇒ every completed repo-scoped run is a * candidate — today's behaviour, for directly-driven route sets whose adapter has no registry. */ canDeliver?: (view: CacheView) => boolean; /** Retry-backoff floor after a failed derivation (tests shorten it). */ retryBaseMs?: number; log?: (msg: string) => void; /** ERROR-level channel for a NON-probe derivation throw — a defect, not weather (the daemon * wires `app.log.error`, which the diagnostics ring also captures). Falls back to `log`. */ logError?: (msg: string) => void; } export declare class DeliveryDerivationCache { private readonly deps; /** runId → the last full derivation. Entries exist only for candidate runs; the sweep prunes * ids the store no longer serves and runs the index has since recorded as delivered. */ private readonly derived; /** The per-run in-flight guard: a run being derived is never enqueued a second time — a * sweep overlapping a terminal-frame warm folds into ONE derivation, not two git pairs. */ private readonly inFlight; /** The pool: `active` counts held permits, `waiting` holds resolvers that each RECEIVE a * permit on release (the count transfers — it never dips below the true concurrency). */ private active; private readonly waiting; private sweeping; private timer; /** Per-run backoff after a failed derivation (reset on the next honest verdict). */ private readonly retryDelayMs; /** Pending retry timers — unref'd, and cleared on {@link stop} so a closed daemon (or a * test's torn-down server) never re-derives into the void. */ private readonly retryTimers; /** Set by {@link stop}: a derivation still in flight when the daemon closes must not arm a * NEW retry timer after the stop already swept the set (Copilot on #435). */ private stopped; constructor(deps: DeliveryDerivationCacheDeps); /** The def-awareness answer for one view — `true` when no `canDeliver` dep is wired (today). */ private canDeliver; /** Only a COMPLETED repo-scoped run THAT COULD HAVE DELIVERED has a full derivation that can * differ from the stat-only tri-state (`deliveryStateWithVacuity` answers `deliveryStateOf` * verbatim, git-free, for every other shape) — so only these runs are ever derived or cached. * The third clause is crew#481: a completed `capture-learnings`/`onboarding` run is `'none'` by * definition, so it is neither probed nor cached. */ private isCandidate; /** * The request path. Synchronous by design — a cache hit answers the background-derived state; * a miss answers the stat-only tri-state (one `existsSync`, no git) and does NOT enqueue a * derivation: scheduling belongs to the sweep and the terminal warm alone, so a request can * never fan a poll out into subprocess spawns, however cold the cache. * * Non-candidate shapes skip the cache entirely: their stat answer IS the full derivation, and * skipping keeps a (theoretical) stale entry from ever outliving its run's candidacy. The * def-awareness rides the same `canDeliver` the candidacy read (crew#481): a completed run whose * def could never have delivered answers `'none'` here, cold or warm. */ read(view: CacheView): DeliveryState; /** * The terminal-frame warm: derive this run once, now, so a just-finished run's label heals in * probe-time instead of waiting for the next tick. Fired from the daemon's CoreEvent * subscription AFTER `resolveRunDelivery` settles — a just-recorded PR URL then skips the * derivation via `isDelivered`. Best-effort like everything on that hook: never throws. */ warm(runId: string): Promise; /** * One background pass: prune entries the store no longer serves (or the index has since * recorded as delivered), then re-derive every candidate through the capped pool. Re-entrant * calls fold — a sweep still draining when the next tick fires is not stacked, so a slow git * day degrades to a slower refresh, never to a growing backlog. */ sweep(): Promise; /** Arm the tick (and sweep once immediately, so a restarted daemon's labels heal in seconds, * not at +30s). Unref'd — the interval must never hold the process open. */ start(intervalMs?: number): void; stop(): void; /** Enqueue one run's full derivation (in-flight-guarded, pool-capped). * * THE LOAD-BEARING RULE (PR #435 review): a derivation whose probes FAILED is never cached * as if it were an honest answer. The production probes throw `VacuityProbeUnavailable` when * git could not answer (a raced teardown, a timeout, a load spike) — recording that as * 'stranded' pinned a WRONG label on vacuous runs, forever where no sweep ticks. On failure * the entry is left untouched (a miss keeps the honest stat-only degrade; a previous honest * verdict stands) and the run re-derives after a short backoff, doubling up to the sweep * cadence — the in-flight guard folds overlapping schedules, so retries can never storm. */ private schedule; private acquire; private release; } //# sourceMappingURL=delivery-cache.d.ts.map