@voyantjs/workflows-orchestrator-cloudflare 0.37.1 → 0.38.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/README.md CHANGED
@@ -65,6 +65,7 @@ export class WorkflowRunDO implements DurableObject {
65
65
  |---|---|
66
66
  | `POST /api/runs` | Trigger a new run. Body: `{ workflowId, workflowVersion, input, tenantMeta, runId? }`. |
67
67
  | `GET /api/runs/:id` | Fetch the current `RunRecord`. |
68
+ | `POST /api/runs/:id/resume` | Start a new run from a failed parent run. Body: `{ input?, workflowId?, resumeFromStep?, seedResults?, runId?, tags?, triggeredByUserId? }`. |
68
69
  | `POST /api/runs/:id/events` | Inject an `EVENT` waitpoint resolution. |
69
70
  | `POST /api/runs/:id/signals` | Inject a `SIGNAL` waitpoint resolution. |
70
71
  | `POST /api/runs/:id/tokens/:tokenId` | Inject a `MANUAL` (token) waitpoint resolution. |
@@ -73,6 +74,10 @@ export class WorkflowRunDO implements DurableObject {
73
74
  Injection bodies are `{ eventType, payload? }` / `{ name, payload? }`
74
75
  / `{ payload? }` respectively.
75
76
 
77
+ If the parent id on `/api/runs/:id/resume` is not stored in this
78
+ orchestrator, pass `workflowId`, `resumeFromStep`, and `seedResults`
79
+ to resume from an external workflow-runs parent.
80
+
76
81
  ## Durable Object model
77
82
 
78
83
  One DO per run, keyed by `idFromName(runId)`. The DO's transactional
@@ -1 +1 @@
1
- {"version":3,"file":"durable-object.d.ts","sourceRoot":"","sources":["../src/durable-object.ts"],"names":[],"mappings":"AAmBA,OAAO,EACL,uBAAuB,EACvB,gBAAgB,EAKhB,KAAK,SAAS,EAEf,MAAM,kCAAkC,CAAA;AAEzC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAEtD,OAAO,KAAK,EAGV,wBAAwB,EAGzB,MAAM,YAAY,CAAA;AACnB,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAA;AAE7D,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,wBAAwB,CAAA;IACjC;;;;;;;;;;;OAWG;IACH,UAAU,EAAE,cAAc,CAAA;IAC1B,aAAa,CAAC,EAAE,0BAA0B,CAAA;IAC1C,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB;AAcD,wBAAsB,0BAA0B,CAC9C,GAAG,EAAE,OAAO,EACZ,IAAI,EAAE,iBAAiB,GACtB,OAAO,CAAC,QAAQ,CAAC,CA+EnB;AAED;;;;;GAKG;AACH,wBAAsB,wBAAwB,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAyCrF;AAyFD,YAAY,EAAE,SAAS,EAAE,CAAA;AAEzB,OAAO,EAAE,uBAAuB,EAAE,gBAAgB,EAAE,CAAA"}
1
+ {"version":3,"file":"durable-object.d.ts","sourceRoot":"","sources":["../src/durable-object.ts"],"names":[],"mappings":"AAmBA,OAAO,EACL,uBAAuB,EACvB,gBAAgB,EAKhB,KAAK,SAAS,EAEf,MAAM,kCAAkC,CAAA;AAEzC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAEtD,OAAO,KAAK,EAGV,wBAAwB,EAGzB,MAAM,YAAY,CAAA;AACnB,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAA;AAE7D,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,wBAAwB,CAAA;IACjC;;;;;;;;;;;OAWG;IACH,UAAU,EAAE,cAAc,CAAA;IAC1B,aAAa,CAAC,EAAE,0BAA0B,CAAA;IAC1C,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB;AAcD,wBAAsB,0BAA0B,CAC9C,GAAG,EAAE,OAAO,EACZ,IAAI,EAAE,iBAAiB,GACtB,OAAO,CAAC,QAAQ,CAAC,CAkFnB;AAED;;;;;GAKG;AACH,wBAAsB,wBAAwB,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAyCrF;AAyFD,YAAY,EAAE,SAAS,EAAE,CAAA;AAEzB,OAAO,EAAE,uBAAuB,EAAE,gBAAgB,EAAE,CAAA"}
@@ -51,6 +51,9 @@ export async function handleDurableObjectRequest(req, deps) {
51
51
  ? new Date(payload.delay.wakeAt)
52
52
  : payload.delay,
53
53
  priority: payload.priority,
54
+ initialJournal: payload.initialJournal,
55
+ initialMetadataAppliedCount: payload.initialMetadataAppliedCount,
56
+ timeoutMs: payload.timeoutMs,
54
57
  }, { store, handler, now: deps.now });
55
58
  await reconcileAlarm(record, store, deps);
56
59
  return json(200, record);
package/dist/types.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Duration, EnvironmentName, RunTrigger } from "@voyantjs/workflows";
2
- import type { WaitpointInjection } from "@voyantjs/workflows-orchestrator";
2
+ import type { JournalSlice, WaitpointInjection } from "@voyantjs/workflows-orchestrator";
3
3
  /**
4
4
  * Subset of Cloudflare's `DurableObjectStorage` we actually use.
5
5
  * Keyed JSON blobs; no transactional guarantees beyond what a single
@@ -59,6 +59,17 @@ export interface TriggerPayload {
59
59
  };
60
60
  priority?: number;
61
61
  triggeredBy?: RunTrigger;
62
+ /**
63
+ * Optional journal seed for failed-step resume / replay flows.
64
+ * Steps already present in the journal are replayed from their
65
+ * stored results, so the workflow starts doing new work at the
66
+ * first unseeded step.
67
+ */
68
+ initialJournal?: JournalSlice;
69
+ /** Cursor paired with `initialJournal.metadataState` for resume replay dedupe. */
70
+ initialMetadataAppliedCount?: number;
71
+ /** Compute-time budget in ms; copied from parent runs during resume. */
72
+ timeoutMs?: number;
62
73
  concurrencyLease?: ConcurrencyLease;
