/** * Deposition handling for `ownership: 'lease'` Step-2 fencing (issue #470). * * "Deposed" means another instance now holds the ownership lease at a strictly * newer epoch, so this instance has lost the store. Deposition is detected at two * sites and both funnel here: * * - A fenced durable write's CAS fails against a newer epoch * ({@link commitFencedEngineWrite} re-reads `lease:epoch` to disambiguate). * - The lease manager's renewal reports `onLeaseLost('deposed')` (a successor * stole the holder out from under an otherwise-idle engine). * * The renewal-loss reason `'renewal-unconfirmable'` is deliberately NOT routed * here: it is a transient storage blip (the holder could not prove it still * holds), and halting a healthy holder on a momentary storage hiccup would be a * self-inflicted outage. Only a confirmed `'deposed'` triggers the halt. * * {@link handleDeposition} is idempotent (guarded on {@link EngineInternals.deposed}) * and does the minimum synchronously — set the flag, warn the operator — then * schedules engine teardown on a later tick. Teardown is never run inline: * detection happens mid-commit while a workflow generator is advancing, and * synchronous {@link disposeEngine} clears maps that the unwinding commit and * other in-flight workflows still touch. The flag (plus the * {@link commitFencedEngineWrite} top short-circuit) carries the halt guarantee; * deferring the actual dispose by a tick is safe because every write in that * window is still fenced on the now-stale epoch and loses its CAS. * * This module is allow-listed for import only from `src/core/engine/**`. */ import type { EngineInternals } from './internals.ts'; /** * The `name` of the `process` warning emitted when an engine configured with * `ownership: 'lease'` can no longer be sure it owns the store. The same name * covers all three signals, so a single `process.on('warning')` handler catches * every "this engine may no longer own the store" event: * * - **deposed (confirmed)** — a renewal's CAS failed (a successor stole the lease), * or a fenced durable write was rejected because a newer epoch holds the store * (Step 2). The engine halts. * - **renewal-unconfirmable (transient)** — a storage error left the holder unable * to prove it still holds within the lease window. This is NOT a confirmed * deposition; the engine keeps running, and a later renewal or fenced write * resolves the truth. The warning is informational so operators can investigate. * * Filter on this name to react to ownership-loss signals (alert, drain, restart). * * @example * ```ts * import { ENGINE_LEASE_LOST_WARNING_NAME } from '@lostgradient/weft'; * * process.on('warning', (warning) => { * if (warning.name === ENGINE_LEASE_LOST_WARNING_NAME) { * // This engine may no longer own the store — investigate / drain / restart. * } * }); * ``` */ export declare const ENGINE_LEASE_LOST_WARNING_NAME = "WeftEngineLeaseLostWarning"; /** * React to a confirmed deposition. Idempotent: the first call sets the deposed * flag, emits the operator warning, and schedules a deferred engine teardown; * subsequent calls are no-ops. Returns immediately — the caller is expected to * then throw {@link EngineDeposedError} to unwind the current commit so it does * not advance in-memory state past a durable write that did not land. */ export declare function handleDeposition(internals: EngineInternals): void; /** * `name` of the operator warning emitted when `ownership: 'workflow-lease'` * loses one workflow's claim — a renewal CAS failure (a successor stole the * claim) or a fenced write rejected because a newer epoch now holds it. Unlike * {@link ENGINE_LEASE_LOST_WARNING_NAME}, this is per-workflow, not per-engine: * the rest of this engine's claimed workflows are unaffected and keep running. * * @example * ```ts * import { WORKFLOW_CLAIM_LOST_WARNING_NAME } from '@lostgradient/weft'; * * process.on('warning', (warning) => { * if (warning.name === WORKFLOW_CLAIM_LOST_WARNING_NAME) { * console.error('lost a workflow claim', warning.message); * } * }); * ``` */ export declare const WORKFLOW_CLAIM_LOST_WARNING_NAME = "WeftWorkflowClaimLostWarning"; /** * Operator diagnostic for a lost per-workflow ownership claim under * `ownership: 'workflow-lease'` (see * [ADR 0002](../../../documentation/contributing/architecture-decisions/0002-multiengine-per-workflow-ownership.md)). * Carries the affected `workflowId` as a real field — `process.emitWarning(message, * name)` only accepts strings, so this is emitted as the warning object itself * (`process.emitWarning(warning)`) rather than passed as a message, which lets * consumers read `warning.workflowId` directly instead of parsing it back out * of the message text. * * @example * ```ts * import { WeftWorkflowClaimLostWarning } from '@lostgradient/weft'; * * process.on('warning', (warning) => { * if (warning instanceof WeftWorkflowClaimLostWarning) { * console.error('deposed workflow:', warning.workflowId); * } * }); * ``` */ export declare class WeftWorkflowClaimLostWarning extends Error { readonly workflowId: string; constructor(workflowId: string); } /** * The wake path a stale in-memory resolver was discarded from, for * {@link WeftWorkflowWakeDiscardedWarning}. Mirrors the `wakeOwnershipCheck` * call sites named in ADR 0002: a durable timer firing, a re-evaluated * `ctx.waitUntil()` condition, a delivered signal, an async-activity * completion/failure, a child workflow's termination, or a deferred inline * macrotask drive. * * @example * ```ts * import type { WorkflowWakeKind } from '@lostgradient/weft'; * * const kind: WorkflowWakeKind = 'signal'; * void kind; * ``` */ export type WorkflowWakeKind = 'sleep' | 'wait-condition' | 'signal' | 'async-activity' | 'child-completion' | 'update' | 'inline-macrotask-drive'; /** * `name` of the operator warning emitted when `wakeOwnershipCheck` discards a * stale in-memory wake resolver — it re-read the durable holder record and * found this engine no longer holds the generation (engine id AND epoch) it * parked the workflow under, so the resolver is dropped without driving the * generator. * * @example * ```ts * import { WORKFLOW_WAKE_DISCARDED_WARNING_NAME } from '@lostgradient/weft'; * * process.on('warning', (warning) => { * if (warning.name === WORKFLOW_WAKE_DISCARDED_WARNING_NAME) { * console.error('discarded a stale wake', warning.message); * } * }); * ``` */ export declare const WORKFLOW_WAKE_DISCARDED_WARNING_NAME = "WeftWorkflowWakeDiscardedWarning"; /** * Operator diagnostic emitted whenever `wakeOwnershipCheck` discards a stale * in-memory wake resolver under `ownership: 'workflow-lease'` (see * [ADR 0002](../../../documentation/contributing/architecture-decisions/0002-multiengine-per-workflow-ownership.md)). * Carries the affected `workflowId` and the `wakeKind` that was discarded as * real fields, for the same reason {@link WeftWorkflowClaimLostWarning} does — * `process.emitWarning(message, name)` cannot carry structured data, so this is * emitted as the warning object itself. * * @example * ```ts * import { WeftWorkflowWakeDiscardedWarning } from '@lostgradient/weft'; * * process.on('warning', (warning) => { * if (warning instanceof WeftWorkflowWakeDiscardedWarning) { * console.error('discarded wake', warning.workflowId, warning.wakeKind); * } * }); * ``` */ export declare class WeftWorkflowWakeDiscardedWarning extends Error { readonly workflowId: string; readonly wakeKind: WorkflowWakeKind; constructor(workflowId: string, wakeKind: WorkflowWakeKind); } /** * Emit one of the two per-workflow warnings above. Injected as a seam * (defaulting to `process.emitWarning(warning)`) so later stages — the claim * renewal loop and `wakeOwnershipCheck` — get deterministic test coverage of * their emission sites, mirroring the `warn` option * {@link createSecondInstanceDetector} already takes. Unlike that seam, which * is message-only because its warning name is a single fixed constant, this * one takes the warning *instance* — `WeftWorkflowClaimLostWarning` and * `WeftWorkflowWakeDiscardedWarning` carry different fields, so a caller * filtering or asserting on a specific warning needs the real object, not a * pre-flattened string. */ export type EmitWorkflowLeaseWarning = (warning: WeftWorkflowClaimLostWarning | WeftWorkflowWakeDiscardedWarning) => void; /** * Emit {@link WeftWorkflowClaimLostWarning} for `workflowId`. `emit` defaults * to `process.emitWarning(warning)`; pass a test double to assert on the * emitted instance deterministically instead of scraping `process`'s global * `warning` event. */ export declare function emitWorkflowClaimLostWarning(workflowId: string, emit?: EmitWorkflowLeaseWarning): void; /** * Emit {@link WeftWorkflowWakeDiscardedWarning} for `workflowId`/`wakeKind`. * `emit` defaults to `process.emitWarning(warning)`; pass a test double to * assert on the emitted instance deterministically instead of scraping * `process`'s global `warning` event. */ export declare function emitWorkflowWakeDiscardedWarning(workflowId: string, wakeKind: WorkflowWakeKind, emit?: EmitWorkflowLeaseWarning): void;