/** * One pass over a queue of durable turns: list what nobody holds, take it, * hand it to a worker, give it back. * * Every primitive this composes already exists — `SessionIndex` lists the * parked turns (`listPendingDecisions`) and the sessions whose turn is * still open, a session's lease arbitrates between processes, and * `resumeSession` carries the lease's fence into every record. This * composes them, so an approval inbox and a crash sweeper do not each have * to get the release into a `finally` and the `null` claim out of the error * path. * * ## What this deliberately is NOT * * A supervisor, a daemon, or a scheduler. There is no timer here, no * process spawn, no retry backoff and no `while (true)`. `drainParkedTurns` makes * ONE bounded pass and returns what happened; running it again is the * caller's decision, made wherever that caller already has a scheduler. A * per-platform supervisor is the same trade the deployment-adapter matrix * was rejected for: one seam beats N adapters. * * Read the scope of that narrowly. This paragraph has been cited as the * kernel's refusal of a model-facing "remind me tomorrow" capability, and * it is not one: it says a HOST brings the timer, which presumes the host * has one rather than ruling the capability out. Whether such a capability * should exist -- and if so, as a store plus a host-driven sweep rather * than a daemon here -- is open. `directory/types.ts` records the adjacent * decision that cut a declarative `schedules/` slot, on grounds of * double-fire and timezone; that is a different question from a runtime * tool, and neither text settles the other. * * The unit of work is a callback, so this module never needs a provider, a * tool registry or a sandbox — the half of a turn that cannot be serialized * stays with the caller, exactly as `resumeSession` already splits it. */ import type { SessionIndex } from '../store/session-index/index.js'; import { type SessionLease, type SessionLog } from '../store/session-log/index.js'; import type { ProjectId, SessionId, TenantId, TurnId } from '../types/ids/index.js'; import type { DurableTurnEntry, ParkState } from '../types/session/durable.js'; /** Turns handled per pass when the caller names no page size. */ export declare const DEFAULT_DRAIN_PAGE_SIZE = 100; /** * What a drainer does with one turn it successfully took. * * Receives the lease, not just its fence: the holder and expiry are what a * worker needs to decide whether it still has time to start. The intended * body is a resume under that lease: * * ```ts * onTurn: (entry, lease) => * resumeSession({ ...yourQueryParams, scope: { ...entry, topicId }, sessionLog, checkpointStore, lease }) * ``` * * A throw is recorded against that turn and the pass continues. */ export type DrainTurn = (entry: DurableTurnEntry, lease: SessionLease) => void | Promise; export interface DrainTurnsParams { /** Where the durable turns are listed from. */ readonly index: SessionIndex; /** Attribution of every entry; the index is per `NAMZU_HOME`, which is per tenant. */ readonly tenantId: TenantId; /** Narrow to one project. */ readonly projectId?: ProjectId; /** Narrow to one session. */ readonly sessionId?: SessionId; /** * Opens the log of a listed session. Default: the disk log at the path * the index files it under. */ readonly openLog?: (sessionId: SessionId, logPath: string) => SessionLog; /** * Who is taking the turns. Per-PROCESS, never per-deployment: two drainers * sharing a string would wait on each other's leases as their own. */ readonly holder: string; /** Lease length in ms. Long enough that the slowest turn finishes inside it. */ readonly ttlMs: number; /** The work. See {@link DrainTurn}. */ readonly onTurn: DrainTurn; /** * Keep only turns whose park is in one of these states. * * **Absent means every open turn nobody holds, parked or not**: what a * crash sweep wants, because a turn that died mid-flight never parked. An * approval inbox passes `['outstanding']`; a reclamation sweep passes * `['expired']`. */ readonly park?: readonly ParkState[]; /** Stop taking new turns. Work already in flight is not interrupted. */ readonly signal?: AbortSignal; /** How many turns may be in flight at once. Defaults to 1. */ readonly maxConcurrent?: number; /** Candidates handled per pass. See {@link DEFAULT_DRAIN_PAGE_SIZE}. */ readonly pageSize?: number; /** Clock for expiry, so one pass judges every lease and park against one instant. */ readonly now?: number; } /** A turn a pass could not finish, and why. */ export interface DrainFailure { readonly turnId: TurnId; readonly error: string; } /** What one pass did. */ export interface DrainTurnsResult { /** Candidates listed, before any of them were contended for. */ readonly listed: number; /** Turns whose `onTurn` returned. */ readonly drained: readonly TurnId[]; /** Turns whose session another worker held. Not failures. */ readonly skipped: readonly TurnId[]; /** * Turns that stopped matching between the listing and the claim (another * drainer finished them, or their park changed state), given straight back. */ readonly stale: readonly TurnId[]; /** Turns whose `onTurn` threw. */ readonly failed: readonly DrainFailure[]; /** Turns that finished but whose lease could not be handed back. */ readonly unreleased: readonly DrainFailure[]; /** Whether the pass stopped early because the signal aborted. */ readonly stopped: boolean; } /** * Take every open turn nobody holds, one bounded pass, and give each one * back when its work returns. * * The shape is: list → claim the session's lease → re-read the turn under * the lease → work → release in a `finally`. Only under the lease is the * re-read stable, and a turn that no longer matches (settled, resumed by * another drainer, or its park answered) is given straight back as * {@link DrainTurnsResult.stale}. An inbox drain whose work answers the park * is therefore exactly-once: doing the work removes the turn from the queue. * * @throws NamzuError `invalid_config` on a lease or concurrency that cannot * mean what it says. */ export declare function drainParkedTurns(params: DrainTurnsParams): Promise; //# sourceMappingURL=drain.d.ts.map