@hasna/loops 0.2.0 → 0.3.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.
@@ -0,0 +1,13 @@
1
+ import type { Store } from "./store.js";
2
+ export type DoctorSeverity = "ok" | "warn" | "fail";
3
+ export interface DoctorCheck {
4
+ id: string;
5
+ status: DoctorSeverity;
6
+ message: string;
7
+ detail?: string;
8
+ }
9
+ export interface DoctorReport {
10
+ ok: boolean;
11
+ checks: DoctorCheck[];
12
+ }
13
+ export declare function runDoctor(store: Store): DoctorReport;
@@ -3,6 +3,8 @@ export interface ExecuteOptions {
3
3
  maxOutputBytes?: number;
4
4
  env?: NodeJS.ProcessEnv;
5
5
  log?: (message: string) => void;
6
+ signal?: AbortSignal;
7
+ onSpawn?: (pid: number) => void;
6
8
  }
7
9
  export interface ExecutionMetadata {
8
10
  loopId?: string;
@@ -14,5 +16,11 @@ export interface ExecutionMetadata {
14
16
  workflowRunId?: string;
15
17
  workflowStepId?: string;
16
18
  }
19
+ export interface PreflightResult {
20
+ command: string;
21
+ accountProfile?: string;
22
+ accountTool?: string;
23
+ }
24
+ export declare function preflightTarget(target: ExecutableTarget, metadata?: ExecutionMetadata, opts?: ExecuteOptions): PreflightResult;
17
25
  export declare function executeTarget(target: ExecutableTarget, metadata?: ExecutionMetadata, opts?: ExecuteOptions): Promise<ExecutorResult>;
18
26
  export declare function executeLoop(loop: Loop, run: LoopRun, opts?: ExecuteOptions): Promise<ExecutorResult>;
@@ -15,4 +15,16 @@ export interface TickResult {
15
15
  recovered: LoopRun[];
16
16
  expired: Loop[];
17
17
  }
18
+ export declare function manualRunScheduledFor(loop: Loop, now?: Date): string;
19
+ export declare function shouldAdvanceManualRun(loop: Loop, scheduledFor: string, now?: Date): boolean;
20
+ export declare function advanceLoop(store: Store, loop: Loop, run: LoopRun, finishedAt: Date, succeeded: boolean): void;
21
+ export declare function executeClaimedRun(deps: {
22
+ store: Store;
23
+ runnerId: string;
24
+ loop: Loop;
25
+ run: LoopRun;
26
+ now?: () => Date;
27
+ execute?: (loop: Loop, run: LoopRun) => Promise<ExecutorResult>;
28
+ onError?: (loop: Loop, error: unknown) => void;
29
+ }): Promise<LoopRun>;
18
30
  export declare function tick(deps: SchedulerDeps): Promise<TickResult>;
@@ -45,6 +45,7 @@ export declare class Store {
45
45
  archiveWorkflow(idOrName: string): WorkflowSpec;
46
46
  createWorkflowRun(input: CreateWorkflowRunInput): WorkflowRun;
47
47
  getWorkflowRun(id: string): WorkflowRun | undefined;
48
+ requireWorkflowRun(id: string): WorkflowRun;
48
49
  listWorkflowRuns(opts?: {
49
50
  workflowId?: string;
50
51
  loopRunId?: string;
@@ -52,13 +53,22 @@ export declare class Store {
52
53
  }): WorkflowRun[];
53
54
  listWorkflowStepRuns(workflowRunId: string): WorkflowStepRun[];
54
55
  getWorkflowStepRun(workflowRunId: string, stepId: string): WorkflowStepRun | undefined;
56
+ isWorkflowRunTerminal(workflowRunId: string): boolean;
55
57
  startWorkflowStepRun(workflowRunId: string, stepId: string): WorkflowStepRun;
58
+ markWorkflowStepPid(workflowRunId: string, stepId: string, pid: number): WorkflowStepRun;
59
+ recoverWorkflowRun(workflowRunId: string, reason?: string): {
60
+ run: WorkflowRun;
61
+ recoveredSteps: WorkflowStepRun[];
62
+ };
56
63
  finalizeWorkflowStepRun(workflowRunId: string, stepId: string, patch: Pick<WorkflowStepRun, "status" | "finishedAt" | "durationMs" | "stdout" | "stderr"> & Partial<Pick<WorkflowStepRun, "exitCode" | "error">>): WorkflowStepRun;
57
64
  skipWorkflowStepRun(workflowRunId: string, stepId: string, reason: string): WorkflowStepRun;
58
65
  finalizeWorkflowRun(workflowRunId: string, status: WorkflowRunStatus, patch?: Partial<Pick<WorkflowRun, "finishedAt" | "durationMs" | "error">>): WorkflowRun;
66
+ cancelWorkflowRun(workflowRunId: string, reason?: string): WorkflowRun;
59
67
  appendWorkflowEvent(workflowRunId: string, eventType: string, stepId?: string, payload?: Record<string, unknown>): WorkflowEvent;
60
68
  listWorkflowEvents(workflowRunId: string, limit?: number): WorkflowEvent[];
61
69
  hasRunningRun(loopId: string): boolean;
70
+ markRunPid(id: string, pid: number, claimedBy?: string): LoopRun | undefined;
71
+ private hasLiveWorkflowStepProcesses;
62
72
  createSkippedRun(loop: Loop, scheduledFor: string, reason: string): LoopRun;
63
73
  getRun(id: string): LoopRun | undefined;
64
74
  getRunBySlot(loopId: string, scheduledFor: string): LoopRun | undefined;
package/dist/lib/store.js CHANGED
@@ -247,6 +247,9 @@ function validateTarget(value, label) {
247
247
  assertObject(value, label);
248
248
  if (value.type === "command") {
249
249
  assertString(value.command, `${label}.command`);
250
+ if (value.shell !== true && /\s/.test(value.command.trim())) {
251
+ throw new Error(`${label}.command must be an executable without spaces when shell is false; put flags in args or set shell true`);
252
+ }
250
253
  return value;
251
254
  }
252
255
  if (value.type === "agent") {
@@ -412,6 +415,7 @@ function rowToWorkflowStepRun(row) {
412
415
  startedAt: row.started_at ?? undefined,
413
416
  finishedAt: row.finished_at ?? undefined,
414
417
  exitCode: row.exit_code ?? undefined,
418
+ pid: row.pid ?? undefined,
415
419
  durationMs: row.duration_ms ?? undefined,
416
420
  stdout: row.stdout ?? undefined,
417
421
  stderr: row.stderr ?? undefined,
@@ -433,6 +437,14 @@ function rowToWorkflowEvent(row) {
433
437
  createdAt: row.created_at
434
438
  };
435
439
  }
440
+ function isProcessAlive(pid) {
441
+ try {
442
+ process.kill(pid, 0);
443
+ return true;
444
+ } catch {
445
+ return false;
446
+ }
447
+ }
436
448
  function rowToLease(row) {
437
449
  return {
438
450
  id: row.id,
@@ -565,6 +577,7 @@ class Store {
565
577
  started_at TEXT,
566
578
  finished_at TEXT,
567
579
  exit_code INTEGER,
580
+ pid INTEGER,
568
581
  duration_ms INTEGER,
569
582
  stdout TEXT,
570
583
  stderr TEXT,
@@ -590,6 +603,9 @@ class Store {
590
603
  );
591
604
  CREATE INDEX IF NOT EXISTS idx_workflow_events_run_sequence ON workflow_events(workflow_run_id, sequence);
592
605
  `);
606
+ try {
607
+ this.db.query("ALTER TABLE workflow_step_runs ADD COLUMN pid INTEGER").run();
608
+ } catch {}
593
609
  this.db.query("INSERT OR IGNORE INTO schema_migrations (id, applied_at) VALUES (?, ?)").run("0001_initial_and_workflows", nowIso());
594
610
  }
595
611
  createLoop(input, from = new Date) {
@@ -770,8 +786,8 @@ class Store {
770
786
  input.workflow.steps.forEach((step, sequence) => {
771
787
  const account = step.account ?? step.target.account;
772
788
  this.db.query(`INSERT INTO workflow_step_runs (id, workflow_run_id, step_id, sequence, status, started_at, finished_at,
773
- exit_code, duration_ms, stdout, stderr, error, account_profile, account_tool, created_at, updated_at)
774
- VALUES ($id, $workflowRunId, $stepId, $sequence, 'pending', NULL, NULL, NULL, NULL, NULL, NULL, NULL,
789
+ exit_code, pid, duration_ms, stdout, stderr, error, account_profile, account_tool, created_at, updated_at)
790
+ VALUES ($id, $workflowRunId, $stepId, $sequence, 'pending', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
775
791
  $accountProfile, $accountTool, $created, $updated)`).run({
776
792
  $id: genId(),
777
793
  $workflowRunId: runId,
@@ -812,6 +828,12 @@ class Store {
812
828
  const row = this.db.query("SELECT * FROM workflow_runs WHERE id = ?").get(id);
813
829
  return row ? rowToWorkflowRun(row) : undefined;
814
830
  }
831
+ requireWorkflowRun(id) {
832
+ const run = this.getWorkflowRun(id);
833
+ if (!run)
834
+ throw new Error(`workflow run not found: ${id}`);
835
+ return run;
836
+ }
815
837
  listWorkflowRuns(opts = {}) {
816
838
  const limit = opts.limit ?? 100;
817
839
  let rows;
@@ -832,23 +854,67 @@ class Store {
832
854
  const row = this.db.query("SELECT * FROM workflow_step_runs WHERE workflow_run_id = ? AND step_id = ?").get(workflowRunId, stepId);
833
855
  return row ? rowToWorkflowStepRun(row) : undefined;
834
856
  }
857
+ isWorkflowRunTerminal(workflowRunId) {
858
+ const run = this.getWorkflowRun(workflowRunId);
859
+ return Boolean(run && ["succeeded", "failed", "timed_out", "cancelled"].includes(run.status));
860
+ }
835
861
  startWorkflowStepRun(workflowRunId, stepId) {
836
862
  const now = nowIso();
837
- this.db.query(`UPDATE workflow_step_runs
863
+ const res = this.db.query(`UPDATE workflow_step_runs
838
864
  SET status='running', started_at=$started, finished_at=NULL, exit_code=NULL, duration_ms=NULL,
839
- stdout=NULL, stderr=NULL, error=NULL, updated_at=$updated
840
- WHERE workflow_run_id=$workflowRunId AND step_id=$stepId AND status IN ('pending', 'running', 'failed', 'timed_out')`).run({ $workflowRunId: workflowRunId, $stepId: stepId, $started: now, $updated: now });
841
- this.appendWorkflowEvent(workflowRunId, "step_started", stepId);
865
+ pid=NULL, stdout=NULL, stderr=NULL, error=NULL, updated_at=$updated
866
+ WHERE workflow_run_id=$workflowRunId
867
+ AND step_id=$stepId
868
+ AND status IN ('pending', 'failed', 'timed_out')
869
+ AND EXISTS (
870
+ SELECT 1 FROM workflow_runs
871
+ WHERE id=$workflowRunId AND status='running'
872
+ )`).run({ $workflowRunId: workflowRunId, $stepId: stepId, $started: now, $updated: now });
842
873
  const run = this.getWorkflowStepRun(workflowRunId, stepId);
843
874
  if (!run)
844
875
  throw new Error(`workflow step run not found: ${workflowRunId}/${stepId}`);
876
+ if (res.changes !== 1) {
877
+ throw new Error(`workflow step is not claimable: ${workflowRunId}/${stepId} status=${run.status}`);
878
+ }
879
+ this.appendWorkflowEvent(workflowRunId, "step_started", stepId);
880
+ return run;
881
+ }
882
+ markWorkflowStepPid(workflowRunId, stepId, pid) {
883
+ const now = nowIso();
884
+ this.db.query(`UPDATE workflow_step_runs SET pid=$pid, updated_at=$updated
885
+ WHERE workflow_run_id=$workflowRunId AND step_id=$stepId AND status='running'`).run({ $workflowRunId: workflowRunId, $stepId: stepId, $pid: pid, $updated: now });
886
+ const run = this.getWorkflowStepRun(workflowRunId, stepId);
887
+ if (!run)
888
+ throw new Error(`workflow step run not found after pid update: ${workflowRunId}/${stepId}`);
845
889
  return run;
846
890
  }
891
+ recoverWorkflowRun(workflowRunId, reason = "workflow run recovered for retry") {
892
+ const now = nowIso();
893
+ const before = this.listWorkflowStepRuns(workflowRunId).filter((step) => step.status === "running");
894
+ const live = before.filter((step) => step.pid !== undefined && isProcessAlive(step.pid));
895
+ if (live.length > 0) {
896
+ throw new Error(`cannot recover workflow run while step processes are still alive: ${live.map((step) => `${step.stepId} pid=${step.pid}`).join(", ")}`);
897
+ }
898
+ this.db.query(`UPDATE workflow_step_runs
899
+ SET status='pending', started_at=NULL, finished_at=NULL, exit_code=NULL, pid=NULL, duration_ms=NULL,
900
+ stdout=NULL, stderr=NULL, error=$reason, updated_at=$updated
901
+ WHERE workflow_run_id=$workflowRunId AND status='running'`).run({ $workflowRunId: workflowRunId, $reason: reason, $updated: now });
902
+ if (before.length > 0) {
903
+ this.appendWorkflowEvent(workflowRunId, "recovered", undefined, {
904
+ reason,
905
+ recoveredSteps: before.map((step) => step.stepId)
906
+ });
907
+ }
908
+ return {
909
+ run: this.requireWorkflowRun(workflowRunId),
910
+ recoveredSteps: before.map((step) => this.getWorkflowStepRun(workflowRunId, step.stepId)).filter(Boolean)
911
+ };
912
+ }
847
913
  finalizeWorkflowStepRun(workflowRunId, stepId, patch) {
848
914
  const finishedAt = patch.finishedAt ?? nowIso();
849
- this.db.query(`UPDATE workflow_step_runs SET status=$status, finished_at=$finished, exit_code=$exitCode, duration_ms=$durationMs,
850
- stdout=$stdout, stderr=$stderr, error=$error, updated_at=$updated
851
- WHERE workflow_run_id=$workflowRunId AND step_id=$stepId`).run({
915
+ const res = this.db.query(`UPDATE workflow_step_runs SET status=$status, finished_at=$finished, exit_code=$exitCode, duration_ms=$durationMs,
916
+ pid=NULL, stdout=$stdout, stderr=$stderr, error=$error, updated_at=$updated
917
+ WHERE workflow_run_id=$workflowRunId AND step_id=$stepId AND status='running'`).run({
852
918
  $workflowRunId: workflowRunId,
853
919
  $stepId: stepId,
854
920
  $status: patch.status,
@@ -860,10 +926,12 @@ class Store {
860
926
  $error: patch.error ?? null,
861
927
  $updated: finishedAt
862
928
  });
863
- this.appendWorkflowEvent(workflowRunId, `step_${patch.status}`, stepId, {
864
- exitCode: patch.exitCode,
865
- error: patch.error
866
- });
929
+ if (res.changes === 1) {
930
+ this.appendWorkflowEvent(workflowRunId, `step_${patch.status}`, stepId, {
931
+ exitCode: patch.exitCode,
932
+ error: patch.error
933
+ });
934
+ }
867
935
  const run = this.getWorkflowStepRun(workflowRunId, stepId);
868
936
  if (!run)
869
937
  throw new Error(`workflow step run not found after finalize: ${workflowRunId}/${stepId}`);
@@ -871,9 +939,10 @@ class Store {
871
939
  }
872
940
  skipWorkflowStepRun(workflowRunId, stepId, reason) {
873
941
  const now = nowIso();
874
- this.db.query(`UPDATE workflow_step_runs SET status='skipped', finished_at=$finished, error=$error, updated_at=$updated
875
- WHERE workflow_run_id=$workflowRunId AND step_id=$stepId`).run({ $workflowRunId: workflowRunId, $stepId: stepId, $finished: now, $error: reason, $updated: now });
876
- this.appendWorkflowEvent(workflowRunId, "step_skipped", stepId, { reason });
942
+ const res = this.db.query(`UPDATE workflow_step_runs SET status='skipped', finished_at=$finished, pid=NULL, error=$error, updated_at=$updated
943
+ WHERE workflow_run_id=$workflowRunId AND step_id=$stepId AND status IN ('pending', 'running')`).run({ $workflowRunId: workflowRunId, $stepId: stepId, $finished: now, $error: reason, $updated: now });
944
+ if (res.changes === 1)
945
+ this.appendWorkflowEvent(workflowRunId, "step_skipped", stepId, { reason });
877
946
  const run = this.getWorkflowStepRun(workflowRunId, stepId);
878
947
  if (!run)
879
948
  throw new Error(`workflow step run not found after skip: ${workflowRunId}/${stepId}`);
@@ -881,8 +950,8 @@ class Store {
881
950
  }
882
951
  finalizeWorkflowRun(workflowRunId, status, patch = {}) {
883
952
  const finishedAt = patch.finishedAt ?? nowIso();
884
- this.db.query(`UPDATE workflow_runs SET status=$status, finished_at=$finished, duration_ms=$durationMs, error=$error, updated_at=$updated
885
- WHERE id=$id`).run({
953
+ const res = this.db.query(`UPDATE workflow_runs SET status=$status, finished_at=$finished, duration_ms=$durationMs, error=$error, updated_at=$updated
954
+ WHERE id=$id AND status NOT IN ('succeeded', 'failed', 'timed_out', 'cancelled')`).run({
886
955
  $id: workflowRunId,
887
956
  $status: status,
888
957
  $finished: finishedAt,
@@ -890,12 +959,36 @@ class Store {
890
959
  $error: patch.error ?? null,
891
960
  $updated: finishedAt
892
961
  });
893
- this.appendWorkflowEvent(workflowRunId, status, undefined, { error: patch.error });
894
962
  const run = this.getWorkflowRun(workflowRunId);
895
963
  if (!run)
896
964
  throw new Error(`workflow run not found after finalize: ${workflowRunId}`);
965
+ if (res.changes === 1)
966
+ this.appendWorkflowEvent(workflowRunId, status, undefined, { error: patch.error });
897
967
  return run;
898
968
  }
969
+ cancelWorkflowRun(workflowRunId, reason = "cancelled by user") {
970
+ const now = nowIso();
971
+ this.db.exec("BEGIN IMMEDIATE");
972
+ try {
973
+ const run = this.requireWorkflowRun(workflowRunId);
974
+ if (!["succeeded", "failed", "timed_out", "cancelled"].includes(run.status)) {
975
+ this.db.query(`UPDATE workflow_runs
976
+ SET status='cancelled', finished_at=$finished, error=$reason, updated_at=$updated
977
+ WHERE id=$id AND status NOT IN ('succeeded', 'failed', 'timed_out', 'cancelled')`).run({ $id: workflowRunId, $finished: now, $reason: reason, $updated: now });
978
+ this.db.query(`UPDATE workflow_step_runs
979
+ SET status='cancelled', finished_at=$finished, pid=NULL, error=$reason, updated_at=$updated
980
+ WHERE workflow_run_id=$workflowRunId AND status IN ('pending', 'running')`).run({ $workflowRunId: workflowRunId, $finished: now, $reason: reason, $updated: now });
981
+ this.appendWorkflowEvent(workflowRunId, "cancelled", undefined, { reason });
982
+ }
983
+ this.db.exec("COMMIT");
984
+ return this.requireWorkflowRun(workflowRunId);
985
+ } catch (error) {
986
+ try {
987
+ this.db.exec("ROLLBACK");
988
+ } catch {}
989
+ throw error;
990
+ }
991
+ }
899
992
  appendWorkflowEvent(workflowRunId, eventType, stepId, payload) {
900
993
  const now = nowIso();
901
994
  const current = this.db.query("SELECT MAX(sequence) AS sequence FROM workflow_events WHERE workflow_run_id = ?").get(workflowRunId);
@@ -924,6 +1017,24 @@ class Store {
924
1017
  const row = this.db.query("SELECT COUNT(*) AS count FROM loop_runs WHERE loop_id = ? AND status = 'running'").get(loopId);
925
1018
  return (row?.count ?? 0) > 0;
926
1019
  }
1020
+ markRunPid(id, pid, claimedBy) {
1021
+ const now = nowIso();
1022
+ const res = claimedBy ? this.db.query(`UPDATE loop_runs SET pid=$pid, updated_at=$updated
1023
+ WHERE id=$id AND status='running' AND claimed_by=$claimedBy`).run({ $id: id, $pid: pid, $updated: now, $claimedBy: claimedBy }) : this.db.query("UPDATE loop_runs SET pid=$pid, updated_at=$updated WHERE id=$id AND status='running'").run({ $id: id, $pid: pid, $updated: now });
1024
+ if (res.changes !== 1)
1025
+ return;
1026
+ return this.getRun(id);
1027
+ }
1028
+ hasLiveWorkflowStepProcesses(loopRunId) {
1029
+ const liveWorkflowSteps = this.db.query(`SELECT wr.id AS workflow_run_id, wsr.step_id AS step_id, wsr.pid AS pid
1030
+ FROM workflow_runs wr
1031
+ JOIN workflow_step_runs wsr ON wsr.workflow_run_id = wr.id
1032
+ WHERE wr.loop_run_id = ?
1033
+ AND wr.status NOT IN ('succeeded', 'failed', 'timed_out', 'cancelled')
1034
+ AND wsr.status = 'running'
1035
+ AND wsr.pid IS NOT NULL`).all(loopRunId);
1036
+ return liveWorkflowSteps.some((step) => isProcessAlive(step.pid));
1037
+ }
927
1038
  createSkippedRun(loop, scheduledFor, reason) {
928
1039
  const now = nowIso();
929
1040
  const run = {
@@ -971,6 +1082,14 @@ class Store {
971
1082
  const existing = this.getRunBySlot(loop.id, scheduledFor);
972
1083
  if (existing) {
973
1084
  if (existing.status === "running") {
1085
+ if (existing.leaseExpiresAt && existing.leaseExpiresAt <= startedAt && existing.pid && isProcessAlive(existing.pid)) {
1086
+ this.db.exec("COMMIT");
1087
+ return;
1088
+ }
1089
+ if (existing.leaseExpiresAt && existing.leaseExpiresAt <= startedAt && this.hasLiveWorkflowStepProcesses(existing.id)) {
1090
+ this.db.exec("COMMIT");
1091
+ return;
1092
+ }
974
1093
  const res3 = this.db.query(`UPDATE loop_runs SET status='running', started_at=$started, finished_at=NULL,
975
1094
  claimed_by=$claimedBy, lease_expires_at=$lease, pid=NULL, exit_code=NULL,
976
1095
  duration_ms=NULL, stdout=NULL, stderr=NULL, error=NULL, updated_at=$updated
@@ -1093,8 +1212,26 @@ class Store {
1093
1212
  const rows = this.db.query("SELECT * FROM loop_runs WHERE status = 'running' AND lease_expires_at <= ?").all(now.toISOString());
1094
1213
  const recovered = [];
1095
1214
  for (const row of rows) {
1215
+ if (row.pid && isProcessAlive(row.pid))
1216
+ continue;
1217
+ if (this.hasLiveWorkflowStepProcesses(row.id))
1218
+ continue;
1219
+ const finished = now.toISOString();
1096
1220
  this.db.query(`UPDATE loop_runs SET status='abandoned', finished_at=$finished, lease_expires_at=NULL,
1097
- error='run lease expired before completion', updated_at=$updated WHERE id=$id`).run({ $id: row.id, $finished: now.toISOString(), $updated: now.toISOString() });
1221
+ error='run lease expired before completion', updated_at=$updated WHERE id=$id`).run({ $id: row.id, $finished: finished, $updated: finished });
1222
+ const workflowRows = this.db.query("SELECT * FROM workflow_runs WHERE loop_run_id = ? AND status NOT IN ('succeeded', 'failed', 'timed_out', 'cancelled')").all(row.id);
1223
+ for (const workflowRow of workflowRows) {
1224
+ this.db.query(`UPDATE workflow_runs
1225
+ SET status='failed', finished_at=$finished, error='parent loop run lease expired before completion', updated_at=$updated
1226
+ WHERE id=$id AND status NOT IN ('succeeded', 'failed', 'timed_out', 'cancelled')`).run({ $id: workflowRow.id, $finished: finished, $updated: finished });
1227
+ this.db.query(`UPDATE workflow_step_runs
1228
+ SET status='skipped', finished_at=$finished, pid=NULL, error='parent loop run lease expired before completion', updated_at=$updated
1229
+ WHERE workflow_run_id=$workflowRunId AND status IN ('pending', 'running')`).run({ $workflowRunId: workflowRow.id, $finished: finished, $updated: finished });
1230
+ this.appendWorkflowEvent(workflowRow.id, "failed", undefined, {
1231
+ error: "parent loop run lease expired before completion",
1232
+ loopRunId: row.id
1233
+ });
1234
+ }
1098
1235
  const run = this.getRun(row.id);
1099
1236
  if (run)
1100
1237
  recovered.push(run);
@@ -1,11 +1,14 @@
1
1
  import type { ExecutorResult, Loop, LoopRun, WorkflowSpec } from "../types.js";
2
- import { type ExecuteOptions } from "./executor.js";
2
+ import { preflightTarget, type ExecuteOptions } from "./executor.js";
3
3
  import type { Store } from "./store.js";
4
4
  export interface ExecuteWorkflowOptions extends ExecuteOptions {
5
5
  loop?: Loop;
6
6
  loopRun?: LoopRun;
7
7
  scheduledFor?: string;
8
8
  idempotencyKey?: string;
9
+ cancelPollMs?: number;
10
+ signalTimeoutMessage?: () => string | undefined;
9
11
  }
10
12
  export declare function executeWorkflow(store: Store, workflow: WorkflowSpec, opts?: ExecuteWorkflowOptions): Promise<ExecutorResult>;
13
+ export declare function preflightWorkflow(workflow: WorkflowSpec, opts?: ExecuteOptions): ReturnType<typeof preflightTarget>[];
11
14
  export declare function executeLoopTarget(store: Store, loop: Loop, run: LoopRun, opts?: ExecuteOptions): Promise<ExecutorResult>;