@objectstack/service-automation 8.0.1 → 9.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -438,8 +438,50 @@ declare class AutomationEngine implements IAutomationService {
438
438
  * restricted to the edge labelled `signal.branchLabel` (e.g. the approval
439
439
  * decision). The continuation may itself suspend again, in which case this
440
440
  * returns `{ status: 'paused', runId }` afresh.
441
+ *
442
+ * **Subflow chains (nested pause, linked-runs model).** A run paused at a
443
+ * `subflow` node (correlation `subflow:<childRunId>`) DELEGATES the signal
444
+ * down to the suspended child; a run that completes and carries
445
+ * `$parentRunId` in its context BUBBLES its output up by auto-resuming the
446
+ * parent. Both directions compose recursively, so arbitrarily nested
447
+ * subflow pauses resolve from either end (UI holds the parent run id;
448
+ * approval/wait infrastructure holds the child's).
441
449
  */
442
450
  resume(runId: string, signal?: ResumeSignal): Promise<AutomationResult>;
451
+ /**
452
+ * @param skipBubble - Set when the caller is the subflow DELEGATION path,
453
+ * which continues the parent itself after the child completes — the
454
+ * child's own up-bubble must stay off so the parent isn't resumed twice.
455
+ */
456
+ private resumeInternal;
457
+ /**
458
+ * Build the resume signal that maps a completed subflow child's output
459
+ * into its parent — mirroring the synchronous path exactly: the engine's
460
+ * standard `signal.output` merge lands it under `${subflowNodeId}.output`,
461
+ * and `signal.variables` writes the bare `config.outputVariable` when the
462
+ * child's context carries one (`$parentOutputVariable`).
463
+ */
464
+ private buildSubflowResumeSignal;
465
+ /**
466
+ * Up-bubble for the subflow chain: when a completed run carries
467
+ * `$parentRunId`, resume that parent with this run's output. Recursion via
468
+ * the parent's own completion bubbles multi-level chains. Best-effort —
469
+ * a failed parent continuation is logged, never thrown back at the
470
+ * caller who resumed the child.
471
+ */
472
+ private bubbleToParent;
473
+ /**
474
+ * Terminally fail a suspended run: consume its continuation and record a
475
+ * `failed` log so it stops surfacing as resumable. Used when a subflow
476
+ * descendant fails — the ancestor awaiting it can never be resumed.
477
+ */
478
+ private failSuspendedRun;
479
+ /**
480
+ * Walk a failed run's `$parentRunId` chain and fail each suspended
481
+ * ancestor (see {@link failSuspendedRun}). Bounded so a corrupt context
482
+ * can't loop forever.
483
+ */
484
+ private failAncestors;
443
485
  /**
444
486
  * List the runs currently suspended awaiting {@link resume} (ADR-0019).
445
487
  * Backs operability surfaces such as a "pending approvals" view.
@@ -1146,10 +1188,9 @@ declare const SysAutomationRun: Omit<{
1146
1188
  } | undefined;
1147
1189
  chart?: {
1148
1190
  chartType: "bar" | "line" | "pie" | "area" | "scatter";
1149
- xAxisField: string;
1150
- yAxisFields: string[];
1151
- aggregation?: "min" | "max" | "count" | "sum" | "avg" | undefined;
1152
- groupByField?: string | undefined;
1191
+ dataset: string;
1192
+ values: string[];
1193
+ dimensions?: string[] | undefined;
1153
1194
  } | undefined;
1154
1195
  description?: string | undefined;
1155
1196
  sharing?: {
package/dist/index.d.ts CHANGED
@@ -438,8 +438,50 @@ declare class AutomationEngine implements IAutomationService {
438
438
  * restricted to the edge labelled `signal.branchLabel` (e.g. the approval
439
439
  * decision). The continuation may itself suspend again, in which case this
440
440
  * returns `{ status: 'paused', runId }` afresh.
441
+ *
442
+ * **Subflow chains (nested pause, linked-runs model).** A run paused at a
443
+ * `subflow` node (correlation `subflow:<childRunId>`) DELEGATES the signal
444
+ * down to the suspended child; a run that completes and carries
445
+ * `$parentRunId` in its context BUBBLES its output up by auto-resuming the
446
+ * parent. Both directions compose recursively, so arbitrarily nested
447
+ * subflow pauses resolve from either end (UI holds the parent run id;
448
+ * approval/wait infrastructure holds the child's).
441
449
  */
442
450
  resume(runId: string, signal?: ResumeSignal): Promise<AutomationResult>;
451
+ /**
452
+ * @param skipBubble - Set when the caller is the subflow DELEGATION path,
453
+ * which continues the parent itself after the child completes — the
454
+ * child's own up-bubble must stay off so the parent isn't resumed twice.
455
+ */
456
+ private resumeInternal;
457
+ /**
458
+ * Build the resume signal that maps a completed subflow child's output
459
+ * into its parent — mirroring the synchronous path exactly: the engine's
460
+ * standard `signal.output` merge lands it under `${subflowNodeId}.output`,
461
+ * and `signal.variables` writes the bare `config.outputVariable` when the
462
+ * child's context carries one (`$parentOutputVariable`).
463
+ */
464
+ private buildSubflowResumeSignal;
465
+ /**
466
+ * Up-bubble for the subflow chain: when a completed run carries
467
+ * `$parentRunId`, resume that parent with this run's output. Recursion via
468
+ * the parent's own completion bubbles multi-level chains. Best-effort —
469
+ * a failed parent continuation is logged, never thrown back at the
470
+ * caller who resumed the child.
471
+ */
472
+ private bubbleToParent;
473
+ /**
474
+ * Terminally fail a suspended run: consume its continuation and record a
475
+ * `failed` log so it stops surfacing as resumable. Used when a subflow
476
+ * descendant fails — the ancestor awaiting it can never be resumed.
477
+ */
478
+ private failSuspendedRun;
479
+ /**
480
+ * Walk a failed run's `$parentRunId` chain and fail each suspended
481
+ * ancestor (see {@link failSuspendedRun}). Bounded so a corrupt context
482
+ * can't loop forever.
483
+ */
484
+ private failAncestors;
443
485
  /**
444
486
  * List the runs currently suspended awaiting {@link resume} (ADR-0019).
445
487
  * Backs operability surfaces such as a "pending approvals" view.
@@ -1146,10 +1188,9 @@ declare const SysAutomationRun: Omit<{
1146
1188
  } | undefined;
1147
1189
  chart?: {
1148
1190
  chartType: "bar" | "line" | "pie" | "area" | "scatter";
1149
- xAxisField: string;
1150
- yAxisFields: string[];
1151
- aggregation?: "min" | "max" | "count" | "sum" | "avg" | undefined;
1152
- groupByField?: string | undefined;
1191
+ dataset: string;
1192
+ values: string[];
1193
+ dimensions?: string[] | undefined;
1153
1194
  } | undefined;
1154
1195
  description?: string | undefined;
1155
1196
  sharing?: {
package/dist/index.js CHANGED
@@ -580,8 +580,24 @@ var AutomationEngine = class {
580
580
  * restricted to the edge labelled `signal.branchLabel` (e.g. the approval
581
581
  * decision). The continuation may itself suspend again, in which case this
582
582
  * returns `{ status: 'paused', runId }` afresh.
583
+ *
584
+ * **Subflow chains (nested pause, linked-runs model).** A run paused at a
585
+ * `subflow` node (correlation `subflow:<childRunId>`) DELEGATES the signal
586
+ * down to the suspended child; a run that completes and carries
587
+ * `$parentRunId` in its context BUBBLES its output up by auto-resuming the
588
+ * parent. Both directions compose recursively, so arbitrarily nested
589
+ * subflow pauses resolve from either end (UI holds the parent run id;
590
+ * approval/wait infrastructure holds the child's).
583
591
  */
584
592
  async resume(runId, signal) {
593
+ return this.resumeInternal(runId, signal, false);
594
+ }
595
+ /**
596
+ * @param skipBubble - Set when the caller is the subflow DELEGATION path,
597
+ * which continues the parent itself after the child completes — the
598
+ * child's own up-bubble must stay off so the parent isn't resumed twice.
599
+ */
600
+ async resumeInternal(runId, signal, skipBubble) {
585
601
  if (this.resuming.has(runId)) {
586
602
  return { success: false, error: `Run '${runId}' is already being resumed` };
587
603
  }
@@ -608,6 +624,35 @@ var AutomationEngine = class {
608
624
  if (!node) {
609
625
  return { success: false, error: `Suspended node '${run.nodeId}' no longer exists in flow '${run.flowName}'` };
610
626
  }
627
+ if (typeof run.correlation === "string" && run.correlation.startsWith("subflow:")) {
628
+ const childRunId = run.correlation.slice("subflow:".length);
629
+ const childRun = this.suspendedRuns.get(childRunId) ?? (this.store ? await this.store.load(childRunId).catch(() => null) : null);
630
+ if (childRun) {
631
+ const childRes = await this.resumeInternal(childRunId, signal, true);
632
+ if (childRes.status === "paused") {
633
+ if (childRes.screen && childRes.screen !== run.screen) {
634
+ await this.persistSuspendedRun({ ...run, screen: childRes.screen });
635
+ }
636
+ return {
637
+ success: true,
638
+ status: "paused",
639
+ runId,
640
+ durationMs: Date.now() - run.startTime,
641
+ screen: childRes.screen
642
+ };
643
+ }
644
+ if (!childRes.success) {
645
+ const error = `subflow run '${childRunId}' (${childRun.flowName}) failed: ${childRes.error ?? "unknown error"}`;
646
+ await this.failSuspendedRun(run, error);
647
+ return { success: false, error, durationMs: Date.now() - run.startTime };
648
+ }
649
+ signal = this.buildSubflowResumeSignal(childRun.context, childRes.output);
650
+ } else {
651
+ this.logger.warn(
652
+ `[automation] run '${runId}' is paused at subflow node '${run.nodeId}' but child run '${childRunId}' is gone \u2014 continuing without child output`
653
+ );
654
+ }
655
+ }
611
656
  await this.forgetSuspendedRun(runId);
612
657
  const variables = new Map(Object.entries(run.variables));
613
658
  if (signal?.output) {
@@ -647,6 +692,9 @@ var AutomationEngine = class {
647
692
  steps,
648
693
  output
649
694
  });
695
+ if (!skipBubble) {
696
+ await this.bubbleToParent(run, output);
697
+ }
650
698
  return { success: true, output, durationMs };
651
699
  } catch (err) {
652
700
  if (isSuspendSignal(err)) {
@@ -693,12 +741,92 @@ var AutomationEngine = class {
693
741
  steps,
694
742
  error: errorMessage
695
743
  });
744
+ if (!skipBubble) {
745
+ await this.failAncestors(run.context, errorMessage);
746
+ }
696
747
  return { success: false, error: errorMessage, durationMs };
697
748
  }
698
749
  } finally {
699
750
  this.resuming.delete(runId);
700
751
  }
701
752
  }
753
+ /**
754
+ * Build the resume signal that maps a completed subflow child's output
755
+ * into its parent — mirroring the synchronous path exactly: the engine's
756
+ * standard `signal.output` merge lands it under `${subflowNodeId}.output`,
757
+ * and `signal.variables` writes the bare `config.outputVariable` when the
758
+ * child's context carries one (`$parentOutputVariable`).
759
+ */
760
+ buildSubflowResumeSignal(childContext, childOutput) {
761
+ const outVar = childContext?.$parentOutputVariable;
762
+ return {
763
+ output: { output: childOutput ?? null },
764
+ ...typeof outVar === "string" && outVar ? { variables: { [outVar]: childOutput ?? null } } : {}
765
+ };
766
+ }
767
+ /**
768
+ * Up-bubble for the subflow chain: when a completed run carries
769
+ * `$parentRunId`, resume that parent with this run's output. Recursion via
770
+ * the parent's own completion bubbles multi-level chains. Best-effort —
771
+ * a failed parent continuation is logged, never thrown back at the
772
+ * caller who resumed the child.
773
+ */
774
+ async bubbleToParent(run, output) {
775
+ const parentRunId = run.context?.$parentRunId;
776
+ if (typeof parentRunId !== "string" || !parentRunId) return;
777
+ try {
778
+ const sig = this.buildSubflowResumeSignal(run.context, output);
779
+ const parentRes = await this.resumeInternal(parentRunId, sig, false);
780
+ if (!parentRes.success) {
781
+ this.logger.warn(
782
+ `[automation] subflow run '${run.runId}' completed but resuming parent '${parentRunId}' failed: ${parentRes.error}`
783
+ );
784
+ }
785
+ } catch (err) {
786
+ this.logger.warn(
787
+ `[automation] subflow run '${run.runId}' completed but resuming parent '${parentRunId}' threw: ${err.message}`
788
+ );
789
+ }
790
+ }
791
+ /**
792
+ * Terminally fail a suspended run: consume its continuation and record a
793
+ * `failed` log so it stops surfacing as resumable. Used when a subflow
794
+ * descendant fails — the ancestor awaiting it can never be resumed.
795
+ */
796
+ async failSuspendedRun(run, error) {
797
+ await this.forgetSuspendedRun(run.runId);
798
+ this.recordLog({
799
+ id: run.runId,
800
+ flowName: run.flowName,
801
+ flowVersion: run.flowVersion,
802
+ status: "failed",
803
+ startedAt: run.startedAt,
804
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
805
+ durationMs: Date.now() - run.startTime,
806
+ trigger: {
807
+ type: run.context?.event ?? "manual",
808
+ userId: run.context?.userId,
809
+ object: run.context?.object
810
+ },
811
+ steps: run.steps,
812
+ error
813
+ });
814
+ }
815
+ /**
816
+ * Walk a failed run's `$parentRunId` chain and fail each suspended
817
+ * ancestor (see {@link failSuspendedRun}). Bounded so a corrupt context
818
+ * can't loop forever.
819
+ */
820
+ async failAncestors(context, error) {
821
+ let parentId = context?.$parentRunId;
822
+ let hops = 0;
823
+ while (typeof parentId === "string" && parentId && hops++ < 32) {
824
+ const parent = this.suspendedRuns.get(parentId) ?? (this.store ? await this.store.load(parentId).catch(() => null) : null);
825
+ if (!parent) return;
826
+ await this.failSuspendedRun(parent, `subflow descendant failed: ${error}`);
827
+ parentId = parent.context?.$parentRunId;
828
+ }
829
+ }
702
830
  /**
703
831
  * List the runs currently suspended awaiting {@link resume} (ADR-0019).
704
832
  * Backs operability surfaces such as a "pending approvals" view.
@@ -2470,10 +2598,11 @@ function registerWaitNode(engine, ctx) {
2470
2598
  const runId = variables.get("$runId");
2471
2599
  if (eventType === "timer") {
2472
2600
  const durationMs = parseIsoDuration(wec.timerDuration ?? loose.timerDuration ?? loose.duration) ?? (typeof wec.timeoutMs === "number" ? wec.timeoutMs : void 0) ?? (typeof loose.timeoutMs === "number" ? loose.timeoutMs : void 0);
2601
+ const at = durationMs && durationMs > 0 ? new Date(Date.now() + durationMs).toISOString() : void 0;
2602
+ const output = at ? { waitUntil: at } : void 0;
2473
2603
  const job = getJobService();
2474
- if (job && runId != null && durationMs && durationMs > 0) {
2604
+ if (job && runId != null && at) {
2475
2605
  const jobName = `flow-wait:${String(runId)}:${node.id}`;
2476
- const at = new Date(Date.now() + durationMs).toISOString();
2477
2606
  try {
2478
2607
  await job.schedule(jobName, { type: "once", at }, async () => {
2479
2608
  try {
@@ -2485,7 +2614,7 @@ function registerWaitNode(engine, ctx) {
2485
2614
  }
2486
2615
  }
2487
2616
  });
2488
- return { success: true, suspend: true, correlation: jobName };
2617
+ return { success: true, suspend: true, correlation: jobName, output };
2489
2618
  } catch (err) {
2490
2619
  ctx.logger.warn(
2491
2620
  `[wait] node '${node.id}': failed to schedule timer resume (${err?.message ?? err}); suspending without auto-resume (resume it via resume(runId))`
@@ -2496,7 +2625,7 @@ function registerWaitNode(engine, ctx) {
2496
2625
  `[wait] node '${node.id}': no job service registered \u2014 suspending without an auto-resume timer (resume it via resume(runId), or install the job service for durable timers)`
2497
2626
  );
2498
2627
  }
2499
- return { success: true, suspend: true, correlation: `timer:${node.id}` };
2628
+ return { success: true, suspend: true, correlation: `timer:${node.id}`, output };
2500
2629
  }
2501
2630
  const signal = String(wec.signalName ?? loose.signalName ?? loose.signal ?? `wait:${node.id}`);
2502
2631
  return { success: true, suspend: true, correlation: signal };
@@ -2504,6 +2633,54 @@ function registerWaitNode(engine, ctx) {
2504
2633
  });
2505
2634
  ctx.logger.info("[Wait Node] 1 built-in node executor registered");
2506
2635
  }
2636
+ async function rearmSuspendedWaitTimers(engine, store, job, logger) {
2637
+ let runs;
2638
+ try {
2639
+ runs = await store.list();
2640
+ } catch (err) {
2641
+ logger.warn(`[wait] timer re-arm: failed to list suspended runs: ${err?.message ?? err}`);
2642
+ return 0;
2643
+ }
2644
+ let rearmed = 0;
2645
+ for (const run of runs) {
2646
+ const wakeAt = run.variables?.[`${run.nodeId}.waitUntil`];
2647
+ if (typeof wakeAt !== "string" || !wakeAt) continue;
2648
+ const atMs = Date.parse(wakeAt);
2649
+ if (Number.isNaN(atMs)) continue;
2650
+ if (atMs <= Date.now()) {
2651
+ try {
2652
+ await engine.resume(run.runId);
2653
+ rearmed++;
2654
+ } catch (err) {
2655
+ logger.warn(`[wait] timer re-arm: resume of overdue run '${run.runId}' failed: ${err?.message ?? err}`);
2656
+ }
2657
+ continue;
2658
+ }
2659
+ if (!job) {
2660
+ logger.warn(
2661
+ `[wait] timer re-arm: run '${run.runId}' waits until ${wakeAt} but no job service is registered \u2014 resume it externally via resume(runId)`
2662
+ );
2663
+ continue;
2664
+ }
2665
+ const jobName = `flow-wait:${run.runId}:${run.nodeId}`;
2666
+ try {
2667
+ await job.schedule(jobName, { type: "once", at: wakeAt }, async () => {
2668
+ try {
2669
+ await engine.resume(run.runId);
2670
+ } finally {
2671
+ try {
2672
+ await job.cancel?.(jobName);
2673
+ } catch {
2674
+ }
2675
+ }
2676
+ });
2677
+ rearmed++;
2678
+ } catch (err) {
2679
+ logger.warn(`[wait] timer re-arm: failed to re-schedule run '${run.runId}': ${err?.message ?? err}`);
2680
+ }
2681
+ }
2682
+ return rearmed;
2683
+ }
2507
2684
  function parseIsoDuration(input) {
2508
2685
  if (typeof input === "number" && Number.isFinite(input)) return input > 0 ? input : void 0;
2509
2686
  if (typeof input !== "string") return void 0;
@@ -2535,7 +2712,10 @@ function registerSubflowNode(engine, ctx) {
2535
2712
  description: "Invoke another flow as a reusable step and capture its output.",
2536
2713
  icon: "workflow",
2537
2714
  category: "logic",
2538
- source: "builtin"
2715
+ source: "builtin",
2716
+ // A child that suspends (approval/screen/wait) suspends this node too —
2717
+ // the parent run pauses here and resumes when the child completes.
2718
+ supportsPause: true
2539
2719
  }),
2540
2720
  async execute(node, variables, context) {
2541
2721
  const cfg = node.config ?? {};
@@ -2552,22 +2732,33 @@ function registerSubflowNode(engine, ctx) {
2552
2732
  }
2553
2733
  const rawInput = cfg.input && typeof cfg.input === "object" ? cfg.input : {};
2554
2734
  const params = interpolate(rawInput, variables, context ?? {});
2735
+ const outVar = typeof cfg.outputVariable === "string" && cfg.outputVariable ? cfg.outputVariable : void 0;
2736
+ const parentRunId = variables.get("$runId");
2555
2737
  const childContext = {
2556
2738
  ...context ?? {},
2557
2739
  $subflowDepth: depth + 1,
2558
- params
2740
+ params,
2741
+ ...parentRunId != null ? {
2742
+ $parentRunId: String(parentRunId),
2743
+ $parentNodeId: node.id,
2744
+ ...outVar ? { $parentOutputVariable: outVar } : {}
2745
+ } : {}
2559
2746
  };
2560
2747
  const child = await engine.execute(flowName, childContext);
2561
2748
  if (child.status === "paused") {
2749
+ if (!child.runId) {
2750
+ return { success: false, error: `subflow '${flowName}' paused without a run id \u2014 cannot link the runs` };
2751
+ }
2562
2752
  return {
2563
- success: false,
2564
- error: `subflow '${flowName}' suspended at a pausing node \u2014 a nested approval/screen/wait pause from a subflow is not yet supported`
2753
+ success: true,
2754
+ suspend: true,
2755
+ correlation: `subflow:${child.runId}`,
2756
+ screen: child.screen
2565
2757
  };
2566
2758
  }
2567
2759
  if (!child.success) {
2568
2760
  return { success: false, error: `subflow '${flowName}' failed: ${child.error ?? "unknown error"}` };
2569
2761
  }
2570
- const outVar = typeof cfg.outputVariable === "string" && cfg.outputVariable ? cfg.outputVariable : void 0;
2571
2762
  if (outVar) variables.set(outVar, child.output ?? null);
2572
2763
  return { success: true, output: { output: child.output ?? null } };
2573
2764
  }
@@ -2636,9 +2827,8 @@ var AutomationServicePlugin = class {
2636
2827
  ctx.logger.info("[Automation] Engine initialized");
2637
2828
  }
2638
2829
  async start(ctx) {
2639
- console.warn("[Automation:start] entering start()");
2640
2830
  if (!this.engine) {
2641
- console.warn("[Automation:start] engine missing, bailing");
2831
+ ctx.logger.warn("[Automation] start() called before init() \u2014 engine missing, skipping");
2642
2832
  return;
2643
2833
  }
2644
2834
  await ctx.trigger("automation:ready", this.engine);
@@ -2646,6 +2836,7 @@ var AutomationServicePlugin = class {
2646
2836
  ctx.logger.info(
2647
2837
  `[Automation] Engine started with ${nodeTypes.length} node types: ${nodeTypes.join(", ") || "(none)"}`
2648
2838
  );
2839
+ let durableStore = null;
2649
2840
  if ((this.options.suspendedRunStore ?? "auto") !== "memory") {
2650
2841
  let dataEngine = null;
2651
2842
  try {
@@ -2657,7 +2848,8 @@ var AutomationServicePlugin = class {
2657
2848
  }
2658
2849
  }
2659
2850
  if (dataEngine && typeof dataEngine.find === "function" && typeof dataEngine.insert === "function") {
2660
- this.engine.setSuspendedRunStore(new ObjectStoreSuspendedRunStore(dataEngine, ctx.logger));
2851
+ durableStore = new ObjectStoreSuspendedRunStore(dataEngine, ctx.logger);
2852
+ this.engine.setSuspendedRunStore(durableStore);
2661
2853
  ctx.logger.info("[Automation] Suspended-run persistence enabled (sys_automation_run)");
2662
2854
  } else {
2663
2855
  ctx.logger.info("[Automation] No ObjectQL engine \u2014 suspended runs kept in-memory only");
@@ -2666,14 +2858,14 @@ var AutomationServicePlugin = class {
2666
2858
  try {
2667
2859
  const ql = ctx.getService("objectql");
2668
2860
  if (!ql) {
2669
- console.warn("[Automation] objectql service not found at start()");
2861
+ ctx.logger.debug("[Automation] objectql service not found at start()");
2670
2862
  } else if (!ql.registry) {
2671
- console.warn("[Automation] objectql.registry is undefined at start()");
2863
+ ctx.logger.debug("[Automation] objectql.registry is undefined at start()");
2672
2864
  } else if (typeof ql.registry.listItems !== "function") {
2673
- console.warn("[Automation] objectql.registry.listItems is not a function");
2865
+ ctx.logger.debug("[Automation] objectql.registry.listItems is not a function");
2674
2866
  }
2675
2867
  const flows = ql?.registry?.listItems?.("flow") ?? [];
2676
- console.warn(`[Automation] flow pull: registry returned ${flows.length} flow(s)`);
2868
+ ctx.logger.debug(`[Automation] flow pull: registry returned ${flows.length} flow(s)`);
2677
2869
  let registered = 0;
2678
2870
  for (const f of flows) {
2679
2871
  const def = f;
@@ -2693,6 +2885,21 @@ var AutomationServicePlugin = class {
2693
2885
  const msg = err instanceof Error ? err.message : String(err);
2694
2886
  ctx.logger.warn(`[Automation] flow pull from ObjectQL registry failed: ${msg}`);
2695
2887
  }
2888
+ if (durableStore) {
2889
+ let job;
2890
+ try {
2891
+ job = ctx.getService("job");
2892
+ } catch {
2893
+ }
2894
+ try {
2895
+ const rearmed = await rearmSuspendedWaitTimers(this.engine, durableStore, job, ctx.logger);
2896
+ if (rearmed > 0) {
2897
+ ctx.logger.info(`[Automation] Re-armed ${rearmed} suspended wait timer(s) after restart`);
2898
+ }
2899
+ } catch (err) {
2900
+ ctx.logger.warn(`[Automation] wait-timer re-arm failed: ${err.message}`);
2901
+ }
2902
+ }
2696
2903
  }
2697
2904
  async destroy() {
2698
2905
  this.engine = void 0;