63
74
  }
64
75
  export interface ConcurrencyLease {
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,QAAQ,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAA;AAChF,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAA;AAE1E;;;;;;;;;GASG;AACH,MAAM,WAAW,wBAAwB;IACvC,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CAAA;IAC3C,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC5C,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IACrC,IAAI,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAA;IAC/E,8DAA8D;IAC9D,QAAQ,CAAC,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;IACnC,4DAA4D;IAC5D,QAAQ,CAAC,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACxC,gCAAgC;IAChC,WAAW,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CAC9B;AAED,uDAAuD;AACvD,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,SAAS,GAAG,QAAQ,GAAG,QAAQ,GAAG,KAAK,CAAA;IAC3C,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,uCAAuC;AACvC,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,kBAAkB,CAAA;CAC9B;AAED,uBAAuB;AACvB,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,MAAM,CAAA;IAClB,eAAe,EAAE,MAAM,CAAA;IACvB,KAAK,EAAE,OAAO,CAAA;IACd,UAAU,EAAE;QACV,QAAQ,EAAE,MAAM,CAAA;QAChB,SAAS,EAAE,MAAM,CAAA;QACjB,cAAc,EAAE,MAAM,CAAA;QACtB;;;;WAIG;QACH,YAAY,CAAC,EAAE,MAAM,CAAA;KACtB,CAAA;IACD,WAAW,CAAC,EAAE,YAAY,GAAG,SAAS,GAAG,aAAa,CAAA;IACtD,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;IACf,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,KAAK,CAAC,EAAE,QAAQ,GAAG;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,CAAA;IACrC,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,WAAW,CAAC,EAAE,UAAU,CAAA;IACxB,gBAAgB,CAAC,EAAE,gBAAgB,CAAA;CACpC;AAED,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,eAAe,CAAA;IAC5B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;CACd;AAED,sBAAsB;AACtB,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED;;;GAGG;AACH,MAAM,WAAW,UAAU,CAAC,WAAW,GAAG,OAAO;IAC/C,8FAA8F;IAC9F,eAAe,EAAE,WAAW,CAAA;CAC7B"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,QAAQ,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAA;AAChF,OAAO,KAAK,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAA;AAExF;;;;;;;;;GASG;AACH,MAAM,WAAW,wBAAwB;IACvC,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CAAA;IAC3C,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC5C,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IACrC,IAAI,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAA;IAC/E,8DAA8D;IAC9D,QAAQ,CAAC,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;IACnC,4DAA4D;IAC5D,QAAQ,CAAC,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACxC,gCAAgC;IAChC,WAAW,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CAC9B;AAED,uDAAuD;AACvD,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,SAAS,GAAG,QAAQ,GAAG,QAAQ,GAAG,KAAK,CAAA;IAC3C,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,uCAAuC;AACvC,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,kBAAkB,CAAA;CAC9B;AAED,uBAAuB;AACvB,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,MAAM,CAAA;IAClB,eAAe,EAAE,MAAM,CAAA;IACvB,KAAK,EAAE,OAAO,CAAA;IACd,UAAU,EAAE;QACV,QAAQ,EAAE,MAAM,CAAA;QAChB,SAAS,EAAE,MAAM,CAAA;QACjB,cAAc,EAAE,MAAM,CAAA;QACtB;;;;WAIG;QACH,YAAY,CAAC,EAAE,MAAM,CAAA;KACtB,CAAA;IACD,WAAW,CAAC,EAAE,YAAY,GAAG,SAAS,GAAG,aAAa,CAAA;IACtD,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;IACf,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,KAAK,CAAC,EAAE,QAAQ,GAAG;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,CAAA;IACrC,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,WAAW,CAAC,EAAE,UAAU,CAAA;IACxB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,YAAY,CAAA;IAC7B,kFAAkF;IAClF,2BAA2B,CAAC,EAAE,MAAM,CAAA;IACpC,wEAAwE;IACxE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,gBAAgB,CAAC,EAAE,gBAAgB,CAAA;CACpC;AAED,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,eAAe,CAAA;IAC5B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;CACd;AAED,sBAAsB;AACtB,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED;;;GAGG;AACH,MAAM,WAAW,UAAU,CAAC,WAAW,GAAG,OAAO;IAC/C,8FAA8F;IAC9F,eAAe,EAAE,WAAW,CAAA;CAC7B"}
@@ -1 +1 @@
1
- {"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../src/worker.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAE7D;;;;GAIG;AACH,MAAM,WAAW,0BAA0B,CAAC,EAAE,GAAG,OAAO;IACtD,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,EAAE,CAAA;IAC5B,GAAG,CAAC,EAAE,EAAE,EAAE,GAAG;QAAE,KAAK,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;KAAE,CAAA;CACxD;AAED,MAAM,WAAW,eAAe,CAAC,EAAE,GAAG,OAAO;IAC3C,KAAK,EAAE,0BAA0B,CAAC,EAAE,CAAC,CAAA;IACrC;;;OAGG;IACH,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACtD,uBAAuB;IACvB,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;IAC/E,iEAAiE;IACjE,WAAW,CAAC,EAAE,MAAM,MAAM,CAAA;IAC1B,0CAA0C;IAC1C,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;IAClB;;;;OAIG;IACH,aAAa,CAAC,EAAE,eAAe,CAAA;IAC/B;;;;;OAKG;IACH,UAAU,CAAC,EAAE;QACX,QAAQ,EAAE,MAAM,CAAA;QAChB,SAAS,EAAE,MAAM,CAAA;QACjB,cAAc,EAAE,MAAM,CAAA;QACtB,YAAY,CAAC,EAAE,MAAM,CAAA;KACtB,CAAA;CACF;AAED,wBAAsB,mBAAmB,CAAC,EAAE,EAC1C,GAAG,EAAE,OAAO,EACZ,IAAI,EAAE,eAAe,CAAC,EAAE,CAAC,GACxB,OAAO,CAAC,QAAQ,CAAC,CAuHnB"}
1
+ {"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../src/worker.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAQ7D;;;;GAIG;AACH,MAAM,WAAW,0BAA0B,CAAC,EAAE,GAAG,OAAO;IACtD,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,EAAE,CAAA;IAC5B,GAAG,CAAC,EAAE,EAAE,EAAE,GAAG;QAAE,KAAK,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;KAAE,CAAA;CACxD;AAED,MAAM,WAAW,eAAe,CAAC,EAAE,GAAG,OAAO;IAC3C,KAAK,EAAE,0BAA0B,CAAC,EAAE,CAAC,CAAA;IACrC;;;OAGG;IACH,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACtD,uBAAuB;IACvB,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;IAC/E,iEAAiE;IACjE,WAAW,CAAC,EAAE,MAAM,MAAM,CAAA;IAC1B,0CAA0C;IAC1C,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;IAClB;;;;OAIG;IACH,aAAa,CAAC,EAAE,eAAe,CAAA;IAC/B;;;;;OAKG;IACH,UAAU,CAAC,EAAE;QACX,QAAQ,EAAE,MAAM,CAAA;QAChB,SAAS,EAAE,MAAM,CAAA;QACjB,cAAc,EAAE,MAAM,CAAA;QACtB,YAAY,CAAC,EAAE,MAAM,CAAA;KACtB,CAAA;CACF;AAED,wBAAsB,mBAAmB,CAAC,EAAE,EAC1C,GAAG,EAAE,OAAO,EACZ,IAAI,EAAE,eAAe,CAAC,EAAE,CAAC,GACxB,OAAO,CAAC,QAAQ,CAAC,CAoMnB"}
package/dist/worker.js CHANGED
@@ -6,12 +6,19 @@
6
6
  // Routes (all JSON bodies):
