@sema-agent/core
Version:
Stateless, task-oriented AI agent core
162 lines (161 loc) • 11.1 kB
TypeScript
/**
* 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>;