import { RetryStrategyConfig } from '@happyvertical/jobs'; import { SmrtCollection, SmrtObject } from '@happyvertical/smrt-core'; /** * Job status type */ export type JobStatus = 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'; /** * Timeout behavior type */ export type TimeoutBehavior = 'fail' | 'kill' | 'warn'; /** * Full job projection with a portable string identity. * * Native DuckDB currently exposes UUID result values as internal objects. * Worker claim/recovery state is keyed by string IDs, so both paths must use * this same projection instead of hydrating a raw UUID from `SELECT *`. */ export declare const SMRT_JOB_PORTABLE_SELECT_COLUMNS = "\n CAST(id AS VARCHAR) AS id,\n slug, context, created_at, updated_at, tenant_id, queue,\n object_type, object_id, method, args, run_at,\n priority, status, attempts, max_attempts, timeout,\n timeout_behavior, started_at, completed_at, last_error,\n result_pointer, task_id, task_owner_id, task_result,\n task_input_requests, task_input_responses, retry_strategy,\n worker_id, worker_heartbeat\n"; /** * Persistent job record stored in the `_smrt_jobs` system table. * * @remarks * Each SmrtJob represents a deferred method call on a SmrtObject. The TaskRunner polls for * pending jobs, resolves the target class via ObjectRegistry, and invokes the method. Jobs * track status (`pending -> running -> completed/failed/cancelled`), retry attempts with * configurable strategies, worker heartbeats for stale-job detection, and optional result pointers. * Priority ordering is `higher = sooner`; the default timeout is 5 minutes (300000ms). */ export declare class SmrtJob extends SmrtObject { /** Tenant context captured for this job, if any */ tenantId: string | null | undefined; /** Queue name for the job */ queue: string; /** Type of object to invoke method on */ objectType: string; /** ID of the specific object (null for static methods) */ objectId: string | null; /** Method name to invoke */ method: string; /** Arguments to pass to the method (JSON) */ args: Record; /** * When to run the job. * * Indexed (single-column, plus the class-level `(status, run_at)` * composite): the claim-loop predicate every runner polls roughly once per * second (#2364). */ runAt: Date; /** Priority (higher = sooner) */ priority: number; /** * Current status. * * Indexed (single-column, plus the class-level `(status, run_at)` * composite): the claim-loop predicate every runner polls roughly once per * second (#2364). `(status, completed_at)` — the retention cleanup * predicate — is a separate index from `ensureJobsSystemTableCompatibility()` * (#2375). */ status: JobStatus; /** Number of execution attempts */ attempts: number; /** Maximum retry attempts */ maxAttempts: number; /** Timeout in milliseconds */ timeout: number; /** What to do on timeout */ timeoutBehavior: TimeoutBehavior; /** When execution started */ startedAt: Date | null; /** When execution completed */ completedAt: Date | null; /** Last error message */ lastError: string | null; /** Pointer to where result is stored */ resultPointer: string | null; /** * Correlation identifier for an MCP Tasks extension task. * * A task is deliberately represented by its backing job rather than a second * queue row: every task transition therefore has exactly one durable worker * target to cancel, recover, and inspect. */ taskId: string | null; /** Principal that created an MCP task, when the transport has one. */ taskOwnerId: string | null; /** Completed MCP CallToolResult, stored only for MCP task jobs. */ taskResult: Record | null; /** Outstanding input requests made through JobExecutionContext.task. */ taskInputRequests: Record | null; /** Accepted task input responses, keyed by the requested input name. */ taskInputResponses: Record | null; /** Retry strategy configuration */ retryStrategy: RetryStrategyConfig; /** ID of the worker processing this job */ workerId: string | null; /** Last heartbeat from the worker */ workerHeartbeat: Date | null; /** * Capture ambient tenant context when a job is saved inside withTenant(). * * Scheduled jobs can also set this explicitly from their owning schedule. */ save(): Promise; /** * Mark the job for retry */ retry(): Promise; /** * Cancel the job */ cancel(): Promise; /** * Get a human-readable description of the job */ getDescription(): string; } /** * Job data type (for create operations) */ export interface SmrtJobData { tenantId?: string | null; queue?: string; objectType: string; objectId?: string | null; method: string; args?: Record; runAt?: Date; priority?: number; maxAttempts?: number; timeout?: number; timeoutBehavior?: TimeoutBehavior; retryStrategy?: RetryStrategyConfig; /** MCP task correlation ID. Internal callers only. */ taskId?: string | null; /** MCP task owner/principal ID. Internal callers only. */ taskOwnerId?: string | null; /** Completed MCP CallToolResult. Internal callers only. */ taskResult?: Record | null; /** Outstanding MCP task input requests. Internal callers only. */ taskInputRequests?: Record | null; /** Accepted MCP task input responses. Internal callers only. */ taskInputResponses?: Record | null; } /** * Options controlling a centralized {@link SmrtJobCollection.enqueueJob} call. */ export interface EnqueueJobOptions { /** * Per-tenant in-flight cap. Defaults to {@link DEFAULT_TENANT_JOB_CAP}. * `0`/negative disables the cap (trusted internal callers). Global * (no-context / null tenant) jobs are always exempt. */ tenantJobCap?: number; } /** * Options for listReady */ export interface ListReadyOptions { limit?: number; queues?: string[]; } /** * Options for atomically claiming ready jobs. */ export interface ClaimReadyOptions extends ListReadyOptions { workerId: string; now?: Date; } /** * Collection for managing SmrtJob objects */ export declare class SmrtJobCollection extends SmrtCollection { static readonly _itemClass: typeof SmrtJob; initialize(): Promise; /** * List jobs by status */ listByStatus(status: JobStatus | JobStatus[], options?: { limit?: number; queue?: string; }): Promise; /** * List pending jobs ready to run */ listReady(options?: { limit?: number; queues?: string[]; }): Promise; /** * Atomically claim pending jobs ready to run for a worker. * * The claim is performed as one conditional UPDATE so concurrent workers * cannot receive the same pending row. PostgreSQL additionally skips rows * locked by other workers instead of waiting behind them. */ claimReady(options: ClaimReadyOptions): Promise; /** * Count non-terminal (pending/running) jobs owned by a tenant. * * Used to enforce the per-tenant creation cap so one tenant cannot exhaust * the shared worker pool (S5 audit #1402). Reads `_smrt_jobs` directly so it * works regardless of ambient tenant context. * * @param tenantId - Tenant to count for. `null` counts global (NULL-tenant) * jobs. */ countInFlightForTenant(tenantId: string | null): Promise; /** * The single creation path for queued jobs. * * Centralizes the two creation-time security guards from the S5 audit (#1402) * so every enqueue — the fluent {@link "./job-builder".JobBuilder} *and* the * ScheduleRunner's cron-triggered jobs — goes through one place: * * 1. `maxAttempts` is clamped to {@link MAX_JOB_RETRIES} so a misconfigured * caller cannot pin a worker on a poison job indefinitely. * 2. A per-tenant in-flight cap bounds how many non-terminal jobs one tenant * may hold, so one tenant cannot exhaust the shared worker pool * (cross-tenant denial of service). The cap applies to the row's effective * tenant (explicit `data.tenantId` or, when absent, the ambient context); * global (null-tenant) jobs are exempt. * * Atomicity note (best-effort soft cap, by design): the cap is a * count-then-insert, NOT a hard transactional invariant. It is intentionally * left non-atomic. A plain transaction would not help — under the adapters' * default isolation two concurrent same-tenant enqueues would each read the * same COUNT and both insert, so serializing them would require either a * per-tenant lock row (`SELECT ... FOR UPDATE`) or SERIALIZABLE-isolation * retry loops. That cross-process locking is fragile (lock-row contention, * adapter-specific isolation behavior, the `transaction` adapter method being * optional) and out of proportion to the threat: this cap is defense in depth * against runaway/accidental creation exhausting the shared worker pool, not a * billing/quota boundary. So under truly simultaneous enqueues a tenant may * momentarily overshoot by the number of in-flight creators; the bound still * prevents unbounded growth and closes the prior ScheduleRunner bypass. If a * hard guarantee is ever needed, enforce it with a DB CHECK/trigger or a * dedicated counter row, not an application-level lock. */ enqueueJob(data: SmrtJobData, options?: EnqueueJobOptions): Promise; /** * Get job statistics */ stats(queue?: string): Promise<{ pending: number; running: number; completed: number; failed: number; cancelled: number; }>; /** * Cleanup old completed/failed jobs. * * Predicates are `(status, completed_at)` pairs, covered by * `idx_smrt_jobs_status_completed_at` (created by the jobs compatibility * path this collection runs on initialize — #2375). * * @param options.dryRun - Count the jobs the cutoffs select without deleting * them, so a retention preview reports exactly what a real run removes. * @returns Number of jobs deleted (or, under `dryRun`, matched). */ cleanup(options: { completedBefore?: Date; failedBefore?: Date; cancelledBefore?: Date; limit?: number; dryRun?: boolean; }): Promise; } export default SmrtJob; //# sourceMappingURL=smrt-job.d.ts.map