UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

162 lines (161 loc) • 11.1 kB
/** * design/252 G-6 — the LIVENESS half the wiring manifest deliberately does not have. * * The manifest's own §0 boundary says it: "this is STATIC ASSEMBLY SELF-DISCLOSURE — seam presence and * shape, as facts. It does NOT prove liveness (a callback bound to the wrong run, a dead queue, an * adapter's internal failure, an answer routed to the wrong instance). Liveness belongs to round-trip * probes, which are a separate obligation." This module is that obligation for the PARK lane, and it * quotes the boundary rather than dissolving it: a manifest reporting `parkLane.effective: true` over a * store declaring `durability: "durable"` says a store is wired and what it CLAIMS. It cannot say the * store accepts a row, returns the same row, and lets the fence win — which is the whole park contract, * and the part a deployment gets wrong (a read-only mount, a serializer that drops a nested field, a * scope column missing from the WHERE). * * TWO CHECKS, DIFFERENT KINDS, DELIBERATELY IN ONE MODULE: * · {@link probeParkRoundTrip} — the live probe: mint a synthetic parked row, read it back, verify the * load-bearing fields survived, fence it, confirm the fence landed. * · {@link durableParkGapOf} — the STATIC two-halves reading: a durable park is a durable checkpoint * store AND a durable session store, and a deployment with one of them is the configuration whose * failure mode is a resume that reports a missing session instead of the seat that is missing. One * module because they are one question asked at two costs, and a second home for "what does a whole * durable park need" is how the two answers drift. * * WHAT THE PROBE WRITES (read this before wiring it): it PUTs a real checkpoint row into the real * store, under a scope of its own (`{@link PARK_SELFCHECK_SCOPE_PREFIX}<random>`) that no task ever * uses, and fences it to `expired` on EVERY exit — including the failing ones, since a pending row is, * to an approval inbox, an approval waiting for someone. * * The expired row then REMAINS, and remains permanently as far as this interface is concerned: there is * no delete verb on `CheckpointStore`, and `reap` moves PENDING rows to expired rather than removing * expired ones. So each probe run costs one small terminal row under one throwaway scope, and reclaiming * them is the backend's own retention concern (a table TTL, a sweep keyed on the scope prefix). An inbox * does not see them — `listByScope` enumerates pending rows only — but `listScopes` may still report the * scope, and during the probe's own window the row is briefly enumerable and, in principle, resolvable * by anything watching the whole store. Both are the price of probing the real lane rather than a * pretend one; a deployment that cannot pay it should not run the probe, and there is no read-only * variant because a read-only probe would prove exactly what the manifest already discloses. */ import type { RunnerDeps, TaskSpec } from "./types.js"; /** The scope prefix every synthetic probe row is filed under — never a task scope, so a probe row can * never be mistaken for (or resolved as) a real pending approval. */ export declare const PARK_SELFCHECK_SCOPE_PREFIX = "sema:park-selfcheck:"; /** * How long any single store call may take before the probe stops waiting and calls it a failure. * * A bound is MANDATORY rather than optional, for the reason this module exists at all: the probe is a * DIAGNOSTIC for storage that may be sick, and the sickness it is most likely to meet — a connection * that neither answers nor errors — is exactly the one an unbounded `await` turns into a wedged * startup. A probe that hangs instead of reporting is worse than no probe: it fails in the shape it was * written to detect, and silently. */ export declare const PARK_SELFCHECK_STEP_TIMEOUT_MS = 10000; /** * How far in the FUTURE the synthetic row's abandonment deadline sits. * * A past deadline would make the row reapable the instant it is filed, and a CORRECT concurrent reaper * could then expire it between the probe's own steps — reporting a healthy deployment as a broken one * (the read that follows would see an expired row, or the enumeration an empty scope). A short future * window keeps the row out of a reaper's reach for the length of the probe while still leaving any * residue from a crashed probe reapable shortly after, without an operator doing anything. */ export declare const PARK_SELFCHECK_ROW_TTL_MS = 60000; /** * The closed set of things a park round-trip can fail at. A machine keys on these; the accompanying * `detail` is prose for a person and is never the discriminator. * * Ordered as the probe walks the contract, which is also the order an operator debugs in: a store that * cannot be written is a different day's work from one that writes and then hands back a row with a * dropped field. */ export type ParkProbeFindingCode = /** `put` threw — the store did not accept a checkpoint at all. */ "put_failed" /** `get` threw, or answered `null` for a token the store had just accepted. */ | "row_not_readable" /** The row came back, but a load-bearing field did not survive the round-trip (see `detail` for which). */ | "row_not_faithful" /** The row came back with a status other than `pending` — a park nobody can resolve. */ | "row_not_pending" /** `listByScope` is implemented and did NOT list the pending row: the inbox enumeration is blind to it. */ | "row_not_enumerable" /** `expire` threw, or lost a CAS it was the only contender for — the fence the reaper and * `TaskStream.destroy` both depend on does not close. */ | "fence_failed" /** The fence reported a win and the row is still `pending` on a re-read: the CAS did not land. */ | "fence_not_durable"; /** One thing that went wrong, with the prose a person needs to act on it. */ export interface ParkProbeFinding { readonly code: ParkProbeFindingCode; readonly detail: string; } /** * What one probe run concluded. * * `"round_trip_ok"` is the ONLY affirmative word, and it is only ever reported with an empty * `findings` list — the invariant is asserted rather than assumed, because a probe whose failure can be * read as a pass is worse than no probe (it is the manifest's disclosure wearing a verification's * clothes, which is precisely what the §0 boundary forbids). */ export interface ParkSelfCheckResult { readonly verdict: "round_trip_ok" | "failed" | "not_probed"; /** NON-EMPTY ⟺ `verdict === "failed"`. The two non-failing verdicts carry none: `"round_trip_ok"` * because nothing went wrong, `"not_probed"` because nothing was attempted (its reason is in * {@link summary} — a finding there would name a defect in a store that was never touched). */ readonly findings: readonly ParkProbeFinding[]; /** The scope the synthetic row was filed under, so an operator can find (and reap) the residue. * Present from the moment a write was ATTEMPTED — including a `put` that threw, because a backend can * commit and then surface a transport error, so a scope on a failed attempt is exactly where an * operator has to look. Absent only on `"not_probed"`, where nothing was attempted. */ readonly scope?: string; /** A single sentence naming the outcome — for a startup log line. Never the machine discriminator. */ readonly summary: string; /** The static two-halves reading taken alongside the probe (see {@link durableParkGapOf}); absent when * the durable topology is whole or does not apply. A probe can pass every round-trip check on a * deployment whose parks still cannot be resumed after a restart, and this is that fact. */ readonly durableTopologyGap?: string; /** Additive, on `"round_trip_ok"` only: the expire CAS reported a LOSS while the row nonetheless * closed. From outside this is indistinguishable from the store's own housekeeping getting there * first — so the verdict stays green — but a backend that transitions the row and then mis-reports * the CAS win would pass on exactly this arm, and `TaskStream.destroy` trusts that boolean. A * machine-readable member rather than a summary sentence, so a harness that wants to alert on it * can (the summary is never the discriminator). */ readonly casLossObserved?: true; } /** * design/252 G-6 (sibling finding, from the G-5 demo) — the DURABLE PARK's two halves, and the sentence * to say when only one of them is wired. * * A park that survives a restart needs BOTH: the checkpoint row (the question) and the session (the * conversation the answer resumes into). A deployment that wires a durable checkpoint store over the * default in-memory session store parks perfectly and then, after a restart, fails the resume with * "session … does not exist" — a SYMPTOM, reported at the seat that is present, naming nothing about * the seat that is missing. Whoever reads that message goes looking for a lost session id. * * Returns the naming sentence, or `undefined` when there is nothing to say: both halves durable (whole), * or no checkpoint store at all (the deployment is not doing durable parks, so there is no gap — the * manifest's `parkLane.capable: false` is the fact there). */ export declare function durableParkGapOf(halves: { checkpointDurable: boolean; checkpointWired: boolean; sessionDurable: boolean; }): string | undefined; /** The two-halves reading taken off a deployment's own seats. Split from {@link durableParkGapOf} so the * sentence can be unit-tested without a Runner and the seat reading has one home. */ export declare function durableParkGapFor(deps: Pick<RunnerDeps, "checkpointStore" | "sessionStore">, spec?: Pick<TaskSpec, "checkpointStore">): string | undefined; /** * design/252 G-6 — drive one synthetic park through the wired store and report what the assembly can * actually DO, as opposed to what it declares. * * Never throws for a store's failure: every step's exception is captured into a {@link ParkProbeFinding} * so a startup self-check can decide for itself whether a dead park lane is fatal. That decision is the * deployment's, deliberately — this module reports, loudly and in a closed vocabulary, and does not * legislate. What it will NOT do is report a pass it did not observe: `"round_trip_ok"` is returned only * with an empty findings list, and the invariant is asserted below rather than left to reading. * * This is a DIAGNOSTIC probe, not a health-poll body: every run files (and then fences) one real * synthetic row under the reserved scope prefix. Mounted on a high-frequency health endpoint it * becomes write amplification against the checkpoint store — run it at startup, on demand, or on a * coarse operator cadence, not per request. */ export declare function probeParkRoundTrip(deps: Pick<RunnerDeps, "checkpointStore" | "sessionStore">, spec?: Pick<TaskSpec, "checkpointStore">): Promise<ParkSelfCheckResult>;