@happyvertical/smrt-jobs 0.30.0 → 0.31.0

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 CHANGED
@@ -1,5 +1,5 @@
1
- import { D as DEFAULT_TENANT_JOB_CAP, c as clampRetries, S as SmrtJobCollection, a as DEFAULT_TASK_HEARTBEAT_INTERVAL_MS, b as SmrtWorkerCollection, r as redactErrorMessage, d as redactErrorForPersistence } from "./chunks/runner-DV8FBO0y.js";
2
- import { J, M, e, f, g, h, T, i, j, k, l, m, n, o } from "./chunks/runner-DV8FBO0y.js";
1
+ import { D as DEFAULT_TENANT_JOB_CAP, c as clampRetries, S as SmrtJobCollection, a as DEFAULT_TASK_HEARTBEAT_INTERVAL_MS, b as SmrtWorkerCollection, r as redactErrorMessage, d as redactErrorForPersistence } from "./chunks/runner-2zRlEef7.js";
2
+ import { J, e, M, f, g, h, i, T, j, k, l, m, n, o, p } from "./chunks/runner-2zRlEef7.js";
3
3
  import { exponential } from "@happyvertical/jobs";
4
4
  import { ObjectRegistry } from "@happyvertical/smrt-core";
5
5
  import { EventEmitter } from "node:events";
@@ -112,7 +112,12 @@ function priorityToNumber(priority) {
112
112
  }
113
113
  }
