@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/AGENTS.md +25 -2
- package/dist/chunks/{runner-DV8FBO0y.js → runner-2zRlEef7.js} +86 -21
- package/dist/chunks/runner-2zRlEef7.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +31 -27
- package/dist/index.js.map +1 -1
- package/dist/job-builder.d.ts.map +1 -1
- package/dist/manifest.json +2 -2
- package/dist/runner.d.ts +43 -1
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +3 -2
- package/dist/schedule-runner.d.ts.map +1 -1
- package/dist/smrt-knowledge.json +6 -6
- package/dist/svelte/components/JobActions.svelte +1 -3
- package/dist/svelte/components/JobActions.svelte.d.ts.map +1 -1
- package/dist/svelte/components/JobDetail.svelte +2 -2
- package/dist/svelte/components/JobList.svelte +13 -0
- package/dist/svelte/components/JobList.svelte.d.ts.map +1 -1
- package/package.json +7 -7
- package/dist/chunks/runner-DV8FBO0y.js.map +0 -1
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-
|
|
2
|
-
import { J,
|
|
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")
|
|
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
|
|
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
|
-
|
|
903
|
+
f as SmrtJob,
|
|
900
904
|
SmrtJobCollection,
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
905
|
+
g as SmrtJobEvent,
|
|
906
|
+
h as SmrtJobEventCollection,
|
|
907
|
+
i as SmrtWorker,
|
|
904
908
|
SmrtWorkerCollection,
|
|
905
909
|
T as TaskRunner,
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
910
|
+
j as TenantJobCapExceededError,
|
|
911
|
+
k as assertWithinTenantCreationCap,
|
|
912
|
+
l as backgroundEligible,
|
|
909
913
|
clampRetries,
|
|
910
914
|
createScheduleRunner,
|
|
911
|
-
|
|
915
|
+
m as createTaskRunner,
|
|
912
916
|
c as createWorkerKey,
|
|
913
|
-
|
|
914
|
-
|
|
917
|
+
n as getBackgroundEligibleMethods,
|
|
918
|
+
o as isBackgroundEligibleMethod,
|
|
915
919
|
isWorkerAlive,
|
|
916
|
-
|
|
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,
|
|
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"}
|
package/dist/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": "1.0.0",
|
|
3
|
-
"timestamp":
|
|
3
|
+
"timestamp": 1782183777306,
|
|
4
4
|
"packageName": "@happyvertical/smrt-jobs",
|
|
5
|
-
"packageVersion": "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.
|
package/dist/runner.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"AAEA,OAAO,wBAAwB,CAAC;AAEhC,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAW3C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAU5D,OAAO,EAAE,KAAK,OAAO,EAAqB,MAAM,eAAe,CAAC;AAChE,OAAO,EAAE,KAAK,YAAY,EAA0B,MAAM,qBAAqB,CAAC;AAmBhF;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,iDAAiD;IACjD,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,2CAA2C;IAC3C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+CAA+C;IAC/C,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,uCAAuC;IACvC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,yCAAyC;IACzC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,4DAA4D;IAC5D,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,yDAAyD;IACzD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,oEAAoE;IACpE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,aAAa,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;IACtC,WAAW,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;IACzD,cAAc,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;IAC5D,eAAe,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IACzD,YAAY,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACnD,cAAc,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACpE,gBAAgB,EAAE,MAAM,IAAI,CAAC;IAC7B,gBAAgB,EAAE,MAAM,IAAI,CAAC;IAC7B,cAAc,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CACxC;
|
|
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,
|
|
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
|
-
|
|
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;
|
|
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"}
|
package/dist/smrt-knowledge.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-06-
|
|
3
|
+
"generatedAt": "2026-06-23T03:02:57.811Z",
|
|
4
4
|
"packageName": "@happyvertical/smrt-jobs",
|
|
5
|
-
"packageVersion": "0.
|
|
5
|
+
"packageVersion": "0.31.0",
|
|
6
6
|
"sourceManifestPath": "dist/manifest.json",
|
|
7
7
|
"agentDocPath": "AGENTS.md",
|
|
8
8
|
"sourceHashes": {
|
|
9
|
-
"manifest": "
|
|
10
|
-
"packageJson": "
|
|
11
|
-
"agents": "
|
|
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**:
|
|
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;
|
|
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"}
|