7
7
  // POST /api/runs → trigger a run
8
8
  // GET /api/runs/:id → fetch a run
9
+ // POST /api/runs/:id/resume → start a new run from a failed parent step
9
10
  // POST /api/runs/:id/signals → inject a SIGNAL waitpoint
10
11
  // POST /api/runs/:id/events → inject an EVENT waitpoint
11
12
  // POST /api/runs/:id/tokens/:token → inject a MANUAL (token) waitpoint
12
13
  // POST /api/runs/:id/cancel → cancel a run
14
+ import { buildResumeJournal, buildSeededResumeJournal, } from "@voyantjs/workflows-orchestrator";
13
15
  import { handleIngestEvent } from "./event-handler.js";
14
16
  import { handleGetManifest, handleRegisterManifest } from "./manifest-handler.js";
17
+ const DEFAULT_TENANT_META = {
18
+ tenantId: "default",
19
+ projectId: "default",
20
+ organizationId: "default",
21
+ };
15
22
  export async function handleWorkerRequest(req, deps) {
16
23
  const url = new URL(req.url);
17
24
  if (req.method === "OPTIONS") {
@@ -79,10 +86,11 @@ export async function handleWorkerRequest(req, deps) {
79
86
  return json(400, { error: "invalid_json", message: errMsg(err) });
80
87
  }
81
88
  const runId = typeof payload.runId === "string" ? payload.runId : defaultRunId(deps);
89
+ const publicPayload = sanitizePublicTriggerPayload(payload);
82
90
  const forward = new Request(`https://do-internal/trigger`, {
83
91
  method: "POST",
84
92
  headers: { "content-type": "application/json" },
85
- body: JSON.stringify({ ...payload, runId }),
93
+ body: JSON.stringify({ ...publicPayload, runId }),
86
94
  });
87
95
  return forwardToRunDO(runId, forward, deps);
88
96
  }
@@ -107,6 +115,76 @@ export async function handleWorkerRequest(req, deps) {
107
115
  body: JSON.stringify(body),
108
116
  }), deps);
