@happyvertical/smrt-jobs 0.36.1 → 0.36.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../src/job-handle.ts","../src/job-builder.ts","../src/object-extension.ts","../src/schedule-runner.ts"],"sourcesContent":["import type { JobStatus, SmrtJob, SmrtJobCollection } from './smrt-job.js';\n\n/**\n * Options for waiting on a job\n */\nexport interface WaitOptions {\n /** Maximum time to wait in milliseconds */\n timeout?: number;\n /** Polling interval in milliseconds */\n pollInterval?: number;\n}\n\n/**\n * Result from a completed job\n */\nexport interface JobResult<T = unknown> {\n /** Whether the job completed successfully */\n success: boolean;\n /** The result data (if successful) */\n result?: T;\n /** Error message (if failed) */\n error?: string;\n /** Pointer to where the full result is stored */\n resultPointer?: string | null;\n}\n\n/**\n * Handle for tracking and managing a background job\n *\n * This provides a convenient interface for:\n * - Checking job status\n * - Waiting for completion\n * - Canceling the job\n * - Retrying failed jobs\n */\nexport class JobHandle<T = unknown> {\n private readonly collection: SmrtJobCollection;\n\n constructor(\n public readonly id: string,\n collection: SmrtJobCollection,\n ) {\n this.collection = collection;\n }\n\n /**\n * Get the current job status\n */\n async status(): Promise<JobStatus> {\n const job = await this.getJob();\n return job.status;\n }\n\n /**\n * Get the full job object\n */\n async getJob(): Promise<SmrtJob> {\n const job = await this.collection.get({ id: this.id });\n if (!job) {\n throw new Error(`Job not found: ${this.id}`);\n }\n return job;\n }\n\n /**\n * Wait for the job to complete\n *\n * @param options - Wait configuration\n * @returns The job result\n * @throws Error if the job fails or times out\n */\n async wait(options: WaitOptions = {}): Promise<JobResult<T>> {\n const { timeout = 60000, pollInterval = 100 } = options;\n const startTime = Date.now();\n\n while (true) {\n const job = await this.getJob();\n\n if (job.status === 'completed') {\n return {\n success: true,\n resultPointer: job.resultPointer,\n };\n }\n\n if (job.status === 'failed') {\n return {\n success: false,\n error: job.lastError ?? 'Job failed',\n };\n }\n\n if (job.status === 'cancelled') {\n return {\n success: false,\n error: 'Job was cancelled',\n };\n }\n\n // Check timeout\n if (Date.now() - startTime >= timeout) {\n throw new Error(`Timeout waiting for job ${this.id}`);\n }\n\n // Wait before next poll\n await new Promise((resolve) => setTimeout(resolve, pollInterval));\n }\n }\n\n /**\n * Cancel the job\n */\n async cancel(): Promise<void> {\n const job = await this.getJob();\n await job.cancel();\n }\n\n /**\n * Retry a failed job\n */\n async retry(): Promise<void> {\n const job = await this.getJob();\n await job.retry();\n }\n\n /**\n * Check if the job is still running\n */\n async isRunning(): Promise<boolean> {\n const status = await this.status();\n return status === 'pending' || status === 'running';\n }\n\n /**\n * Check if the job has completed (successfully or not)\n */\n async isDone(): Promise<boolean> {\n const status = await this.status();\n return (\n status === 'completed' || status === 'failed' || status === 'cancelled'\n );\n }\n}\n\nexport default JobHandle;\n","import {\n exponential,\n type RetryStrategy,\n type RetryStrategyConfig,\n} from '@happyvertical/jobs';\nimport { clampRetries, DEFAULT_TENANT_JOB_CAP } from './background-policy.js';\nimport { JobHandle } from './job-handle.js';\nimport type { SmrtJobCollection, TimeoutBehavior } from './smrt-job.js';\n\n/**\n * Priority levels for jobs\n */\nexport type Priority = 'critical' | 'high' | 'normal' | 'low' | number;\n\n/**\n * Convert priority to numeric value\n */\nexport function priorityToNumber(priority: Priority): number {\n if (typeof priority === 'number') return priority;\n switch (priority) {\n case 'critical':\n return 100;\n case 'high':\n return 75;\n case 'normal':\n return 50;\n case 'low':\n return 25;\n default:\n return 50;\n }\n}\n\n/**\n * Parse delay string to milliseconds\n */\nexport function parseDelay(delay: string | number): number {\n if (typeof delay === 'number') {\n // Guard against NaN/Infinity: a non-finite delay flows into\n // `new Date(Date.now() + delay)`, persisting an `Invalid Date` `runAt`\n // that the claim query can never match (S3 in the #1401 review).\n if (!Number.isFinite(delay)) {\n throw new Error(`Invalid delay value: ${delay}`);\n }\n return delay;\n }\n\n const match = delay.match(/^(\\d+)(ms|s|m|h|d)?$/);\n if (!match) {\n throw new Error(`Invalid delay format: ${delay}`);\n }\n\n const value = parseInt(match[1], 10);\n const unit = match[2] || 'ms';\n\n switch (unit) {\n case 'ms':\n return value;\n case 's':\n return value * 1000;\n case 'm':\n return value * 60 * 1000;\n case 'h':\n return value * 60 * 60 * 1000;\n case 'd':\n return value * 24 * 60 * 60 * 1000;\n default:\n return value;\n }\n}\n\n/**\n * Fluent builder for creating background jobs\n *\n * Example:\n * ```typescript\n * const handle = await doc.background('generateSummary', { format: 'md' })\n * .delay('5m')\n * .retries(5)\n * .priority('high')\n * .queue('summaries')\n * .timeout(300000)\n * .enqueue();\n * ```\n */\nexport class JobBuilder<T = unknown> {\n private _queue: string = 'default';\n private _delay: number = 0;\n private _retries: number = 3;\n private _priority: number = 50;\n private _timeout: number = 300000;\n private _timeoutBehavior: TimeoutBehavior = 'fail';\n private _retryStrategy: RetryStrategy = exponential();\n private _tenantJobCap: number = DEFAULT_TENANT_JOB_CAP;\n\n constructor(\n private readonly objectType: string,\n private readonly objectId: string | null,\n private readonly method: string,\n private readonly args: Record<string, unknown>,\n private readonly collection: SmrtJobCollection,\n ) {}\n\n /**\n * Set the queue name\n */\n queue(name: string): this {\n this._queue = name;\n return this;\n }\n\n /**\n * Set a delay before the job runs\n * @param delay - Delay as milliseconds or string like '5m', '1h', '30s'\n */\n delay(delay: string | number): this {\n this._delay = parseDelay(delay);\n return this;\n }\n\n /**\n * Set when the job should run\n */\n runAt(date: Date): this {\n this._delay = date.getTime() - Date.now();\n return this;\n }\n\n /**\n * Set the maximum number of retry attempts.\n *\n * Clamped to {@link MAX_JOB_RETRIES} so a misconfigured caller cannot pin a\n * worker on a poison job indefinitely (S5 audit #1402).\n */\n retries(count: number): this {\n this._retries = clampRetries(count);\n return this;\n }\n\n /**\n * Set the retry strategy\n */\n retryStrategy(strategy: RetryStrategy): this {\n this._retryStrategy = strategy;\n return this;\n }\n\n /**\n * Set the job priority\n */\n priority(level: Priority): this {\n this._priority = priorityToNumber(level);\n return this;\n }\n\n /**\n * Set the job timeout in milliseconds\n */\n timeout(ms: number): this {\n this._timeout = ms;\n return this;\n }\n\n /**\n * Set what happens when the job times out\n */\n timeoutBehavior(behavior: TimeoutBehavior): this {\n this._timeoutBehavior = behavior;\n return this;\n }\n\n /**\n * Override the per-tenant in-flight job cap for this enqueue.\n *\n * Defaults to {@link DEFAULT_TENANT_JOB_CAP}. Pass `0` (or a negative value)\n * to disable the cap for trusted internal callers (S5 audit #1402).\n */\n tenantJobCap(max: number): this {\n this._tenantJobCap = max;\n return this;\n }\n\n /**\n * Enqueue the job and return a handle\n */\n async enqueue(): Promise<JobHandle<T>> {\n const runAt = new Date(Date.now() + this._delay);\n\n const retryConfig: RetryStrategyConfig =\n 'toConfig' in this._retryStrategy\n ? this._retryStrategy.toConfig()\n : (this._retryStrategy as RetryStrategyConfig);\n\n // Route through the collection's single creation path so the per-tenant\n // in-flight cap and the retry ceiling are enforced in one place, shared with\n // the ScheduleRunner (S5 audit #1402). The cap applies to the ambient tenant\n // (resolved inside enqueueJob); global (no-context) jobs are exempt.\n const job = await this.collection.enqueueJob(\n {\n queue: this._queue,\n objectType: this.objectType,\n objectId: this.objectId,\n method: this.method,\n args: this.args,\n runAt,\n priority: this._priority,\n maxAttempts: this._retries,\n timeout: this._timeout,\n timeoutBehavior: this._timeoutBehavior,\n retryStrategy: retryConfig,\n },\n { tenantJobCap: this._tenantJobCap },\n );\n\n const jobId = job.id;\n if (!jobId) {\n throw new Error('Job was created but has no ID');\n }\n\n return new JobHandle<T>(jobId, this.collection);\n }\n}\n\nexport default JobBuilder;\n","import { ObjectRegistry, type SmrtObject } from '@happyvertical/smrt-core';\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport {\n JobBuilder,\n type Priority,\n parseDelay,\n priorityToNumber,\n} from './job-builder.js';\nimport type { JobHandle } from './job-handle.js';\nimport { SmrtJobCollection } from './smrt-job.js';\n\n/**\n * Options for the simple .bg() method\n */\nexport interface BgOptions {\n /** Queue name */\n queue?: string;\n /** Priority level */\n priority?: 'critical' | 'high' | 'normal' | 'low' | number;\n /** Delay before running (ms or string like '5m') */\n delay?: string | number;\n /** Maximum retries */\n retries?: number;\n /** Timeout in milliseconds */\n timeout?: number;\n}\n\n/**\n * Type for the extended SmrtObject with background job methods\n */\nexport interface BackgroundCapable {\n /**\n * Simple background job submission\n *\n * @param method - Method name to invoke\n * @param args - Arguments to pass to the method\n * @param options - Job options\n * @returns JobHandle for tracking the job\n *\n * @example\n * const handle = await doc.bg('generateSummary', { format: 'md' });\n */\n bg<T = unknown>(\n method: string,\n args?: Record<string, unknown>,\n options?: BgOptions,\n ): Promise<JobHandle<T>>;\n\n /**\n * Fluent job builder for advanced options\n *\n * @param method - Method name to invoke\n * @param args - Arguments to pass to the method\n * @returns JobBuilder for fluent configuration\n *\n * @example\n * const handle = await doc.background('generateSummary', { format: 'md' })\n * .delay('5m')\n * .retries(5)\n * .priority('high')\n * .enqueue();\n */\n background<T = unknown>(\n method: string,\n args?: Record<string, unknown>,\n ): JobBuilder<T>;\n}\n\n// Cache for job collections per database\nconst collectionCache = new WeakMap<DatabaseInterface, SmrtJobCollection>();\n\n/**\n * Get or create a job collection for the given database\n */\nasync function getJobCollection(\n db: DatabaseInterface,\n): Promise<SmrtJobCollection> {\n let collection = collectionCache.get(db);\n if (!collection) {\n collection = await SmrtJobCollection.create({\n db: { type: 'sqlite', url: ':memory:' }, // Placeholder\n });\n // Override internal db reference\n (collection as unknown as { _db: DatabaseInterface })._db = db;\n collectionCache.set(db, collection);\n }\n return collection;\n}\n\nfunction getObjectTypeName(instance: SmrtObject): string {\n const metaType = (instance as { _meta_type?: unknown })._meta_type;\n if (typeof metaType === 'string' && metaType.length > 0) {\n return metaType;\n }\n\n const className = instance.constructor.name;\n return ObjectRegistry.getClass(className)?.qualifiedName || className;\n}\n\n// Type for a SmrtObject constructor\ntype SmrtObjectConstructor = new (...args: any[]) => SmrtObject;\ntype BackgroundCapableConstructor<T extends SmrtObjectConstructor> = T & {\n new (...args: ConstructorParameters<T>): InstanceType<T> & BackgroundCapable;\n};\n\nfunction requireObjectDb(instance: SmrtObject): DatabaseInterface {\n const db = (instance as unknown as { _db?: DatabaseInterface })._db;\n if (!db) {\n throw new Error('Object not initialized. Call initialize() first.');\n }\n\n return db;\n}\n\nasync function bgImpl<R = unknown>(\n this: SmrtObject,\n method: string,\n args: Record<string, unknown> = {},\n options: BgOptions = {},\n): Promise<JobHandle<R>> {\n const db = requireObjectDb(this);\n const collection = await getJobCollection(db);\n const builder = new JobBuilder<R>(\n getObjectTypeName(this),\n this.id ?? null,\n method,\n args,\n collection,\n );\n\n if (options.queue) builder.queue(options.queue);\n if (options.priority) builder.priority(options.priority);\n if (options.delay) builder.delay(options.delay);\n if (options.retries !== undefined) builder.retries(options.retries);\n if (options.timeout) builder.timeout(options.timeout);\n\n return builder.enqueue();\n}\n\nfunction backgroundImpl<R = unknown>(\n this: SmrtObject,\n method: string,\n args: Record<string, unknown> = {},\n): JobBuilder<R> {\n const db = requireObjectDb(this);\n const objectType = getObjectTypeName(this);\n const objectId = this.id ?? null;\n\n // Create a proxy builder that defers collection access until enqueue().\n const lazyBuilder = {\n _queue: 'default',\n _delay: 0,\n _retries: 3,\n _priority: 50,\n _timeout: 300000,\n _timeoutBehavior: 'fail' as 'fail' | 'kill' | 'warn',\n _retryStrategy: null as unknown,\n // `undefined` => fall through to JobBuilder's DEFAULT_TENANT_JOB_CAP. A\n // caller can override (incl. `0` to disable) via tenantJobCap() below.\n _tenantJobCap: undefined as number | undefined,\n\n queue(name: string) {\n this._queue = name;\n return this;\n },\n delay(d: string | number) {\n this._delay = parseDelay(d);\n return this;\n },\n runAt(date: Date) {\n this._delay = date.getTime() - Date.now();\n return this;\n },\n retries(count: number) {\n this._retries = count;\n return this;\n },\n retryStrategy(strategy: unknown) {\n this._retryStrategy = strategy;\n return this;\n },\n priority(level: Priority) {\n this._priority = priorityToNumber(level);\n return this;\n },\n timeout(ms: number) {\n this._timeout = ms;\n return this;\n },\n timeoutBehavior(behavior: 'fail' | 'kill' | 'warn') {\n this._timeoutBehavior = behavior;\n return this;\n },\n tenantJobCap(max: number) {\n this._tenantJobCap = max;\n return this;\n },\n async enqueue(): Promise<JobHandle<R>> {\n const collection = await getJobCollection(db);\n const builder = new JobBuilder<R>(\n objectType,\n objectId,\n method,\n args,\n collection,\n );\n\n builder.queue(this._queue);\n builder.delay(this._delay);\n builder.retries(this._retries);\n builder.priority(this._priority);\n builder.timeout(this._timeout);\n builder.timeoutBehavior(this._timeoutBehavior);\n // Only forward an explicit override; leaving it unset preserves the\n // JobBuilder default (DEFAULT_TENANT_JOB_CAP).\n if (this._tenantJobCap !== undefined) {\n builder.tenantJobCap(this._tenantJobCap);\n }\n\n if (this._retryStrategy) {\n builder.retryStrategy(\n this._retryStrategy as Parameters<typeof builder.retryStrategy>[0],\n );\n }\n\n return builder.enqueue();\n },\n };\n\n return lazyBuilder as unknown as JobBuilder<R>;\n}\n\n/**\n * Extend a SmrtObject class with background job methods\n *\n * @param BaseClass - The SmrtObject class to extend\n * @returns Extended class with .bg() and .background() methods\n *\n * @example\n * const BackgroundDocument = withBackgroundJobs(Document);\n * const doc = new BackgroundDocument({ ... });\n * const handle = await doc.bg('generateSummary', { format: 'md' });\n */\nexport function withBackgroundJobs<T extends SmrtObjectConstructor>(\n BaseClass: T,\n): BackgroundCapableConstructor<T> {\n const prototype = BaseClass.prototype as SmrtObject &\n Partial<BackgroundCapable>;\n\n if (typeof prototype.bg !== 'function') {\n Object.defineProperty(prototype, 'bg', {\n value: bgImpl,\n writable: true,\n configurable: true,\n });\n }\n\n if (typeof prototype.background !== 'function') {\n Object.defineProperty(prototype, 'background', {\n value: backgroundImpl,\n writable: true,\n configurable: true,\n });\n }\n\n return BaseClass as BackgroundCapableConstructor<T>;\n}\n\nexport default withBackgroundJobs;\n","import { EventEmitter } from 'node:events';\nimport { createLogger } from '@happyvertical/logger';\nimport { ObjectRegistry } from '@happyvertical/smrt-core';\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport { createId } from '@happyvertical/utils';\nimport {\n redactErrorForPersistence,\n redactErrorMessage,\n} from './error-redaction.js';\nimport { SmrtJobCollection } from './smrt-job.js';\nimport { SmrtWorkerCollection } from './smrt-worker.js';\nimport { DEFAULT_TASK_HEARTBEAT_INTERVAL_MS } from './stale-recovery.js';\nimport { isWorkerAlive } from './worker-liveness.js';\n\n/**\n * ScheduleRunner configuration\n */\nexport interface ScheduleRunnerConfig {\n /** Runner ID (auto-generated if not provided) */\n id?: string;\n /** Polling interval in milliseconds (default: 60000 - 1 minute) */\n pollInterval?: number;\n /** Maximum schedules to process per poll */\n batchSize?: number;\n /**\n * @deprecated No longer used. Slot reconciliation keys on worker liveness\n * (the `_smrt_workers` lease), not per-job heartbeat staleness (#1474).\n */\n staleJobThresholdMs?: number;\n /**\n * @deprecated No longer used. See {@link staleJobThresholdMs}.\n */\n taskHeartbeatInterval?: number;\n}\n\n/**\n * ScheduleRunner events\n */\nexport interface ScheduleRunnerEvents {\n 'schedule:triggered': (schedule: ScheduleInfo) => void;\n 'schedule:error': (schedule: ScheduleInfo, error: Error) => void;\n 'schedule:completed': (scheduleId: string) => void;\n 'schedule:failed': (scheduleId: string, error: string) => void;\n 'runner:started': () => void;\n 'runner:stopped': () => void;\n 'runner:error': (error: Error) => void;\n}\n\n/**\n * Schedule info for events\n */\nexport interface ScheduleInfo {\n id: string;\n agentType: string;\n agentId: string | null;\n cron: string;\n}\n\n/**\n * Default configuration\n */\nconst DEFAULT_CONFIG: Required<ScheduleRunnerConfig> = {\n id: '',\n pollInterval: 60000, // 1 minute\n batchSize: 50,\n staleJobThresholdMs: 90000,\n taskHeartbeatInterval: DEFAULT_TASK_HEARTBEAT_INTERVAL_MS,\n};\n\n/**\n * ScheduleRunner polls for due agent schedules and creates jobs for them\n *\n * This runner works in conjunction with TaskRunner:\n * 1. ScheduleRunner checks for due schedules based on cron expressions\n * 2. When a schedule is due, it creates a SmrtJob for the agent\n * 3. TaskRunner picks up and executes the job\n * 4. On job completion/failure, call handleJobCompletion() to update the schedule\n *\n * @example\n * ```typescript\n * const scheduleRunner = new ScheduleRunner({ pollInterval: 30000 });\n * await scheduleRunner.initialize(db);\n * await scheduleRunner.start();\n *\n * // Wire up TaskRunner events to update schedule state\n * taskRunner.on('job:completed', (job) => {\n * const scheduleId = job.args?._scheduleId;\n * if (scheduleId) scheduleRunner.handleJobCompletion(scheduleId, true);\n * });\n * taskRunner.on('job:failed', (job, error) => {\n * const scheduleId = job.args?._scheduleId;\n * if (scheduleId) scheduleRunner.handleJobCompletion(scheduleId, false, error.message);\n * });\n *\n * // Graceful shutdown\n * process.on('SIGTERM', () => scheduleRunner.stop());\n * ```\n */\nexport class ScheduleRunner extends EventEmitter {\n readonly id: string;\n private readonly config: Required<ScheduleRunnerConfig>;\n private jobCollection: SmrtJobCollection | null = null;\n private workerCollection: SmrtWorkerCollection | null = null;\n private running = false;\n private pollTimer: NodeJS.Timeout | null = null;\n private db: DatabaseInterface | null = null;\n private logger = createLogger(true);\n\n constructor(config: ScheduleRunnerConfig = {}) {\n super();\n this.config = {\n ...DEFAULT_CONFIG,\n ...config,\n id: config.id || `schedule_${createId().slice(0, 8)}`,\n };\n this.id = this.config.id;\n }\n\n /**\n * Initialize the runner with database connection\n */\n async initialize(db: DatabaseInterface): Promise<void> {\n this.db = db;\n this.jobCollection = await SmrtJobCollection.create({ db });\n this.workerCollection = await SmrtWorkerCollection.create({ db });\n }\n\n /**\n * Start processing schedules\n */\n async start(): Promise<void> {\n if (this.running) return;\n if (!this.db) {\n throw new Error(\n 'ScheduleRunner not initialized. Call initialize() first.',\n );\n }\n\n this.running = true;\n\n // Start polling loop\n this.startPolling();\n\n this.emit('runner:started');\n this.logger.info('ScheduleRunner started', { id: this.id });\n }\n\n /**\n * Stop processing schedules\n */\n async stop(): Promise<void> {\n if (!this.running) return;\n\n this.running = false;\n\n if (this.pollTimer) {\n clearTimeout(this.pollTimer);\n this.pollTimer = null;\n }\n\n this.emit('runner:stopped');\n this.logger.info('ScheduleRunner stopped', { id: this.id });\n }\n\n /**\n * Check if runner is running\n */\n isRunning(): boolean {\n return this.running;\n }\n\n /**\n * Handle job completion for a scheduled job.\n *\n * Call this from TaskRunner's job:completed / job:failed events\n * when the job has a `_scheduleId` in its args.\n */\n async handleJobCompletion(\n scheduleId: string,\n success: boolean,\n errorMessage?: string,\n ): Promise<void> {\n if (!this.db) return;\n\n // `last_error` is persisted to a durable schedule row; strip secret-shaped\n // substrings the same way the job runner does (S5 audit #1402).\n const safeErrorMessage = redactErrorMessage(\n errorMessage ?? 'Unknown error',\n );\n\n try {\n if (success) {\n await this.db.query(\n `UPDATE _smrt_agent_schedules\n SET running_count = CASE WHEN COALESCE(running_count, 0) > 0 THEN running_count - 1 ELSE 0 END,\n last_run = ?,\n last_status = 'success',\n last_error = NULL,\n run_count = COALESCE(run_count, 0) + 1,\n success_count = COALESCE(success_count, 0) + 1\n WHERE id = ?`,\n new Date().toISOString(),\n scheduleId,\n );\n this.emit('schedule:completed', scheduleId);\n } else {\n await this.db.query(\n `UPDATE _smrt_agent_schedules\n SET running_count = CASE WHEN COALESCE(running_count, 0) > 0 THEN running_count - 1 ELSE 0 END,\n last_run = ?,\n last_status = 'failed',\n last_error = ?,\n run_count = COALESCE(run_count, 0) + 1,\n failure_count = COALESCE(failure_count, 0) + 1\n WHERE id = ?`,\n new Date().toISOString(),\n safeErrorMessage,\n scheduleId,\n );\n this.emit('schedule:failed', scheduleId, safeErrorMessage);\n }\n } catch (err) {\n this.logger.error('Failed to update schedule after job completion', {\n scheduleId,\n error: err,\n });\n }\n }\n\n /**\n * Start the polling loop\n */\n private startPolling(): void {\n const poll = async () => {\n if (!this.running) return;\n\n try {\n await this.poll();\n } catch (error) {\n this.emit('runner:error', error as Error);\n this.logger.error('ScheduleRunner poll error', { error });\n }\n\n // Schedule next poll\n if (this.running) {\n this.pollTimer = setTimeout(poll, this.config.pollInterval);\n }\n };\n\n // Start immediately\n poll();\n }\n\n /**\n * Poll for due schedules and create jobs\n */\n private async poll(): Promise<void> {\n if (!this.db || !this.jobCollection) return;\n\n await this.recoverStaleScheduleState();\n\n const now = new Date().toISOString();\n\n // Find due schedules\n const result = await this.db.query(\n `SELECT * FROM _smrt_agent_schedules\n WHERE enabled = true\n AND status = 'active'\n AND next_run <= ?\n AND COALESCE(running_count, 0) < COALESCE(max_concurrent, 1)\n ORDER BY next_run ASC\n LIMIT ?`,\n now,\n this.config.batchSize,\n );\n\n for (const row of result.rows) {\n await this.triggerSchedule(row as ScheduleRow);\n }\n }\n\n /**\n * Reconcile stuck schedule slots against running jobs.\n *\n * This handles two failure modes:\n * - a running job's owning worker is no longer alive (dead/restarted)\n * - a schedule slot remains occupied even though no running job still exists\n *\n * Staleness keys on worker *liveness* (issue #1474), not per-job heartbeat\n * freshness: a job whose `worker_id` is live in this process or holds a fresh\n * lease in `_smrt_workers` is healthy even if its handler is holding the loop\n * synchronously. ScheduleRunner has no in-process active-job set, so this is\n * its entire correctness mechanism.\n */\n private async recoverStaleScheduleState(): Promise<void> {\n if (!this.db || !this.workerCollection) return;\n\n const schedulesResult = await this.db.query(\n `SELECT id, running_count\n FROM _smrt_agent_schedules\n WHERE COALESCE(running_count, 0) > 0`,\n );\n const schedules = schedulesResult.rows as Array<{\n id: string;\n running_count: number;\n }>;\n if (schedules.length === 0) return;\n\n // Without the workers table we cannot reason about liveness; treat every\n // running job as alive (reconcile slot drift only, never fail jobs).\n const workersReady = await this.workerCollection.tableReady();\n const freshLeaseKeys = workersReady\n ? await this.workerCollection.freshLeaseWorkerKeys()\n : new Set<string>();\n\n const jobsResult = await this.db.query(\n `SELECT id, args, worker_id\n FROM _smrt_jobs\n WHERE status = 'running'`,\n );\n const jobRows = jobsResult.rows as Array<{\n id: string;\n args: unknown;\n worker_id: string | null;\n }>;\n\n type ScheduleState = { live: number; staleJobIds: string[] };\n const stateBySchedule = new Map<string, ScheduleState>();\n for (const schedule of schedules) {\n stateBySchedule.set(schedule.id, { live: 0, staleJobIds: [] });\n }\n\n for (const row of jobRows) {\n const scheduleId = this.getScheduleIdFromJobArgs(row.args);\n if (!scheduleId) continue;\n\n const state = stateBySchedule.get(scheduleId);\n if (!state) continue;\n\n const alive = workersReady\n ? isWorkerAlive(row.worker_id, freshLeaseKeys)\n : true;\n\n if (!alive) {\n state.staleJobIds.push(row.id);\n } else {\n state.live += 1;\n }\n }\n\n const now = new Date().toISOString();\n const staleJobIds = schedules.flatMap((schedule) => {\n const state = stateBySchedule.get(schedule.id);\n return state?.staleJobIds ?? [];\n });\n\n // Only the jobs this pass actually transitioned to 'failed' — RETURNING id\n // (not rowCount, which DuckDB/JSON always report as ≥1) so a job another\n // recoverer already failed isn't double-counted into the schedule's\n // run_count/failure_count.\n const recoveredJobIds = new Set<string>();\n if (staleJobIds.length > 0) {\n const placeholders = staleJobIds.map(() => '?').join(', ');\n const result = await this.db.query(\n `UPDATE _smrt_jobs\n SET status = 'failed',\n completed_at = ?,\n last_error = ?,\n worker_id = NULL,\n worker_heartbeat = NULL\n WHERE status = 'running'\n AND id IN (${placeholders})\n RETURNING id`,\n now,\n 'Recovered orphaned scheduled job: its owning worker is no longer ' +\n 'alive (no fresh liveness lease in _smrt_workers and not running in ' +\n 'this process).',\n ...staleJobIds,\n );\n for (const row of result.rows as Array<{ id?: unknown }>) {\n if (typeof row.id === 'string') recoveredJobIds.add(row.id);\n }\n }\n\n for (const schedule of schedules) {\n const state = stateBySchedule.get(schedule.id);\n if (!state) continue;\n\n const desiredRunningCount = state.live;\n const recoveredCount = state.staleJobIds.filter((id) =>\n recoveredJobIds.has(id),\n ).length;\n\n if (\n Number(schedule.running_count) === desiredRunningCount &&\n recoveredCount === 0\n ) {\n continue;\n }\n\n if (recoveredCount > 0) {\n await this.db.query(\n `UPDATE _smrt_agent_schedules\n SET running_count = ?,\n last_run = ?,\n last_status = 'failed',\n last_error = ?,\n run_count = COALESCE(run_count, 0) + ?,\n failure_count = COALESCE(failure_count, 0) + ?\n WHERE id = ?`,\n desiredRunningCount,\n now,\n `Recovered ${recoveredCount} orphaned scheduled job(s) from dead worker(s)`,\n recoveredCount,\n recoveredCount,\n schedule.id,\n );\n this.emit(\n 'schedule:failed',\n schedule.id,\n `Recovered ${recoveredCount} orphaned scheduled job(s)`,\n );\n } else {\n await this.db.query(\n `UPDATE _smrt_agent_schedules\n SET running_count = ?\n WHERE id = ?`,\n desiredRunningCount,\n schedule.id,\n );\n }\n }\n }\n\n private getScheduleIdFromJobArgs(args: unknown): string | null {\n if (!args) return null;\n\n let parsedArgs = args;\n if (typeof parsedArgs === 'string') {\n try {\n parsedArgs = JSON.parse(parsedArgs) as Record<string, unknown>;\n } catch {\n return null;\n }\n }\n\n if (\n !parsedArgs ||\n typeof parsedArgs !== 'object' ||\n Array.isArray(parsedArgs)\n ) {\n return null;\n }\n\n const scheduleId = (parsedArgs as Record<string, unknown>)._scheduleId;\n return typeof scheduleId === 'string' && scheduleId.length > 0\n ? scheduleId\n : null;\n }\n\n /**\n * Trigger a schedule by creating a job\n */\n private async triggerSchedule(schedule: ScheduleRow): Promise<void> {\n if (!this.db || !this.jobCollection) return;\n\n const rawAgentType = schedule.agent_type as string;\n const canonicalAgentType =\n ObjectRegistry.getClass(rawAgentType)?.qualifiedName || rawAgentType;\n\n const scheduleInfo: ScheduleInfo = {\n id: schedule.id as string,\n agentType: canonicalAgentType,\n agentId: schedule.agent_id as string | null,\n cron: schedule.cron as string,\n };\n\n try {\n // Parse method_args and agent_config from JSON strings if needed\n let methodArgs: Record<string, unknown> = {};\n if (schedule.method_args) {\n methodArgs =\n typeof schedule.method_args === 'string'\n ? JSON.parse(schedule.method_args as string)\n : (schedule.method_args as Record<string, unknown>);\n }\n let agentConfig: Record<string, unknown> = {};\n if (schedule.agent_config) {\n agentConfig =\n typeof schedule.agent_config === 'string'\n ? JSON.parse(schedule.agent_config as string)\n : (schedule.agent_config as Record<string, unknown>);\n }\n\n // Compute next run time from cron before creating the job\n const nextRun = getNextCronDate(schedule.cron as string);\n\n // Create a job for this schedule\n // Nest agent_config under _agentConfig so TaskRunner can pass it\n // to the agent constructor separately from method args\n const args: Record<string, unknown> = {\n ...methodArgs,\n _scheduleId: schedule.id,\n };\n if (Object.keys(agentConfig).length > 0) {\n args._agentConfig = agentConfig;\n }\n\n // Enqueue the job BEFORE advancing next_run / incrementing running_count.\n // Previously next_run was advanced and running_count incremented first; if\n // enqueueJob then threw (a transient tenant-cap hit or DB blip), the catch\n // disabled the schedule but never rolled next_run back, permanently losing\n // that run slot AND taking the schedule out of the poll until manual\n // re-activation (#4 in the #1401 review). By enqueuing first, a transient\n // failure leaves next_run untouched, so the same due slot is retried on\n // the next poll and the schedule stays active.\n //\n // Route scheduled jobs through the same centralized creation path as the\n // fluent builder so the per-tenant in-flight cap and retry ceiling apply\n // here too — previously a direct create() bypassed both (S5 audit #1402).\n // The schedule's own tenant is passed explicitly so the cap is enforced\n // for the owning tenant even with no ambient context.\n const job = await this.jobCollection.enqueueJob({\n tenantId:\n typeof schedule.tenant_id === 'string' &&\n schedule.tenant_id.length > 0\n ? (schedule.tenant_id as string)\n : null,\n queue: 'agents',\n objectType: canonicalAgentType,\n objectId: schedule.agent_id as string | null,\n method: (schedule.method as string) || 'run',\n args,\n priority: 75, // High priority for scheduled agents\n maxAttempts: 3,\n timeout: (schedule.timeout as number) || 3600000,\n });\n\n // Only now that the job is durably enqueued do we consume the slot:\n // increment running_count and advance next_run in one update.\n await this.db.query(\n `UPDATE _smrt_agent_schedules\n SET agent_type = ?,\n running_count = running_count + 1,\n next_run = ?\n WHERE id = ?`,\n canonicalAgentType,\n nextRun.toISOString(),\n schedule.id,\n );\n\n this.emit('schedule:triggered', scheduleInfo);\n this.logger.info('Schedule triggered', {\n scheduleId: schedule.id,\n agentType: canonicalAgentType,\n jobId: job.id,\n nextRun: nextRun.toISOString(),\n });\n } catch (error) {\n // The slot was NOT consumed (enqueue runs before the next_run/\n // running_count advance), so there is nothing to roll back: leave\n // next_run and status='active' untouched so the next poll retries the\n // same due slot rather than skipping it or disabling the schedule on a\n // transient failure (#4). Record last_error for operator visibility only.\n await this.db.query(\n `UPDATE _smrt_agent_schedules\n SET last_error = ?\n WHERE id = ?`,\n // Tolerate non-Error throwables: a thrown string/object has no\n // `.message`, which would otherwise persist an empty `last_error`.\n redactErrorForPersistence(error),\n schedule.id,\n );\n\n this.emit('schedule:error', scheduleInfo, error as Error);\n this.logger.error('Schedule trigger failed', {\n scheduleId: schedule.id,\n error,\n });\n }\n }\n}\n\n/**\n * Database row type for schedule\n */\ninterface ScheduleRow {\n id: unknown;\n agent_type: unknown;\n agent_id: unknown;\n tenant_id: unknown;\n agent_config: unknown;\n cron: unknown;\n method: unknown;\n method_args: unknown;\n timeout: unknown;\n}\n\n// --- Cron helpers (self-contained, no external dependency) ---\n\n/**\n * Inclusive valid range for each cron field, by position.\n * minute, hour, day-of-month, month, day-of-week.\n * Day-of-week accepts 0-7 where both 0 and 7 represent Sunday.\n */\nconst CRON_FIELD_RANGES: ReadonlyArray<{\n name: string;\n min: number;\n max: number;\n}> = [\n { name: 'minute', min: 0, max: 59 },\n { name: 'hour', min: 0, max: 23 },\n { name: 'day-of-month', min: 1, max: 31 },\n { name: 'month', min: 1, max: 12 },\n { name: 'day-of-week', min: 0, max: 7 },\n];\n\n/**\n * Validate that every numeric component of a single cron field falls within\n * the field's inclusive range. Rejects malformed values up front so an\n * out-of-range field (e.g. `minute=70`) fails fast at schedule-trigger time\n * instead of silently scanning ~525k candidate minutes and never matching\n * (S5 audit #1402).\n */\nfunction validateCronField(\n expr: string,\n range: { name: string; min: number; max: number },\n): void {\n if (expr === '*') return;\n\n const reject = (detail: string): never => {\n throw new Error(\n `Invalid cron expression: ${range.name} field \"${expr}\" ${detail} ` +\n `(valid range ${range.min}-${range.max})`,\n );\n };\n\n const assertInRange = (value: number): void => {\n if (!Number.isInteger(value) || value < range.min || value > range.max) {\n reject('is out of range');\n }\n };\n\n for (const term of expr.split(',')) {\n if (term === '') reject('contains an empty value');\n\n let body = term;\n if (body.includes('/')) {\n // Exactly one '/' is valid (`base/step`). `1/2/3` must be rejected, not\n // silently parsed as `1/2` by dropping the trailing segment.\n const stepParts = body.split('/');\n if (stepParts.length !== 2) {\n reject('has malformed step syntax');\n }\n const [rangePart, stepStr] = stepParts;\n const step = Number(stepStr);\n if (!Number.isInteger(step) || step <= 0) {\n reject('has an invalid step');\n }\n body = rangePart;\n if (body === '*') continue;\n }\n\n if (body.includes('-')) {\n // Exactly one '-' is valid (`start-end`). `1-2-3` must be rejected, not\n // silently parsed as `1-2` by dropping the trailing segment.\n const rangeParts = body.split('-');\n if (rangeParts.length !== 2) {\n reject('has malformed range syntax');\n }\n const [startStr, endStr] = rangeParts;\n if (startStr === '' || endStr === '') {\n reject('has an empty range part');\n }\n const start = Number(startStr);\n const end = Number(endStr);\n assertInRange(start);\n assertInRange(end);\n if (start > end) reject('has an inverted range');\n } else {\n assertInRange(Number(body));\n }\n }\n}\n\n/**\n * Validate a standard 5-field cron expression: field count plus per-field\n * value ranges. Throws a descriptive `Error` on the first invalid field.\n *\n * Exposed so callers (and the agents package, which owns schedule creation)\n * can reject a bad cron at write time rather than letting an out-of-range\n * field silently never match (S5 audit #1402).\n *\n * @param cron - The cron expression to validate.\n * @returns The trimmed, whitespace-split fields when valid.\n */\nexport function validateCronExpression(cron: string): string[] {\n const parts = cron.trim().split(/\\s+/);\n if (parts.length !== 5) {\n throw new Error(\n `Invalid cron expression: expected 5 fields, got ${parts.length}`,\n );\n }\n\n parts.forEach((field, index) => {\n validateCronField(field, CRON_FIELD_RANGES[index]);\n });\n\n return parts;\n}\n\n/**\n * Parse a cron expression and get the next run date.\n * Supports standard 5-field cron: minute hour day-of-month month day-of-week\n *\n * Limitations:\n * - Numeric values only (no abbreviated names like JAN, MON)\n * - Day-of-week accepts 0-7 where both 0 and 7 represent Sunday\n *\n * Out-of-range fields are rejected eagerly (see {@link validateCronExpression}).\n *\n * Exported for unit testing of the matching logic (not re-exported from the\n * package index — the public surface is unchanged).\n */\nexport function getNextCronDate(cron: string): Date {\n const [minuteExpr, hourExpr, dayExpr, monthExpr, dowExpr] =\n validateCronExpression(cron);\n\n const now = new Date();\n const candidate = new Date(now);\n candidate.setSeconds(0);\n candidate.setMilliseconds(0);\n\n // Move to next minute at minimum\n candidate.setMinutes(candidate.getMinutes() + 1);\n\n // Standard cron DOM/DOW semantics:\n // When both day-of-month and day-of-week are restricted (not *),\n // a date matches if EITHER condition is met (OR logic).\n const dayIsWildcard = dayExpr === '*';\n const dowIsWildcard = dowExpr === '*';\n\n // Search for next matching date (limit to 1 year)\n const maxIterations = 525600;\n for (let i = 0; i < maxIterations; i++) {\n const dayMatches = matchesCronField(candidate.getDate(), dayExpr);\n // getDay() returns 0 for Sunday; standard cron accepts both 0 and 7\n const dow = candidate.getDay();\n const dowMatches =\n matchesCronField(dow, dowExpr) ||\n (dow === 0 && matchesCronField(7, dowExpr));\n\n let dayOfMonthOrWeekMatches: boolean;\n if (!dayIsWildcard && !dowIsWildcard) {\n dayOfMonthOrWeekMatches = dayMatches || dowMatches;\n } else if (!dayIsWildcard) {\n dayOfMonthOrWeekMatches = dayMatches;\n } else if (!dowIsWildcard) {\n dayOfMonthOrWeekMatches = dowMatches;\n } else {\n dayOfMonthOrWeekMatches = true;\n }\n\n if (\n matchesCronField(candidate.getMonth() + 1, monthExpr) &&\n dayOfMonthOrWeekMatches &&\n matchesCronField(candidate.getHours(), hourExpr) &&\n matchesCronField(candidate.getMinutes(), minuteExpr)\n ) {\n return candidate;\n }\n\n candidate.setMinutes(candidate.getMinutes() + 1);\n }\n\n throw new Error(`Could not find next run date for cron: ${cron}`);\n}\n\n/**\n * Check if a value matches a cron field expression\n */\nfunction matchesCronField(value: number, expr: string): boolean {\n if (expr === '*') return true;\n\n // Step values (*/5, 0-30/2)\n if (expr.includes('/')) {\n const [range, stepStr] = expr.split('/');\n const step = parseInt(stepStr, 10);\n if (range === '*') return value % step === 0;\n if (range.includes('-')) {\n const [startStr, endStr] = range.split('-');\n const start = parseInt(startStr, 10);\n const end = parseInt(endStr, 10);\n if (value < start || value > end) return false;\n return (value - start) % step === 0;\n }\n }\n\n // Ranges (1-5)\n if (expr.includes('-')) {\n const [startStr, endStr] = expr.split('-');\n const start = parseInt(startStr, 10);\n const end = parseInt(endStr, 10);\n return value >= start && value <= end;\n }\n\n // Lists (1,3,5)\n if (expr.includes(',')) {\n const values = expr.split(',').map((v) => parseInt(v.trim(), 10));\n return values.includes(value);\n }\n\n // Exact match\n return value === parseInt(expr, 10);\n}\n\n/**\n * Create a ScheduleRunner instance\n */\nexport function createScheduleRunner(\n config?: ScheduleRunnerConfig,\n): ScheduleRunner {\n return new ScheduleRunner(config);\n}\n\nexport default ScheduleRunner;\n"],"names":["i"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/job-handle.ts","../src/job-builder.ts","../src/object-extension.ts","../src/schedule-runner.ts"],"sourcesContent":["import type { JobStatus, SmrtJob, SmrtJobCollection } from './smrt-job.js';\n\n/**\n * Options for waiting on a job\n */\nexport interface WaitOptions {\n /** Maximum time to wait in milliseconds */\n timeout?: number;\n /** Polling interval in milliseconds */\n pollInterval?: number;\n}\n\n/**\n * Result from a completed job\n */\nexport interface JobResult<T = unknown> {\n /** Whether the job completed successfully */\n success: boolean;\n /** The result data (if successful) */\n result?: T;\n /** Error message (if failed) */\n error?: string;\n /** Pointer to where the full result is stored */\n resultPointer?: string | null;\n}\n\n/**\n * Handle for tracking and managing a background job\n *\n * This provides a convenient interface for:\n * - Checking job status\n * - Waiting for completion\n * - Canceling the job\n * - Retrying failed jobs\n */\nexport class JobHandle<T = unknown> {\n private readonly collection: SmrtJobCollection;\n\n constructor(\n public readonly id: string,\n collection: SmrtJobCollection,\n ) {\n this.collection = collection;\n }\n\n /**\n * Get the current job status\n */\n async status(): Promise<JobStatus> {\n const job = await this.getJob();\n return job.status;\n }\n\n /**\n * Get the full job object\n */\n async getJob(): Promise<SmrtJob> {\n const job = await this.collection.get({ id: this.id });\n if (!job) {\n throw new Error(`Job not found: ${this.id}`);\n }\n return job;\n }\n\n /**\n * Wait for the job to complete\n *\n * @param options - Wait configuration\n * @returns The job result\n * @throws Error if the job fails or times out\n */\n async wait(options: WaitOptions = {}): Promise<JobResult<T>> {\n const { timeout = 60000, pollInterval = 100 } = options;\n const startTime = Date.now();\n\n while (true) {\n const job = await this.getJob();\n\n if (job.status === 'completed') {\n return {\n success: true,\n resultPointer: job.resultPointer,\n };\n }\n\n if (job.status === 'failed') {\n return {\n success: false,\n error: job.lastError ?? 'Job failed',\n };\n }\n\n if (job.status === 'cancelled') {\n return {\n success: false,\n error: 'Job was cancelled',\n };\n }\n\n // Check timeout\n if (Date.now() - startTime >= timeout) {\n throw new Error(`Timeout waiting for job ${this.id}`);\n }\n\n // Wait before next poll\n await new Promise((resolve) => setTimeout(resolve, pollInterval));\n }\n }\n\n /**\n * Cancel the job\n */\n async cancel(): Promise<void> {\n const job = await this.getJob();\n await job.cancel();\n }\n\n /**\n * Retry a failed job\n */\n async retry(): Promise<void> {\n const job = await this.getJob();\n await job.retry();\n }\n\n /**\n * Check if the job is still running\n */\n async isRunning(): Promise<boolean> {\n const status = await this.status();\n return status === 'pending' || status === 'running';\n }\n\n /**\n * Check if the job has completed (successfully or not)\n */\n async isDone(): Promise<boolean> {\n const status = await this.status();\n return (\n status === 'completed' || status === 'failed' || status === 'cancelled'\n );\n }\n}\n\nexport default JobHandle;\n","import {\n exponential,\n type RetryStrategy,\n type RetryStrategyConfig,\n} from '@happyvertical/jobs';\nimport { clampRetries, DEFAULT_TENANT_JOB_CAP } from './background-policy.js';\nimport { JobHandle } from './job-handle.js';\nimport type { SmrtJobCollection, TimeoutBehavior } from './smrt-job.js';\n\n/**\n * Priority levels for jobs\n */\nexport type Priority = 'critical' | 'high' | 'normal' | 'low' | number;\n\n/**\n * Convert priority to numeric value\n */\nexport function priorityToNumber(priority: Priority): number {\n if (typeof priority === 'number') return priority;\n switch (priority) {\n case 'critical':\n return 100;\n case 'high':\n return 75;\n case 'normal':\n return 50;\n case 'low':\n return 25;\n default:\n return 50;\n }\n}\n\n/**\n * Parse delay string to milliseconds\n */\nexport function parseDelay(delay: string | number): number {\n if (typeof delay === 'number') {\n // Guard against NaN/Infinity: a non-finite delay flows into\n // `new Date(Date.now() + delay)`, persisting an `Invalid Date` `runAt`\n // that the claim query can never match (S3 in the #1401 review).\n if (!Number.isFinite(delay)) {\n throw new Error(`Invalid delay value: ${delay}`);\n }\n return delay;\n }\n\n const match = delay.match(/^(\\d+)(ms|s|m|h|d)?$/);\n if (!match) {\n throw new Error(`Invalid delay format: ${delay}`);\n }\n\n const value = parseInt(match[1], 10);\n const unit = match[2] || 'ms';\n\n switch (unit) {\n case 'ms':\n return value;\n case 's':\n return value * 1000;\n case 'm':\n return value * 60 * 1000;\n case 'h':\n return value * 60 * 60 * 1000;\n case 'd':\n return value * 24 * 60 * 60 * 1000;\n default:\n return value;\n }\n}\n\n/**\n * Fluent builder for creating background jobs\n *\n * Example:\n * ```typescript\n * const handle = await doc.background('generateSummary', { format: 'md' })\n * .delay('5m')\n * .retries(5)\n * .priority('high')\n * .queue('summaries')\n * .timeout(300000)\n * .enqueue();\n * ```\n */\nexport class JobBuilder<T = unknown> {\n private _queue: string = 'default';\n private _delay: number = 0;\n private _retries: number = 3;\n private _priority: number = 50;\n private _timeout: number = 300000;\n private _timeoutBehavior: TimeoutBehavior = 'fail';\n private _retryStrategy: RetryStrategy = exponential();\n private _tenantJobCap: number = DEFAULT_TENANT_JOB_CAP;\n\n constructor(\n private readonly objectType: string,\n private readonly objectId: string | null,\n private readonly method: string,\n private readonly args: Record<string, unknown>,\n private readonly collection: SmrtJobCollection,\n ) {}\n\n /**\n * Set the queue name\n */\n queue(name: string): this {\n this._queue = name;\n return this;\n }\n\n /**\n * Set a delay before the job runs\n * @param delay - Delay as milliseconds or string like '5m', '1h', '30s'\n */\n delay(delay: string | number): this {\n this._delay = parseDelay(delay);\n return this;\n }\n\n /**\n * Set when the job should run\n */\n runAt(date: Date): this {\n this._delay = date.getTime() - Date.now();\n return this;\n }\n\n /**\n * Set the maximum number of retry attempts.\n *\n * Clamped to {@link MAX_JOB_RETRIES} so a misconfigured caller cannot pin a\n * worker on a poison job indefinitely (S5 audit #1402).\n */\n retries(count: number): this {\n this._retries = clampRetries(count);\n return this;\n }\n\n /**\n * Set the retry strategy\n */\n retryStrategy(strategy: RetryStrategy): this {\n this._retryStrategy = strategy;\n return this;\n }\n\n /**\n * Set the job priority\n */\n priority(level: Priority): this {\n this._priority = priorityToNumber(level);\n return this;\n }\n\n /**\n * Set the job timeout in milliseconds\n */\n timeout(ms: number): this {\n this._timeout = ms;\n return this;\n }\n\n /**\n * Set what happens when the job times out\n */\n timeoutBehavior(behavior: TimeoutBehavior): this {\n this._timeoutBehavior = behavior;\n return this;\n }\n\n /**\n * Override the per-tenant in-flight job cap for this enqueue.\n *\n * Defaults to {@link DEFAULT_TENANT_JOB_CAP}. Pass `0` (or a negative value)\n * to disable the cap for trusted internal callers (S5 audit #1402).\n */\n tenantJobCap(max: number): this {\n this._tenantJobCap = max;\n return this;\n }\n\n /**\n * Enqueue the job and return a handle\n */\n async enqueue(): Promise<JobHandle<T>> {\n const runAt = new Date(Date.now() + this._delay);\n\n const retryConfig: RetryStrategyConfig =\n 'toConfig' in this._retryStrategy\n ? this._retryStrategy.toConfig()\n : (this._retryStrategy as RetryStrategyConfig);\n\n // Route through the collection's single creation path so the per-tenant\n // in-flight cap and the retry ceiling are enforced in one place, shared with\n // the ScheduleRunner (S5 audit #1402). The cap applies to the ambient tenant\n // (resolved inside enqueueJob); global (no-context) jobs are exempt.\n const job = await this.collection.enqueueJob(\n {\n queue: this._queue,\n objectType: this.objectType,\n objectId: this.objectId,\n method: this.method,\n args: this.args,\n runAt,\n priority: this._priority,\n maxAttempts: this._retries,\n timeout: this._timeout,\n timeoutBehavior: this._timeoutBehavior,\n retryStrategy: retryConfig,\n },\n { tenantJobCap: this._tenantJobCap },\n );\n\n const jobId = job.id;\n if (!jobId) {\n throw new Error('Job was created but has no ID');\n }\n\n return new JobHandle<T>(jobId, this.collection);\n }\n}\n\nexport default JobBuilder;\n","import {\n ObjectRegistry,\n type SmrtObject,\n type SmrtObjectOptions,\n} from '@happyvertical/smrt-core';\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport {\n JobBuilder,\n type Priority,\n parseDelay,\n priorityToNumber,\n} from './job-builder.js';\nimport type { JobHandle } from './job-handle.js';\nimport { SmrtJobCollection } from './smrt-job.js';\n\n/**\n * Options for the simple .bg() method\n */\nexport interface BgOptions {\n /** Queue name */\n queue?: string;\n /** Priority level */\n priority?: 'critical' | 'high' | 'normal' | 'low' | number;\n /** Delay before running (ms or string like '5m') */\n delay?: string | number;\n /** Maximum retries */\n retries?: number;\n /** Timeout in milliseconds */\n timeout?: number;\n}\n\n/**\n * Type for the extended SmrtObject with background job methods\n */\nexport interface BackgroundCapable {\n /**\n * Simple background job submission\n *\n * @param method - Method name to invoke\n * @param args - Arguments to pass to the method\n * @param options - Job options\n * @returns JobHandle for tracking the job\n *\n * @example\n * const handle = await doc.bg('generateSummary', { format: 'md' });\n */\n bg<T = unknown>(\n method: string,\n args?: Record<string, unknown>,\n options?: BgOptions,\n ): Promise<JobHandle<T>>;\n\n /**\n * Fluent job builder for advanced options\n *\n * @param method - Method name to invoke\n * @param args - Arguments to pass to the method\n * @returns JobBuilder for fluent configuration\n *\n * @example\n * const handle = await doc.background('generateSummary', { format: 'md' })\n * .delay('5m')\n * .retries(5)\n * .priority('high')\n * .enqueue();\n */\n background<T = unknown>(\n method: string,\n args?: Record<string, unknown>,\n ): JobBuilder<T>;\n}\n\n// Cache for job collections per database\nconst collectionCache = new WeakMap<DatabaseInterface, SmrtJobCollection>();\n\n/**\n * Get or create a job collection for the given database\n */\nasync function getJobCollection(\n db: DatabaseInterface,\n): Promise<SmrtJobCollection> {\n let collection = collectionCache.get(db);\n if (!collection) {\n collection = await SmrtJobCollection.create({\n db: { type: 'sqlite', url: ':memory:' }, // Placeholder\n });\n // Override internal db reference\n (collection as unknown as { _db: DatabaseInterface })._db = db;\n collectionCache.set(db, collection);\n }\n return collection;\n}\n\nfunction getObjectTypeName(instance: SmrtObject): string {\n const metaType = (instance as { _meta_type?: unknown })._meta_type;\n if (typeof metaType === 'string' && metaType.length > 0) {\n return metaType;\n }\n\n const className = instance.constructor.name;\n return ObjectRegistry.getClass(className)?.qualifiedName || className;\n}\n\n// Type for a SmrtObject constructor. `never[]` (not `any[]`) satisfies\n// noExplicitAny while preserving assignability: constructor parameters are\n// checked contravariantly under strictFunctionTypes, and `never` (the bottom\n// type) is assignable to any concrete parameter type, so a typed-options\n// constructor like `new (options?: SmrtObjectOptions) => Doc` still satisfies\n// the constraint — `unknown[]` would NOT, since `unknown` is not assignable to\n// `SmrtObjectOptions`, breaking the documented `withBackgroundJobs(Document)`\n// API for consumers.\ntype SmrtObjectConstructor = new (...args: never[]) => SmrtObject;\ntype BackgroundCapableConstructor<T extends SmrtObjectConstructor> = T & {\n new (...args: ConstructorParameters<T>): InstanceType<T> & BackgroundCapable;\n};\n\n// Compile-time regression guard (#1658, PR #1658): a typed-options SmrtObject\n// subclass constructor — the documented `withBackgroundJobs(Document)` pattern,\n// where a model's ctor is `(options?: SomeOptions)` — MUST satisfy the\n// constraint. A `unknown[]` rest param (instead of `never[]`) makes this\n// resolve to `false` and fails the build, since `unknown` is not assignable to\n// the typed options parameter. Test files are excluded from `tsc`, so this\n// lives in typechecked source rather than a `*.test.ts`.\ntype AssertTrue<T extends true> = T;\ntype _TypedOptionsCtorStaysAssignable = AssertTrue<\n (new (\n options?: SmrtObjectOptions,\n ) => SmrtObject) extends SmrtObjectConstructor\n ? true\n : false\n>;\n\nfunction requireObjectDb(instance: SmrtObject): DatabaseInterface {\n const db = (instance as unknown as { _db?: DatabaseInterface })._db;\n if (!db) {\n throw new Error('Object not initialized. Call initialize() first.');\n }\n\n return db;\n}\n\nasync function bgImpl<R = unknown>(\n this: SmrtObject,\n method: string,\n args: Record<string, unknown> = {},\n options: BgOptions = {},\n): Promise<JobHandle<R>> {\n const db = requireObjectDb(this);\n const collection = await getJobCollection(db);\n const builder = new JobBuilder<R>(\n getObjectTypeName(this),\n this.id ?? null,\n method,\n args,\n collection,\n );\n\n if (options.queue) builder.queue(options.queue);\n if (options.priority) builder.priority(options.priority);\n if (options.delay) builder.delay(options.delay);\n if (options.retries !== undefined) builder.retries(options.retries);\n if (options.timeout) builder.timeout(options.timeout);\n\n return builder.enqueue();\n}\n\nfunction backgroundImpl<R = unknown>(\n this: SmrtObject,\n method: string,\n args: Record<string, unknown> = {},\n): JobBuilder<R> {\n const db = requireObjectDb(this);\n const objectType = getObjectTypeName(this);\n const objectId = this.id ?? null;\n\n // Create a proxy builder that defers collection access until enqueue().\n const lazyBuilder = {\n _queue: 'default',\n _delay: 0,\n _retries: 3,\n _priority: 50,\n _timeout: 300000,\n _timeoutBehavior: 'fail' as 'fail' | 'kill' | 'warn',\n _retryStrategy: null as unknown,\n // `undefined` => fall through to JobBuilder's DEFAULT_TENANT_JOB_CAP. A\n // caller can override (incl. `0` to disable) via tenantJobCap() below.\n _tenantJobCap: undefined as number | undefined,\n\n queue(name: string) {\n this._queue = name;\n return this;\n },\n delay(d: string | number) {\n this._delay = parseDelay(d);\n return this;\n },\n runAt(date: Date) {\n this._delay = date.getTime() - Date.now();\n return this;\n },\n retries(count: number) {\n this._retries = count;\n return this;\n },\n retryStrategy(strategy: unknown) {\n this._retryStrategy = strategy;\n return this;\n },\n priority(level: Priority) {\n this._priority = priorityToNumber(level);\n return this;\n },\n timeout(ms: number) {\n this._timeout = ms;\n return this;\n },\n timeoutBehavior(behavior: 'fail' | 'kill' | 'warn') {\n this._timeoutBehavior = behavior;\n return this;\n },\n tenantJobCap(max: number) {\n this._tenantJobCap = max;\n return this;\n },\n async enqueue(): Promise<JobHandle<R>> {\n const collection = await getJobCollection(db);\n const builder = new JobBuilder<R>(\n objectType,\n objectId,\n method,\n args,\n collection,\n );\n\n builder.queue(this._queue);\n builder.delay(this._delay);\n builder.retries(this._retries);\n builder.priority(this._priority);\n builder.timeout(this._timeout);\n builder.timeoutBehavior(this._timeoutBehavior);\n // Only forward an explicit override; leaving it unset preserves the\n // JobBuilder default (DEFAULT_TENANT_JOB_CAP).\n if (this._tenantJobCap !== undefined) {\n builder.tenantJobCap(this._tenantJobCap);\n }\n\n if (this._retryStrategy) {\n builder.retryStrategy(\n this._retryStrategy as Parameters<typeof builder.retryStrategy>[0],\n );\n }\n\n return builder.enqueue();\n },\n };\n\n return lazyBuilder as unknown as JobBuilder<R>;\n}\n\n/**\n * Extend a SmrtObject class with background job methods\n *\n * @param BaseClass - The SmrtObject class to extend\n * @returns Extended class with .bg() and .background() methods\n *\n * @example\n * const BackgroundDocument = withBackgroundJobs(Document);\n * const doc = new BackgroundDocument({ ... });\n * const handle = await doc.bg('generateSummary', { format: 'md' });\n */\nexport function withBackgroundJobs<T extends SmrtObjectConstructor>(\n BaseClass: T,\n): BackgroundCapableConstructor<T> {\n const prototype = BaseClass.prototype as SmrtObject &\n Partial<BackgroundCapable>;\n\n if (typeof prototype.bg !== 'function') {\n Object.defineProperty(prototype, 'bg', {\n value: bgImpl,\n writable: true,\n configurable: true,\n });\n }\n\n if (typeof prototype.background !== 'function') {\n Object.defineProperty(prototype, 'background', {\n value: backgroundImpl,\n writable: true,\n configurable: true,\n });\n }\n\n return BaseClass as BackgroundCapableConstructor<T>;\n}\n\nexport default withBackgroundJobs;\n","import { EventEmitter } from 'node:events';\nimport { createLogger } from '@happyvertical/logger';\nimport { ObjectRegistry } from '@happyvertical/smrt-core';\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport { createId } from '@happyvertical/utils';\nimport {\n redactErrorForPersistence,\n redactErrorMessage,\n} from './error-redaction.js';\nimport { SmrtJobCollection } from './smrt-job.js';\nimport { SmrtWorkerCollection } from './smrt-worker.js';\nimport { DEFAULT_TASK_HEARTBEAT_INTERVAL_MS } from './stale-recovery.js';\nimport { isWorkerAlive } from './worker-liveness.js';\n\n/**\n * ScheduleRunner configuration\n */\nexport interface ScheduleRunnerConfig {\n /** Runner ID (auto-generated if not provided) */\n id?: string;\n /** Polling interval in milliseconds (default: 60000 - 1 minute) */\n pollInterval?: number;\n /** Maximum schedules to process per poll */\n batchSize?: number;\n /**\n * @deprecated No longer used. Slot reconciliation keys on worker liveness\n * (the `_smrt_workers` lease), not per-job heartbeat staleness (#1474).\n */\n staleJobThresholdMs?: number;\n /**\n * @deprecated No longer used. See {@link staleJobThresholdMs}.\n */\n taskHeartbeatInterval?: number;\n}\n\n/**\n * ScheduleRunner events\n */\nexport interface ScheduleRunnerEvents {\n 'schedule:triggered': (schedule: ScheduleInfo) => void;\n 'schedule:error': (schedule: ScheduleInfo, error: Error) => void;\n 'schedule:completed': (scheduleId: string) => void;\n 'schedule:failed': (scheduleId: string, error: string) => void;\n 'runner:started': () => void;\n 'runner:stopped': () => void;\n 'runner:error': (error: Error) => void;\n}\n\n/**\n * Schedule info for events\n */\nexport interface ScheduleInfo {\n id: string;\n agentType: string;\n agentId: string | null;\n cron: string;\n}\n\n/**\n * Default configuration\n */\nconst DEFAULT_CONFIG: Required<ScheduleRunnerConfig> = {\n id: '',\n pollInterval: 60000, // 1 minute\n batchSize: 50,\n staleJobThresholdMs: 90000,\n taskHeartbeatInterval: DEFAULT_TASK_HEARTBEAT_INTERVAL_MS,\n};\n\n/**\n * ScheduleRunner polls for due agent schedules and creates jobs for them\n *\n * This runner works in conjunction with TaskRunner:\n * 1. ScheduleRunner checks for due schedules based on cron expressions\n * 2. When a schedule is due, it creates a SmrtJob for the agent\n * 3. TaskRunner picks up and executes the job\n * 4. On job completion/failure, call handleJobCompletion() to update the schedule\n *\n * @example\n * ```typescript\n * const scheduleRunner = new ScheduleRunner({ pollInterval: 30000 });\n * await scheduleRunner.initialize(db);\n * await scheduleRunner.start();\n *\n * // Wire up TaskRunner events to update schedule state\n * taskRunner.on('job:completed', (job) => {\n * const scheduleId = job.args?._scheduleId;\n * if (scheduleId) scheduleRunner.handleJobCompletion(scheduleId, true);\n * });\n * taskRunner.on('job:failed', (job, error) => {\n * const scheduleId = job.args?._scheduleId;\n * if (scheduleId) scheduleRunner.handleJobCompletion(scheduleId, false, error.message);\n * });\n *\n * // Graceful shutdown\n * process.on('SIGTERM', () => scheduleRunner.stop());\n * ```\n */\nexport class ScheduleRunner extends EventEmitter {\n readonly id: string;\n private readonly config: Required<ScheduleRunnerConfig>;\n private jobCollection: SmrtJobCollection | null = null;\n private workerCollection: SmrtWorkerCollection | null = null;\n private running = false;\n private pollTimer: NodeJS.Timeout | null = null;\n private db: DatabaseInterface | null = null;\n private logger = createLogger(true);\n\n constructor(config: ScheduleRunnerConfig = {}) {\n super();\n this.config = {\n ...DEFAULT_CONFIG,\n ...config,\n id: config.id || `schedule_${createId().slice(0, 8)}`,\n };\n this.id = this.config.id;\n }\n\n /**\n * Initialize the runner with database connection\n */\n async initialize(db: DatabaseInterface): Promise<void> {\n this.db = db;\n this.jobCollection = await SmrtJobCollection.create({ db });\n this.workerCollection = await SmrtWorkerCollection.create({ db });\n }\n\n /**\n * Start processing schedules\n */\n async start(): Promise<void> {\n if (this.running) return;\n if (!this.db) {\n throw new Error(\n 'ScheduleRunner not initialized. Call initialize() first.',\n );\n }\n\n this.running = true;\n\n // Start polling loop\n this.startPolling();\n\n this.emit('runner:started');\n this.logger.info('ScheduleRunner started', { id: this.id });\n }\n\n /**\n * Stop processing schedules\n */\n async stop(): Promise<void> {\n if (!this.running) return;\n\n this.running = false;\n\n if (this.pollTimer) {\n clearTimeout(this.pollTimer);\n this.pollTimer = null;\n }\n\n this.emit('runner:stopped');\n this.logger.info('ScheduleRunner stopped', { id: this.id });\n }\n\n /**\n * Check if runner is running\n */\n isRunning(): boolean {\n return this.running;\n }\n\n /**\n * Handle job completion for a scheduled job.\n *\n * Call this from TaskRunner's job:completed / job:failed events\n * when the job has a `_scheduleId` in its args.\n */\n async handleJobCompletion(\n scheduleId: string,\n success: boolean,\n errorMessage?: string,\n ): Promise<void> {\n if (!this.db) return;\n\n // `last_error` is persisted to a durable schedule row; strip secret-shaped\n // substrings the same way the job runner does (S5 audit #1402).\n const safeErrorMessage = redactErrorMessage(\n errorMessage ?? 'Unknown error',\n );\n\n try {\n if (success) {\n await this.db.query(\n `UPDATE _smrt_agent_schedules\n SET running_count = CASE WHEN COALESCE(running_count, 0) > 0 THEN running_count - 1 ELSE 0 END,\n last_run = ?,\n last_status = 'success',\n last_error = NULL,\n run_count = COALESCE(run_count, 0) + 1,\n success_count = COALESCE(success_count, 0) + 1\n WHERE id = ?`,\n new Date().toISOString(),\n scheduleId,\n );\n this.emit('schedule:completed', scheduleId);\n } else {\n await this.db.query(\n `UPDATE _smrt_agent_schedules\n SET running_count = CASE WHEN COALESCE(running_count, 0) > 0 THEN running_count - 1 ELSE 0 END,\n last_run = ?,\n last_status = 'failed',\n last_error = ?,\n run_count = COALESCE(run_count, 0) + 1,\n failure_count = COALESCE(failure_count, 0) + 1\n WHERE id = ?`,\n new Date().toISOString(),\n safeErrorMessage,\n scheduleId,\n );\n this.emit('schedule:failed', scheduleId, safeErrorMessage);\n }\n } catch (err) {\n this.logger.error('Failed to update schedule after job completion', {\n scheduleId,\n error: err,\n });\n }\n }\n\n /**\n * Start the polling loop\n */\n private startPolling(): void {\n const poll = async () => {\n if (!this.running) return;\n\n try {\n await this.poll();\n } catch (error) {\n this.emit('runner:error', error as Error);\n this.logger.error('ScheduleRunner poll error', { error });\n }\n\n // Schedule next poll\n if (this.running) {\n this.pollTimer = setTimeout(poll, this.config.pollInterval);\n }\n };\n\n // Start immediately\n poll();\n }\n\n /**\n * Poll for due schedules and create jobs\n */\n private async poll(): Promise<void> {\n if (!this.db || !this.jobCollection) return;\n\n await this.recoverStaleScheduleState();\n\n const now = new Date().toISOString();\n\n // Find due schedules\n const result = await this.db.query(\n `SELECT * FROM _smrt_agent_schedules\n WHERE enabled = true\n AND status = 'active'\n AND next_run <= ?\n AND COALESCE(running_count, 0) < COALESCE(max_concurrent, 1)\n ORDER BY next_run ASC\n LIMIT ?`,\n now,\n this.config.batchSize,\n );\n\n for (const row of result.rows) {\n await this.triggerSchedule(row as ScheduleRow);\n }\n }\n\n /**\n * Reconcile stuck schedule slots against running jobs.\n *\n * This handles two failure modes:\n * - a running job's owning worker is no longer alive (dead/restarted)\n * - a schedule slot remains occupied even though no running job still exists\n *\n * Staleness keys on worker *liveness* (issue #1474), not per-job heartbeat\n * freshness: a job whose `worker_id` is live in this process or holds a fresh\n * lease in `_smrt_workers` is healthy even if its handler is holding the loop\n * synchronously. ScheduleRunner has no in-process active-job set, so this is\n * its entire correctness mechanism.\n */\n private async recoverStaleScheduleState(): Promise<void> {\n if (!this.db || !this.workerCollection) return;\n\n const schedulesResult = await this.db.query(\n `SELECT id, running_count\n FROM _smrt_agent_schedules\n WHERE COALESCE(running_count, 0) > 0`,\n );\n const schedules = schedulesResult.rows as Array<{\n id: string;\n running_count: number;\n }>;\n if (schedules.length === 0) return;\n\n // Without the workers table we cannot reason about liveness; treat every\n // running job as alive (reconcile slot drift only, never fail jobs).\n const workersReady = await this.workerCollection.tableReady();\n const freshLeaseKeys = workersReady\n ? await this.workerCollection.freshLeaseWorkerKeys()\n : new Set<string>();\n\n const jobsResult = await this.db.query(\n `SELECT id, args, worker_id\n FROM _smrt_jobs\n WHERE status = 'running'`,\n );\n const jobRows = jobsResult.rows as Array<{\n id: string;\n args: unknown;\n worker_id: string | null;\n }>;\n\n type ScheduleState = { live: number; staleJobIds: string[] };\n const stateBySchedule = new Map<string, ScheduleState>();\n for (const schedule of schedules) {\n stateBySchedule.set(schedule.id, { live: 0, staleJobIds: [] });\n }\n\n for (const row of jobRows) {\n const scheduleId = this.getScheduleIdFromJobArgs(row.args);\n if (!scheduleId) continue;\n\n const state = stateBySchedule.get(scheduleId);\n if (!state) continue;\n\n const alive = workersReady\n ? isWorkerAlive(row.worker_id, freshLeaseKeys)\n : true;\n\n if (!alive) {\n state.staleJobIds.push(row.id);\n } else {\n state.live += 1;\n }\n }\n\n const now = new Date().toISOString();\n const staleJobIds = schedules.flatMap((schedule) => {\n const state = stateBySchedule.get(schedule.id);\n return state?.staleJobIds ?? [];\n });\n\n // Only the jobs this pass actually transitioned to 'failed' — RETURNING id\n // (not rowCount, which DuckDB/JSON always report as ≥1) so a job another\n // recoverer already failed isn't double-counted into the schedule's\n // run_count/failure_count.\n const recoveredJobIds = new Set<string>();\n if (staleJobIds.length > 0) {\n const placeholders = staleJobIds.map(() => '?').join(', ');\n const result = await this.db.query(\n `UPDATE _smrt_jobs\n SET status = 'failed',\n completed_at = ?,\n last_error = ?,\n worker_id = NULL,\n worker_heartbeat = NULL\n WHERE status = 'running'\n AND id IN (${placeholders})\n RETURNING id`,\n now,\n 'Recovered orphaned scheduled job: its owning worker is no longer ' +\n 'alive (no fresh liveness lease in _smrt_workers and not running in ' +\n 'this process).',\n ...staleJobIds,\n );\n for (const row of result.rows as Array<{ id?: unknown }>) {\n if (typeof row.id === 'string') recoveredJobIds.add(row.id);\n }\n }\n\n for (const schedule of schedules) {\n const state = stateBySchedule.get(schedule.id);\n if (!state) continue;\n\n const desiredRunningCount = state.live;\n const recoveredCount = state.staleJobIds.filter((id) =>\n recoveredJobIds.has(id),\n ).length;\n\n if (\n Number(schedule.running_count) === desiredRunningCount &&\n recoveredCount === 0\n ) {\n continue;\n }\n\n if (recoveredCount > 0) {\n await this.db.query(\n `UPDATE _smrt_agent_schedules\n SET running_count = ?,\n last_run = ?,\n last_status = 'failed',\n last_error = ?,\n run_count = COALESCE(run_count, 0) + ?,\n failure_count = COALESCE(failure_count, 0) + ?\n WHERE id = ?`,\n desiredRunningCount,\n now,\n `Recovered ${recoveredCount} orphaned scheduled job(s) from dead worker(s)`,\n recoveredCount,\n recoveredCount,\n schedule.id,\n );\n this.emit(\n 'schedule:failed',\n schedule.id,\n `Recovered ${recoveredCount} orphaned scheduled job(s)`,\n );\n } else {\n await this.db.query(\n `UPDATE _smrt_agent_schedules\n SET running_count = ?\n WHERE id = ?`,\n desiredRunningCount,\n schedule.id,\n );\n }\n }\n }\n\n private getScheduleIdFromJobArgs(args: unknown): string | null {\n if (!args) return null;\n\n let parsedArgs = args;\n if (typeof parsedArgs === 'string') {\n try {\n parsedArgs = JSON.parse(parsedArgs) as Record<string, unknown>;\n } catch {\n return null;\n }\n }\n\n if (\n !parsedArgs ||\n typeof parsedArgs !== 'object' ||\n Array.isArray(parsedArgs)\n ) {\n return null;\n }\n\n const scheduleId = (parsedArgs as Record<string, unknown>)._scheduleId;\n return typeof scheduleId === 'string' && scheduleId.length > 0\n ? scheduleId\n : null;\n }\n\n /**\n * Trigger a schedule by creating a job\n */\n private async triggerSchedule(schedule: ScheduleRow): Promise<void> {\n if (!this.db || !this.jobCollection) return;\n\n const rawAgentType = schedule.agent_type as string;\n const canonicalAgentType =\n ObjectRegistry.getClass(rawAgentType)?.qualifiedName || rawAgentType;\n\n const scheduleInfo: ScheduleInfo = {\n id: schedule.id as string,\n agentType: canonicalAgentType,\n agentId: schedule.agent_id as string | null,\n cron: schedule.cron as string,\n };\n\n try {\n // Parse method_args and agent_config from JSON strings if needed\n let methodArgs: Record<string, unknown> = {};\n if (schedule.method_args) {\n methodArgs =\n typeof schedule.method_args === 'string'\n ? JSON.parse(schedule.method_args as string)\n : (schedule.method_args as Record<string, unknown>);\n }\n let agentConfig: Record<string, unknown> = {};\n if (schedule.agent_config) {\n agentConfig =\n typeof schedule.agent_config === 'string'\n ? JSON.parse(schedule.agent_config as string)\n : (schedule.agent_config as Record<string, unknown>);\n }\n\n // Compute next run time from cron before creating the job\n const nextRun = getNextCronDate(schedule.cron as string);\n\n // Create a job for this schedule\n // Nest agent_config under _agentConfig so TaskRunner can pass it\n // to the agent constructor separately from method args\n const args: Record<string, unknown> = {\n ...methodArgs,\n _scheduleId: schedule.id,\n };\n if (Object.keys(agentConfig).length > 0) {\n args._agentConfig = agentConfig;\n }\n\n // Enqueue the job BEFORE advancing next_run / incrementing running_count.\n // Previously next_run was advanced and running_count incremented first; if\n // enqueueJob then threw (a transient tenant-cap hit or DB blip), the catch\n // disabled the schedule but never rolled next_run back, permanently losing\n // that run slot AND taking the schedule out of the poll until manual\n // re-activation (#4 in the #1401 review). By enqueuing first, a transient\n // failure leaves next_run untouched, so the same due slot is retried on\n // the next poll and the schedule stays active.\n //\n // Route scheduled jobs through the same centralized creation path as the\n // fluent builder so the per-tenant in-flight cap and retry ceiling apply\n // here too — previously a direct create() bypassed both (S5 audit #1402).\n // The schedule's own tenant is passed explicitly so the cap is enforced\n // for the owning tenant even with no ambient context.\n const job = await this.jobCollection.enqueueJob({\n tenantId:\n typeof schedule.tenant_id === 'string' &&\n schedule.tenant_id.length > 0\n ? (schedule.tenant_id as string)\n : null,\n queue: 'agents',\n objectType: canonicalAgentType,\n objectId: schedule.agent_id as string | null,\n method: (schedule.method as string) || 'run',\n args,\n priority: 75, // High priority for scheduled agents\n maxAttempts: 3,\n timeout: (schedule.timeout as number) || 3600000,\n });\n\n // Only now that the job is durably enqueued do we consume the slot:\n // increment running_count and advance next_run in one update.\n await this.db.query(\n `UPDATE _smrt_agent_schedules\n SET agent_type = ?,\n running_count = running_count + 1,\n next_run = ?\n WHERE id = ?`,\n canonicalAgentType,\n nextRun.toISOString(),\n schedule.id,\n );\n\n this.emit('schedule:triggered', scheduleInfo);\n this.logger.info('Schedule triggered', {\n scheduleId: schedule.id,\n agentType: canonicalAgentType,\n jobId: job.id,\n nextRun: nextRun.toISOString(),\n });\n } catch (error) {\n // The slot was NOT consumed (enqueue runs before the next_run/\n // running_count advance), so there is nothing to roll back: leave\n // next_run and status='active' untouched so the next poll retries the\n // same due slot rather than skipping it or disabling the schedule on a\n // transient failure (#4). Record last_error for operator visibility only.\n await this.db.query(\n `UPDATE _smrt_agent_schedules\n SET last_error = ?\n WHERE id = ?`,\n // Tolerate non-Error throwables: a thrown string/object has no\n // `.message`, which would otherwise persist an empty `last_error`.\n redactErrorForPersistence(error),\n schedule.id,\n );\n\n this.emit('schedule:error', scheduleInfo, error as Error);\n this.logger.error('Schedule trigger failed', {\n scheduleId: schedule.id,\n error,\n });\n }\n }\n}\n\n/**\n * Database row type for schedule\n */\ninterface ScheduleRow {\n id: unknown;\n agent_type: unknown;\n agent_id: unknown;\n tenant_id: unknown;\n agent_config: unknown;\n cron: unknown;\n method: unknown;\n method_args: unknown;\n timeout: unknown;\n}\n\n// --- Cron helpers (self-contained, no external dependency) ---\n\n/**\n * Inclusive valid range for each cron field, by position.\n * minute, hour, day-of-month, month, day-of-week.\n * Day-of-week accepts 0-7 where both 0 and 7 represent Sunday.\n */\nconst CRON_FIELD_RANGES: ReadonlyArray<{\n name: string;\n min: number;\n max: number;\n}> = [\n { name: 'minute', min: 0, max: 59 },\n { name: 'hour', min: 0, max: 23 },\n { name: 'day-of-month', min: 1, max: 31 },\n { name: 'month', min: 1, max: 12 },\n { name: 'day-of-week', min: 0, max: 7 },\n];\n\n/**\n * Validate that every numeric component of a single cron field falls within\n * the field's inclusive range. Rejects malformed values up front so an\n * out-of-range field (e.g. `minute=70`) fails fast at schedule-trigger time\n * instead of silently scanning ~525k candidate minutes and never matching\n * (S5 audit #1402).\n */\nfunction validateCronField(\n expr: string,\n range: { name: string; min: number; max: number },\n): void {\n if (expr === '*') return;\n\n const reject = (detail: string): never => {\n throw new Error(\n `Invalid cron expression: ${range.name} field \"${expr}\" ${detail} ` +\n `(valid range ${range.min}-${range.max})`,\n );\n };\n\n const assertInRange = (value: number): void => {\n if (!Number.isInteger(value) || value < range.min || value > range.max) {\n reject('is out of range');\n }\n };\n\n for (const term of expr.split(',')) {\n if (term === '') reject('contains an empty value');\n\n let body = term;\n if (body.includes('/')) {\n // Exactly one '/' is valid (`base/step`). `1/2/3` must be rejected, not\n // silently parsed as `1/2` by dropping the trailing segment.\n const stepParts = body.split('/');\n if (stepParts.length !== 2) {\n reject('has malformed step syntax');\n }\n const [rangePart, stepStr] = stepParts;\n const step = Number(stepStr);\n if (!Number.isInteger(step) || step <= 0) {\n reject('has an invalid step');\n }\n body = rangePart;\n if (body === '*') continue;\n }\n\n if (body.includes('-')) {\n // Exactly one '-' is valid (`start-end`). `1-2-3` must be rejected, not\n // silently parsed as `1-2` by dropping the trailing segment.\n const rangeParts = body.split('-');\n if (rangeParts.length !== 2) {\n reject('has malformed range syntax');\n }\n const [startStr, endStr] = rangeParts;\n if (startStr === '' || endStr === '') {\n reject('has an empty range part');\n }\n const start = Number(startStr);\n const end = Number(endStr);\n assertInRange(start);\n assertInRange(end);\n if (start > end) reject('has an inverted range');\n } else {\n assertInRange(Number(body));\n }\n }\n}\n\n/**\n * Validate a standard 5-field cron expression: field count plus per-field\n * value ranges. Throws a descriptive `Error` on the first invalid field.\n *\n * Exposed so callers (and the agents package, which owns schedule creation)\n * can reject a bad cron at write time rather than letting an out-of-range\n * field silently never match (S5 audit #1402).\n *\n * @param cron - The cron expression to validate.\n * @returns The trimmed, whitespace-split fields when valid.\n */\nexport function validateCronExpression(cron: string): string[] {\n const parts = cron.trim().split(/\\s+/);\n if (parts.length !== 5) {\n throw new Error(\n `Invalid cron expression: expected 5 fields, got ${parts.length}`,\n );\n }\n\n parts.forEach((field, index) => {\n validateCronField(field, CRON_FIELD_RANGES[index]);\n });\n\n return parts;\n}\n\n/**\n * Parse a cron expression and get the next run date.\n * Supports standard 5-field cron: minute hour day-of-month month day-of-week\n *\n * Limitations:\n * - Numeric values only (no abbreviated names like JAN, MON)\n * - Day-of-week accepts 0-7 where both 0 and 7 represent Sunday\n *\n * Out-of-range fields are rejected eagerly (see {@link validateCronExpression}).\n *\n * Exported for unit testing of the matching logic (not re-exported from the\n * package index — the public surface is unchanged).\n */\nexport function getNextCronDate(cron: string): Date {\n const [minuteExpr, hourExpr, dayExpr, monthExpr, dowExpr] =\n validateCronExpression(cron);\n\n const now = new Date();\n const candidate = new Date(now);\n candidate.setSeconds(0);\n candidate.setMilliseconds(0);\n\n // Move to next minute at minimum\n candidate.setMinutes(candidate.getMinutes() + 1);\n\n // Standard cron DOM/DOW semantics:\n // When both day-of-month and day-of-week are restricted (not *),\n // a date matches if EITHER condition is met (OR logic).\n const dayIsWildcard = dayExpr === '*';\n const dowIsWildcard = dowExpr === '*';\n\n // Search for next matching date (limit to 1 year)\n const maxIterations = 525600;\n for (let i = 0; i < maxIterations; i++) {\n const dayMatches = matchesCronField(candidate.getDate(), dayExpr);\n // getDay() returns 0 for Sunday; standard cron accepts both 0 and 7\n const dow = candidate.getDay();\n const dowMatches =\n matchesCronField(dow, dowExpr) ||\n (dow === 0 && matchesCronField(7, dowExpr));\n\n let dayOfMonthOrWeekMatches: boolean;\n if (!dayIsWildcard && !dowIsWildcard) {\n dayOfMonthOrWeekMatches = dayMatches || dowMatches;\n } else if (!dayIsWildcard) {\n dayOfMonthOrWeekMatches = dayMatches;\n } else if (!dowIsWildcard) {\n dayOfMonthOrWeekMatches = dowMatches;\n } else {\n dayOfMonthOrWeekMatches = true;\n }\n\n if (\n matchesCronField(candidate.getMonth() + 1, monthExpr) &&\n dayOfMonthOrWeekMatches &&\n matchesCronField(candidate.getHours(), hourExpr) &&\n matchesCronField(candidate.getMinutes(), minuteExpr)\n ) {\n return candidate;\n }\n\n candidate.setMinutes(candidate.getMinutes() + 1);\n }\n\n throw new Error(`Could not find next run date for cron: ${cron}`);\n}\n\n/**\n * Check if a value matches a cron field expression\n */\nfunction matchesCronField(value: number, expr: string): boolean {\n if (expr === '*') return true;\n\n // Step values (*/5, 0-30/2)\n if (expr.includes('/')) {\n const [range, stepStr] = expr.split('/');\n const step = parseInt(stepStr, 10);\n if (range === '*') return value % step === 0;\n if (range.includes('-')) {\n const [startStr, endStr] = range.split('-');\n const start = parseInt(startStr, 10);\n const end = parseInt(endStr, 10);\n if (value < start || value > end) return false;\n return (value - start) % step === 0;\n }\n }\n\n // Ranges (1-5)\n if (expr.includes('-')) {\n const [startStr, endStr] = expr.split('-');\n const start = parseInt(startStr, 10);\n const end = parseInt(endStr, 10);\n return value >= start && value <= end;\n }\n\n // Lists (1,3,5)\n if (expr.includes(',')) {\n const values = expr.split(',').map((v) => parseInt(v.trim(), 10));\n return values.includes(value);\n }\n\n // Exact match\n return value === parseInt(expr, 10);\n}\n\n/**\n * Create a ScheduleRunner instance\n */\nexport function createScheduleRunner(\n config?: ScheduleRunnerConfig,\n): ScheduleRunner {\n return new ScheduleRunner(config);\n}\n\nexport default ScheduleRunner;\n"],"names":["i"],"mappings":"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"}
|
package/dist/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": "1.0.0",
|
|
3
|
-
"timestamp":
|
|
3
|
+
"timestamp": 1782455243435,
|
|
4
4
|
"packageName": "@happyvertical/smrt-jobs",
|
|
5
|
-
"packageVersion": "0.36.
|
|
5
|
+
"packageVersion": "0.36.3",
|
|
6
6
|
"objects": {
|
|
7
7
|
"@happyvertical/smrt-jobs:SmrtJobEvent": {
|
|
8
8
|
"name": "smrtjobevent",
|
|
@@ -48,7 +48,7 @@ export interface BackgroundCapable {
|
|
|
48
48
|
*/
|
|
49
49
|
background<T = unknown>(method: string, args?: Record<string, unknown>): JobBuilder<T>;
|
|
50
50
|
}
|
|
51
|
-
type SmrtObjectConstructor = new (...args:
|
|
51
|
+
type SmrtObjectConstructor = new (...args: never[]) => SmrtObject;
|
|
52
52
|
type BackgroundCapableConstructor<T extends SmrtObjectConstructor> = T & {
|
|
53
53
|
new (...args: ConstructorParameters<T>): InstanceType<T> & BackgroundCapable;
|
|
54
54
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"object-extension.d.ts","sourceRoot":"","sources":["../src/object-extension.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"object-extension.d.ts","sourceRoot":"","sources":["../src/object-extension.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,UAAU,EAEhB,MAAM,0BAA0B,CAAC;AAElC,OAAO,EACL,UAAU,EAIX,MAAM,kBAAkB,CAAC;AAC1B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAGjD;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,iBAAiB;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qBAAqB;IACrB,QAAQ,CAAC,EAAE,UAAU,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAC;IAC3D,oDAAoD;IACpD,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,sBAAsB;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,8BAA8B;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;;;;;;;;OAUG;IACH,EAAE,CAAC,CAAC,GAAG,OAAO,EACZ,MAAM,EAAE,MAAM,EACd,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,OAAO,CAAC,EAAE,SAAS,GAClB,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;IAEzB;;;;;;;;;;;;;OAaG;IACH,UAAU,CAAC,CAAC,GAAG,OAAO,EACpB,MAAM,EAAE,MAAM,EACd,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,UAAU,CAAC,CAAC,CAAC,CAAC;CAClB;AAyCD,KAAK,qBAAqB,GAAG,KAAK,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,UAAU,CAAC;AAClE,KAAK,4BAA4B,CAAC,CAAC,SAAS,qBAAqB,IAAI,CAAC,GAAG;IACvE,KAAK,GAAG,IAAI,EAAE,qBAAqB,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC,GAAG,iBAAiB,CAAC;CAC9E,CAAC;AAiJF;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS,qBAAqB,EAChE,SAAS,EAAE,CAAC,GACX,4BAA4B,CAAC,CAAC,CAAC,CAqBjC;AAED,eAAe,kBAAkB,CAAC"}
|
package/dist/smrt-knowledge.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-06-
|
|
3
|
+
"generatedAt": "2026-06-26T06:27:23.819Z",
|
|
4
4
|
"packageName": "@happyvertical/smrt-jobs",
|
|
5
|
-
"packageVersion": "0.36.
|
|
5
|
+
"packageVersion": "0.36.3",
|
|
6
6
|
"sourceManifestPath": "dist/manifest.json",
|
|
7
7
|
"agentDocPath": "AGENTS.md",
|
|
8
8
|
"sourceHashes": {
|
|
9
|
-
"manifest": "
|
|
10
|
-
"packageJson": "
|
|
9
|
+
"manifest": "35865a2569bb965bdb1e9a76f9c2457ba8ddcbddafa497edc3cec0144bbfaf3f",
|
|
10
|
+
"packageJson": "959f0f605751db92177ae4681f6068d2d2831511cbfe6cc634f358eecb6fc13c",
|
|
11
11
|
"agents": "2684535d0fdc2f7d44296046cc8ea4a2cb7c0a14089b6910f29dd2e7194e2015"
|
|
12
12
|
},
|
|
13
13
|
"exports": [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@happyvertical/smrt-jobs",
|
|
3
|
-
"version": "0.36.
|
|
3
|
+
"version": "0.36.3",
|
|
4
4
|
"description": "Background job processing for SMRT objects with persistence and scheduling",
|
|
5
5
|
"author": "HappyVertical",
|
|
6
6
|
"type": "module",
|
|
@@ -54,11 +54,11 @@
|
|
|
54
54
|
"@happyvertical/logger": "^0.74.7",
|
|
55
55
|
"@happyvertical/sql": "^0.74.7",
|
|
56
56
|
"@happyvertical/utils": "^0.74.7",
|
|
57
|
-
"@happyvertical/smrt-config": "0.36.
|
|
58
|
-
"@happyvertical/smrt-
|
|
59
|
-
"@happyvertical/smrt-
|
|
60
|
-
"@happyvertical/smrt-
|
|
61
|
-
"@happyvertical/smrt-
|
|
57
|
+
"@happyvertical/smrt-config": "0.36.3",
|
|
58
|
+
"@happyvertical/smrt-ui": "0.36.3",
|
|
59
|
+
"@happyvertical/smrt-types": "0.36.3",
|
|
60
|
+
"@happyvertical/smrt-core": "0.36.3",
|
|
61
|
+
"@happyvertical/smrt-tenancy": "0.36.3"
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
64
|
"@sveltejs/package": "^2.5.7",
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"typescript": "^5.9.3",
|
|
70
70
|
"vite": "^7.3.1",
|
|
71
71
|
"vitest": "^4.0.17",
|
|
72
|
-
"@happyvertical/smrt-vitest": "0.36.
|
|
72
|
+
"@happyvertical/smrt-vitest": "0.36.3"
|
|
73
73
|
},
|
|
74
74
|
"publishConfig": {
|
|
75
75
|
"registry": "https://registry.npmjs.org",
|