@voltro/workflow 0.85.0 → 0.86.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/CHANGELOG.md +666 -0
- package/THIRD-PARTY-NOTICES.md +5 -33
- package/dist/admission.js +140 -0
- package/dist/admissionDrainer.js +705 -0
- package/dist/admissionGate.js +115 -0
- package/dist/admissionStore.js +347 -0
- package/dist/admissionSubmission.js +297 -0
- package/dist/admissionTables.js +137 -0
- package/dist/admissionTransaction.js +21 -0
- package/dist/awaitEvent.js +82 -0
- package/dist/budgetHold.js +136 -0
- package/dist/cancelOnPlan.js +75 -0
- package/dist/cancelOnSweep.js +188 -0
- package/dist/cancelPendingStart.js +50 -0
- package/dist/cluster.js +8 -2
- package/dist/clusterCron.js +11 -0
- package/dist/clusterDiagnostics.js +20 -0
- package/dist/clusterDiagnosticsSql.js +145 -0
- package/dist/clusterLayer.js +110 -0
- package/dist/clusterMigrationGate.js +61 -0
- package/dist/clusterRunnerLayer.js +21 -0
- package/dist/clusterTestSuite.js +101 -101
- package/dist/controlMonitor.js +13 -0
- package/dist/controlRequest.js +367 -0
- package/dist/executionRefusal.js +162 -0
- package/dist/failureHandoff.js +183 -0
- package/dist/failureMessage.js +12 -0
- package/dist/failureRecovery.js +50 -0
- package/dist/failureReportStore.js +60 -0
- package/dist/flowControl.js +142 -0
- package/dist/forwardStartReceipts.js +41 -0
- package/dist/inMemoryLayer.js +7 -0
- package/dist/inMemoryRecorder.js +56 -0
- package/dist/index.d.ts +82 -8
- package/dist/index.js +49 -5
- package/dist/inspectWorkflow.js +101 -0
- package/dist/{journalEncryption-DTyhUzaS.js → journalEncryption.js} +30 -37
- package/dist/messages.js +92 -0
- package/dist/nondeterminism.js +51 -0
- package/dist/parentClosure.js +141 -0
- package/dist/parentClosureRecovery.js +54 -0
- package/dist/pendingSubmission.js +85 -0
- package/dist/prepareSubmissionDispatch.js +17 -0
- package/dist/preparedExecution.js +14 -0
- package/dist/primitives.js +225 -2
- package/dist/queuedCancellationRequest.js +364 -0
- package/dist/queuedControl.js +21 -0
- package/dist/recorder.js +11 -0
- package/dist/residentExecutionEvidence.js +64 -0
- package/dist/retryPolicy.js +57 -0
- package/dist/runEventsTable.js +17 -0
- package/dist/runGeneration.js +345 -0
- package/dist/runRecording.js +615 -0
- package/dist/runStepsTable.js +28 -0
- package/dist/runnerStorageGuard.js +29 -0
- package/dist/runsTable.js +42 -0
- package/dist/scheduleDeclaration.js +44 -0
- package/dist/signalHint.js +23 -0
- package/dist/signals.js +70 -0
- package/dist/signalsSuspending.js +57 -0
- package/dist/sqliteWorkflowOwnership.js +26 -0
- package/dist/stalenessSweep.js +191 -0
- package/dist/startContextsTable.js +20 -0
- package/dist/startReceipt.js +143 -0
- package/dist/submissionRecovery.js +122 -0
- package/dist/submissionRejection.js +10 -0
- package/dist/submissionRetry.js +71 -0
- package/dist/suspensionUnavailable.js +9 -0
- package/dist/updates.js +87 -0
- package/dist/watermarkTable.js +9 -0
- package/dist/webCrypto.js +17 -0
- package/dist/workflowDelivery.js +24 -0
- package/dist/workflowJournal.js +124 -0
- package/dist/workflowJournalReset.js +111 -0
- package/dist/workflowReadSelections.js +12 -0
- package/dist/workflowRestartDispatch.js +29 -0
- package/dist/workflowRestartObservation.js +60 -0
- package/dist/workflowRestartPreparation.js +109 -0
- package/dist/workflowRestartReservation.js +426 -0
- package/dist/workflowResumeDispatch.js +29 -0
- package/package.json +4 -4
- package/dist/cluster-F_bhqVP7.js +0 -679
- package/dist/primitives-DX04WTWh.js +0 -507
- package/dist/src-C2NEyUM8.js +0 -3925
- package/dist/workflowReadSelections-CZAUjP7f.js +0 -2096
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { boolean as e, id as t, integer as n, json as r, table as i, text as a, timestamp as o } from "@voltro/database";
|
|
2
|
+
//#region src/runsTable.ts
|
|
3
|
+
var s = i("_voltro_workflow_runs", {
|
|
4
|
+
id: t({ prefix: "wfrun" }),
|
|
5
|
+
tag: a(),
|
|
6
|
+
executionId: a().unique(),
|
|
7
|
+
status: a().oneOf([
|
|
8
|
+
"running",
|
|
9
|
+
"succeeded",
|
|
10
|
+
"failed",
|
|
11
|
+
"cancelled",
|
|
12
|
+
"suspended"
|
|
13
|
+
]).default("running"),
|
|
14
|
+
payload: r(),
|
|
15
|
+
workflowVersion: a().nullable(),
|
|
16
|
+
workflowPatches: r().nullable(),
|
|
17
|
+
runnerEnteredAt: o().nullable(),
|
|
18
|
+
runnerGeneration: a().nullable(),
|
|
19
|
+
engineDelivery: r().nullable(),
|
|
20
|
+
reclaimCount: n().default(0),
|
|
21
|
+
output: r().nullable(),
|
|
22
|
+
subject: r().nullable(),
|
|
23
|
+
source: a().nullable(),
|
|
24
|
+
errorTag: a().nullable(),
|
|
25
|
+
errorMessage: a().nullable(),
|
|
26
|
+
cancelled: e().default(!1),
|
|
27
|
+
discardedAt: o().nullable(),
|
|
28
|
+
discardedBy: a().nullable(),
|
|
29
|
+
startedAt: o().default("now"),
|
|
30
|
+
completedAt: o().nullable(),
|
|
31
|
+
durationMs: n().nullable(),
|
|
32
|
+
traceId: a().nullable(),
|
|
33
|
+
parentExecutionId: a().nullable(),
|
|
34
|
+
parentGeneration: a().nullable(),
|
|
35
|
+
parentClosePolicy: a().oneOf([
|
|
36
|
+
"cancel",
|
|
37
|
+
"terminate",
|
|
38
|
+
"abandon"
|
|
39
|
+
]).nullable()
|
|
40
|
+
}).index(["tag"]).index(["status"]).index(["source"]).index(["startedAt"]).index(["parentExecutionId"]).index(["parentClosePolicy"]);
|
|
41
|
+
//#endregion
|
|
42
|
+
export { s as _voltroWorkflowRunsTable };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Cron as e, Duration as t, Result as n } from "effect";
|
|
2
|
+
//#region src/scheduleDeclaration.ts
|
|
3
|
+
var r = 864e5, i = /* @__PURE__ */ new Set([
|
|
4
|
+
"skip",
|
|
5
|
+
"buffer",
|
|
6
|
+
"cancelOther"
|
|
7
|
+
]), a = /* @__PURE__ */ new Set([
|
|
8
|
+
"skip",
|
|
9
|
+
"latest",
|
|
10
|
+
"all"
|
|
11
|
+
]), o = (t, o) => {
|
|
12
|
+
if (o === void 0) return;
|
|
13
|
+
if (typeof o.timezone != "string" || o.timezone.trim() === "") throw Error(`workflow '${t}': schedule.timezone is required (e.g. "UTC", "Europe/Berlin") — never server-local, which differs between your machine and the container.`);
|
|
14
|
+
let c = e.parse(o.cron, o.timezone);
|
|
15
|
+
if (n.isFailure(c)) throw Error(`workflow '${t}': schedule.cron "${o.cron}" (tz="${o.timezone}") is not a valid cron expression — ${String(c.failure)}`);
|
|
16
|
+
let l = o.onOverlap ?? "skip";
|
|
17
|
+
if (!i.has(l)) throw Error(`workflow '${t}': schedule.onOverlap must be 'skip' | 'buffer' | 'cancelOther', got ${JSON.stringify(l)}`);
|
|
18
|
+
let u = o.backfill ?? "skip";
|
|
19
|
+
if (!a.has(u)) throw Error(`workflow '${t}': schedule.backfill must be 'skip' | 'latest' | 'all', got ${JSON.stringify(u)}`);
|
|
20
|
+
let d = r;
|
|
21
|
+
return o.maxRuntime !== void 0 && (d = s(o.maxRuntime, t)), {
|
|
22
|
+
cron: o.cron,
|
|
23
|
+
timezone: o.timezone,
|
|
24
|
+
...o.payload === void 0 ? {} : { payload: o.payload },
|
|
25
|
+
onOverlap: l,
|
|
26
|
+
backfill: u,
|
|
27
|
+
maxRuntimeMs: d
|
|
28
|
+
};
|
|
29
|
+
}, s = (e, n) => {
|
|
30
|
+
let r;
|
|
31
|
+
try {
|
|
32
|
+
r = t.toMillis(t.fromInputUnsafe(e));
|
|
33
|
+
} catch {
|
|
34
|
+
throw Error(`workflow '${n}': schedule.maxRuntime is not a duration Effect can read (got ${JSON.stringify(e)}). Use '2 hours', a number of milliseconds, or a Duration.`);
|
|
35
|
+
}
|
|
36
|
+
if (!Number.isFinite(r) || r <= 0) throw Error(`workflow '${n}': schedule.maxRuntime must be a positive finite duration`);
|
|
37
|
+
return r;
|
|
38
|
+
}, c = "__voltroWorkflowSchedule", l = (e) => {
|
|
39
|
+
if (typeof e != "object" && typeof e != "function" || e === null) return;
|
|
40
|
+
let t = e[c];
|
|
41
|
+
return t === void 0 ? void 0 : t;
|
|
42
|
+
};
|
|
43
|
+
//#endregion
|
|
44
|
+
export { r as DEFAULT_WORKFLOW_SCHEDULE_MAX_RUNTIME_MS, c as WorkflowScheduleProperty, l as getWorkflowSchedule, o as validateWorkflowSchedule };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region src/signalHint.ts
|
|
2
|
+
var e = 3e5, t, n = (e) => {
|
|
3
|
+
t = e !== void 0 && Number.isFinite(e) && e > 0 ? e : void 0;
|
|
4
|
+
}, r = () => {
|
|
5
|
+
let e = process.env.VOLTRO_WORKFLOW_SUSPEND_HINT_MS;
|
|
6
|
+
if (e !== void 0) {
|
|
7
|
+
let t = Number(e);
|
|
8
|
+
if (Number.isFinite(t) && t > 0) return t;
|
|
9
|
+
}
|
|
10
|
+
return t ?? 3e5;
|
|
11
|
+
}, i = /* @__PURE__ */ new Set(), a = () => {
|
|
12
|
+
i.clear();
|
|
13
|
+
}, o = (e) => {
|
|
14
|
+
let t = r();
|
|
15
|
+
if (!(e.timeoutMs > t)) return !1;
|
|
16
|
+
let n = `${e.workflowName ?? ""}\u0000${e.signalName}`;
|
|
17
|
+
if (i.has(n)) return !1;
|
|
18
|
+
i.add(n);
|
|
19
|
+
let a = e.workflowName === void 0 ? "" : ` in workflow '${e.workflowName}'`;
|
|
20
|
+
return (e.warn ?? ((e) => console.warn(e)))(`awaitSignal('${e.signalName}')${a} declares a ${Math.round(e.timeoutMs / 1e3)}s timeout — a wait that long holds a worker fiber for its whole duration. For human-approval-length waits, awaitSignalSuspending (same call shape, from @voltro/workflow) SUSPENDS the run and frees the slot until the signal arrives. This is a hint, not a behaviour change; it is logged once per workflow. Threshold: workflows.suspendSignalHintMs in app.config.ts, or VOLTRO_WORKFLOW_SUSPEND_HINT_MS.`), !0;
|
|
21
|
+
};
|
|
22
|
+
//#endregion
|
|
23
|
+
export { e as DEFAULT_SUSPEND_HINT_MS, o as maybeHintSuspendingSignal, a as resetSuspendHintsForTest, r as resolveSuspendHintMs, n as setSuspendSignalHintMs };
|
package/dist/signals.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { encryptionAwareActivity as e, openEventPayload as t } from "./journalEncryption.js";
|
|
2
|
+
import { CurrentWorkflowRunId as n, WorkflowRunRecorder as r } from "./recorder.js";
|
|
3
|
+
import { maybeHintSuspendingSignal as i } from "./signalHint.js";
|
|
4
|
+
import { Effect as a, Schema as o } from "effect";
|
|
5
|
+
import { and as s, eq as c } from "@voltro/database";
|
|
6
|
+
import { WorkflowInstance as l } from "effect/workflow/WorkflowEngine";
|
|
7
|
+
//#region src/signals.ts
|
|
8
|
+
var u = async (e, t) => {
|
|
9
|
+
if (e) try {
|
|
10
|
+
await e.recordEvent(t);
|
|
11
|
+
} catch {}
|
|
12
|
+
}, d = (d, f) => {
|
|
13
|
+
let p = a.gen(function* () {
|
|
14
|
+
let e = f.pollIntervalMs ?? 200, p = f.maxPollIntervalMs ?? 5e3, m = f.timeoutMs ?? 864e5, h = yield* a.serviceOption(l), g;
|
|
15
|
+
i({
|
|
16
|
+
workflowName: h._tag === "Some" ? h.value.workflow._tag : void 0,
|
|
17
|
+
signalName: f.name,
|
|
18
|
+
timeoutMs: m,
|
|
19
|
+
warn: (e) => {
|
|
20
|
+
g = e;
|
|
21
|
+
}
|
|
22
|
+
}), g !== void 0 && (yield* a.logWarning(g));
|
|
23
|
+
let _ = yield* n, v = yield* a.serviceOption(r), y = v._tag === "Some" ? v.value : void 0;
|
|
24
|
+
if (_ === void 0) return yield* a.die(/* @__PURE__ */ Error("awaitSignal: no active workflow run id in scope"));
|
|
25
|
+
let b = _;
|
|
26
|
+
yield* a.promise(() => u(y, {
|
|
27
|
+
runId: b,
|
|
28
|
+
eventType: "signal-awaited",
|
|
29
|
+
payload: { signalName: f.name }
|
|
30
|
+
}));
|
|
31
|
+
let x = Date.now(), S = e;
|
|
32
|
+
for (;;) {
|
|
33
|
+
let e = Date.now() - x;
|
|
34
|
+
if (e > m) return yield* a.die(/* @__PURE__ */ Error(`awaitSignal('${f.name}') timed out after ${e}ms`));
|
|
35
|
+
let n = {
|
|
36
|
+
table: "_voltro_workflow_run_events",
|
|
37
|
+
order: [{
|
|
38
|
+
column: "occurredAt",
|
|
39
|
+
direction: "asc"
|
|
40
|
+
}],
|
|
41
|
+
predicate: s(c("runId", b), c("eventType", "signal-sent")),
|
|
42
|
+
projection: void 0,
|
|
43
|
+
skip: void 0,
|
|
44
|
+
take: 1e3
|
|
45
|
+
}, r = (yield* a.tryPromise({
|
|
46
|
+
try: () => d.store.query(n),
|
|
47
|
+
catch: (e) => e
|
|
48
|
+
})).find((e) => e.payload?.signalName === f.name);
|
|
49
|
+
if (r !== void 0) {
|
|
50
|
+
let e = t(r.payload)?.value, n = yield* o.decodeUnknownEffect(f.schema)(e);
|
|
51
|
+
return yield* a.promise(() => u(y, {
|
|
52
|
+
runId: b,
|
|
53
|
+
eventType: "signal-received",
|
|
54
|
+
payload: {
|
|
55
|
+
signalName: f.name,
|
|
56
|
+
value: e
|
|
57
|
+
}
|
|
58
|
+
})), n;
|
|
59
|
+
}
|
|
60
|
+
yield* a.sleep(`${S} millis`), S = Math.min(S * 2, p);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
return e({
|
|
64
|
+
name: `await-signal:${f.name}`,
|
|
65
|
+
success: f.schema,
|
|
66
|
+
execute: p
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
//#endregion
|
|
70
|
+
export { d as awaitSignal };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { isWorkflowEncrypted as e, sealedSchema as t } from "./journalEncryption.js";
|
|
2
|
+
import { CurrentWorkflowRunId as n, WorkflowRunRecorder as r } from "./recorder.js";
|
|
3
|
+
import { SuspensionUnavailable as i, WorkflowCannotPark as a } from "./suspensionUnavailable.js";
|
|
4
|
+
import { resolveWorkflowMessageRun as o } from "./messages.js";
|
|
5
|
+
import { DurableClock as s, DurableDeferred as c, Workflow as l, WorkflowEngine as u } from "effect/workflow";
|
|
6
|
+
import { Cause as d, Effect as f, Exit as p, Schema as m } from "effect";
|
|
7
|
+
//#region src/signalsSuspending.ts
|
|
8
|
+
var h = 864e5, g = "voltro/suspending-signal", _ = (e, t) => `${g}/${e}/${t}`, v = (n, r) => c.make(n, { success: e(r) ? t(m.Unknown, `signal '${n}' value`) : m.Unknown }), y = async (e, t) => {
|
|
9
|
+
if (e) try {
|
|
10
|
+
await e.recordEvent(t);
|
|
11
|
+
} catch {}
|
|
12
|
+
}, b = (e, t) => f.gen(function* () {
|
|
13
|
+
let e = yield* i;
|
|
14
|
+
if (e !== void 0) return yield* f.die(new a(`awaitSignalSuspending('${t.name}')`, e));
|
|
15
|
+
let o = t.timeoutMs ?? h, p = yield* u.WorkflowInstance, g = v(_(p.workflow._tag, t.name), p.workflow._tag), b = yield* n, x = yield* f.serviceOption(r), S = x._tag === "Some" ? x.value : void 0;
|
|
16
|
+
b !== void 0 && (yield* f.promise(() => y(S, {
|
|
17
|
+
runId: b,
|
|
18
|
+
eventType: "signal-awaited",
|
|
19
|
+
payload: { signalName: t.name }
|
|
20
|
+
})));
|
|
21
|
+
let C = c.await(g), w = Number.isFinite(o) ? yield* f.race(C, s.sleep({
|
|
22
|
+
name: `await-signal-timeout/${t.name}`,
|
|
23
|
+
inMemoryThreshold: "0 millis",
|
|
24
|
+
duration: `${o} millis`
|
|
25
|
+
}).pipe(f.andThen(f.die(/* @__PURE__ */ Error(`awaitSignalSuspending('${t.name}') timed out after ${o}ms`))))).pipe(f.catchCause((e) => p.suspended ? d.hasInterruptsOnly(e) ? l.suspend(p) : f.failCause(d.fromReasons(e.reasons.filter((e) => !d.isInterruptReason(e)))) : f.failCause(e))) : yield* C, T = yield* m.decodeUnknownEffect(t.schema)(w);
|
|
26
|
+
return b !== void 0 && (yield* f.promise(() => y(S, {
|
|
27
|
+
runId: b,
|
|
28
|
+
eventType: "signal-received",
|
|
29
|
+
payload: {
|
|
30
|
+
signalName: t.name,
|
|
31
|
+
value: T
|
|
32
|
+
}
|
|
33
|
+
}))), T;
|
|
34
|
+
}), x = (e) => f.gen(function* () {
|
|
35
|
+
let t = e.target.executionId, n = e.target.workflowName;
|
|
36
|
+
if (t === void 0 || n === void 0) {
|
|
37
|
+
let r = e.store;
|
|
38
|
+
if (r === void 0) return yield* f.die(/* @__PURE__ */ Error("completeSuspendingSignal: provide a store, or target.executionId AND target.workflowName"));
|
|
39
|
+
let i = yield* f.tryPromise({
|
|
40
|
+
try: () => o(r, e.target),
|
|
41
|
+
catch: (e) => e
|
|
42
|
+
});
|
|
43
|
+
t ??= i.executionId, n ??= i.tag;
|
|
44
|
+
}
|
|
45
|
+
if (t === void 0 || n === void 0) return { completed: !1 };
|
|
46
|
+
let r = _(n, e.signalName), i = v(r, n), a = new c.TokenParsed({
|
|
47
|
+
workflowName: n,
|
|
48
|
+
executionId: t,
|
|
49
|
+
deferredName: r
|
|
50
|
+
}).asToken;
|
|
51
|
+
return yield* c.done(i, {
|
|
52
|
+
token: a,
|
|
53
|
+
exit: p.succeed(e.payload)
|
|
54
|
+
}), { completed: !0 };
|
|
55
|
+
});
|
|
56
|
+
//#endregion
|
|
57
|
+
export { b as awaitSignalSuspending, x as completeSuspendingSignal, _ as suspendingSignalDeferredName };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Effect as e } from "effect";
|
|
2
|
+
import { realpath as t } from "node:fs/promises";
|
|
3
|
+
//#region src/sqliteWorkflowOwnership.ts
|
|
4
|
+
var n = (n) => e.gen(function* () {
|
|
5
|
+
let i = yield* e.tryPromise({
|
|
6
|
+
try: () => t(n),
|
|
7
|
+
catch: r
|
|
8
|
+
}), a = yield* e.acquireRelease(e.tryPromise({
|
|
9
|
+
try: async () => {
|
|
10
|
+
let { DatabaseSync: e } = await import("node:sqlite");
|
|
11
|
+
return new e(`${i}.voltro-workflow-lock`);
|
|
12
|
+
},
|
|
13
|
+
catch: r
|
|
14
|
+
}), (t) => e.sync(() => t.close()));
|
|
15
|
+
yield* e.try({
|
|
16
|
+
try: () => {
|
|
17
|
+
a.exec("PRAGMA busy_timeout=0"), a.exec("CREATE TABLE IF NOT EXISTS workflow_mutex(id INTEGER PRIMARY KEY)"), a.exec("BEGIN IMMEDIATE");
|
|
18
|
+
},
|
|
19
|
+
catch: (e) => {
|
|
20
|
+
let t = r(e), n = t.errcode;
|
|
21
|
+
return n !== void 0 && ((n & 255) == 5 || (n & 255) == 6) ? Error("SQLite workflow journal is already owned by another engine. Stop that engine before starting another, or use a SQL dialect with shared runner coordination for replicas.", { cause: t }) : Error(`Could not acquire SQLite workflow ownership for ${i}`, { cause: t });
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}), r = (e) => e instanceof Error ? e : Error(String(e));
|
|
25
|
+
//#endregion
|
|
26
|
+
export { n as acquireSqliteWorkflowOwnership };
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { and as e, eq as t, gt as n, inSet as r, lt as i, or as a } from "@voltro/database";
|
|
2
|
+
//#region src/stalenessSweep.ts
|
|
3
|
+
var o = 18e5, s = 200, c = 20, l = (e) => {
|
|
4
|
+
if (e instanceof Date) return e;
|
|
5
|
+
if (typeof e == "number") return new Date(e);
|
|
6
|
+
if (typeof e == "string") {
|
|
7
|
+
let t = new Date(e);
|
|
8
|
+
if (!Number.isNaN(t.getTime())) return t;
|
|
9
|
+
}
|
|
10
|
+
}, u = (e) => e instanceof Error ? e.message : String(e), d = async (e, n, r) => (await e.query({
|
|
11
|
+
table: "_voltro_workflow_run_events",
|
|
12
|
+
predicate: t("runId", n),
|
|
13
|
+
order: [{
|
|
14
|
+
column: "occurredAt",
|
|
15
|
+
direction: "desc"
|
|
16
|
+
}],
|
|
17
|
+
take: r,
|
|
18
|
+
skip: void 0,
|
|
19
|
+
projection: void 0
|
|
20
|
+
})).flatMap((e) => {
|
|
21
|
+
let t = e, n = l(t.occurredAt);
|
|
22
|
+
return typeof t.eventType != "string" || n === void 0 ? [] : [{
|
|
23
|
+
eventType: t.eventType,
|
|
24
|
+
occurredAt: n,
|
|
25
|
+
payload: t.payload
|
|
26
|
+
}];
|
|
27
|
+
}), f = async (e, n) => {
|
|
28
|
+
let r = (await e.query({
|
|
29
|
+
table: "_voltro_workflow_run_steps",
|
|
30
|
+
predicate: t("runId", n),
|
|
31
|
+
order: [{
|
|
32
|
+
column: "startedAt",
|
|
33
|
+
direction: "desc"
|
|
34
|
+
}],
|
|
35
|
+
take: 1,
|
|
36
|
+
skip: void 0,
|
|
37
|
+
projection: void 0
|
|
38
|
+
}))[0];
|
|
39
|
+
if (r === void 0) return;
|
|
40
|
+
let i = l(r.startedAt), a = l(r.completedAt);
|
|
41
|
+
return i === void 0 ? a : a === void 0 ? i : a.getTime() > i.getTime() ? a : i;
|
|
42
|
+
}, p = (e, t) => {
|
|
43
|
+
for (let n of e) {
|
|
44
|
+
if (n.eventType === "timer-fired") return !1;
|
|
45
|
+
if (n.eventType !== "timer-set") continue;
|
|
46
|
+
let e = l(n.payload?.scheduledWakeAt);
|
|
47
|
+
return e !== void 0 && e.getTime() > t;
|
|
48
|
+
}
|
|
49
|
+
return !1;
|
|
50
|
+
}, m = (e, t) => {
|
|
51
|
+
if (e !== "suspended") return "no-progress";
|
|
52
|
+
for (let e of t) {
|
|
53
|
+
if (e.eventType === "signal-received" || e.eventType === "signal-sent") return "suspended";
|
|
54
|
+
if (e.eventType === "signal-awaited") return "awaiting-signal";
|
|
55
|
+
}
|
|
56
|
+
return "suspended";
|
|
57
|
+
}, h = async (o, s = {}) => {
|
|
58
|
+
let c = o.now?.() ?? Date.now(), h = s.stallAfterMs ?? 18e5, g = s.runPage ?? 200, _ = s.eventLookback ?? 20, v = new Date(c - h), y = [], b = [], x = 0, S = 0, C, w;
|
|
59
|
+
try {
|
|
60
|
+
if (!Number.isSafeInteger(g) || g < 1) throw Error("Staleness run page must be a positive integer");
|
|
61
|
+
let c = s.afterRun;
|
|
62
|
+
if (c !== void 0 && (!(c.startedAt instanceof Date) || !Number.isFinite(c.startedAt.getTime()) || typeof c.runId != "string" || c.runId.length === 0)) throw Error("Invalid staleness run cursor");
|
|
63
|
+
C = await o.store.query({
|
|
64
|
+
table: "_voltro_workflow_runs",
|
|
65
|
+
predicate: e(r("status", ["running", "suspended"]), i("startedAt", v), ...c === void 0 ? [] : [a(n("startedAt", c.startedAt), e(t("startedAt", c.startedAt), n("id", c.runId)))]),
|
|
66
|
+
order: [{
|
|
67
|
+
column: "startedAt",
|
|
68
|
+
direction: "asc"
|
|
69
|
+
}, {
|
|
70
|
+
column: "id",
|
|
71
|
+
direction: "asc"
|
|
72
|
+
}],
|
|
73
|
+
take: g,
|
|
74
|
+
skip: void 0,
|
|
75
|
+
projection: void 0
|
|
76
|
+
});
|
|
77
|
+
let u = c;
|
|
78
|
+
for (let e of C) {
|
|
79
|
+
let t = l(e.startedAt);
|
|
80
|
+
if (t === void 0 || !Number.isFinite(t.getTime()) || typeof e.id != "string" || e.id.length === 0) throw Error("Staleness candidate has no valid pagination identity");
|
|
81
|
+
if (u !== void 0 && (t.getTime() < u.startedAt.getTime() || t.getTime() === u.startedAt.getTime() && e.id <= u.runId)) throw Error("Staleness candidate page did not advance");
|
|
82
|
+
u = {
|
|
83
|
+
startedAt: t,
|
|
84
|
+
runId: e.id
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
C.length >= g && (w = u);
|
|
88
|
+
} catch (e) {
|
|
89
|
+
return {
|
|
90
|
+
examined: 0,
|
|
91
|
+
stalled: [],
|
|
92
|
+
alreadyReported: 0,
|
|
93
|
+
waitingOnTimer: 0,
|
|
94
|
+
sawFullRunPage: !1,
|
|
95
|
+
failures: [{
|
|
96
|
+
subject: "_voltro_workflow_runs",
|
|
97
|
+
detail: u(e)
|
|
98
|
+
}]
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
for (let e of C) {
|
|
102
|
+
let t = e, n = l(t.startedAt);
|
|
103
|
+
if (typeof t.id != "string" || typeof t.tag != "string" || typeof t.executionId != "string" || typeof t.status != "string" || n === void 0) continue;
|
|
104
|
+
let r, i;
|
|
105
|
+
try {
|
|
106
|
+
r = await d(o.store, t.id, _), i = await f(o.store, t.id);
|
|
107
|
+
} catch (e) {
|
|
108
|
+
y.push({
|
|
109
|
+
subject: t.id,
|
|
110
|
+
detail: u(e)
|
|
111
|
+
});
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (p(r, c)) {
|
|
115
|
+
S += 1;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
let a = r.find((e) => e.eventType !== "run-stalled")?.occurredAt, s = n;
|
|
119
|
+
for (let e of [i, a]) e !== void 0 && e.getTime() > s.getTime() && (s = e);
|
|
120
|
+
let g = c - s.getTime();
|
|
121
|
+
if (g < h) continue;
|
|
122
|
+
if (r.some((e) => e.eventType === "run-stalled" && e.occurredAt.getTime() > s.getTime())) {
|
|
123
|
+
x += 1;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
let v = {
|
|
127
|
+
runId: t.id,
|
|
128
|
+
tag: t.tag,
|
|
129
|
+
executionId: t.executionId,
|
|
130
|
+
status: t.status,
|
|
131
|
+
lastProgressAt: s,
|
|
132
|
+
idleMs: g,
|
|
133
|
+
reason: m(t.status, r)
|
|
134
|
+
};
|
|
135
|
+
b.push(v);
|
|
136
|
+
let C = {};
|
|
137
|
+
if (o.describeRun !== void 0) try {
|
|
138
|
+
C = await o.describeRun(v) ?? {};
|
|
139
|
+
} catch (e) {
|
|
140
|
+
y.push({
|
|
141
|
+
subject: `${v.runId}:describeRun`,
|
|
142
|
+
detail: u(e)
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
if (o.log?.warn("workflow.staleness: run has made no progress", {
|
|
146
|
+
workflow: v.tag,
|
|
147
|
+
runId: v.runId,
|
|
148
|
+
idleMs: v.idleMs,
|
|
149
|
+
reason: v.reason,
|
|
150
|
+
...C
|
|
151
|
+
}), o.recordEvent !== void 0) try {
|
|
152
|
+
await o.recordEvent({
|
|
153
|
+
runId: v.runId,
|
|
154
|
+
eventType: "run-stalled",
|
|
155
|
+
payload: {
|
|
156
|
+
idleMs: v.idleMs,
|
|
157
|
+
stallAfterMs: h,
|
|
158
|
+
lastProgressAt: v.lastProgressAt.toISOString(),
|
|
159
|
+
reason: v.reason,
|
|
160
|
+
tag: v.tag,
|
|
161
|
+
executionId: v.executionId,
|
|
162
|
+
...C
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
} catch (e) {
|
|
166
|
+
y.push({
|
|
167
|
+
subject: v.runId,
|
|
168
|
+
detail: u(e)
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
if (o.onStalled !== void 0) try {
|
|
172
|
+
await o.onStalled(v);
|
|
173
|
+
} catch (e) {
|
|
174
|
+
y.push({
|
|
175
|
+
subject: `${v.runId}:onStalled`,
|
|
176
|
+
detail: u(e)
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
examined: C.length,
|
|
182
|
+
stalled: b,
|
|
183
|
+
alreadyReported: x,
|
|
184
|
+
waitingOnTimer: S,
|
|
185
|
+
sawFullRunPage: C.length >= g,
|
|
186
|
+
failures: y,
|
|
187
|
+
...w === void 0 ? {} : { nextRun: w }
|
|
188
|
+
};
|
|
189
|
+
};
|
|
190
|
+
//#endregion
|
|
191
|
+
export { o as DEFAULT_STALL_AFTER_MS, c as DEFAULT_STALL_EVENT_LOOKBACK, s as DEFAULT_STALL_RUN_PAGE, h as sweepStalledRuns };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { id as e, json as t, table as n, text as r, timestamp as i } from "@voltro/database";
|
|
2
|
+
//#region src/startContextsTable.ts
|
|
3
|
+
var a = n("_voltro_workflow_start_contexts", {
|
|
4
|
+
id: e({ prefix: "wfctx" }),
|
|
5
|
+
workflowName: r(),
|
|
6
|
+
executionId: r().unique(),
|
|
7
|
+
subject: t().nullable(),
|
|
8
|
+
traceId: r().nullable(),
|
|
9
|
+
source: r().nullable(),
|
|
10
|
+
parentExecutionId: r().nullable(),
|
|
11
|
+
parentGeneration: r().nullable(),
|
|
12
|
+
parentClosePolicy: r().oneOf([
|
|
13
|
+
"cancel",
|
|
14
|
+
"terminate",
|
|
15
|
+
"abandon"
|
|
16
|
+
]).nullable(),
|
|
17
|
+
createdAt: i().default("now")
|
|
18
|
+
}).index(["workflowName"]).index(["executionId"]).index(["parentExecutionId"]);
|
|
19
|
+
//#endregion
|
|
20
|
+
export { a as _voltroWorkflowStartContextsTable };
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { readWorkflowFailureRecord as e } from "./failureReportStore.js";
|
|
2
|
+
import { readAdmissionSubmission as t } from "./admissionSubmission.js";
|
|
3
|
+
import { readProvenSubmissionRejection as n } from "./submissionRejection.js";
|
|
4
|
+
import { PENDING_TABLE as r, toPendingIntent as i } from "./admissionStore.js";
|
|
5
|
+
import { Schema as a } from "effect";
|
|
6
|
+
import { WorkflowStartTerminalOutcomeSchema as o } from "@voltro/protocol";
|
|
7
|
+
import { and as s, eq as c, isNull as l } from "@voltro/database";
|
|
8
|
+
//#region src/startReceipt.ts
|
|
9
|
+
var u = "_voltro_workflow_start_receipts", d = class extends Error {
|
|
10
|
+
_tag = "WorkflowStartRequestConflictError";
|
|
11
|
+
constructor() {
|
|
12
|
+
super("Workflow start request identity belongs to a different start"), this.name = "WorkflowStartRequestConflictError";
|
|
13
|
+
}
|
|
14
|
+
}, f = a.Struct({
|
|
15
|
+
id: a.String,
|
|
16
|
+
workflowName: a.String,
|
|
17
|
+
tenantId: a.NullOr(a.String),
|
|
18
|
+
pendingId: a.String,
|
|
19
|
+
submissionId: a.NullOr(a.String),
|
|
20
|
+
terminal: a.NullOr(o),
|
|
21
|
+
requestFingerprint: a.optional(a.NullOr(a.String.pipe(a.check(a.isPattern(/^[a-f0-9]{64}$/)))))
|
|
22
|
+
}).pipe(a.check(a.makeFilter((e) => e.submissionId === null || e.terminal === null, { message: "A start receipt cannot be both submitted and terminal before admission" }))), p = a.decodeUnknownSync(f), m = async (e, t) => {
|
|
23
|
+
let n = await e.query({
|
|
24
|
+
table: u,
|
|
25
|
+
predicate: c("id", t),
|
|
26
|
+
order: [],
|
|
27
|
+
take: 1,
|
|
28
|
+
skip: void 0,
|
|
29
|
+
projection: void 0
|
|
30
|
+
});
|
|
31
|
+
return n[0] === void 0 ? void 0 : p(n[0]);
|
|
32
|
+
}, h = async (e, t) => {
|
|
33
|
+
if (t.id.trim().length === 0 || !/^[a-f0-9]{64}$/.test(t.requestFingerprint)) throw Error("Workflow start request requires an identity and a SHA-256 fingerprint");
|
|
34
|
+
let n = await m(e, t.id);
|
|
35
|
+
if (n !== void 0) {
|
|
36
|
+
if (n.workflowName !== t.workflowName || n.tenantId !== t.tenantId || n.requestFingerprint !== t.requestFingerprint) throw new d();
|
|
37
|
+
return n;
|
|
38
|
+
}
|
|
39
|
+
}, g = async (a, o) => {
|
|
40
|
+
let s = await m(a, o.receiptId);
|
|
41
|
+
if (s === void 0 || s.workflowName !== o.workflowName || s.tenantId !== o.tenantId) throw Error("Workflow start receipt is missing or belongs to another workflow or tenant");
|
|
42
|
+
if (s.terminal !== null) {
|
|
43
|
+
if (s.terminal.kind === "abandoned") {
|
|
44
|
+
let t = await e(a, s.terminal.failureId);
|
|
45
|
+
if (t === void 0 || t.workflowName !== s.workflowName || t.tenantId !== s.tenantId || t.sourceKind !== "intent" || t.report.executionId !== null || t.report.runId !== null) throw Error("Abandoned workflow receipt has no matching failure report");
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
kind: "terminal",
|
|
49
|
+
outcome: s.terminal
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if (s.submissionId === null) {
|
|
53
|
+
let e = await a.query({
|
|
54
|
+
table: r,
|
|
55
|
+
predicate: c("id", s.pendingId),
|
|
56
|
+
order: [],
|
|
57
|
+
take: 1,
|
|
58
|
+
skip: void 0,
|
|
59
|
+
projection: void 0
|
|
60
|
+
});
|
|
61
|
+
if (e[0] === void 0) throw Error("Workflow receipt has no pending input or submission");
|
|
62
|
+
let t = i(e[0]);
|
|
63
|
+
if (t.workflowName !== s.workflowName || t.tenantId !== s.tenantId) throw Error("Workflow receipt pending input belongs to another workflow or tenant");
|
|
64
|
+
if (t.deadLetteredAt !== void 0) throw Error("Workflow receipt input is dead-lettered; reconciliation required");
|
|
65
|
+
return { kind: "pending" };
|
|
66
|
+
}
|
|
67
|
+
let l = await t(a, s.submissionId);
|
|
68
|
+
if (l === void 0 || !l.receiptsBound || l.workflowName !== s.workflowName || l.tenantId !== s.tenantId || !l.intentIds.includes(s.pendingId)) throw Error("Workflow receipt has no matching admission submission");
|
|
69
|
+
if (l.failureId !== null) return {
|
|
70
|
+
kind: "rejected",
|
|
71
|
+
failureId: await n(a, l)
|
|
72
|
+
};
|
|
73
|
+
if (l.executionId === null) {
|
|
74
|
+
if (l.retry.stoppedAt !== null) throw Error("Workflow start acceptance is unresolved; reconciliation required");
|
|
75
|
+
return { kind: "pending" };
|
|
76
|
+
}
|
|
77
|
+
if (l.executionId.length === 0 || l.executionId !== l.preparedExecutionId) throw Error("Workflow start acknowledgement contradicts its prepared identity");
|
|
78
|
+
return {
|
|
79
|
+
kind: "accepted",
|
|
80
|
+
executionId: l.executionId
|
|
81
|
+
};
|
|
82
|
+
}, _ = async (e, t) => {
|
|
83
|
+
if (t.id.length === 0 || t.id === t.pendingId) throw Error("Workflow start receipt requires an identity distinct from its queue slot");
|
|
84
|
+
if (t.requestFingerprint != null && !/^[a-f0-9]{64}$/.test(t.requestFingerprint)) throw Error("Workflow start request requires a SHA-256 fingerprint");
|
|
85
|
+
let n = p((await e.insertIgnoreWithOutcome(u, {
|
|
86
|
+
...t,
|
|
87
|
+
requestFingerprint: t.requestFingerprint ?? null,
|
|
88
|
+
submissionId: null,
|
|
89
|
+
terminal: null
|
|
90
|
+
}, { conflictColumns: ["id"] })).row);
|
|
91
|
+
if (n.workflowName !== t.workflowName || n.tenantId !== t.tenantId || n.pendingId !== t.pendingId || (n.requestFingerprint ?? null) !== (t.requestFingerprint ?? null)) throw Error("Workflow start receipt identity belongs to a different start");
|
|
92
|
+
return n;
|
|
93
|
+
}, v = async (e, n) => {
|
|
94
|
+
let r = await t(e, n);
|
|
95
|
+
if (r === void 0) throw Error("Cannot bind receipts to an unknown admission submission");
|
|
96
|
+
if (r.receiptsBound) return 0;
|
|
97
|
+
let i = 0;
|
|
98
|
+
for (let t of new Set(r.intentIds)) for (;;) {
|
|
99
|
+
let n = await e.query({
|
|
100
|
+
table: u,
|
|
101
|
+
predicate: s(c("pendingId", t), l("submissionId"), l("terminal")),
|
|
102
|
+
order: [{
|
|
103
|
+
column: "id",
|
|
104
|
+
direction: "asc"
|
|
105
|
+
}],
|
|
106
|
+
take: 100,
|
|
107
|
+
skip: void 0,
|
|
108
|
+
projection: void 0
|
|
109
|
+
});
|
|
110
|
+
if (n.length === 0) break;
|
|
111
|
+
for (let t of n) {
|
|
112
|
+
let n = p(t);
|
|
113
|
+
if (n.workflowName !== r.workflowName || n.tenantId !== r.tenantId) throw Error("Pending receipt belongs to a different workflow or tenant");
|
|
114
|
+
if (await e.update("_voltro_workflow_start_receipts", n.id, { submissionId: r.id }) === null) throw Error("Workflow start receipt disappeared during queue transfer");
|
|
115
|
+
i++;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (await e.update("_voltro_workflow_submissions", n, { receiptsBound: !0 }) === null) throw Error("Admission submission disappeared during receipt binding");
|
|
119
|
+
return i;
|
|
120
|
+
}, y = async (e, t) => {
|
|
121
|
+
let n = a.decodeUnknownSync(o)(t.terminal);
|
|
122
|
+
for (;;) {
|
|
123
|
+
let r = await e.query({
|
|
124
|
+
table: u,
|
|
125
|
+
predicate: s(c("pendingId", t.pendingId), l("submissionId"), l("terminal")),
|
|
126
|
+
order: [{
|
|
127
|
+
column: "id",
|
|
128
|
+
direction: "asc"
|
|
129
|
+
}],
|
|
130
|
+
take: 100,
|
|
131
|
+
skip: void 0,
|
|
132
|
+
projection: void 0
|
|
133
|
+
});
|
|
134
|
+
if (r.length === 0) return;
|
|
135
|
+
for (let i of r) {
|
|
136
|
+
let r = p(i);
|
|
137
|
+
if (r.workflowName !== t.workflowName || r.tenantId !== t.tenantId) throw Error("Terminal receipt belongs to a different workflow or tenant");
|
|
138
|
+
if (await e.update("_voltro_workflow_start_receipts", r.id, { terminal: n }) === null) throw Error("Start receipt disappeared while recording its terminal outcome");
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
//#endregion
|
|
143
|
+
export { u as START_RECEIPTS_TABLE, d as WorkflowStartRequestConflictError, v as bindWorkflowStartReceipts, p as decodeWorkflowStartReceipt, m as readWorkflowStartReceipt, h as readWorkflowStartRequest, _ as recordWorkflowStartReceipt, g as resolveWorkflowStartReceipt, y as settlePendingStartReceipts };
|