109
117
  }
118
+ if (req.method === "POST" && tail === "/resume") {
119
+ const body = await safeJson(req);
120
+ if (isErrorBody(body))
121
+ return json(400, body);
122
+ const parsed = parseResumeRunBody(body);
123
+ if ("error" in parsed)
124
+ return json(400, parsed);
125
+ const parent = await fetchRunRecord(runId, deps);
126
+ if ("error" in parent)
127
+ return parent.error;
128
+ const workflowId = parsed.body.workflowId ?? parent.record?.workflowId;
129
+ if (!workflowId) {
130
+ return json(404, {
131
+ error: "resume_failed",
132
+ message: `parent run "${runId}" not found; pass workflowId, resumeFromStep, ` +
133
+ "and seedResults to resume from an external workflow-runs parent",
134
+ });
135
+ }
136
+ let resumeSeed;
137
+ try {
138
+ resumeSeed = parent.record
139
+ ? buildResumeJournal({
140
+ parent: parent.record,
141
+ resumeFromStep: parsed.body.resumeFromStep,
142
+ seedResults: parsed.body.seedResults,
143
+ now: deps.now,
144
+ })
145
+ : buildSeededResumeJournal({
146
+ parentRunId: runId,
147
+ resumeFromStep: requireExternalResumeFromStep(parsed.body.resumeFromStep),
148
+ seedResults: requireExternalSeedResults(parsed.body.seedResults),
149
+ now: deps.now,
150
+ });
151
+ }
152
+ catch (err) {
153
+ return json(400, { error: "resume_failed", message: errMsg(err) });
154
+ }
155
+ const nextRunId = parsed.body.runId ?? defaultRunId(deps);
156
+ const triggerRes = await forwardToRunDO(nextRunId, new Request("https://do-internal/trigger", {
157
+ method: "POST",
158
+ headers: { "content-type": "application/json" },
159
+ body: JSON.stringify({
160
+ workflowId,
161
+ workflowVersion: parent.record?.workflowVersion ?? "local",
162
+ input: parsed.body.hasInput ? parsed.body.input : parent.record?.input,
163
+ tenantMeta: parent.record?.tenantMeta ?? deps.tenantMeta ?? DEFAULT_TENANT_META,
164
+ environment: parent.record?.environment,
165
+ tags: mergeTags(parent.record?.tags, [
166
+ "resume:true",
167
+ `parentRunId:${parent.record?.id ?? runId}`,
168
+ ...(parsed.body.tags ?? []),
169
+ ]),
170
+ runId: nextRunId,
171
+ triggeredBy: parsed.body.triggeredByUserId === undefined || parsed.body.triggeredByUserId === null
172
+ ? { kind: "api" }
173
+ : { kind: "api", actor: parsed.body.triggeredByUserId },
174
+ timeoutMs: parent.record?.timeoutMs,
175
+ initialJournal: resumeSeed.journal,
176
+ initialMetadataAppliedCount: resumeSeed.metadataAppliedCount,
177
+ }),
178
+ }), deps);
179
+ if (!triggerRes.ok)
180
+ return triggerRes;
181
+ const saved = (await triggerRes.json());
182
+ return json(200, {
183
+ saved,
184
+ parentRunId: parent.record?.id ?? runId,
185
+ resumeFromStep: resumeSeed.resumeFromStep,
186
+ });
187
+ }
110
188
  // Waitpoint injections: events, signals, tokens.
111
189
  const body = await safeJson(req);
112
190
  if (isErrorBody(body))
