import type { WorkerOptions, WorkflowTaskPayload, ActivityTaskPayload, WorkflowCompletion, ActivityCompletion } from '../types.js'; /** * Worker — connects to the memoflow server over ONE bidirectional * WorkerSession stream (ADR-0002, P9.2): the server pushes workflow/activity * tasks as frames; completions go back as frames acknowledged by DurableAck * only after the server's store writes are durable (Law 2); Ping frames * renew in-flight task leases (the P7.1 keepalive, now in-band); and * cancellation arrives as CancelNotice frames. `GetCheckpoints` (lazy * hydration) stays on the plain WorkerService client. */ export declare class Worker { private options; private backoffConfig; private running; private inflightWorkflows; private inflightActivities; private workflowGate; private activityGate; private shutdownResolve; private reconnectAbort; /** WorkerService client — post-P9.2 used only for GetCheckpoints (hydration). */ private client; /** WorkerSession service client (the duplex Session stream lives on this channel). */ private sessionGrpcClient; /** The live session; replaced on every reconnect, null between sessions. */ private session; private sessionWaiters; /** A stream whose Hello was sent but whose HelloAck has not arrived yet. */ private connectingSession; /** In-flight workflow-task tokens — carried on every keepalive Ping (P7.1 lease renewal). */ private inflightWfTokens; /** * Runs cancelled by a CancelNotice frame; activity heartbeats read this. * Grow-only by design: a cancelled run is terminal (a restart gets a fresh * run_id), so a lingering key can never mis-cancel later work. */ private cancelledRuns; private activeRunCounts; /** Deterministic hash of all registered workflows + activities. */ private buildId; /** * T4: consecutive Evicted frames without an intervening healthy session. * Eviction means the server has a POLICY reason to refuse this worker; * reconnecting into the same policy forever is churn, not resilience. * After MAX_CONSECUTIVE_EVICTIONS the worker stops with an * operator-actionable message instead of warm noise. */ private consecutiveEvictions; /** * P6.3 transactional step (Postgres tier only): populated from the first * session hello_ack's `sql_contract` (ADR-0002 folded the old * GetSqlContract round-trip into session setup), ONLY when * `options.transactionalStep` is set. `undefined` means "handshake not * done / not applicable" — `ctx.transaction()` treats that as * "unsupported". */ private sqlContractHandshake; private aiInvocationLedger; private aiBudgetReservations; private aiRecoveredBudgetSettlement; private activityOutputReads; constructor(options: WorkerOptions); /** * Start the worker. Connects to the server and begins polling for tasks. * This method blocks until shutdown() is called. */ run(): Promise; /** * P6.3 transactional step (Postgres tier only) — startup handshake, * validated against the `sql_contract` the session hello_ack carries * (replacing the pre-P9.2 GetSqlContract round-trip). Either records the * result (so `ctx.transaction()` can use it) or throws, refusing to start * `run()` at all. This is the "FAIL LOUDLY AT STARTUP" requirement: a * version-skewed worker (its SDK build's SQL contract is older than what * the server reports) or a non-Postgres server must never limp along and * discover the problem only when a workflow finally calls * ctx.transaction(). */ private applySqlContractHandshake; /** * Graceful shutdown: stop accepting new tasks, wait for in-flight to complete. */ shutdown(): Promise; /** Change per-kind execution limits without reconnecting the worker. */ updateConcurrency(maxConcurrentWorkflows: number, maxConcurrentActivities: number): Promise; /** * P6.4: upsert a durable schedule for every `@Cron`-decorated workflow * registered on this worker's task queue. Uses a short-lived * `WorkflowClient` (separate from the WorkerService streaming client) since * schedule registration is a client-facing (WorkflowService) RPC, not a * worker-facing one. Any registration failure aborts worker startup: a * worker must never advertise healthy service while silently omitting a * schedule declared by its code. */ private registerSchedules; /** * Run the session lifecycle: connect → hello/hello_ack → serve frames * until the stream drops → reconnect with backoff, for as long as the * worker runs. Reconnecting with the same (namespace, task_queue, * identity) within the server's reap grace suppresses the disconnect * reap (proto invariant 1), so a network blip never double-executes a * slice; in-flight completions that lost their session retry on the next * one (task tokens fence duplicates server-side). */ private sessionLoop; /** ServerFrame handlers wired into every session (stable across reconnects). */ private sessionEvents; /** * P7.1 lease renewal, in-band: every `workflowTaskKeepaliveMs`, one Ping * frame carrying every in-flight workflow-task token, so the server keeps * pushing each WORKFLOW_TASK_TIMEOUT reaper deadline out (the beat is the * liveness proof) — critical when a slice awaits an implicit HTTP step * (an LLM call) slower than the server's task timeout. Activity * heartbeats ride separate on-demand pings (ctx.heartbeat()). */ private startPingKeepalive; private handleWorkflowTask; private handleActivityTask; /** * Lazy hydration (P0.4, extended by P3.4): when the server stripped step * outputs from the dispatched task — either because total output volume * exceeded the inline budget, or because a step's output was externalized * to the claim-check blob store — fetch the full checkpoints once via * GetCheckpoints before running the workflow. `resolve_blobs: true` makes * the server fetch + sha256-verify any blob-backed checkpoint and inline * the resolved bytes (Design Law 4: the server errors the whole RPC on a * missing/corrupt blob rather than returning partial/empty data), so * workflow code always sees plain inlined values (Design Law 3) — this * worker has no object-store credentials and never talks to the blob * store directly. * * Guard: old servers never set `outputs_inline` (proto3 decodes absent as * false) but DO inline outputs — so only hydrate when some completed step * actually looks stripped (output_size > 0 with an empty output buffer) OR * carries a claim-check ref (output_blob_ref set, output empty). * Public for tests. */ hydrateOutputsIfNeeded(payload: WorkflowTaskPayload): Promise; /** * Report a workflow-task completion as a session frame. Resolves only * when the server's DurableAck arrives (Law 2 — the write is durable); * rejects with FrameRejectedError on a terminal server rejection, or * SessionClosedError when the stream dropped mid-flight (transient: * retryCompletion resends on the next session — the task token fences * duplicates server-side). */ private completeWorkflowTask; private completeActivity; /** * P9.3 eager flush: persist implicit-step effect records mid-slice as ONE * RecordEffects frame, resolving only after the server's DurableAck. A * dropped session retries on the reconnected one (the task token fences * duplicates and stale slices server-side); a FrameError (e.g. * InvalidTaskToken after a disconnect reap re-dispatched this slice) is * terminal and rejects — which rejects the awaiting fetch loudly (F19). */ private flushEffects; /** * Retain completion ownership through reconnect until the server-issued * task deadline (or a bounded legacy fallback). Retries only * transient failures: a dropped/absent session (SessionClosedError — the * next attempt lands on the reconnected session) or a transient gRPC * code. A FrameRejectedError is terminal by contract (the server never * retries a rejected seq) and is never resent. */ private retryCompletion; private waitForSessionOrDelay; private setSession; private retainActiveRun; private releaseActiveRun; private pruneCancelledRuns; executeWorkflowTask(task: WorkflowTaskPayload): Promise; executeActivityTask(task: ActivityTaskPayload): Promise; private readDurableOutput; /** * Activity heartbeat, in-band (replaces the unary Heartbeat RPC): sends a * Ping frame carrying this activity's token — the server extends its * heartbeat_timeout lease exactly as before — and reports whether a * CancelNotice for this run has arrived on the session. Cancellation is * push-based now: a cancel raced against an in-flight beat surfaces on * the NEXT heartbeat (the server answers a Ping on a cancelled run with a * CancelNotice, so poll-fallback semantics hold for workers between * pushes). Without a session (unit tests, lite-style direct calls) * heartbeats are no-ops that never report cancellation. */ private sendHeartbeat; /** * Relay a single `ctx.stream()` chunk as a session frame (P3.5). * Best-effort: without a session this resolves immediately (same "no-op * outside a real worker" behavior as sendHeartbeat); a transport failure * rejects the returned promise so `ActivityContextImpl.stream()` can log * it once — never thrown back into the activity. */ private sendStreamChunk; private sendDurableOutputChunk; private sendAiInvocation; private sendAiToolEffect; private sendAiBudgetReservation; private sendAiBudgetSettlement; private sendAiBudgetResolution; private sendRecoveredAiBudgetSettlement; private checkDrain; } /** * Compute a deterministic build ID from all registered workflow and activity code. * Changes when workflow/activity source code changes — detects stale workers. */ export declare function computeBuildId(): string;