@objectstack/service-automation 8.0.1 → 9.0.1

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.cjs CHANGED
@@ -496,6 +496,8 @@ var AutomationEngine = class {
496
496
  }
497
497
  const runId = this.nextRunId();
498
498
  variables.set("$runId", runId);
499
+ variables.set("$flowName", flowName);
500
+ variables.set("$flowLabel", flow.label ?? flowName);
499
501
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
500
502
  const steps = [];
501
503
  try {
@@ -616,8 +618,24 @@ var AutomationEngine = class {
616
618
  * restricted to the edge labelled `signal.branchLabel` (e.g. the approval
617
619
  * decision). The continuation may itself suspend again, in which case this
618
620
  * returns `{ status: 'paused', runId }` afresh.
621
+ *
622
+ * **Subflow chains (nested pause, linked-runs model).** A run paused at a
623
+ * `subflow` node (correlation `subflow:<childRunId>`) DELEGATES the signal
624
+ * down to the suspended child; a run that completes and carries
625
+ * `$parentRunId` in its context BUBBLES its output up by auto-resuming the
626
+ * parent. Both directions compose recursively, so arbitrarily nested
627
+ * subflow pauses resolve from either end (UI holds the parent run id;
628
+ * approval/wait infrastructure holds the child's).
619
629
  */
620
630
  async resume(runId, signal) {
631
+ return this.resumeInternal(runId, signal, false);
632
+ }
633
+ /**
634
+ * @param skipBubble - Set when the caller is the subflow DELEGATION path,
635
+ * which continues the parent itself after the child completes — the
636
+ * child's own up-bubble must stay off so the parent isn't resumed twice.
637
+ */
638
+ async resumeInternal(runId, signal, skipBubble) {
621
639
  if (this.resuming.has(runId)) {
622
640
  return { success: false, error: `Run '${runId}' is already being resumed` };
623
641
  }
@@ -644,6 +662,35 @@ var AutomationEngine = class {
644
662
  if (!node) {
645
663
  return { success: false, error: `Suspended node '${run.nodeId}' no longer exists in flow '${run.flowName}'` };
646
664
  }
665
+ if (typeof run.correlation === "string" && run.correlation.startsWith("subflow:")) {
666
+ const childRunId = run.correlation.slice("subflow:".length);
667
+ const childRun = this.suspendedRuns.get(childRunId) ?? (this.store ? await this.store.load(childRunId).catch(() => null) : null);
668
+ if (childRun) {
669
+ const childRes = await this.resumeInternal(childRunId, signal, true);
670
+ if (childRes.status === "paused") {
671
+ if (childRes.screen && childRes.screen !== run.screen) {
672
+ await this.persistSuspendedRun({ ...run, screen: childRes.screen });
673
+ }
674
+ return {
675
+ success: true,
676
+ status: "paused",
677
+ runId,
678
+ durationMs: Date.now() - run.startTime,
679
+ screen: childRes.screen
680
+ };
681
+ }
682
+ if (!childRes.success) {
683
+ const error = `subflow run '${childRunId}' (${childRun.flowName}) failed: ${childRes.error ?? "unknown error"}`;
684
+ await this.failSuspendedRun(run, error);
685
+ return { success: false, error, durationMs: Date.now() - run.startTime };
686
+ }
687
+ signal = this.buildSubflowResumeSignal(childRun.context, childRes.output);
688
+ } else {
689
+ this.logger.warn(
690
+ `[automation] run '${runId}' is paused at subflow node '${run.nodeId}' but child run '${childRunId}' is gone \u2014 continuing without child output`
691
+ );
692
+ }
693
+ }
647
694
  await this.forgetSuspendedRun(runId);
648
695
  const variables = new Map(Object.entries(run.variables));
649
696
  if (signal?.output) {
@@ -659,7 +706,11 @@ var AutomationEngine = class {
659
706
  const steps = run.steps;
660
707
  const context = run.context;
661
708
  try {
662
- await this.traverseNext(node, flow, variables, context, steps, signal?.branchLabel);
709
+ if (typeof run.correlation === "string" && run.correlation.startsWith("map:")) {
710
+ await this.executeNode(node, flow, variables, context, steps);
711
+ } else {
712
+ await this.traverseNext(node, flow, variables, context, steps, signal?.branchLabel);
713
+ }
663
714
  const output = {};
664
715
  if (flow.variables) {
665
716
  for (const v of flow.variables) {
@@ -683,6 +734,9 @@ var AutomationEngine = class {
683
734
  steps,
684
735
  output
685
736
  });
737
+ if (!skipBubble) {
738
+ await this.bubbleToParent(run, output);
739
+ }
686
740
  return { success: true, output, durationMs };
687
741
  } catch (err) {
688
742
  if (isSuspendSignal(err)) {
@@ -729,12 +783,94 @@ var AutomationEngine = class {
729
783
  steps,
730
784
  error: errorMessage
731
785
  });
786
+ if (!skipBubble) {
787
+ await this.failAncestors(run.context, errorMessage);
788
+ }
732
789
  return { success: false, error: errorMessage, durationMs };
733
790
  }
734
791
  } finally {
735
792
  this.resuming.delete(runId);
736
793
  }
737
794
  }
795
+ /**
796
+ * Build the resume signal that maps a completed subflow child's output
797
+ * into its parent — mirroring the synchronous path exactly: the engine's
798
+ * standard `signal.output` merge lands it under `${subflowNodeId}.output`,
799
+ * and `signal.variables` writes the bare `config.outputVariable` when the
800
+ * child's context carries one (`$parentOutputVariable`).
801
+ */
802
+ buildSubflowResumeSignal(childContext, childOutput) {
803
+ const outVar = childContext?.$parentOutputVariable;
804
+ return {
805
+ output: { output: childOutput ?? null },
806
+ ...typeof outVar === "string" && outVar ? { variables: { [outVar]: childOutput ?? null } } : {}
807
+ };
808
+ }
809
+ /**
810
+ * Up-bubble for the subflow chain: when a completed run carries
811
+ * `$parentRunId`, resume that parent with this run's output. Recursion via
812
+ * the parent's own completion bubbles multi-level chains. Best-effort —
813
+ * a failed parent continuation is logged, never thrown back at the
814
+ * caller who resumed the child.
815
+ */
816
+ async bubbleToParent(run, output) {
817
+ const ctx = run.context;
818
+ const parentRunId = ctx?.$parentRunId;
819
+ if (typeof parentRunId !== "string" || !parentRunId) return;
820
+ try {
821
+ const mapNode = ctx?.$parentMapNode;
822
+ const sig = typeof mapNode === "string" && mapNode ? { variables: { [`${mapNode}.$mapItemOutput`]: output ?? null, [`${mapNode}.$mapItemDone`]: true } } : this.buildSubflowResumeSignal(run.context, output);
823
+ const parentRes = await this.resumeInternal(parentRunId, sig, false);
824
+ if (!parentRes.success) {
825
+ this.logger.warn(
826
+ `[automation] subflow run '${run.runId}' completed but resuming parent '${parentRunId}' failed: ${parentRes.error}`
827
+ );
828
+ }
829
+ } catch (err) {
830
+ this.logger.warn(
831
+ `[automation] subflow run '${run.runId}' completed but resuming parent '${parentRunId}' threw: ${err.message}`
832
+ );
833
+ }
834
+ }
835
+ /**
836
+ * Terminally fail a suspended run: consume its continuation and record a
837
+ * `failed` log so it stops surfacing as resumable. Used when a subflow
838
+ * descendant fails — the ancestor awaiting it can never be resumed.
839
+ */
840
+ async failSuspendedRun(run, error) {
841
+ await this.forgetSuspendedRun(run.runId);
842
+ this.recordLog({
843
+ id: run.runId,
844
+ flowName: run.flowName,
845
+ flowVersion: run.flowVersion,
846
+ status: "failed",
847
+ startedAt: run.startedAt,
848
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
849
+ durationMs: Date.now() - run.startTime,
850
+ trigger: {
851
+ type: run.context?.event ?? "manual",
852
+ userId: run.context?.userId,
853
+ object: run.context?.object
854
+ },
855
+ steps: run.steps,
856
+ error
857
+ });
858
+ }
859
+ /**
860
+ * Walk a failed run's `$parentRunId` chain and fail each suspended
861
+ * ancestor (see {@link failSuspendedRun}). Bounded so a corrupt context
862
+ * can't loop forever.
863
+ */
864
+ async failAncestors(context, error) {
865
+ let parentId = context?.$parentRunId;
866
+ let hops = 0;
867
+ while (typeof parentId === "string" && parentId && hops++ < 32) {
868
+ const parent = this.suspendedRuns.get(parentId) ?? (this.store ? await this.store.load(parentId).catch(() => null) : null);
869
+ if (!parent) return;
870
+ await this.failSuspendedRun(parent, `subflow descendant failed: ${error}`);
871
+ parentId = parent.context?.$parentRunId;
872
+ }
873
+ }
738
874
  /**
739
875
  * List the runs currently suspended awaiting {@link resume} (ADR-0019).
740
876
  * Backs operability surfaces such as a "pending approvals" view.
@@ -2506,10 +2642,11 @@ function registerWaitNode(engine, ctx) {
2506
2642
  const runId = variables.get("$runId");
2507
2643
  if (eventType === "timer") {
2508
2644
  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);
2645
+ const at = durationMs && durationMs > 0 ? new Date(Date.now() + durationMs).toISOString() : void 0;
2646
+ const output = at ? { waitUntil: at } : void 0;
2509
2647
  const job = getJobService();
2510
- if (job && runId != null && durationMs && durationMs > 0) {
2648
+ if (job && runId != null && at) {
2511
2649
  const jobName = `flow-wait:${String(runId)}:${node.id}`;
2512
- const at = new Date(Date.now() + durationMs).toISOString();
2513
2650
  try {
2514
2651
  await job.schedule(jobName, { type: "once", at }, async () => {
2515
2652
  try {
@@ -2521,7 +2658,7 @@ function registerWaitNode(engine, ctx) {
2521
2658
  }
2522
2659
  }
2523
2660
  });
2524
- return { success: true, suspend: true, correlation: jobName };
2661
+ return { success: true, suspend: true, correlation: jobName, output };
2525
2662
  } catch (err) {
2526
2663
  ctx.logger.warn(
2527
2664
  `[wait] node '${node.id}': failed to schedule timer resume (${err?.message ?? err}); suspending without auto-resume (resume it via resume(runId))`
@@ -2532,7 +2669,7 @@ function registerWaitNode(engine, ctx) {
2532
2669
  `[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)`
2533
2670
  );
2534
2671
  }
2535
- return { success: true, suspend: true, correlation: `timer:${node.id}` };
2672
+ return { success: true, suspend: true, correlation: `timer:${node.id}`, output };
2536
2673
  }
2537
2674
  const signal = String(wec.signalName ?? loose.signalName ?? loose.signal ?? `wait:${node.id}`);
2538
2675
  return { success: true, suspend: true, correlation: signal };
@@ -2540,6 +2677,54 @@ function registerWaitNode(engine, ctx) {
2540
2677
  });
2541
2678
  ctx.logger.info("[Wait Node] 1 built-in node executor registered");
2542
2679
  }
2680
+ async function rearmSuspendedWaitTimers(engine, store, job, logger) {
2681
+ let runs;
2682
+ try {
2683
+ runs = await store.list();
2684
+ } catch (err) {
2685
+ logger.warn(`[wait] timer re-arm: failed to list suspended runs: ${err?.message ?? err}`);
2686
+ return 0;
2687
+ }
2688
+ let rearmed = 0;
2689
+ for (const run of runs) {
2690
+ const wakeAt = run.variables?.[`${run.nodeId}.waitUntil`];
2691
+ if (typeof wakeAt !== "string" || !wakeAt) continue;
2692
+ const atMs = Date.parse(wakeAt);
2693
+ if (Number.isNaN(atMs)) continue;
2694
+ if (atMs <= Date.now()) {
2695
+ try {
2696
+ await engine.resume(run.runId);
2697
+ rearmed++;
2698
+ } catch (err) {
2699
+ logger.warn(`[wait] timer re-arm: resume of overdue run '${run.runId}' failed: ${err?.message ?? err}`);
2700
+ }
2701
+ continue;
2702
+ }
2703
+ if (!job) {
2704
+ logger.warn(
2705
+ `[wait] timer re-arm: run '${run.runId}' waits until ${wakeAt} but no job service is registered \u2014 resume it externally via resume(runId)`
2706
+ );
2707
+ continue;
2708
+ }
2709
+ const jobName = `flow-wait:${run.runId}:${run.nodeId}`;
2710
+ try {
2711
+ await job.schedule(jobName, { type: "once", at: wakeAt }, async () => {
2712
+ try {
2713
+ await engine.resume(run.runId);
2714
+ } finally {
2715
+ try {
2716
+ await job.cancel?.(jobName);
2717
+ } catch {
2718
+ }
2719
+ }
2720
+ });
2721
+ rearmed++;
2722
+ } catch (err) {
2723
+ logger.warn(`[wait] timer re-arm: failed to re-schedule run '${run.runId}': ${err?.message ?? err}`);
2724
+ }
2725
+ }
2726
+ return rearmed;
2727
+ }
2543
2728
  function parseIsoDuration(input) {
2544
2729
  if (typeof input === "number" && Number.isFinite(input)) return input > 0 ? input : void 0;
2545
2730
  if (typeof input !== "string") return void 0;
@@ -2571,7 +2756,10 @@ function registerSubflowNode(engine, ctx) {
2571
2756
  description: "Invoke another flow as a reusable step and capture its output.",
2572
2757
  icon: "workflow",
2573
2758
  category: "logic",
2574
- source: "builtin"
2759
+ source: "builtin",
2760
+ // A child that suspends (approval/screen/wait) suspends this node too —
2761
+ // the parent run pauses here and resumes when the child completes.
2762
+ supportsPause: true
2575
2763
  }),
2576
2764
  async execute(node, variables, context) {
2577
2765
  const cfg = node.config ?? {};
@@ -2588,22 +2776,33 @@ function registerSubflowNode(engine, ctx) {
2588
2776
  }
2589
2777
  const rawInput = cfg.input && typeof cfg.input === "object" ? cfg.input : {};
2590
2778
  const params = interpolate(rawInput, variables, context ?? {});
2779
+ const outVar = typeof cfg.outputVariable === "string" && cfg.outputVariable ? cfg.outputVariable : void 0;
2780
+ const parentRunId = variables.get("$runId");
2591
2781
  const childContext = {
2592
2782
  ...context ?? {},
2593
2783
  $subflowDepth: depth + 1,
2594
- params
2784
+ params,
2785
+ ...parentRunId != null ? {
2786
+ $parentRunId: String(parentRunId),
2787
+ $parentNodeId: node.id,
2788
+ ...outVar ? { $parentOutputVariable: outVar } : {}
2789
+ } : {}
2595
2790
  };
2596
2791
  const child = await engine.execute(flowName, childContext);
2597
2792
  if (child.status === "paused") {
2793
+ if (!child.runId) {
2794
+ return { success: false, error: `subflow '${flowName}' paused without a run id \u2014 cannot link the runs` };
2795
+ }
2598
2796
  return {
2599
- success: false,
2600
- error: `subflow '${flowName}' suspended at a pausing node \u2014 a nested approval/screen/wait pause from a subflow is not yet supported`
2797
+ success: true,
2798
+ suspend: true,
2799
+ correlation: `subflow:${child.runId}`,
2800
+ screen: child.screen
2601
2801
  };
2602
2802
  }
2603
2803
  if (!child.success) {
2604
2804
  return { success: false, error: `subflow '${flowName}' failed: ${child.error ?? "unknown error"}` };
2605
2805
  }
2606
- const outVar = typeof cfg.outputVariable === "string" && cfg.outputVariable ? cfg.outputVariable : void 0;
2607
2806
  if (outVar) variables.set(outVar, child.output ?? null);
2608
2807
  return { success: true, output: { output: child.output ?? null } };
2609
2808
  }
@@ -2611,6 +2810,96 @@ function registerSubflowNode(engine, ctx) {
2611
2810
  ctx.logger.info("[Subflow Node] 1 built-in node executor registered");
2612
2811
  }
2613
2812
 
2813
+ // src/builtin/map-node.ts
2814
+ var import_automation13 = require("@objectstack/spec/automation");
2815
+ var MAX_MAP_ITEMS = 1e4;
2816
+ function registerMapNode(engine, ctx) {
2817
+ engine.registerNodeExecutor({
2818
+ type: "map",
2819
+ descriptor: (0, import_automation13.defineActionDescriptor)({
2820
+ type: "map",
2821
+ version: "1.0.0",
2822
+ name: "Map",
2823
+ description: "Run a per-item subflow for each element of a collection, one at a time (each item may pause).",
2824
+ icon: "list-check",
2825
+ category: "logic",
2826
+ source: "builtin",
2827
+ // Each item's subflow may pause, so the map suspends and resumes per item.
2828
+ supportsPause: true,
2829
+ isAsync: true
2830
+ }),
2831
+ async execute(node, variables, context) {
2832
+ const cfg = node.config ?? {};
2833
+ const flowName = typeof cfg.flowName === "string" ? cfg.flowName : typeof cfg.flow === "string" ? cfg.flow : void 0;
2834
+ if (!flowName) {
2835
+ return { success: false, error: `map '${node.id}': config.flowName (the per-item subflow) is required` };
2836
+ }
2837
+ const iteratorVariable = typeof cfg.iteratorVariable === "string" && cfg.iteratorVariable ? cfg.iteratorVariable : "item";
2838
+ const indexVariable = typeof cfg.indexVariable === "string" && cfg.indexVariable ? cfg.indexVariable : void 0;
2839
+ const outVar = typeof cfg.outputVariable === "string" && cfg.outputVariable ? cfg.outputVariable : void 0;
2840
+ const rawCollection = cfg.collection;
2841
+ let collection;
2842
+ if (Array.isArray(rawCollection)) {
2843
+ collection = rawCollection;
2844
+ } else if (typeof rawCollection === "string") {
2845
+ collection = interpolate(rawCollection, variables, context ?? {});
2846
+ if (collection == null && variables.has(rawCollection)) collection = variables.get(rawCollection);
2847
+ }
2848
+ if (!Array.isArray(collection)) {
2849
+ return { success: false, error: `map '${node.id}': collection '${String(rawCollection)}' did not resolve to an array` };
2850
+ }
2851
+ if (collection.length > MAX_MAP_ITEMS) {
2852
+ return { success: false, error: `map '${node.id}': collection length ${collection.length} exceeds the ${MAX_MAP_ITEMS} cap` };
2853
+ }
2854
+ const stateKey = `${node.id}.$mapState`;
2855
+ const state = variables.get(stateKey) ?? {
2856
+ started: 0,
2857
+ results: []
2858
+ };
2859
+ if (variables.get(`${node.id}.$mapItemDone`) === true) {
2860
+ state.results.push(variables.get(`${node.id}.$mapItemOutput`) ?? null);
2861
+ variables.delete(`${node.id}.$mapItemDone`);
2862
+ variables.delete(`${node.id}.$mapItemOutput`);
2863
+ }
2864
+ const parentRunId = variables.get("$runId");
2865
+ while (state.started < collection.length) {
2866
+ const idx = state.started;
2867
+ const item = collection[idx];
2868
+ variables.set(iteratorVariable, item);
2869
+ if (indexVariable) variables.set(indexVariable, idx);
2870
+ const rawInput = cfg.input && typeof cfg.input === "object" ? cfg.input : {};
2871
+ const params = interpolate(rawInput, variables, context ?? {});
2872
+ const itemIsRecord = item != null && typeof item === "object" && typeof item.id === "string";
2873
+ const itemObject = typeof cfg.itemObject === "string" ? cfg.itemObject : context?.object;
2874
+ const childContext = {
2875
+ ...context ?? {},
2876
+ params,
2877
+ ...itemIsRecord ? { record: item, object: itemObject } : {},
2878
+ ...parentRunId != null ? { $parentRunId: String(parentRunId), $parentMapNode: node.id } : {}
2879
+ };
2880
+ const child = await engine.execute(flowName, childContext);
2881
+ if (child.status === "paused") {
2882
+ if (!child.runId) {
2883
+ return { success: false, error: `map '${node.id}': item ${idx} paused without a run id \u2014 cannot link the runs` };
2884
+ }
2885
+ state.started = idx + 1;
2886
+ variables.set(stateKey, state);
2887
+ return { success: true, suspend: true, correlation: `map:${child.runId}` };
2888
+ }
2889
+ if (!child.success) {
2890
+ return { success: false, error: `map '${node.id}': item ${idx} (subflow '${flowName}') failed: ${child.error ?? "unknown error"}` };
2891
+ }
2892
+ state.started = idx + 1;
2893
+ state.results.push(child.output ?? null);
2894
+ }
2895
+ variables.set(stateKey, state);
2896
+ if (outVar) variables.set(outVar, state.results);
2897
+ return { success: true, output: { results: state.results, count: state.results.length } };
2898
+ }
2899
+ });
2900
+ ctx.logger.info("[Map Node] 1 built-in node executor registered");
2901
+ }
2902
+
2614
2903
  // src/builtin/index.ts
2615
2904
  function installBuiltinNodes(engine, ctx) {
2616
2905
  registerLogicNodes(engine, ctx);
@@ -2624,6 +2913,7 @@ function installBuiltinNodes(engine, ctx) {
2624
2913
  registerNotifyNode(engine, ctx);
2625
2914
  registerWaitNode(engine, ctx);
2626
2915
  registerSubflowNode(engine, ctx);
2916
+ registerMapNode(engine, ctx);
2627
2917
  const types = engine.getRegisteredNodeTypes();
2628
2918
  ctx.logger.info(
2629
2919
  `[Automation] ${types.length} built-in node executors installed: ${types.join(", ")}`
@@ -2672,9 +2962,8 @@ var AutomationServicePlugin = class {
2672
2962
  ctx.logger.info("[Automation] Engine initialized");
2673
2963
  }
2674
2964
  async start(ctx) {
2675
- console.warn("[Automation:start] entering start()");
2676
2965
  if (!this.engine) {
2677
- console.warn("[Automation:start] engine missing, bailing");
2966
+ ctx.logger.warn("[Automation] start() called before init() \u2014 engine missing, skipping");
2678
2967
  return;
2679
2968
  }
2680
2969
  await ctx.trigger("automation:ready", this.engine);
@@ -2682,6 +2971,7 @@ var AutomationServicePlugin = class {
2682
2971
  ctx.logger.info(
2683
2972
  `[Automation] Engine started with ${nodeTypes.length} node types: ${nodeTypes.join(", ") || "(none)"}`
2684
2973
  );
2974
+ let durableStore = null;
2685
2975
  if ((this.options.suspendedRunStore ?? "auto") !== "memory") {
2686
2976
  let dataEngine = null;
2687
2977
  try {
@@ -2693,7 +2983,8 @@ var AutomationServicePlugin = class {
2693
2983
  }
2694
2984
  }
2695
2985
  if (dataEngine && typeof dataEngine.find === "function" && typeof dataEngine.insert === "function") {
2696
- this.engine.setSuspendedRunStore(new ObjectStoreSuspendedRunStore(dataEngine, ctx.logger));
2986
+ durableStore = new ObjectStoreSuspendedRunStore(dataEngine, ctx.logger);
2987
+ this.engine.setSuspendedRunStore(durableStore);
2697
2988
  ctx.logger.info("[Automation] Suspended-run persistence enabled (sys_automation_run)");
2698
2989
  } else {
2699
2990
  ctx.logger.info("[Automation] No ObjectQL engine \u2014 suspended runs kept in-memory only");
@@ -2702,14 +2993,14 @@ var AutomationServicePlugin = class {
2702
2993
  try {
2703
2994
  const ql = ctx.getService("objectql");
2704
2995
  if (!ql) {
2705
- console.warn("[Automation] objectql service not found at start()");
2996
+ ctx.logger.debug("[Automation] objectql service not found at start()");
2706
2997
  } else if (!ql.registry) {
2707
- console.warn("[Automation] objectql.registry is undefined at start()");
2998
+ ctx.logger.debug("[Automation] objectql.registry is undefined at start()");
2708
2999
  } else if (typeof ql.registry.listItems !== "function") {
2709
- console.warn("[Automation] objectql.registry.listItems is not a function");
3000
+ ctx.logger.debug("[Automation] objectql.registry.listItems is not a function");
2710
3001
  }
2711
3002
  const flows = ql?.registry?.listItems?.("flow") ?? [];
2712
- console.warn(`[Automation] flow pull: registry returned ${flows.length} flow(s)`);
3003
+ ctx.logger.debug(`[Automation] flow pull: registry returned ${flows.length} flow(s)`);
2713
3004
  let registered = 0;
2714
3005
  for (const f of flows) {
2715
3006
  const def = f;
@@ -2729,6 +3020,21 @@ var AutomationServicePlugin = class {
2729
3020
  const msg = err instanceof Error ? err.message : String(err);
2730
3021
  ctx.logger.warn(`[Automation] flow pull from ObjectQL registry failed: ${msg}`);
2731
3022
  }
3023
+ if (durableStore) {
3024
+ let job;
3025
+ try {
3026
+ job = ctx.getService("job");
3027
+ } catch {
3028
+ }
3029
+ try {
3030
+ const rearmed = await rearmSuspendedWaitTimers(this.engine, durableStore, job, ctx.logger);
3031
+ if (rearmed > 0) {
3032
+ ctx.logger.info(`[Automation] Re-armed ${rearmed} suspended wait timer(s) after restart`);
3033
+ }
3034
+ } catch (err) {
3035
+ ctx.logger.warn(`[Automation] wait-timer re-arm failed: ${err.message}`);
3036
+ }
3037
+ }
2732
3038
  }
2733
3039
  async destroy() {
2734
3040
  this.engine = void 0;