@@ -150,6 +228,55 @@ function parseInjection(tail, body) {
150
228
  }
151
229
  return { error: "route_not_found", message: `unknown path suffix ${tail}` };
152
230
  }
231
+ function parseResumeRunBody(body) {
232
+ if (!isPlainObject(body)) {
233
+ return { error: "invalid_body", message: "request body must be an object" };
234
+ }
235
+ if (body.resumeFromStep !== undefined && typeof body.resumeFromStep !== "string") {
236
+ return { error: "invalid_body", message: "`resumeFromStep` must be a string when provided" };
237
+ }
238
+ if (body.workflowId !== undefined && typeof body.workflowId !== "string") {
239
+ return { error: "invalid_body", message: "`workflowId` must be a string when provided" };
240
+ }
241
+ if (body.runId !== undefined && typeof body.runId !== "string") {
242
+ return { error: "invalid_body", message: "`runId` must be a string when provided" };
243
+ }
244
+ if (body.triggeredByUserId !== undefined &&
245
+ body.triggeredByUserId !== null &&
246
+ typeof body.triggeredByUserId !== "string") {
247
+ return {
248
+ error: "invalid_body",
249
+ message: "`triggeredByUserId` must be a string or null when provided",
250
+ };
251
+ }
252
+ if (body.tags !== undefined && !isStringArray(body.tags)) {
253
+ return { error: "invalid_body", message: "`tags` must be an array of strings when provided" };
254
+ }
255
+ if (body.seedResults !== undefined && !isPlainObject(body.seedResults)) {
256
+ return { error: "invalid_body", message: "`seedResults` must be an object when provided" };
257
+ }
258
+ return {
259
+ body: {
260
+ hasInput: Object.hasOwn(body, "input"),
261
+ input: body.input,
262
+ workflowId: body.workflowId,
263
+ resumeFromStep: body.resumeFromStep,
264
+ seedResults: body.seedResults,
265
+ runId: body.runId,
266
+ tags: body.tags,
267
+ triggeredByUserId: body.triggeredByUserId,
268
+ },
269
+ };
270
+ }
271
+ async function fetchRunRecord(runId, deps) {
272
+ const res = await forwardToRunDO(runId, new Request("https://do-internal/get", { method: "GET" }), deps);
273
+ if (res.status === 404) {
274
+ return {};
275
+ }
276
+ if (!res.ok)
277
+ return { error: res };
278
+ return { record: (await res.json()) };
279
+ }
153
280
  async function forwardToRunDO(runId, req, deps) {
154
281
  const id = deps.runDO.idFromName(runId);
155
282
  const stub = deps.runDO.get(id);
@@ -179,12 +306,42 @@ async function safeJson(req) {
179
306
  if (text.length === 0)
180
307
  return {};
181
308
  try {
182
- return JSON.parse(text);
309
+ const parsed = JSON.parse(text);
310
+ if (!isPlainObject(parsed)) {
311
+ return { error: "invalid_body", message: "request body must be an object" };
312
+ }
313
+ return parsed;
183
314
  }
184
315
  catch (err) {
185
316
  return { error: "invalid_json", message: errMsg(err) };
186
317
  }
187
318
  }
319
+ function isPlainObject(value) {
320
+ return typeof value === "object" && value !== null && !Array.isArray(value);
321
+ }
322
+ function isStringArray(value) {
323
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string");
324
+ }
325
+ function mergeTags(...groups) {
326
+ const tags = new Set();
327
+ for (const group of groups) {
328
+ for (const tag of group ?? [])
329
+ tags.add(tag);
330
+ }
331
+ return Array.from(tags);
332
+ }
333
+ function requireExternalResumeFromStep(resumeFromStep) {
334
+ if (!resumeFromStep) {
335
+ throw new Error("resumeFromStep is required when the parent run is not stored by this worker");
336
+ }
337
+ return resumeFromStep;
338
+ }
339
+ function requireExternalSeedResults(seedResults) {
340
+ if (!seedResults) {
341
+ throw new Error("seedResults is required when the parent run is not stored by this worker");
342
+ }
343
+ return seedResults;
344
+ }
188
345
  function corsHeaders(methods) {
189
346
  return {
190
347
  "access-control-allow-origin": "*",
@@ -192,6 +349,10 @@ function corsHeaders(methods) {
192
349
  "access-control-allow-headers": "content-type, x-voyant-protocol",
193
350
  };
194
351
  }
352
+ function sanitizePublicTriggerPayload(payload) {
353
+ const { initialJournal: _initialJournal, initialMetadataAppliedCount: _initialMetadataAppliedCount, timeoutMs: _timeoutMs, ...publicPayload } = payload;
354
+ return publicPayload;
355
+ }
195
356
  function json(status, body) {
196
357
  return new Response(JSON.stringify(body), {
197
358
  status,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voyantjs/workflows-orchestrator-cloudflare",
3
- "version": "0.37.1",
3
+ "version": "0.38.1",
4
4
  "description": "Cloudflare Worker + Durable Object adapter for @voyantjs/workflows-orchestrator. Dispatches workflow-step requests to tenant Workers via a Workers-for-Platforms dispatch namespace.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -26,8 +26,8 @@
26
26
  "NOTICE"
27
27
  ],
28
28
  "dependencies": {
29
- "@voyantjs/workflows-orchestrator": "0.37.1",
30
- "@voyantjs/workflows": "0.37.1"
29
+ "@voyantjs/workflows-orchestrator": "0.38.1",
30
+ "@voyantjs/workflows": "0.38.1"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@cloudflare/vitest-pool-workers": "^0.15.1",
@@ -102,6 +102,9 @@ export async function handleDurableObjectRequest(
102
102
  ? new Date(payload.delay.wakeAt)
103
103
  : payload.delay,
104
104
  priority: payload.priority,
105
+ initialJournal: payload.initialJournal,
106
+ initialMetadataAppliedCount: payload.initialMetadataAppliedCount,
107
+ timeoutMs: payload.timeoutMs,
105
108
  },
106
109
  { store, handler, now: deps.now },
107
110
  )
package/src/types.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  // shape is enough, and tests can pass plain objects.
4
4
 
5
5
  import type { Duration, EnvironmentName, RunTrigger } from "@voyantjs/workflows"
6
- import type { WaitpointInjection } from "@voyantjs/workflows-orchestrator"
6
+ import type { JournalSlice, WaitpointInjection } from "@voyantjs/workflows-orchestrator"
7
7
 
8
8
  /**
9
9
  * Subset of Cloudflare's `DurableObjectStorage` we actually use.
@@ -62,6 +62,17 @@ export interface TriggerPayload {
62
62
  delay?: Duration | { wakeAt: number }
63
63
  priority?: number
64
64
  triggeredBy?: RunTrigger
65
+ /**
66
+ * Optional journal seed for failed-step resume / replay flows.
67
+ * Steps already present in the journal are replayed from their
68
+ * stored results, so the workflow starts doing new work at the
69
+ * first unseeded step.
70
+ */
71
+ initialJournal?: JournalSlice
72
+ /** Cursor paired with `initialJournal.metadataState` for resume replay dedupe. */
73
+ initialMetadataAppliedCount?: number
74
+ /** Compute-time budget in ms; copied from parent runs during resume. */
75
+ timeoutMs?: number
65
76
  concurrencyLease?: ConcurrencyLease
66
77
  }
67
78
 
package/src/worker.ts CHANGED
@@ -6,17 +6,29 @@
6
6
  // Routes (all JSON bodies):
7
7
  // POST /api/runs → trigger a run
8
8
  // GET /api/runs/:id → fetch a run
9
+ // POST /api/runs/:id/resume → start a new run from a failed parent step
9
10
  // POST /api/runs/:id/signals → inject a SIGNAL waitpoint
10
11
  // POST /api/runs/:id/events → inject an EVENT waitpoint
11
12
  // POST /api/runs/:id/tokens/:token → inject a MANUAL (token) waitpoint
12
13
  // POST /api/runs/:id/cancel → cancel a run
13
14
 
14
- import type { WaitpointInjection } from "@voyantjs/workflows-orchestrator"
15
+ import {
16
+ buildResumeJournal,
17
+ buildSeededResumeJournal,
18
+ type RunRecord,
19
+ type WaitpointInjection,
20
+ } from "@voyantjs/workflows-orchestrator"
15
21
 
16
22
  import { handleIngestEvent } from "./event-handler.js"
17
23
  import { handleGetManifest, handleRegisterManifest } from "./manifest-handler.js"
18
24
  import type { CfManifestStore } from "./manifest-kv-store.js"
19
25
 
26
+ const DEFAULT_TENANT_META = {
27
+ tenantId: "default",
28
+ projectId: "default",
29
+ organizationId: "default",
30
+ }
31
+
20
32
  /**
21
33
  * Minimal shape of a DO namespace. `idFromName` returns an opaque id;
22
34
  * `get(id)` returns a stub with `fetch` (matching the CF DO API).
@@ -133,10 +145,11 @@ export async function handleWorkerRequest<Id>(
133
145
  return json(400, { error: "invalid_json", message: errMsg(err) })
134
146
  }
135
147
  const runId = typeof payload.runId === "string" ? payload.runId : defaultRunId(deps)
148
+ const publicPayload = sanitizePublicTriggerPayload(payload)
136
149
  const forward = new Request(`https://do-internal/trigger`, {
137
150
  method: "POST",
138
151
  headers: { "content-type": "application/json" },
139
- body: JSON.stringify({ ...payload, runId }),
152
+ body: JSON.stringify({ ...publicPayload, runId }),
140
153
  })
141
154
  return forwardToRunDO(runId, forward, deps)
142
155
  }
@@ -168,6 +181,82 @@ export async function handleWorkerRequest<Id>(
168
181
  )
169
182
  }
170
183
 
184
+ if (req.method === "POST" && tail === "/resume") {
185
+ const body = await safeJson(req)
186
+ if (isErrorBody(body)) return json(400, body)
187
+ const parsed = parseResumeRunBody(body)
188
+ if ("error" in parsed) return json(400, parsed)
189
+
190
+ const parent = await fetchRunRecord(runId, deps)
191
+ if ("error" in parent) return parent.error
192
+
193
+ const workflowId = parsed.body.workflowId ?? parent.record?.workflowId
194
+ if (!workflowId) {
195
+ return json(404, {
196
+ error: "resume_failed",
197
+ message:
198
+ `parent run "${runId}" not found; pass workflowId, resumeFromStep, ` +
199
+ "and seedResults to resume from an external workflow-runs parent",
200
+ })
201
+ }
202
+
203
+ let resumeSeed: ReturnType<typeof buildResumeJournal>
204
+ try {
205
+ resumeSeed = parent.record
206
+ ? buildResumeJournal({
207
+ parent: parent.record,
208
+ resumeFromStep: parsed.body.resumeFromStep,
209
+ seedResults: parsed.body.seedResults,
210
+ now: deps.now,
211
+ })
212
+ : buildSeededResumeJournal({
213
+ parentRunId: runId,
214
+ resumeFromStep: requireExternalResumeFromStep(parsed.body.resumeFromStep),
215
+ seedResults: requireExternalSeedResults(parsed.body.seedResults),
216
+ now: deps.now,
217
+ })
218
+ } catch (err) {
219
+ return json(400, { error: "resume_failed", message: errMsg(err) })
220
+ }
221
+
222
+ const nextRunId = parsed.body.runId ?? defaultRunId(deps)
223
+ const triggerRes = await forwardToRunDO(
224
+ nextRunId,
225
+ new Request("https://do-internal/trigger", {
226
+ method: "POST",
227
+ headers: { "content-type": "application/json" },
228
+ body: JSON.stringify({
229
+ workflowId,
230
+ workflowVersion: parent.record?.workflowVersion ?? "local",
231
+ input: parsed.body.hasInput ? parsed.body.input : parent.record?.input,
232
+ tenantMeta: parent.record?.tenantMeta ?? deps.tenantMeta ?? DEFAULT_TENANT_META,
233
+ environment: parent.record?.environment,
234
+ tags: mergeTags(parent.record?.tags, [
235
+ "resume:true",
236
+ `parentRunId:${parent.record?.id ?? runId}`,
237
+ ...(parsed.body.tags ?? []),
238
+ ]),
239
+ runId: nextRunId,
240
+ triggeredBy:
241
+ parsed.body.triggeredByUserId === undefined || parsed.body.triggeredByUserId === null
242
+ ? { kind: "api" }
243
+ : { kind: "api", actor: parsed.body.triggeredByUserId },
244
+ timeoutMs: parent.record?.timeoutMs,
245
+ initialJournal: resumeSeed.journal,
246
+ initialMetadataAppliedCount: resumeSeed.metadataAppliedCount,
247
+ }),
248
+ }),
249
+ deps,
250
+ )
251
+ if (!triggerRes.ok) return triggerRes
252
+ const saved = (await triggerRes.json()) as RunRecord
253
+ return json(200, {
254
+ saved,
255
+ parentRunId: parent.record?.id ?? runId,
256
+ resumeFromStep: resumeSeed.resumeFromStep,
257
+ })
258
+ }
259
+
171
260
  // Waitpoint injections: events, signals, tokens.
172
261
  const body = await safeJson(req)
173
262
  if (isErrorBody(body)) return json(400, body)
@@ -221,6 +310,78 @@ function parseInjection(
221
310
  return { error: "route_not_found", message: `unknown path suffix ${tail}` }
222
311
  }
223
312
 
313
+ function parseResumeRunBody(body: Record<string, unknown>):
314
+ | {
315
+ body: {
316
+ hasInput: boolean
317
+ input?: unknown
318
+ workflowId?: string
319
+ resumeFromStep?: string
320
+ seedResults?: Record<string, unknown>
321
+ runId?: string
322
+ tags?: string[]
323
+ triggeredByUserId?: string | null
324
+ }
325
+ }
326
+ | { error: string; message: string } {
327
+ if (!isPlainObject(body)) {
328
+ return { error: "invalid_body", message: "request body must be an object" }
329
+ }
330
+ if (body.resumeFromStep !== undefined && typeof body.resumeFromStep !== "string") {
331
+ return { error: "invalid_body", message: "`resumeFromStep` must be a string when provided" }
332
+ }
333
+ if (body.workflowId !== undefined && typeof body.workflowId !== "string") {
334
+ return { error: "invalid_body", message: "`workflowId` must be a string when provided" }
335
+ }
336
+ if (body.runId !== undefined && typeof body.runId !== "string") {
337
+ return { error: "invalid_body", message: "`runId` must be a string when provided" }
338
+ }
339
+ if (
340
+ body.triggeredByUserId !== undefined &&
341
+ body.triggeredByUserId !== null &&
342
+ typeof body.triggeredByUserId !== "string"
343
+ ) {
344
+ return {
345
+ error: "invalid_body",
346
+ message: "`triggeredByUserId` must be a string or null when provided",
347
+ }
348
+ }
349
+ if (body.tags !== undefined && !isStringArray(body.tags)) {
350
+ return { error: "invalid_body", message: "`tags` must be an array of strings when provided" }
351
+ }
352
+ if (body.seedResults !== undefined && !isPlainObject(body.seedResults)) {
353
+ return { error: "invalid_body", message: "`seedResults` must be an object when provided" }
354
+ }
355
+ return {
356
+ body: {
357
+ hasInput: Object.hasOwn(body, "input"),
358
+ input: body.input,
359
+ workflowId: body.workflowId,
360
+ resumeFromStep: body.resumeFromStep,
361
+ seedResults: body.seedResults as Record<string, unknown> | undefined,
362
+ runId: body.runId,
363
+ tags: body.tags as string[] | undefined,
364
+ triggeredByUserId: body.triggeredByUserId as string | null | undefined,
365
+ },
366
+ }
367
+ }
368
+
369
+ async function fetchRunRecord<Id>(
370
+ runId: string,
371
+ deps: WorkerFetchDeps<Id>,
372
+ ): Promise<{ record?: RunRecord } | { error: Response }> {
373
+ const res = await forwardToRunDO(
374
+ runId,
375
+ new Request("https://do-internal/get", { method: "GET" }),
376
+ deps,
377
+ )
378
+ if (res.status === 404) {
379
+ return {}
380
+ }
381
+ if (!res.ok) return { error: res }
382
+ return { record: (await res.json()) as RunRecord }
383
+ }
384
+
224
385
  async function forwardToRunDO<Id>(
225
386
  runId: string,
226
387
  req: Request,
@@ -255,12 +416,48 @@ async function safeJson(
255
416
  const text = await req.text()
256
417
  if (text.length === 0) return {}
257
418
  try {
258
- return JSON.parse(text) as Record<string, unknown>
419
+ const parsed = JSON.parse(text) as unknown
420
+ if (!isPlainObject(parsed)) {
421
+ return { error: "invalid_body", message: "request body must be an object" }
422
+ }
423
+ return parsed
259
424
  } catch (err) {
260
425
  return { error: "invalid_json", message: errMsg(err) }
261
426
  }
262
427
  }
263
428
 
429
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
430
+ return typeof value === "object" && value !== null && !Array.isArray(value)
431
+ }
432
+
433
+ function isStringArray(value: unknown): value is string[] {
434
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string")
435
+ }
436
+
437
+ function mergeTags(...groups: ReadonlyArray<ReadonlyArray<string> | undefined>): string[] {
438
+ const tags = new Set<string>()
439
+ for (const group of groups) {
440
+ for (const tag of group ?? []) tags.add(tag)
441
+ }
442
+ return Array.from(tags)
443
+ }
444
+
445
+ function requireExternalResumeFromStep(resumeFromStep: string | undefined): string {
446
+ if (!resumeFromStep) {
447
+ throw new Error("resumeFromStep is required when the parent run is not stored by this worker")
448
+ }
449
+ return resumeFromStep
450
+ }
451
+
452
+ function requireExternalSeedResults(
453
+ seedResults: Record<string, unknown> | undefined,
454
+ ): Record<string, unknown> {
455
+ if (!seedResults) {
456
+ throw new Error("seedResults is required when the parent run is not stored by this worker")
457
+ }
458
+ return seedResults
459
+ }
460
+
264
461
  function corsHeaders(methods: string): Record<string, string> {
265
462
  return {
266
463
  "access-control-allow-origin": "*",
@@ -269,6 +466,16 @@ function corsHeaders(methods: string): Record<string, string> {
269
466
  }
270
467
  }
271
468
 
469
+ function sanitizePublicTriggerPayload(payload: Record<string, unknown>): Record<string, unknown> {
470
+ const {
471
+ initialJournal: _initialJournal,
472
+ initialMetadataAppliedCount: _initialMetadataAppliedCount,
473
+ timeoutMs: _timeoutMs,
474
+ ...publicPayload
475
+ } = payload
476
+ return publicPayload
477
+ }
478
+
272
479
  function json(status: number, body: unknown): Response {
273
480
  return new Response(JSON.stringify(body), {
274
481
  status,