@open-mercato/core 0.10.1-develop.7736.1.88e07906bf → 0.10.1-develop.7741.1.61a2652671
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/.turbo/turbo-build.log +1 -1
- package/dist/generated/entities/api_key/index.js +2 -0
- package/dist/generated/entities/api_key/index.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +1 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/api_keys/data/entities.js +7 -0
- package/dist/modules/api_keys/data/entities.js.map +2 -2
- package/dist/modules/api_keys/migrations/Migration20261012202110_api_keys.js +30 -0
- package/dist/modules/api_keys/migrations/Migration20261012202110_api_keys.js.map +7 -0
- package/dist/modules/api_keys/services/apiKeyService.js +31 -9
- package/dist/modules/api_keys/services/apiKeyService.js.map +2 -2
- package/dist/modules/workflows/api/portal/tasks/[id]/complete/route.js +1 -1
- package/dist/modules/workflows/api/portal/tasks/[id]/complete/route.js.map +2 -2
- package/dist/modules/workflows/api/tasks/[id]/complete/route.js +7 -1
- package/dist/modules/workflows/api/tasks/[id]/complete/route.js.map +2 -2
- package/dist/modules/workflows/backend/tasks/[id]/page.js +2 -1
- package/dist/modules/workflows/backend/tasks/[id]/page.js.map +2 -2
- package/dist/modules/workflows/lib/instance-status.js +16 -0
- package/dist/modules/workflows/lib/instance-status.js.map +7 -0
- package/dist/modules/workflows/lib/task-handler.js +16 -8
- package/dist/modules/workflows/lib/task-handler.js.map +2 -2
- package/dist/modules/workflows/lib/task-sla.js +10 -0
- package/dist/modules/workflows/lib/task-sla.js.map +2 -2
- package/dist/modules/workflows/lib/workflow-executor.js +29 -1
- package/dist/modules/workflows/lib/workflow-executor.js.map +2 -2
- package/generated/entities/api_key/index.ts +1 -0
- package/generated/entity-fields-registry.ts +1 -0
- package/package.json +7 -7
- package/src/modules/api_keys/data/entities.ts +25 -1
- package/src/modules/api_keys/migrations/.snapshot-open-mercato.json +234 -68
- package/src/modules/api_keys/migrations/Migration20261012202110_api_keys.ts +47 -0
- package/src/modules/api_keys/services/apiKeyService.ts +55 -9
- package/src/modules/auth/i18n/de.json +3 -0
- package/src/modules/auth/i18n/en.json +3 -0
- package/src/modules/auth/i18n/es.json +3 -0
- package/src/modules/auth/i18n/ko.json +3 -0
- package/src/modules/auth/i18n/pl.json +3 -0
- package/src/modules/workflows/api/portal/tasks/[id]/complete/route.ts +5 -1
- package/src/modules/workflows/api/tasks/[id]/complete/route.ts +7 -1
- package/src/modules/workflows/backend/tasks/[id]/page.tsx +6 -2
- package/src/modules/workflows/i18n/de.json +2 -0
- package/src/modules/workflows/i18n/en.json +2 -0
- package/src/modules/workflows/i18n/es.json +2 -0
- package/src/modules/workflows/i18n/ko.json +2 -0
- package/src/modules/workflows/i18n/pl.json +2 -0
- package/src/modules/workflows/lib/instance-status.ts +29 -0
- package/src/modules/workflows/lib/task-handler.ts +18 -10
- package/src/modules/workflows/lib/task-sla.ts +13 -0
- package/src/modules/workflows/lib/workflow-executor.ts +44 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/workflows/lib/task-sla.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Task SLA scheduler \u2014 reminders and deadline breach for USER_TASK steps.\n *\n * `dueDate` has been display-only until now: nothing ever read it back, and the\n * authored `escalationRules` were dead config. This module makes the deadline\n * real, and it does so on the queue rather than with a poller \u2014 reusing the\n * Phase-3a absolute-deadline backstop the WAIT_FOR_CONDITION step already\n * relies on:\n *\n * - every job is enqueued ONCE, at task creation, with a delay;\n * - `deadlineAt` is ABSOLUTE and rides on the payload, so a queue running\n * behind cannot silently extend the deadline the author configured;\n * - the handler is IDEMPOTENT \u2014 a completed, cancelled or already-breached\n * task makes the job a no-op, and the breach is claimed with a conditional\n * UPDATE so at-least-once delivery still fires it exactly once.\n *\n * `computeTaskSlaSchedule` is PURE (no ORM, DI or registry imports) so the\n * scheduling arithmetic is unit-testable without a workflow.\n */\n\nimport type { EntityManager } from '@mikro-orm/core'\nimport type { AwilixContainer } from 'awilix'\nimport { StepInstance, UserTask, WorkflowInstance } from '../data/entities'\nimport type { TaskOnBreach, TaskReminder } from '../data/validators'\nimport { parseDuration } from './duration'\nimport { resolveTaskBreachHandling, type TaskBreachHandling } from './breach-routing'\nimport {\n findAgentReviewConfig,\n resolveAgentReviewBreachHandling,\n INVOKE_AGENT_ACTIVITY_TYPE,\n type AgentReviewBreachHandling,\n type AgentReviewStepLike,\n} from './agent-review'\nimport { findDefinitionForInstance } from './find-definition'\nimport { emitWorkflowsEvent } from '../events'\nimport type * as stepHandlerModule from './step-handler'\nimport type * as transitionHandlerModule from './transition-handler'\nimport type * as workflowExecutorModule from './workflow-executor'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('workflows')\n\nexport type TaskSlaPhase = 'reminder' | 'breach'\n\nexport interface TaskSlaScheduleEntry {\n phase: TaskSlaPhase\n /** Absolute time this job should run. */\n fireAt: string\n /** Delay from `now`; never negative. */\n delayMs: number\n}\n\nexport interface TaskSlaSchedule {\n entries: TaskSlaScheduleEntry[]\n /** Reminder offsets that could not be parsed, so the caller can log them. */\n invalidOffsets: string[]\n}\n\n/** Statuses a task is still workable in \u2014 anything else makes an SLA job moot. */\nconst OPEN_TASK_STATUSES = ['PENDING', 'IN_PROGRESS'] as const\n\n/**\n * Resolve the jobs to enqueue for one task.\n *\n * A task with no deadline schedules NOTHING \u2014 reminders without a deadline have\n * nothing to count back from, so they are silently irrelevant rather than an\n * error. A reminder whose moment has already passed is dropped: firing it at\n * creation time would be noise, not a nudge. A reminder offset at or beyond the\n * whole deadline is likewise dropped \u2014 the breach job already speaks for that\n * moment.\n */\nexport function computeTaskSlaSchedule(options: {\n dueDate: Date | null | undefined\n reminders?: TaskReminder[] | null\n now?: Date\n}): TaskSlaSchedule {\n const { dueDate } = options\n if (!dueDate || Number.isNaN(dueDate.getTime())) return { entries: [], invalidOffsets: [] }\n\n const nowMs = (options.now ?? new Date()).getTime()\n const deadlineMs = dueDate.getTime()\n const entries: TaskSlaScheduleEntry[] = []\n const invalidOffsets: string[] = []\n\n for (const reminder of options.reminders ?? []) {\n const offset = reminder?.offset\n if (typeof offset !== 'string' || !offset.length) continue\n\n let offsetMs: number\n try {\n offsetMs = parseDuration(offset)\n } catch {\n invalidOffsets.push(offset)\n continue\n }\n\n if (offsetMs <= 0) continue\n const remindAtMs = deadlineMs - offsetMs\n if (remindAtMs <= nowMs || remindAtMs >= deadlineMs) continue\n\n entries.push({\n phase: 'reminder',\n fireAt: new Date(remindAtMs).toISOString(),\n delayMs: remindAtMs - nowMs,\n })\n }\n\n entries.push({\n phase: 'breach',\n fireAt: dueDate.toISOString(),\n delayMs: Math.max(0, deadlineMs - nowMs),\n })\n\n entries.sort((left, right) => left.delayMs - right.delayMs)\n\n return { entries, invalidOffsets }\n}\n\nexport interface ScheduleUserTaskSlaOptions {\n workflowInstanceId: string\n stepInstanceId: string\n branchInstanceId?: string | null\n userTaskId: string\n dueDate: Date | null | undefined\n reminders?: TaskReminder[] | null\n tenantId: string\n organizationId: string\n userId?: string\n now?: Date\n}\n\n/**\n * Enqueue every SLA job a freshly created task needs.\n *\n * Best-effort by contract: a queue failure must never break the workflow step\n * that created the task, exactly as the assignment event's emission does not.\n */\nexport async function scheduleUserTaskSla(\n options: ScheduleUserTaskSlaOptions\n): Promise<string[]> {\n const schedule = computeTaskSlaSchedule({\n dueDate: options.dueDate,\n reminders: options.reminders,\n now: options.now,\n })\n\n if (schedule.invalidOffsets.length) {\n logger.warn('Task reminder offsets skipped because they are not valid durations', {\n component: 'task-sla',\n taskId: options.userTaskId,\n invalidOffsets: schedule.invalidOffsets,\n })\n }\n\n if (schedule.entries.length === 0) return []\n\n const deadlineAt = options.dueDate ? options.dueDate.toISOString() : null\n if (!deadlineAt) return []\n\n const jobIds: string[] = []\n try {\n const { enqueueTaskSlaJob } = await import('./activity-executor')\n for (const entry of schedule.entries) {\n const jobId = await enqueueTaskSlaJob({\n workflowInstanceId: options.workflowInstanceId,\n stepInstanceId: options.stepInstanceId,\n branchInstanceId: options.branchInstanceId ?? null,\n userTaskId: options.userTaskId,\n phase: entry.phase,\n deadlineAt,\n fireAt: entry.fireAt,\n delayMs: entry.delayMs,\n tenantId: options.tenantId,\n organizationId: options.organizationId,\n userId: options.userId,\n })\n jobIds.push(jobId)\n }\n } catch (error) {\n logger.error('Failed to schedule task SLA jobs', {\n component: 'task-sla',\n taskId: options.userTaskId,\n err: error,\n })\n }\n\n return jobIds\n}\n\nexport type TaskSlaOutcome = 'noop' | 'reminded' | 'breached'\n\nexport interface RunTaskSlaJobOptions {\n userTaskId: string\n stepInstanceId: string\n workflowInstanceId: string\n branchInstanceId?: string | null\n phase: TaskSlaPhase\n deadlineAt: string\n tenantId: string\n organizationId: string\n userId?: string\n}\n\n/**\n * Run one SLA job.\n *\n * Every early return is a deliberate no-op rather than an error: the queue\n * retries, and a task that was completed in the meantime is the ordinary case,\n * not a failure.\n */\nexport async function runTaskSlaJob(\n em: EntityManager,\n container: AwilixContainer,\n options: RunTaskSlaJobOptions\n): Promise<TaskSlaOutcome> {\n const { tenantId, organizationId } = options\n\n const task = await em.findOne(UserTask, {\n id: options.userTaskId,\n tenantId,\n organizationId,\n })\n\n if (!task) return 'noop'\n if (!OPEN_TASK_STATUSES.includes(task.status as (typeof OPEN_TASK_STATUSES)[number])) return 'noop'\n\n const deadlineMs = Date.parse(options.deadlineAt)\n if (Number.isNaN(deadlineMs)) return 'noop'\n\n if (options.phase === 'reminder') {\n // A reminder that arrives after the deadline has nothing left to warn\n // about, and one for an already-breached task is strictly noise.\n if (task.escalatedAt) return 'noop'\n if (Date.now() >= deadlineMs) return 'noop'\n\n await logTaskSlaEvent(em, container, options, 'USER_TASK_REMINDER_DUE', {\n taskId: task.id,\n taskName: task.taskName,\n dueDate: options.deadlineAt,\n })\n\n await emitTaskSlaEvent(em, 'workflows.task.reminder_due', task, options)\n return 'reminded'\n }\n\n // Claim the breach with a conditional UPDATE rather than a read-then-write:\n // the queue delivers at least once, and two deliveries reading `escalated_at\n // IS NULL` would both fire. The loser updates zero rows and no-ops.\n const now = new Date()\n const claimed = await em.nativeUpdate(\n UserTask,\n {\n id: task.id,\n tenantId,\n organizationId,\n status: { $in: [...OPEN_TASK_STATUSES] },\n escalatedAt: null,\n },\n { escalatedAt: now, updatedAt: now }\n )\n\n if (claimed === 0) return 'noop'\n\n task.escalatedAt = now\n task.updatedAt = now\n\n // WHICH path the breach took is part of the audit trail, not an implementation\n // detail: \"the deadline passed\" and \"the deadline passed and the run was\n // routed away from this task\" are different facts about the same instant.\n const handling = await applyBreachHandling(em, container, task, options, now)\n\n await logTaskSlaEvent(em, container, options, 'USER_TASK_DEADLINE_BREACHED', {\n taskId: task.id,\n taskName: task.taskName,\n dueDate: options.deadlineAt,\n breachedAt: now.toISOString(),\n onBreach: handling.outcome,\n ...(handling.detail ?? {}),\n })\n\n await emitTaskSlaEvent(em, 'workflows.task.deadline_breached', task, options)\n\n // A reassignment only means anything once the new owner is told about it, and\n // the assignment event is the one path that already reaches both an individual\n // and a role queue.\n if (handling.outcome === 'reassigned') {\n await emitTaskSlaEvent(em, 'workflows.task.assigned', task, options)\n }\n\n if (handling.resumeInstance) {\n await resumeAfterBreachRoute(em, container, task, options, handling.resumeInstance)\n }\n\n return 'breached'\n}\n\ntype BreachOutcome =\n | 'none'\n | 'notified'\n | 'reassigned'\n | 'routed'\n | 'route_skipped_branch'\n | 'attention'\n\ninterface AppliedBreachHandling {\n outcome: BreachOutcome\n detail?: Record<string, unknown>\n resumeInstance?: { stepId: string; toStepId: string; transitionId?: string }\n}\n\ntype BreachResolution = TaskBreachHandling | AgentReviewBreachHandling\n\n/**\n * A step whose breached task is an agent DISPOSITION task rather than an\n * authored USER_TASK. Recognised structurally from the compiled step (an\n * AUTOMATED step carrying an INVOKE_AGENT activity), which is the same shape\n * `graph-utils` round-trips the node from.\n */\nfunction isInvokeAgentStepDef(step: AgentReviewStepLike | undefined): boolean {\n return (step?.activities ?? []).some(\n (activity) => activity?.activityType === INVOKE_AGENT_ACTIVITY_TYPE\n )\n}\n\n/**\n * Apply the resolver's answer to the task row. Routing itself is deferred to\n * `resumeAfterBreachRoute` so the workflow only advances after the breach has\n * been recorded and announced.\n *\n * Which resolver answers depends on WHAT the task is. An authored USER_TASK\n * keeps the full \u00A76.1 vocabulary, including \"follow a route\". An agent\n * disposition task (\u00A77.5) gets the escalate-only one: notify, reassign, or mark\n * the run for attention. It can reach neither a verdict nor a route, because\n * both would resolve a pending proposal without a human \u2014 routing the run past\n * a proposal nobody answered silently drops the mutation the agent proposed,\n * which is a rejection in everything but name. A hand-authored `kind:'slaBreach'`\n * transition on an agent step therefore cannot fire either: the branch below is\n * taken from the STEP's shape, not from the config the author supplied.\n */\nasync function applyBreachHandling(\n em: EntityManager,\n container: AwilixContainer,\n task: UserTask,\n options: RunTaskSlaJobOptions,\n now: Date\n): Promise<AppliedBreachHandling> {\n let resolution: BreachResolution = { kind: 'none' }\n let stepId: string | null = null\n let instance: WorkflowInstance | null = null\n\n try {\n const stepInstance = await em.findOne(StepInstance, {\n id: options.stepInstanceId,\n tenantId: options.tenantId,\n organizationId: options.organizationId,\n })\n stepId = stepInstance?.stepId ?? null\n\n instance = await em.findOne(WorkflowInstance, {\n id: options.workflowInstanceId,\n tenantId: options.tenantId,\n organizationId: options.organizationId,\n })\n\n if (stepId && instance) {\n const definition = await findDefinitionForInstance(em, instance)\n const stepDef = (definition?.definition?.steps ?? []).find(\n (step: { stepId?: string }) => step.stepId === stepId\n ) as\n | ({ userTaskConfig?: { onBreach?: TaskOnBreach | null } } & AgentReviewStepLike)\n | undefined\n\n resolution = isInvokeAgentStepDef(stepDef)\n ? resolveAgentReviewBreachHandling(findAgentReviewConfig(stepDef)?.onBreach ?? null)\n : resolveTaskBreachHandling(\n definition?.definition ?? null,\n stepId,\n stepDef?.userTaskConfig?.onBreach ?? null\n )\n }\n } catch (error) {\n // A definition the engine can no longer read must not strand the breach:\n // the task is already marked escalated and the event still fires.\n logger.error('Failed to resolve breach handling; falling back to notify-only', {\n component: 'task-sla',\n taskId: task.id,\n err: error,\n })\n return { outcome: 'none' }\n }\n\n if (resolution.kind === 'notify') return { outcome: 'notified' }\n\n if (resolution.kind === 'attention') {\n if (!instance) return { outcome: 'none' }\n await markInstanceForAttention(em, instance, stepId, task, now)\n return { outcome: 'attention', detail: { stepId } }\n }\n\n if (resolution.kind === 'reassign') {\n task.assignedTo = resolution.assignedTo\n task.assignedToRoles = resolution.assignedToRoles\n task.claimedBy = null\n task.claimedAt = null\n task.status = 'PENDING'\n task.escalatedTo = resolution.assignedTo ?? resolution.assignedToRoles?.[0] ?? null\n task.reassignedAt = now\n task.reassignReason = 'sla_breach'\n task.updatedAt = now\n await em.flush()\n return {\n outcome: 'reassigned',\n detail: {\n reassignedTo: resolution.assignedTo,\n reassignedToRoles: resolution.assignedToRoles,\n },\n }\n }\n\n if (resolution.kind === 'route') {\n const toStepId = resolution.transition.toStepId\n if (!stepId || !toStepId) return { outcome: 'none' }\n\n // A branch-scoped task does NOT follow its breach route, for the same\n // reason a decision button does not select a route inside a branch\n // (`completeUserTask`): a branch advances through `resumeBranch` with its\n // own token, and overriding that is a parallel-execution change rather than\n // a task-surface one.\n if (options.branchInstanceId) {\n return { outcome: 'route_skipped_branch', detail: { transitionId: resolution.transition.transitionId } }\n }\n\n // The task is superseded by the route, not abandoned: `ESCALATED` is an\n // existing `UserTaskStatus`, so no state machine changes.\n task.status = 'ESCALATED'\n task.updatedAt = now\n await em.flush()\n\n return {\n outcome: 'routed',\n detail: { transitionId: resolution.transition.transitionId, toStepId },\n resumeInstance: { stepId, toStepId, transitionId: resolution.transition.transitionId },\n }\n }\n\n return { outcome: 'none' }\n}\n\n/**\n * Mark a run for triage without touching its state machine.\n *\n * The instance is ALREADY paused \u2014 an agent step parks on the proposal-ready\n * signal \u2014 so this writes only the engine-owned `metadata.attention` marker the\n * failure queue already uses (`GET /api/workflows/instances?attention=true`\n * lists it). Deliberately no status write, no `errorMessage`: the run is not\n * failing, it is waiting on a person who is late, and the proposal is still\n * pending for whoever picks it up.\n */\nasync function markInstanceForAttention(\n em: EntityManager,\n instance: WorkflowInstance,\n stepId: string | null,\n task: UserTask,\n now: Date\n): Promise<void> {\n instance.metadata = {\n ...(instance.metadata || {}),\n attention: {\n reason: 'DISPOSITION_SLA_BREACH',\n ...(stepId ? { stepId } : {}),\n at: now.toISOString(),\n },\n }\n instance.updatedAt = now\n await em.flush()\n\n logger.warn('Agent disposition deadline passed; run marked for attention', {\n component: 'task-sla',\n taskId: task.id,\n workflowInstanceId: instance.id,\n stepId,\n })\n}\n\n/**\n * Follow the breach route: exit the task's step and drive the run onward,\n * mirroring how `fireTimer` resumes a run parked on a WAIT_FOR_TIMER step.\n *\n * Best-effort: a routing failure leaves the run exactly where it was, with the\n * breach already recorded, rather than throwing the queue job into a retry loop\n * that would re-announce a breach that has already fired.\n */\nasync function resumeAfterBreachRoute(\n em: EntityManager,\n container: AwilixContainer,\n task: UserTask,\n options: RunTaskSlaJobOptions,\n route: { stepId: string; toStepId: string; transitionId?: string }\n): Promise<void> {\n try {\n const instance = await em.findOne(WorkflowInstance, {\n id: options.workflowInstanceId,\n tenantId: options.tenantId,\n organizationId: options.organizationId,\n })\n if (!instance) return\n\n const stepHandler = container.resolve<typeof stepHandlerModule>('stepHandler')\n const transitionHandler = container.resolve<typeof transitionHandlerModule>('transitionHandler')\n const workflowExecutor = container.resolve<typeof workflowExecutorModule>('workflowExecutor')\n\n const stepInstance = await em.findOne(StepInstance, {\n id: options.stepInstanceId,\n status: 'ACTIVE',\n })\n if (stepInstance) {\n await stepHandler.exitStep(em, stepInstance, {\n userTaskId: task.id,\n slaBreached: true,\n })\n }\n\n const transitionResult = await transitionHandler.executeTransition(\n em,\n container,\n instance,\n route.stepId,\n route.toStepId,\n {\n workflowContext: instance.context,\n userId: options.userId,\n transitionId: route.transitionId,\n },\n )\n\n if (!transitionResult.success) {\n logger.error('SLA-breach transition failed', {\n component: 'task-sla',\n taskId: task.id,\n err: transitionResult.error,\n })\n return\n }\n\n await workflowExecutor.executeWorkflow(em, container, instance.id, { userId: options.userId })\n } catch (error) {\n logger.error('Failed to follow the SLA-breach route', {\n component: 'task-sla',\n taskId: task.id,\n err: error,\n })\n }\n}\n\nasync function logTaskSlaEvent(\n em: EntityManager,\n container: AwilixContainer,\n options: RunTaskSlaJobOptions,\n eventType: string,\n eventData: Record<string, unknown>\n): Promise<void> {\n try {\n const eventLogger = container.resolve<{\n logWorkflowEvent: (\n em: EntityManager,\n event: Record<string, unknown>\n ) => Promise<unknown>\n }>('eventLogger')\n\n await eventLogger.logWorkflowEvent(em, {\n workflowInstanceId: options.workflowInstanceId,\n stepInstanceId: options.stepInstanceId,\n ...(options.branchInstanceId ? { branchInstanceId: options.branchInstanceId } : {}),\n eventType,\n eventData,\n userId: options.userId,\n tenantId: options.tenantId,\n organizationId: options.organizationId,\n })\n } catch (error) {\n logger.error('Failed to log task SLA workflow event', {\n component: 'task-sla',\n taskId: options.userTaskId,\n eventType,\n err: error,\n })\n }\n}\n\nasync function emitTaskSlaEvent(\n em: EntityManager,\n eventId: 'workflows.task.reminder_due' | 'workflows.task.deadline_breached' | 'workflows.task.assigned',\n task: UserTask,\n options: RunTaskSlaJobOptions\n): Promise<void> {\n try {\n const instance = await em.findOne(WorkflowInstance, {\n id: options.workflowInstanceId,\n tenantId: options.tenantId,\n organizationId: options.organizationId,\n })\n\n await emitWorkflowsEvent(\n eventId,\n {\n taskId: task.id,\n taskName: task.taskName,\n workflowInstanceId: options.workflowInstanceId,\n workflowId: instance?.workflowId ?? null,\n workflowName: instance?.workflowId ?? '',\n assignedUserId: task.claimedBy ?? task.assignedTo ?? null,\n assignedToRoles: task.assignedToRoles ?? null,\n dueDate: options.deadlineAt,\n entityBindings: task.entityBindings ?? null,\n tenantId: options.tenantId,\n organizationId: options.organizationId,\n },\n { persistent: true }\n )\n } catch (error) {\n logger.error('Failed to emit task SLA event', {\n component: 'task-sla',\n taskId: task.id,\n eventId,\n err: error,\n })\n }\n}\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["/**\n * Task SLA scheduler \u2014 reminders and deadline breach for USER_TASK steps.\n *\n * `dueDate` has been display-only until now: nothing ever read it back, and the\n * authored `escalationRules` were dead config. This module makes the deadline\n * real, and it does so on the queue rather than with a poller \u2014 reusing the\n * Phase-3a absolute-deadline backstop the WAIT_FOR_CONDITION step already\n * relies on:\n *\n * - every job is enqueued ONCE, at task creation, with a delay;\n * - `deadlineAt` is ABSOLUTE and rides on the payload, so a queue running\n * behind cannot silently extend the deadline the author configured;\n * - the handler is IDEMPOTENT \u2014 a completed, cancelled or already-breached\n * task makes the job a no-op, and the breach is claimed with a conditional\n * UPDATE so at-least-once delivery still fires it exactly once.\n *\n * `computeTaskSlaSchedule` is PURE (no ORM, DI or registry imports) so the\n * scheduling arithmetic is unit-testable without a workflow.\n */\n\nimport type { EntityManager } from '@mikro-orm/core'\nimport type { AwilixContainer } from 'awilix'\nimport { StepInstance, UserTask, WorkflowInstance } from '../data/entities'\nimport type { TaskOnBreach, TaskReminder } from '../data/validators'\nimport { parseDuration } from './duration'\nimport { resolveTaskBreachHandling, type TaskBreachHandling } from './breach-routing'\nimport {\n findAgentReviewConfig,\n resolveAgentReviewBreachHandling,\n INVOKE_AGENT_ACTIVITY_TYPE,\n type AgentReviewBreachHandling,\n type AgentReviewStepLike,\n} from './agent-review'\nimport { findDefinitionForInstance } from './find-definition'\nimport { isRunClosedToUserTasks } from './instance-status'\nimport { emitWorkflowsEvent } from '../events'\nimport type * as stepHandlerModule from './step-handler'\nimport type * as transitionHandlerModule from './transition-handler'\nimport type * as workflowExecutorModule from './workflow-executor'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('workflows')\n\nexport type TaskSlaPhase = 'reminder' | 'breach'\n\nexport interface TaskSlaScheduleEntry {\n phase: TaskSlaPhase\n /** Absolute time this job should run. */\n fireAt: string\n /** Delay from `now`; never negative. */\n delayMs: number\n}\n\nexport interface TaskSlaSchedule {\n entries: TaskSlaScheduleEntry[]\n /** Reminder offsets that could not be parsed, so the caller can log them. */\n invalidOffsets: string[]\n}\n\n/** Statuses a task is still workable in \u2014 anything else makes an SLA job moot. */\nconst OPEN_TASK_STATUSES = ['PENDING', 'IN_PROGRESS'] as const\n\n/**\n * Resolve the jobs to enqueue for one task.\n *\n * A task with no deadline schedules NOTHING \u2014 reminders without a deadline have\n * nothing to count back from, so they are silently irrelevant rather than an\n * error. A reminder whose moment has already passed is dropped: firing it at\n * creation time would be noise, not a nudge. A reminder offset at or beyond the\n * whole deadline is likewise dropped \u2014 the breach job already speaks for that\n * moment.\n */\nexport function computeTaskSlaSchedule(options: {\n dueDate: Date | null | undefined\n reminders?: TaskReminder[] | null\n now?: Date\n}): TaskSlaSchedule {\n const { dueDate } = options\n if (!dueDate || Number.isNaN(dueDate.getTime())) return { entries: [], invalidOffsets: [] }\n\n const nowMs = (options.now ?? new Date()).getTime()\n const deadlineMs = dueDate.getTime()\n const entries: TaskSlaScheduleEntry[] = []\n const invalidOffsets: string[] = []\n\n for (const reminder of options.reminders ?? []) {\n const offset = reminder?.offset\n if (typeof offset !== 'string' || !offset.length) continue\n\n let offsetMs: number\n try {\n offsetMs = parseDuration(offset)\n } catch {\n invalidOffsets.push(offset)\n continue\n }\n\n if (offsetMs <= 0) continue\n const remindAtMs = deadlineMs - offsetMs\n if (remindAtMs <= nowMs || remindAtMs >= deadlineMs) continue\n\n entries.push({\n phase: 'reminder',\n fireAt: new Date(remindAtMs).toISOString(),\n delayMs: remindAtMs - nowMs,\n })\n }\n\n entries.push({\n phase: 'breach',\n fireAt: dueDate.toISOString(),\n delayMs: Math.max(0, deadlineMs - nowMs),\n })\n\n entries.sort((left, right) => left.delayMs - right.delayMs)\n\n return { entries, invalidOffsets }\n}\n\nexport interface ScheduleUserTaskSlaOptions {\n workflowInstanceId: string\n stepInstanceId: string\n branchInstanceId?: string | null\n userTaskId: string\n dueDate: Date | null | undefined\n reminders?: TaskReminder[] | null\n tenantId: string\n organizationId: string\n userId?: string\n now?: Date\n}\n\n/**\n * Enqueue every SLA job a freshly created task needs.\n *\n * Best-effort by contract: a queue failure must never break the workflow step\n * that created the task, exactly as the assignment event's emission does not.\n */\nexport async function scheduleUserTaskSla(\n options: ScheduleUserTaskSlaOptions\n): Promise<string[]> {\n const schedule = computeTaskSlaSchedule({\n dueDate: options.dueDate,\n reminders: options.reminders,\n now: options.now,\n })\n\n if (schedule.invalidOffsets.length) {\n logger.warn('Task reminder offsets skipped because they are not valid durations', {\n component: 'task-sla',\n taskId: options.userTaskId,\n invalidOffsets: schedule.invalidOffsets,\n })\n }\n\n if (schedule.entries.length === 0) return []\n\n const deadlineAt = options.dueDate ? options.dueDate.toISOString() : null\n if (!deadlineAt) return []\n\n const jobIds: string[] = []\n try {\n const { enqueueTaskSlaJob } = await import('./activity-executor')\n for (const entry of schedule.entries) {\n const jobId = await enqueueTaskSlaJob({\n workflowInstanceId: options.workflowInstanceId,\n stepInstanceId: options.stepInstanceId,\n branchInstanceId: options.branchInstanceId ?? null,\n userTaskId: options.userTaskId,\n phase: entry.phase,\n deadlineAt,\n fireAt: entry.fireAt,\n delayMs: entry.delayMs,\n tenantId: options.tenantId,\n organizationId: options.organizationId,\n userId: options.userId,\n })\n jobIds.push(jobId)\n }\n } catch (error) {\n logger.error('Failed to schedule task SLA jobs', {\n component: 'task-sla',\n taskId: options.userTaskId,\n err: error,\n })\n }\n\n return jobIds\n}\n\nexport type TaskSlaOutcome = 'noop' | 'reminded' | 'breached'\n\nexport interface RunTaskSlaJobOptions {\n userTaskId: string\n stepInstanceId: string\n workflowInstanceId: string\n branchInstanceId?: string | null\n phase: TaskSlaPhase\n deadlineAt: string\n tenantId: string\n organizationId: string\n userId?: string\n}\n\n/**\n * Run one SLA job.\n *\n * Every early return is a deliberate no-op rather than an error: the queue\n * retries, and a task that was completed in the meantime is the ordinary case,\n * not a failure.\n */\nexport async function runTaskSlaJob(\n em: EntityManager,\n container: AwilixContainer,\n options: RunTaskSlaJobOptions\n): Promise<TaskSlaOutcome> {\n const { tenantId, organizationId } = options\n\n const task = await em.findOne(UserTask, {\n id: options.userTaskId,\n tenantId,\n organizationId,\n })\n\n if (!task) return 'noop'\n if (!OPEN_TASK_STATUSES.includes(task.status as (typeof OPEN_TASK_STATUSES)[number])) return 'noop'\n\n const deadlineMs = Date.parse(options.deadlineAt)\n if (Number.isNaN(deadlineMs)) return 'noop'\n\n if (options.phase === 'reminder') {\n // A reminder that arrives after the deadline has nothing left to warn\n // about, and one for an already-breached task is strictly noise.\n if (task.escalatedAt) return 'noop'\n if (Date.now() >= deadlineMs) return 'noop'\n\n await logTaskSlaEvent(em, container, options, 'USER_TASK_REMINDER_DUE', {\n taskId: task.id,\n taskName: task.taskName,\n dueDate: options.deadlineAt,\n })\n\n await emitTaskSlaEvent(em, 'workflows.task.reminder_due', task, options)\n return 'reminded'\n }\n\n // Claim the breach with a conditional UPDATE rather than a read-then-write:\n // the queue delivers at least once, and two deliveries reading `escalated_at\n // IS NULL` would both fire. The loser updates zero rows and no-ops.\n const now = new Date()\n const claimed = await em.nativeUpdate(\n UserTask,\n {\n id: task.id,\n tenantId,\n organizationId,\n status: { $in: [...OPEN_TASK_STATUSES] },\n escalatedAt: null,\n },\n { escalatedAt: now, updatedAt: now }\n )\n\n if (claimed === 0) return 'noop'\n\n task.escalatedAt = now\n task.updatedAt = now\n\n // WHICH path the breach took is part of the audit trail, not an implementation\n // detail: \"the deadline passed\" and \"the deadline passed and the run was\n // routed away from this task\" are different facts about the same instant.\n const handling = await applyBreachHandling(em, container, task, options, now)\n\n await logTaskSlaEvent(em, container, options, 'USER_TASK_DEADLINE_BREACHED', {\n taskId: task.id,\n taskName: task.taskName,\n dueDate: options.deadlineAt,\n breachedAt: now.toISOString(),\n onBreach: handling.outcome,\n ...(handling.detail ?? {}),\n })\n\n await emitTaskSlaEvent(em, 'workflows.task.deadline_breached', task, options)\n\n // A reassignment only means anything once the new owner is told about it, and\n // the assignment event is the one path that already reaches both an individual\n // and a role queue.\n if (handling.outcome === 'reassigned') {\n await emitTaskSlaEvent(em, 'workflows.task.assigned', task, options)\n }\n\n if (handling.resumeInstance) {\n await resumeAfterBreachRoute(em, container, task, options, handling.resumeInstance)\n }\n\n return 'breached'\n}\n\ntype BreachOutcome =\n | 'none'\n | 'notified'\n | 'reassigned'\n | 'routed'\n | 'route_skipped_branch'\n | 'route_skipped_closed_run'\n | 'attention'\n | 'attention_skipped_closed_run'\n\ninterface AppliedBreachHandling {\n outcome: BreachOutcome\n detail?: Record<string, unknown>\n resumeInstance?: { stepId: string; toStepId: string; transitionId?: string }\n}\n\ntype BreachResolution = TaskBreachHandling | AgentReviewBreachHandling\n\n/**\n * A step whose breached task is an agent DISPOSITION task rather than an\n * authored USER_TASK. Recognised structurally from the compiled step (an\n * AUTOMATED step carrying an INVOKE_AGENT activity), which is the same shape\n * `graph-utils` round-trips the node from.\n */\nfunction isInvokeAgentStepDef(step: AgentReviewStepLike | undefined): boolean {\n return (step?.activities ?? []).some(\n (activity) => activity?.activityType === INVOKE_AGENT_ACTIVITY_TYPE\n )\n}\n\n/**\n * Apply the resolver's answer to the task row. Routing itself is deferred to\n * `resumeAfterBreachRoute` so the workflow only advances after the breach has\n * been recorded and announced.\n *\n * Which resolver answers depends on WHAT the task is. An authored USER_TASK\n * keeps the full \u00A76.1 vocabulary, including \"follow a route\". An agent\n * disposition task (\u00A77.5) gets the escalate-only one: notify, reassign, or mark\n * the run for attention. It can reach neither a verdict nor a route, because\n * both would resolve a pending proposal without a human \u2014 routing the run past\n * a proposal nobody answered silently drops the mutation the agent proposed,\n * which is a rejection in everything but name. A hand-authored `kind:'slaBreach'`\n * transition on an agent step therefore cannot fire either: the branch below is\n * taken from the STEP's shape, not from the config the author supplied.\n */\nasync function applyBreachHandling(\n em: EntityManager,\n container: AwilixContainer,\n task: UserTask,\n options: RunTaskSlaJobOptions,\n now: Date\n): Promise<AppliedBreachHandling> {\n let resolution: BreachResolution = { kind: 'none' }\n let stepId: string | null = null\n let instance: WorkflowInstance | null = null\n\n try {\n const stepInstance = await em.findOne(StepInstance, {\n id: options.stepInstanceId,\n tenantId: options.tenantId,\n organizationId: options.organizationId,\n })\n stepId = stepInstance?.stepId ?? null\n\n instance = await em.findOne(WorkflowInstance, {\n id: options.workflowInstanceId,\n tenantId: options.tenantId,\n organizationId: options.organizationId,\n })\n\n if (stepId && instance) {\n const definition = await findDefinitionForInstance(em, instance)\n const stepDef = (definition?.definition?.steps ?? []).find(\n (step: { stepId?: string }) => step.stepId === stepId\n ) as\n | ({ userTaskConfig?: { onBreach?: TaskOnBreach | null } } & AgentReviewStepLike)\n | undefined\n\n resolution = isInvokeAgentStepDef(stepDef)\n ? resolveAgentReviewBreachHandling(findAgentReviewConfig(stepDef)?.onBreach ?? null)\n : resolveTaskBreachHandling(\n definition?.definition ?? null,\n stepId,\n stepDef?.userTaskConfig?.onBreach ?? null\n )\n }\n } catch (error) {\n // A definition the engine can no longer read must not strand the breach:\n // the task is already marked escalated and the event still fires.\n logger.error('Failed to resolve breach handling; falling back to notify-only', {\n component: 'task-sla',\n taskId: task.id,\n err: error,\n })\n return { outcome: 'none' }\n }\n\n if (resolution.kind === 'notify') return { outcome: 'notified' }\n\n if (resolution.kind === 'attention') {\n if (!instance) return { outcome: 'none' }\n if (isRunClosedToUserTasks(instance.status)) {\n return { outcome: 'attention_skipped_closed_run', detail: { stepId, status: instance.status } }\n }\n await markInstanceForAttention(em, instance, stepId, task, now)\n return { outcome: 'attention', detail: { stepId } }\n }\n\n if (resolution.kind === 'reassign') {\n task.assignedTo = resolution.assignedTo\n task.assignedToRoles = resolution.assignedToRoles\n task.claimedBy = null\n task.claimedAt = null\n task.status = 'PENDING'\n task.escalatedTo = resolution.assignedTo ?? resolution.assignedToRoles?.[0] ?? null\n task.reassignedAt = now\n task.reassignReason = 'sla_breach'\n task.updatedAt = now\n await em.flush()\n return {\n outcome: 'reassigned',\n detail: {\n reassignedTo: resolution.assignedTo,\n reassignedToRoles: resolution.assignedToRoles,\n },\n }\n }\n\n if (resolution.kind === 'route') {\n const toStepId = resolution.transition.toStepId\n if (!stepId || !toStepId) return { outcome: 'none' }\n\n // A branch-scoped task does NOT follow its breach route, for the same\n // reason a decision button does not select a route inside a branch\n // (`completeUserTask`): a branch advances through `resumeBranch` with its\n // own token, and overriding that is a parallel-execution change rather than\n // a task-surface one.\n if (options.branchInstanceId) {\n return { outcome: 'route_skipped_branch', detail: { transitionId: resolution.transition.transitionId } }\n }\n\n if (isRunClosedToUserTasks(instance?.status)) {\n return {\n outcome: 'route_skipped_closed_run',\n detail: { transitionId: resolution.transition.transitionId, status: instance?.status },\n }\n }\n\n // The task is superseded by the route, not abandoned: `ESCALATED` is an\n // existing `UserTaskStatus`, so no state machine changes.\n task.status = 'ESCALATED'\n task.updatedAt = now\n await em.flush()\n\n return {\n outcome: 'routed',\n detail: { transitionId: resolution.transition.transitionId, toStepId },\n resumeInstance: { stepId, toStepId, transitionId: resolution.transition.transitionId },\n }\n }\n\n return { outcome: 'none' }\n}\n\n/**\n * Mark a run for triage without touching its state machine.\n *\n * The instance is ALREADY paused \u2014 an agent step parks on the proposal-ready\n * signal \u2014 so this writes only the engine-owned `metadata.attention` marker the\n * failure queue already uses (`GET /api/workflows/instances?attention=true`\n * lists it). Deliberately no status write, no `errorMessage`: the run is not\n * failing, it is waiting on a person who is late, and the proposal is still\n * pending for whoever picks it up.\n */\nasync function markInstanceForAttention(\n em: EntityManager,\n instance: WorkflowInstance,\n stepId: string | null,\n task: UserTask,\n now: Date\n): Promise<void> {\n instance.metadata = {\n ...(instance.metadata || {}),\n attention: {\n reason: 'DISPOSITION_SLA_BREACH',\n ...(stepId ? { stepId } : {}),\n at: now.toISOString(),\n },\n }\n instance.updatedAt = now\n await em.flush()\n\n logger.warn('Agent disposition deadline passed; run marked for attention', {\n component: 'task-sla',\n taskId: task.id,\n workflowInstanceId: instance.id,\n stepId,\n })\n}\n\n/**\n * Follow the breach route: exit the task's step and drive the run onward,\n * mirroring how `fireTimer` resumes a run parked on a WAIT_FOR_TIMER step.\n *\n * Best-effort: a routing failure leaves the run exactly where it was, with the\n * breach already recorded, rather than throwing the queue job into a retry loop\n * that would re-announce a breach that has already fired.\n */\nasync function resumeAfterBreachRoute(\n em: EntityManager,\n container: AwilixContainer,\n task: UserTask,\n options: RunTaskSlaJobOptions,\n route: { stepId: string; toStepId: string; transitionId?: string }\n): Promise<void> {\n try {\n const instance = await em.findOne(WorkflowInstance, {\n id: options.workflowInstanceId,\n tenantId: options.tenantId,\n organizationId: options.organizationId,\n })\n if (!instance) return\n\n const stepHandler = container.resolve<typeof stepHandlerModule>('stepHandler')\n const transitionHandler = container.resolve<typeof transitionHandlerModule>('transitionHandler')\n const workflowExecutor = container.resolve<typeof workflowExecutorModule>('workflowExecutor')\n\n const stepInstance = await em.findOne(StepInstance, {\n id: options.stepInstanceId,\n status: 'ACTIVE',\n })\n if (stepInstance) {\n await stepHandler.exitStep(em, stepInstance, {\n userTaskId: task.id,\n slaBreached: true,\n })\n }\n\n const transitionResult = await transitionHandler.executeTransition(\n em,\n container,\n instance,\n route.stepId,\n route.toStepId,\n {\n workflowContext: instance.context,\n userId: options.userId,\n transitionId: route.transitionId,\n },\n )\n\n if (!transitionResult.success) {\n logger.error('SLA-breach transition failed', {\n component: 'task-sla',\n taskId: task.id,\n err: transitionResult.error,\n })\n return\n }\n\n await workflowExecutor.executeWorkflow(em, container, instance.id, { userId: options.userId })\n } catch (error) {\n logger.error('Failed to follow the SLA-breach route', {\n component: 'task-sla',\n taskId: task.id,\n err: error,\n })\n }\n}\n\nasync function logTaskSlaEvent(\n em: EntityManager,\n container: AwilixContainer,\n options: RunTaskSlaJobOptions,\n eventType: string,\n eventData: Record<string, unknown>\n): Promise<void> {\n try {\n const eventLogger = container.resolve<{\n logWorkflowEvent: (\n em: EntityManager,\n event: Record<string, unknown>\n ) => Promise<unknown>\n }>('eventLogger')\n\n await eventLogger.logWorkflowEvent(em, {\n workflowInstanceId: options.workflowInstanceId,\n stepInstanceId: options.stepInstanceId,\n ...(options.branchInstanceId ? { branchInstanceId: options.branchInstanceId } : {}),\n eventType,\n eventData,\n userId: options.userId,\n tenantId: options.tenantId,\n organizationId: options.organizationId,\n })\n } catch (error) {\n logger.error('Failed to log task SLA workflow event', {\n component: 'task-sla',\n taskId: options.userTaskId,\n eventType,\n err: error,\n })\n }\n}\n\nasync function emitTaskSlaEvent(\n em: EntityManager,\n eventId: 'workflows.task.reminder_due' | 'workflows.task.deadline_breached' | 'workflows.task.assigned',\n task: UserTask,\n options: RunTaskSlaJobOptions\n): Promise<void> {\n try {\n const instance = await em.findOne(WorkflowInstance, {\n id: options.workflowInstanceId,\n tenantId: options.tenantId,\n organizationId: options.organizationId,\n })\n\n await emitWorkflowsEvent(\n eventId,\n {\n taskId: task.id,\n taskName: task.taskName,\n workflowInstanceId: options.workflowInstanceId,\n workflowId: instance?.workflowId ?? null,\n workflowName: instance?.workflowId ?? '',\n assignedUserId: task.claimedBy ?? task.assignedTo ?? null,\n assignedToRoles: task.assignedToRoles ?? null,\n dueDate: options.deadlineAt,\n entityBindings: task.entityBindings ?? null,\n tenantId: options.tenantId,\n organizationId: options.organizationId,\n },\n { persistent: true }\n )\n } catch (error) {\n logger.error('Failed to emit task SLA event', {\n component: 'task-sla',\n taskId: task.id,\n eventId,\n err: error,\n })\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { LockMode } from "@mikro-orm/core";
|
|
2
|
-
import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
2
|
+
import { findOneWithDecryption, findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
3
3
|
import {
|
|
4
|
+
UserTask,
|
|
4
5
|
WorkflowInstance,
|
|
5
6
|
WorkflowEvent
|
|
6
7
|
} from "../data/entities.js";
|
|
@@ -999,6 +1000,18 @@ async function completeWorkflow(em, container, instanceId, status, result) {
|
|
|
999
1000
|
}
|
|
1000
1001
|
}
|
|
1001
1002
|
}
|
|
1003
|
+
const cancelledTasks = status === "CANCELLED" ? await findWithDecryption(
|
|
1004
|
+
em,
|
|
1005
|
+
UserTask,
|
|
1006
|
+
{
|
|
1007
|
+
workflowInstanceId: instance.id,
|
|
1008
|
+
tenantId: instance.tenantId,
|
|
1009
|
+
organizationId: instance.organizationId,
|
|
1010
|
+
status: { $in: ["PENDING", "IN_PROGRESS"] }
|
|
1011
|
+
},
|
|
1012
|
+
void 0,
|
|
1013
|
+
{ tenantId: instance.tenantId, organizationId: instance.organizationId }
|
|
1014
|
+
) : [];
|
|
1002
1015
|
const now = /* @__PURE__ */ new Date();
|
|
1003
1016
|
instance.status = status;
|
|
1004
1017
|
instance.updatedAt = now;
|
|
@@ -1021,7 +1034,22 @@ async function completeWorkflow(em, container, instanceId, status, result) {
|
|
|
1021
1034
|
break;
|
|
1022
1035
|
}
|
|
1023
1036
|
await writeRunOutcome(em, instance, status);
|
|
1037
|
+
for (const task of cancelledTasks) {
|
|
1038
|
+
task.status = "CANCELLED";
|
|
1039
|
+
task.updatedAt = now;
|
|
1040
|
+
}
|
|
1024
1041
|
await em.flush();
|
|
1042
|
+
for (const task of cancelledTasks) {
|
|
1043
|
+
await logWorkflowEvent(em, {
|
|
1044
|
+
workflowInstanceId: instanceId,
|
|
1045
|
+
stepInstanceId: task.stepInstanceId,
|
|
1046
|
+
...task.branchInstanceId ? { branchInstanceId: task.branchInstanceId } : {},
|
|
1047
|
+
eventType: "USER_TASK_CANCELLED",
|
|
1048
|
+
eventData: { taskId: task.id, taskName: task.taskName, reason: "workflow-cancelled" },
|
|
1049
|
+
tenantId: instance.tenantId,
|
|
1050
|
+
organizationId: instance.organizationId
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1025
1053
|
const eventType = status === "COMPLETED" ? "WORKFLOW_COMPLETED" : status === "FAILED" ? "WORKFLOW_FAILED" : "WORKFLOW_CANCELLED";
|
|
1026
1054
|
await logWorkflowEvent(em, {
|
|
1027
1055
|
workflowInstanceId: instanceId,
|