114
114
  function parseDelay(delay) {
115
- if (typeof delay === "number") return delay;
115
+ if (typeof delay === "number") {
116
+ if (!Number.isFinite(delay)) {
117
+ throw new Error(`Invalid delay value: ${delay}`);
118
+ }
119
+ return delay;
120
+ }
116
121
  const match = delay.match(/^(\d+)(ms|s|m|h|d)?$/);
117
122
  if (!match) {
118
123
  throw new Error(`Invalid delay format: ${delay}`);
@@ -707,16 +712,6 @@ class ScheduleRunner extends EventEmitter {
707
712
  agentConfig = typeof schedule.agent_config === "string" ? JSON.parse(schedule.agent_config) : schedule.agent_config;
708
713
  }
709
714
  const nextRun = getNextCronDate(schedule.cron);
710
- await this.db.query(
711
- `UPDATE _smrt_agent_schedules
712
- SET agent_type = ?,
713
- running_count = running_count + 1,
714
- next_run = ?
715
- WHERE id = ?`,
716
- canonicalAgentType,
717
- nextRun.toISOString(),
718
- schedule.id
719
- );
720
715
  const args = {
721
716
  ...methodArgs,
722
717
  _scheduleId: schedule.id
@@ -736,6 +731,16 @@ class ScheduleRunner extends EventEmitter {
736
731
  maxAttempts: 3,
737
732
  timeout: schedule.timeout || 36e5
738
733
  });
734
+ await this.db.query(
735
+ `UPDATE _smrt_agent_schedules
736
+ SET agent_type = ?,
737
+ running_count = running_count + 1,
738
+ next_run = ?
739
+ WHERE id = ?`,
740
+ canonicalAgentType,
741
+ nextRun.toISOString(),
742
+ schedule.id
743
+ );
739
744
  this.emit("schedule:triggered", scheduleInfo);
740
745
  this.logger.info("Schedule triggered", {
741
746
  scheduleId: schedule.id,
@@ -746,9 +751,7 @@ class ScheduleRunner extends EventEmitter {
746
751
  } catch (error) {
747
752
  await this.db.query(
748
753
  `UPDATE _smrt_agent_schedules
749
- SET running_count = running_count - 1,
750
- status = 'error',
751
- last_error = ?
754
+ SET last_error = ?
752
755
  WHERE id = ?`,
753
756
  // Tolerate non-Error throwables: a thrown string/object has no
754
757
  // `.message`, which would otherwise persist an empty `last_error`.
@@ -894,26 +897,27 @@ export {
894
897
  JobBuilder,
895
898
  J as JobContextLogger,
896
899
  JobHandle,
900
+ e as JobTimeoutError,
897
901
  M as MAX_JOB_RETRIES,
898
902
  ScheduleRunner,
899
- e as SmrtJob,
903
+ f as SmrtJob,
900
904
  SmrtJobCollection,
901
- f as SmrtJobEvent,
902
- g as SmrtJobEventCollection,
903
- h as SmrtWorker,
905
+ g as SmrtJobEvent,
906
+ h as SmrtJobEventCollection,
907
+ i as SmrtWorker,
904
908
  SmrtWorkerCollection,
905
909
  T as TaskRunner,
906
- i as TenantJobCapExceededError,
907
- j as assertWithinTenantCreationCap,
908
- k as backgroundEligible,
910
+ j as TenantJobCapExceededError,
911
+ k as assertWithinTenantCreationCap,
912
+ l as backgroundEligible,
909
913
  clampRetries,
910
914
  createScheduleRunner,
911
- l as createTaskRunner,
915
+ m as createTaskRunner,
912
916
  c as createWorkerKey,
913
- m as getBackgroundEligibleMethods,
914
- n as isBackgroundEligibleMethod,
917
+ n as getBackgroundEligibleMethods,
918
+ o as isBackgroundEligibleMethod,
915
919
  isWorkerAlive,
916
- o as markBackgroundEligible,
920
+ p as markBackgroundEligible,
917
921
  parseDelay,
918
922
  priorityToNumber,
919
923
  redactErrorForPersistence,
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') return delay;\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 // 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 // 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 // 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 this direct create() bypassed both (S5 audit\n // #1402). The schedule's own tenant is passed explicitly so the cap is\n // enforced 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 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 // Decrement running count on failure\n await this.db.query(\n `UPDATE _smrt_agent_schedules\n SET running_count = running_count - 1,\n status = 'error',\n 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 */\nfunction 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 { 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 */\nfunction 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 +1 @@
1
- {"version":3,"file":"job-builder.d.ts","sourceRoot":"","sources":["../src/job-builder.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,aAAa,EAEnB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,KAAK,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAExE;;GAEG;AACH,MAAM,MAAM,QAAQ,GAAG,UAAU,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAC;AAEvE;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,CAc3D;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAyBzD;AAED;;;;;;;;;;;;;GAaG;AACH,qBAAa,UAAU,CAAC,CAAC,GAAG,OAAO;IAW/B,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAd7B,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,MAAM,CAAa;IAC3B,OAAO,CAAC,QAAQ,CAAa;IAC7B,OAAO,CAAC,SAAS,CAAc;IAC/B,OAAO,CAAC,QAAQ,CAAkB;IAClC,OAAO,CAAC,gBAAgB,CAA2B;IACnD,OAAO,CAAC,cAAc,CAAgC;IACtD,OAAO,CAAC,aAAa,CAAkC;gBAGpC,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GAAG,IAAI,EACvB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,UAAU,EAAE,iBAAiB;IAGhD;;OAEG;IACH,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAKzB;;;OAGG;IACH,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAKnC;;OAEG;IACH,KAAK,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI;IAKvB;;;;;OAKG;IACH,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAK5B;;OAEG;IACH,aAAa,CAAC,QAAQ,EAAE,aAAa,GAAG,IAAI;IAK5C;;OAEG;IACH,QAAQ,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI;IAK/B;;OAEG;IACH,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAKzB;;OAEG;IACH,eAAe,CAAC,QAAQ,EAAE,eAAe,GAAG,IAAI;IAKhD;;;;;OAKG;IACH,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAK/B;;OAEG;IACG,OAAO,IAAI,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;CAoCvC;AAED,eAAe,UAAU,CAAC"}
1
+ {"version":3,"file":"job-builder.d.ts","sourceRoot":"","sources":["../src/job-builder.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,aAAa,EAEnB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,KAAK,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAExE;;GAEG;AACH,MAAM,MAAM,QAAQ,GAAG,UAAU,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAC;AAEvE;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,CAc3D;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAiCzD;AAED;;;;;;;;;;;;;GAaG;AACH,qBAAa,UAAU,CAAC,CAAC,GAAG,OAAO;IAW/B,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAd7B,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,MAAM,CAAa;IAC3B,OAAO,CAAC,QAAQ,CAAa;IAC7B,OAAO,CAAC,SAAS,CAAc;IAC/B,OAAO,CAAC,QAAQ,CAAkB;IAClC,OAAO,CAAC,gBAAgB,CAA2B;IACnD,OAAO,CAAC,cAAc,CAAgC;IACtD,OAAO,CAAC,aAAa,CAAkC;gBAGpC,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GAAG,IAAI,EACvB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,UAAU,EAAE,iBAAiB;IAGhD;;OAEG;IACH,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAKzB;;;OAGG;IACH,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAKnC;;OAEG;IACH,KAAK,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI;IAKvB;;;;;OAKG;IACH,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAK5B;;OAEG;IACH,aAAa,CAAC,QAAQ,EAAE,aAAa,GAAG,IAAI;IAK5C;;OAEG;IACH,QAAQ,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI;IAK/B;;OAEG;IACH,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAKzB;;OAEG;IACH,eAAe,CAAC,QAAQ,EAAE,eAAe,GAAG,IAAI;IAKhD;;;;;OAKG;IACH,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAK/B;;OAEG;IACG,OAAO,IAAI,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;CAoCvC;AAED,eAAe,UAAU,CAAC"}
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "version": "1.0.0",
3
- "timestamp": 1782177079355,
3
+ "timestamp": 1782183777306,
4
4
  "packageName": "@happyvertical/smrt-jobs",
5
- "packageVersion": "0.30.0",
5
+ "packageVersion": "0.31.0",
6
6
  "objects": {
7
7
  "@happyvertical/smrt-jobs:SmrtJobEvent": {
8
8
  "name": "smrtjobevent",
package/dist/runner.d.ts CHANGED
@@ -42,6 +42,20 @@ export interface TaskRunnerEvents {
42
42
  'runner:stopped': () => void;
43
43
  'runner:error': (error: Error) => void;
44
44
  }
45
+ /**
46
+ * Raised when a job exceeds its timeout under `timeoutBehavior` `'fail'`/`'kill'`.
47
+ *
48
+ * Distinguished from an ordinary handler error so the failure path can choose
49
+ * NOT to auto-retry: the original handler keeps running after a timeout (JS
50
+ * can't preempt it), so re-queueing the row as `pending` while the original
51
+ * still executes guarantees concurrent duplicate execution (see #2 in the
52
+ * #1401 review). A timed-out job is therefore failed terminally rather than
53
+ * retried, shrinking — though, given the at-least-once contract, not fully
54
+ * eliminating — the overlap window.
55
+ */
56
+ export declare class JobTimeoutError extends Error {
57
+ constructor(message: string);
58
+ }
45
59
  /**
46
60
  * TaskRunner processes SMRT jobs by invoking methods on SmrtObjects
47
61
  *
@@ -108,9 +122,37 @@ export declare class TaskRunner extends EventEmitter {
108
122
  */
109
123
  private poll;
110
124
  /**
111
- * Process a single job
125
+ * Process a single job.
126
+ *
127
+ * AT-LEAST-ONCE EXECUTION CONTRACT: a `timeout` only races the handler's
128
+ * promise — JavaScript cannot preempt an already-running handler, so on a
129
+ * `'fail'` (or `'kill'`) timeout the original handler keeps executing in the
130
+ * background while the job row is failed. Timeouts are NOT auto-retried (see
131
+ * handleJobError) precisely so a still-running handler is not duplicated by a
132
+ * retry; but the orphaned handler's own side effects still happen, and an
133
+ * ordinary (non-timeout) failure IS retried and re-claimable by any worker.
134
+ * Handlers invoked from a job MUST be idempotent (e.g. keyed by
135
+ * `context.job.jobId` or a caller-supplied idempotency key); do not rely on a
136
+ * job body running exactly once. See AGENTS.md "Timeouts & at-least-once".
112
137
  */
113
138
  private processJob;
139
+ /**
140
+ * Execute a job honoring its {@link SmrtJob.timeoutBehavior}.
141
+ *
142
+ * - `'fail'` (default) and `'kill'`: race the handler against a timeout. On
143
+ * timeout the race rejects and the caller fails/retries the job.
144
+ * `'kill'` cannot actually preempt the handler in-process (JavaScript has
145
+ * no thread interruption), so it is treated identically to `'fail'` — the
146
+ * handler keeps running in the background; only the job row is failed. This
147
+ * is documented in AGENTS.md so `'kill'` is honest about what it does
148
+ * rather than silently behaving like a no-op.
149
+ * - `'warn'`: do NOT fail on timeout. Arm a one-shot warning (logged + emitted
150
+ * as a job event) at the deadline, but await the handler to completion so a
151
+ * slow-but-successful handler still completes. This makes `'warn'` honest:
152
+ * previously every timeout was treated as `'fail'` regardless of the
153
+ * persisted/UI-shown behavior.
154
+ */
155
+ private runWithTimeout;
114
156
  /**
115
157
  * Apply a terminal/retry state transition to a job only if this worker still
116
158
  * owns it and it is still `running`. Returns whether the write applied.
@@ -1 +1 @@
1
- {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"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"}
package/dist/runner.js CHANGED
@@ -1,4 +1,4 @@
1
- import { T, l, T as T2 } from "./chunks/runner-DV8FBO0y.js";
1
+ import { e, T, m, T as T2 } from "./chunks/runner-2zRlEef7.js";
2
2
  import "node:events";
3
3
  import "node:worker_threads";
4
4
  import "@happyvertical/jobs";
@@ -8,8 +8,9 @@ import "@happyvertical/smrt-tenancy";
8
8
  import "@happyvertical/utils";
9
9
  import "./chunks/worker-liveness-DOTjoIjr.js";
10
10
  export {
11
+ e as JobTimeoutError,
11
12
  T as TaskRunner,
12
- l as createTaskRunner,
13
+ m as createTaskRunner,
13
14
  T2 as default
14
15
  };
15
16
  //# sourceMappingURL=runner.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"schedule-runner.d.ts","sourceRoot":"","sources":["../src/schedule-runner.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAG3C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAW5D;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,iDAAiD;IACjD,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,mEAAmE;IACnE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,4CAA4C;IAC5C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;OAEG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,oBAAoB,EAAE,CAAC,QAAQ,EAAE,YAAY,KAAK,IAAI,CAAC;IACvD,gBAAgB,EAAE,CAAC,QAAQ,EAAE,YAAY,EAAE,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACjE,oBAAoB,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IACnD,iBAAiB,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/D,gBAAgB,EAAE,MAAM,IAAI,CAAC;IAC7B,gBAAgB,EAAE,MAAM,IAAI,CAAC;IAC7B,cAAc,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CACxC;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;CACd;AAaD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,qBAAa,cAAe,SAAQ,YAAY;IAC9C,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiC;IACxD,OAAO,CAAC,aAAa,CAAkC;IACvD,OAAO,CAAC,gBAAgB,CAAqC;IAC7D,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,SAAS,CAA+B;IAChD,OAAO,CAAC,EAAE,CAAkC;IAC5C,OAAO,CAAC,MAAM,CAAsB;gBAExB,MAAM,GAAE,oBAAyB;IAU7C;;OAEG;IACG,UAAU,CAAC,EAAE,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAMtD;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAiB5B;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAc3B;;OAEG;IACH,SAAS,IAAI,OAAO;IAIpB;;;;;OAKG;IACG,mBAAmB,CACvB,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,OAAO,EAChB,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,IAAI,CAAC;IAgDhB;;OAEG;IACH,OAAO,CAAC,YAAY;IAqBpB;;OAEG;YACW,IAAI;IAyBlB;;;;;;;;;;;;OAYG;YACW,yBAAyB;IA4IvC,OAAO,CAAC,wBAAwB;IA0BhC;;OAEG;YACW,eAAe;CA0G9B;AAwGD;;;;;;;;;;GAUG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAa7D;AAwGD;;GAEG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,CAAC,EAAE,oBAAoB,GAC5B,cAAc,CAEhB;AAED,eAAe,cAAc,CAAC"}
1
+ {"version":3,"file":"schedule-runner.d.ts","sourceRoot":"","sources":["../src/schedule-runner.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAG3C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAW5D;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,iDAAiD;IACjD,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,mEAAmE;IACnE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,4CAA4C;IAC5C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;OAEG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,oBAAoB,EAAE,CAAC,QAAQ,EAAE,YAAY,KAAK,IAAI,CAAC;IACvD,gBAAgB,EAAE,CAAC,QAAQ,EAAE,YAAY,EAAE,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACjE,oBAAoB,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IACnD,iBAAiB,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/D,gBAAgB,EAAE,MAAM,IAAI,CAAC;IAC7B,gBAAgB,EAAE,MAAM,IAAI,CAAC;IAC7B,cAAc,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CACxC;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;CACd;AAaD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,qBAAa,cAAe,SAAQ,YAAY;IAC9C,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiC;IACxD,OAAO,CAAC,aAAa,CAAkC;IACvD,OAAO,CAAC,gBAAgB,CAAqC;IAC7D,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,SAAS,CAA+B;IAChD,OAAO,CAAC,EAAE,CAAkC;IAC5C,OAAO,CAAC,MAAM,CAAsB;gBAExB,MAAM,GAAE,oBAAyB;IAU7C;;OAEG;IACG,UAAU,CAAC,EAAE,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAMtD;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAiB5B;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAc3B;;OAEG;IACH,SAAS,IAAI,OAAO;IAIpB;;;;;OAKG;IACG,mBAAmB,CACvB,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,OAAO,EAChB,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,IAAI,CAAC;IAgDhB;;OAEG;IACH,OAAO,CAAC,YAAY;IAqBpB;;OAEG;YACW,IAAI;IAyBlB;;;;;;;;;;;;OAYG;YACW,yBAAyB;IA4IvC,OAAO,CAAC,wBAAwB;IA0BhC;;OAEG;YACW,eAAe;CAsH9B;AAwGD;;;;;;;;;;GAUG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAa7D;AAwGD;;GAEG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,CAAC,EAAE,oBAAoB,GAC5B,cAAc,CAEhB;AAED,eAAe,cAAc,CAAC"}
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generatedAt": "2026-06-23T01:11:19.800Z",
3
+ "generatedAt": "2026-06-23T03:02:57.811Z",
4
4
  "packageName": "@happyvertical/smrt-jobs",
5
- "packageVersion": "0.30.0",
5
+ "packageVersion": "0.31.0",
6
6
  "sourceManifestPath": "dist/manifest.json",
7
7
  "agentDocPath": "AGENTS.md",
8
8
  "sourceHashes": {
9
- "manifest": "8bed9aec574feed7d15e21164393b59c9f976f2533e62f588445f314e8326ed4",
10
- "packageJson": "3b3e6a6efac6ef8f88e0a8c91b899c37d21a90cc922fe6d4d05d6d40f23f073c",
11
- "agents": "407e0ecd3f0ce3ae02d7dc84e86e457ddb6e75677d682b017768b4155b5a68ac"
9
+ "manifest": "3599faf95287c7ca2824986c735e4de0dc8056c924a3851c4c0be2b9a8b7797b",
10
+ "packageJson": "54bc82d929d8f94f9b9158e432db9a02c70771103db585553eb7f846ea27422d",
11
+ "agents": "2684535d0fdc2f7d44296046cc8ea4a2cb7c0a14089b6910f29dd2e7194e2015"
12
12
  },
13
13
  "exports": [
14
14
  ".",
@@ -504,5 +504,5 @@
504
504
  "polymorphicAssociations": 0,
505
505
  "uuidColumns": 9
506
506
  },
507
- "agentDoc": "# @happyvertical/smrt-jobs\n\nBackground job execution with persistent queue, scheduling, and fluent builder API.\n\n## Architecture\n\n```\nSmrtObject.bg('method') → SmrtJob (in _smrt_jobs) → TaskRunner picks up → executes via ObjectRegistry\nAgentSchedule (cron) → ScheduleRunner creates SmrtJob → TaskRunner executes → ScheduleRunner updates\nTaskRunner.start() → SmrtWorker lease (in _smrt_workers) → recovery keys on worker liveness, not heartbeat\n```\n\n## SmrtJob\n\nPersistent in `_smrt_jobs`. Fields: `queue` (default), `objectType`, `objectId`, `method`, `args`, `runAt`, `priority` (higher=sooner), `status`, `attempts`/`maxAttempts`, `timeout` (default 5min), `retryStrategy`, `workerId` (the owning runner's incarnation key), `workerHeartbeat` (telemetry only — no longer gates recovery).\n\nStatus: `pending → running → completed/failed/cancelled`.\n\n## TaskRunner\n\nPolling-based execution engine. Config: `concurrency` (5), `pollInterval` (1s), `heartbeatInterval` (30s, telemetry only), `leaseTtlMs` (30s), `leaseTickMs` (10s), `shutdownTimeout` (30s).\n\n1. `start()` calls `assertReady()` (fail fast if `_smrt_workers` unmigrated), registers a seeded `SmrtWorker` lease, and adds its worker key to the process-global live set — all **before** polling\n2. Polls `claimReady()` to atomically claim pending jobs (`runAt <= NOW`, ordered by `priority DESC, runAt ASC, created_at ASC, id ASC`)\n3. Claim sets `status='running'`, `workerId=<incarnation key>`, heartbeat/start timestamps, and increments `attempts`\n4. Resolves class via `ObjectRegistry.getClass(objectType)`, creates instance, calls method\n5. **Internal args**: `_agentConfig` and `_scheduleId` stripped from args before calling method\n6. Terminal/retry writes are **conditional** (`WHERE worker_id=? AND status='running'`) so a recovered row is never stomped\n7. Retry: uses strategy from `@happyvertical/jobs`, schedules future `runAt` on failure\n8. Events: `job:started`, `job:completed`, `job:failed`, `job:retrying`, `runner:started/stopped`\n\n## Worker liveness & recovery (#1474)\n\nRecovery keys on **worker-process liveness**, never per-job heartbeat freshness (a CPU-bound synchronous handler used to starve the heartbeat and false-recover its own running jobs).\n\n- **`SmrtWorker` / `_smrt_workers`**: one lease row per runner *incarnation* (`workerId` is per-incarnation unique via `createWorkerKey`, so a restart never looks like it still owns the previous crash's jobs). `leaseExpiresAt` is a `datetime` (an integer epoch-ms column overflows `int4`/`INT32` on Postgres/DuckDB). Stage 1 writes/compares it against the host clock — the same approach the old heartbeat recovery used, so it's no more skew-sensitive than the code it replaced.\n- **Process-global live set** (`worker-liveness.ts`, `globalThis.__smrtLiveWorkers`): checked synchronously, so it can't be starved by a blocked loop. Covers all same-process topologies.\n- **Off-loop ticker** (`worker-liveness-ticker.ts` + `worker-liveness-thread.ts`): for engines a second connection can reach (Postgres, file-backed SQLite — `offLoopEligible()`), `start()` spawns a `node:worker_threads` ticker that renews the lease on its own thread, so a CPU-bound synchronous handler on the main loop can't starve it. In-memory SQLite / DuckDB, a thread-spawn failure, a start-handshake timeout, or the thread dying mid-run all fall back to main-loop renewal; the in-process live set keeps same-process correct regardless. The worker entry is a separate build entry resolved via `import.meta.resolve('@happyvertical/smrt-jobs/worker-liveness-thread')`.\n- **Recovery rule** (both runners): a `running` job is orphaned iff its worker is *not alive* = not in the live set **and** no fresh `_smrt_workers` lease. The live set takes precedence over a stale lease. TaskRunner also never recovers a job in its own `activeJobs`. If `_smrt_workers` is absent, recovery skips lease checks (never mass-recovers). Recovery is swept at most once per lease tick, and terminal/recovery writes use `RETURNING id` (not `rowCount`, which DuckDB/JSON adapters always report as ≥1).\n- **Lease clock**: the lease is compared against the host clock (same as the old heartbeat; fine with NTP + a 30s TTL). A dead process stops renewing and the lease expires within its TTL — that is how recovery detects death. (Instant cross-process detection via Postgres session advisory locks was prototyped on `@happyvertical/sql`'s `acquireSession()` but deferred — treating a free lock as proof-of-death false-recovers any worker legitimately in main-loop fallback mode.)\n\n## ScheduleRunner\n\nPolls `_smrt_agent_schedules` every 60s for due entries. Creates SmrtJob with `queue='agents'`, `priority=75`. Wires to TaskRunner events for completion/failure tracking. Slot reconciliation keys on worker liveness (it has no in-process active-job set, so the lease/live-set is its whole mechanism).\n\nCustom cron parser: 5-field (minute hour dom month dow). `*`, ranges, lists, steps supported. **Not timezone-aware** (UTC).\n\n## JobBuilder — Fluent API\n\n```typescript\nconst handle = await doc.background('analyze', { detailed: true })\n .delay('5m').priority('high').retries(5).queue('analysis').timeout(600000).enqueue();\n\nawait handle.wait({ timeout: 60000, pollInterval: 100 }); // polling-based\n```\n\n`bg()` is shorthand: `await doc.bg('analyze', args)` → enqueues immediately, returns JobHandle.\n\n## withBackgroundJobs(Class)\n\nMixin that adds `bg()` and `background()` to any SmrtObject. Uses WeakMap for collection caching per DB instance.\n\n## Gotchas\n\n- **Cron not timezone-aware**: all times treated as UTC\n- **No dead letter queue**: failed jobs stay in DB with `status='failed'` — manual intervention\n- **Result storage**: `resultPointer` is just a string — app must implement result backend\n- **Lazy builder**: `background()` returns builder — nothing happens until `enqueue()`\n- **wait() is polling**: JobHandle.wait() polls DB every 100ms (configurable)\n- **Migrate before start()**: `TaskRunner.start()` throws if `_smrt_workers` is missing — run `smrt db:migrate` after upgrading. Tables are never created at runtime.\n- **Recovery is liveness-based**: don't reintroduce heartbeat-threshold recovery; a blocked event loop must not look dead (see Worker liveness section, #1474)\n"
507
+ "agentDoc": "# @happyvertical/smrt-jobs\n\nBackground job execution with persistent queue, scheduling, and fluent builder API.\n\n## Architecture\n\n```\nSmrtObject.bg('method') → SmrtJob (in _smrt_jobs) → TaskRunner picks up → executes via ObjectRegistry\nAgentSchedule (cron) → ScheduleRunner creates SmrtJob → TaskRunner executes → ScheduleRunner updates\nTaskRunner.start() → SmrtWorker lease (in _smrt_workers) → recovery keys on worker liveness, not heartbeat\n```\n\n## SmrtJob\n\nPersistent in `_smrt_jobs`. Fields: `queue` (default), `objectType`, `objectId`, `method`, `args`, `runAt`, `priority` (higher=sooner), `status`, `attempts`/`maxAttempts`, `timeout` (default 5min), `retryStrategy`, `workerId` (the owning runner's incarnation key), `workerHeartbeat` (telemetry only — no longer gates recovery).\n\nStatus: `pending → running → completed/failed/cancelled`.\n\n## TaskRunner\n\nPolling-based execution engine. Config: `concurrency` (5), `pollInterval` (1s), `heartbeatInterval` (30s, telemetry only), `leaseTtlMs` (30s), `leaseTickMs` (10s), `shutdownTimeout` (30s).\n\n1. `start()` calls `assertReady()` (fail fast if `_smrt_workers` unmigrated), registers a seeded `SmrtWorker` lease, and adds its worker key to the process-global live set — all **before** polling\n2. Polls `claimReady()` to atomically claim pending jobs (`runAt <= NOW`, ordered by `priority DESC, runAt ASC, created_at ASC, id ASC`)\n3. Claim sets `status='running'`, `workerId=<incarnation key>`, heartbeat/start timestamps, and increments `attempts`\n4. Resolves class via `ObjectRegistry.getClass(objectType)`, creates instance, calls method\n5. **Internal args**: `_agentConfig` and `_scheduleId` stripped from args before calling method\n6. Terminal/retry writes are **conditional** (`WHERE worker_id=? AND status='running'`) so a recovered row is never stomped\n7. Retry: uses strategy from `@happyvertical/jobs`, schedules future `runAt` on failure\n8. Events: `job:started`, `job:completed`, `job:failed`, `job:retrying`, `runner:started/stopped`\n\nA rejection from `processJob` can never escape the poll loop: the caller attaches `.catch(e => emit('runner:error', e))` and the error path (`handleJobError`) is itself try/caught, so a failure-path write that rejects is surfaced as a `runner:error` event instead of crashing the worker with an unhandled rejection.\n\n## Timeouts & at-least-once\n\n**Execution is at-least-once, never exactly-once. Make job handlers idempotent.**\n\nA job `timeout` only races the handler's promise — JavaScript cannot preempt an already-running function. When a handler exceeds its timeout:\n\n- The job is **failed terminally and NOT auto-retried** (a timed-out handler is still running; re-queueing it would let a second worker run a concurrent duplicate). This narrows, but does not eliminate, the overlap window — the orphaned handler keeps running until it returns on its own.\n- The orphaned handler's eventual terminal write is dropped by the ownership guard (`WHERE worker_id=? AND status='running'`), so it cannot resurrect the failed row — but any **side effects** it performs (external API calls, writes to other tables) still happen.\n- Key any non-idempotent work by `context.job.jobId` or a caller-supplied idempotency key.\n\n`timeoutBehavior` (persisted + shown in the UI) is now honored:\n\n- **`'fail'`** (default): on timeout the job fails (and, per above, is not retried).\n- **`'warn'`**: the handler is **not** raced against the timeout — it runs to completion, and at the deadline the runner logs a warning and emits a `timeout-warning` job event. A slow-but-successful handler still completes successfully.\n- **`'kill'`**: treated identically to `'fail'`. In-process JavaScript has no thread interruption, so a true \"kill\" of a running handler is impossible without worker isolation; this value is honest that it only fails the job row, it does not stop the handler. Prefer `'fail'` unless you specifically want the label.\n\n## Worker liveness & recovery (#1474)\n\nRecovery keys on **worker-process liveness**, never per-job heartbeat freshness (a CPU-bound synchronous handler used to starve the heartbeat and false-recover its own running jobs).\n\n- **`SmrtWorker` / `_smrt_workers`**: one lease row per runner *incarnation* (`workerId` is per-incarnation unique via `createWorkerKey`, so a restart never looks like it still owns the previous crash's jobs). `leaseExpiresAt` is a `datetime` (an integer epoch-ms column overflows `int4`/`INT32` on Postgres/DuckDB). Stage 1 writes/compares it against the host clock — the same approach the old heartbeat recovery used, so it's no more skew-sensitive than the code it replaced.\n- **Process-global live set** (`worker-liveness.ts`, `globalThis.__smrtLiveWorkers`): checked synchronously, so it can't be starved by a blocked loop. Covers all same-process topologies.\n- **Off-loop ticker** (`worker-liveness-ticker.ts` + `worker-liveness-thread.ts`): for engines a second connection can reach (Postgres, file-backed SQLite — `offLoopEligible()`), `start()` spawns a `node:worker_threads` ticker that renews the lease on its own thread, so a CPU-bound synchronous handler on the main loop can't starve it. In-memory SQLite / DuckDB, a thread-spawn failure, a start-handshake timeout, or the thread dying mid-run all fall back to main-loop renewal; the in-process live set keeps same-process correct regardless. The worker entry is a separate build entry resolved via `import.meta.resolve('@happyvertical/smrt-jobs/worker-liveness-thread')`.\n- **Recovery rule** (both runners): a `running` job is orphaned iff its worker is *not alive* = not in the live set **and** no fresh `_smrt_workers` lease. The live set takes precedence over a stale lease. TaskRunner also never recovers a job in its own `activeJobs`. If `_smrt_workers` is absent, recovery skips lease checks (never mass-recovers). Recovery is swept at most once per lease tick, and terminal/recovery writes use `RETURNING id` (not `rowCount`, which DuckDB/JSON adapters always report as ≥1).\n- **Lease clock**: the lease is compared against the host clock (same as the old heartbeat; fine with NTP + a 30s TTL). A dead process stops renewing and the lease expires within its TTL — that is how recovery detects death. (Instant cross-process detection via Postgres session advisory locks was prototyped on `@happyvertical/sql`'s `acquireSession()` but deferred — treating a free lock as proof-of-death false-recovers any worker legitimately in main-loop fallback mode.)\n\n## ScheduleRunner\n\nPolls `_smrt_agent_schedules` every 60s for due entries. Creates SmrtJob with `queue='agents'`, `priority=75`. Wires to TaskRunner events for completion/failure tracking. Slot reconciliation keys on worker liveness (it has no in-process active-job set, so the lease/live-set is its whole mechanism).\n\nThe job is enqueued **before** `next_run` is advanced and `running_count` is incremented: a transient enqueue failure (tenant-cap hit, DB blip) therefore leaves `next_run` and `status='active'` untouched so the same due slot is retried on the next poll, rather than losing the slot and disabling the schedule.\n\n`next_run` is always recomputed from *now*, never from the previous `next_run` — this is **fire-once-forward**: runs that came due while the runner was down are not caught up, only the next future occurrence fires. There is no missed-run backfill.\n\nCustom cron parser: 5-field (minute hour dom month dow). `*`, ranges, lists, steps supported. **Not timezone-aware**: cron fields are matched against the **server's local time** (the parser uses `getHours`/`getDate`/`getDay`/… local accessors), so `0 0 * * *` fires at local midnight on the host, not at 00:00 UTC. Deploy runners in a known timezone (e.g. `TZ=UTC`) for UTC semantics. A per-schedule timezone option is a possible future enhancement.\n\n## JobBuilder — Fluent API\n\n```typescript\nconst handle = await doc.background('analyze', { detailed: true })\n .delay('5m').priority('high').retries(5).queue('analysis').timeout(600000).enqueue();\n\nawait handle.wait({ timeout: 60000, pollInterval: 100 }); // polling-based\n```\n\n`bg()` is shorthand: `await doc.bg('analyze', args)` → enqueues immediately, returns JobHandle.\n\n## withBackgroundJobs(Class)\n\nMixin that adds `bg()` and `background()` to any SmrtObject. Uses WeakMap for collection caching per DB instance.\n\n## Gotchas\n\n- **Cron not timezone-aware**: cron fields match the server's **local** time, not UTC (set `TZ` for UTC); no missed-run catch-up (fire-once-forward)\n- **At-least-once execution**: a timeout cannot preempt a running handler; timed-out jobs fail without retry but the handler keeps running — make handlers idempotent (see \"Timeouts & at-least-once\")\n- **No dead letter queue**: failed jobs stay in DB with `status='failed'` — manual intervention\n- **Result storage**: `resultPointer` is just a string — app must implement result backend\n- **Lazy builder**: `background()` returns builder — nothing happens until `enqueue()`\n- **wait() is polling**: JobHandle.wait() polls DB every 100ms (configurable)\n- **Migrate before start()**: `TaskRunner.start()` throws if `_smrt_workers` is missing — run `smrt db:migrate` after upgrading. Tables are never created at runtime.\n- **Recovery is liveness-based**: don't reintroduce heartbeat-threshold recovery; a blocked event loop must not look dead (see Worker liveness section, #1474)\n"
508
508
  }
@@ -40,9 +40,7 @@ const canRetry = $derived(
40
40
  job.status === 'failed' || job.status === 'cancelled',
41
41
  );
42
42
  const canCancel = $derived(
43
- job.status === 'pending' ||
44
- job.status === 'ready' ||
45
- job.status === 'running',
43
+ job.status === 'pending' || job.status === 'running',
46
44
  );
47
45
 
48
46
  function handleRetry() {
@@ -1 +1 @@
1
- {"version":3,"file":"JobActions.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/JobActions.svelte.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAG1C,MAAM,WAAW,KAAK;IACpB,gCAAgC;IAChC,GAAG,EAAE,OAAO,CAAC;IACb,wBAAwB;IACxB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,yBAAyB;IACzB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,yBAAyB;IACzB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,qCAAqC;IACrC,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;IACjC,sCAAsC;IACtC,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;IAClC,sCAAsC;IACtC,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;IAClC,mBAAmB;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAiED,QAAA,MAAM,UAAU,2CAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
1
+ {"version":3,"file":"JobActions.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/JobActions.svelte.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAG1C,MAAM,WAAW,KAAK;IACpB,gCAAgC;IAChC,GAAG,EAAE,OAAO,CAAC;IACb,wBAAwB;IACxB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,yBAAyB;IACzB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,yBAAyB;IACzB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,qCAAqC;IACrC,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;IACjC,sCAAsC;IACtC,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;IAClC,sCAAsC;IACtC,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;IAClC,mBAAmB;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AA+DD,QAAA,MAAM,UAAU,2CAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}