@kb-labs/workflow-constants 2.118.2 → 2.119.0-canary.0b1ec4151

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,46 @@
1
+ /**
2
+ * Single source of truth for legal status transitions across the workflow
3
+ * engine. Every status-write call site (engine.ts, worker.ts) must route
4
+ * through `assertTransition` (via StateStore.transitionRun/Job/Step) instead
5
+ * of mutating `.status` directly — this is what turns "any status can go to
6
+ * any status" into a checked invariant.
7
+ *
8
+ * See docs/adr and the workflow status audit for the incident this closes:
9
+ * a job could be marked `failed` while its step still sat in
10
+ * `waiting_approval`, because nothing validated that transition was legal.
11
+ */
12
+ type EntityKind = 'run' | 'job' | 'step';
13
+ declare const RUN_TRANSITIONS: Record<RunState, readonly RunState[]>;
14
+ declare const JOB_TRANSITIONS: Record<JobState, readonly JobState[]>;
15
+ declare const STEP_TRANSITIONS: Record<StepState, readonly StepState[]>;
16
+ declare function isTerminal(state: string, kind: EntityKind): boolean;
17
+ declare function isParked(state: string, kind: EntityKind): boolean;
18
+ declare function isActive(state: string, kind: EntityKind): boolean;
19
+ declare class IllegalStateTransitionError extends Error {
20
+ readonly kind: EntityKind;
21
+ readonly from: string;
22
+ readonly to: string;
23
+ readonly context?: Record<string, unknown> | undefined;
24
+ constructor(kind: EntityKind, from: string, to: string, context?: Record<string, unknown> | undefined);
25
+ }
26
+ interface AssertTransitionOptions {
27
+ /**
28
+ * Bulk-reset escape hatch for replayRun/cleanupStaleRuns-style resets that
29
+ * deliberately move a terminal entity back to `queued` (or similar) outside
30
+ * the normal transition table. Must be passed explicitly and is grep-able
31
+ * by design — never add a silent bypass inside the table itself.
32
+ */
33
+ allowReset?: boolean;
34
+ }
35
+ /**
36
+ * Throws IllegalStateTransitionError if `to` is not reachable from `from`
37
+ * per the transition table for `kind`. No-ops (does not throw) when
38
+ * `from === to` — idempotent re-writes of the same status are always legal.
39
+ */
40
+ declare function assertTransition(kind: EntityKind, from: string, to: string, options?: AssertTransitionOptions): void;
41
+
1
42
  declare const RUN_STATES: readonly ["queued", "running", "success", "failed", "cancelled", "skipped", "dlq"];
2
- declare const JOB_STATES: readonly ["queued", "running", "success", "failed", "cancelled", "skipped", "interrupted"];
43
+ declare const JOB_STATES: readonly ["queued", "running", "success", "failed", "cancelled", "skipped", "interrupted", "waiting_approval", "waiting_child"];
3
44
  declare const STEP_STATES: readonly ["queued", "running", "success", "failed", "cancelled", "skipped", "dlq", "waiting_approval", "waiting_child"];
4
45
  declare const JOB_PRIORITIES: readonly ["high", "normal", "low"];
5
46
 
@@ -7,6 +48,7 @@ type RunState = (typeof RUN_STATES)[number];
7
48
  type JobState = (typeof JOB_STATES)[number];
8
49
  type StepState = (typeof STEP_STATES)[number];
9
50
  type JobPriority = (typeof JOB_PRIORITIES)[number];
51
+
10
52
  declare const EVENT_NAMES: {
11
53
  readonly run: {
12
54
  readonly created: "run.created";
@@ -65,4 +107,4 @@ declare const REDIS_MODE_ENV = "KB_REDIS_MODE";
65
107
  declare const REDIS_NAMESPACE_ENV = "KB_REDIS_NAMESPACE";
66
108
  type RedisMode = 'standalone' | 'cluster' | 'sentinel';
67
109
 
68
- export { CONCURRENCY_TTL_ENV, DEFAULT_REDIS_NAMESPACE, EVENT_NAMES, IDEMPOTENCY_TTL_ENV, JOB_PRIORITIES, JOB_STATES, type JobPriority, type JobState, REDIS_MODE_ENV, REDIS_NAMESPACE_ENV, REDIS_URL_ENV, RUN_STATES, type RedisKeyFactory, type RedisKeyFactoryOptions, type RedisMode, type RunState, STEP_STATES, type StepState, WORKFLOW_REDIS_CHANNEL, type WorkflowEventName, createRedisKeyFactory };
110
+ export { type AssertTransitionOptions, CONCURRENCY_TTL_ENV, DEFAULT_REDIS_NAMESPACE, EVENT_NAMES, type EntityKind, IDEMPOTENCY_TTL_ENV, IllegalStateTransitionError, JOB_PRIORITIES, JOB_STATES, JOB_TRANSITIONS, type JobPriority, type JobState, REDIS_MODE_ENV, REDIS_NAMESPACE_ENV, REDIS_URL_ENV, RUN_STATES, RUN_TRANSITIONS, type RedisKeyFactory, type RedisKeyFactoryOptions, type RedisMode, type RunState, STEP_STATES, STEP_TRANSITIONS, type StepState, WORKFLOW_REDIS_CHANNEL, type WorkflowEventName, assertTransition, createRedisKeyFactory, isActive, isParked, isTerminal };
package/dist/index.js CHANGED
@@ -1,6 +1,133 @@
1
+ // src/state-machine.ts
2
+ var RUN_TRANSITIONS = {
3
+ // 'success'/'failed': a run can finish without ever visiting 'running' —
4
+ // e.g. a run whose only job is skipped via `if:` (`skipJob` marks the job
5
+ // success directly and never calls `markJobStarted`, so `run.status` can
6
+ // still be 'queued' by the time `checkRunCompletion` finalizes the run).
7
+ queued: ["running", "cancelled", "success", "failed"],
8
+ running: ["success", "failed", "cancelled", "dlq"],
9
+ success: [],
10
+ failed: [],
11
+ cancelled: [],
12
+ skipped: [],
13
+ dlq: []
14
+ };
15
+ var JOB_TRANSITIONS = {
16
+ // 'success': `skipJob` treats an `if:`-skipped job as success directly,
17
+ // without ever passing through 'running' (see engine.ts `skipJob`).
18
+ // 'failed': `cleanupStaleRuns` force-fails abandoned jobs that never even
19
+ // started (daemon restarted before they were dequeued).
20
+ queued: ["running", "cancelled", "skipped", "success", "failed"],
21
+ // 'queued': a running job can be re-parked back to queued mid-execution
22
+ // when its current step legitimately pauses (waiting_child reconciliation
23
+ // resume, gate restart) — not a hard requirement that 'running' only ever
24
+ // moves forward.
25
+ running: ["success", "failed", "cancelled", "interrupted", "waiting_approval", "waiting_child", "queued"],
26
+ // 'failed': a parked job can be force-failed directly, without ever
27
+ // passing through 'running' again — e.g. `failChildInvocation` fails the
28
+ // parent job when its child run was cancelled/not found/itself failed,
29
+ // while the parent job is still sitting in 'waiting_child'.
30
+ waiting_approval: ["queued", "cancelled", "failed"],
31
+ waiting_child: ["queued", "cancelled", "failed"],
32
+ interrupted: ["queued", "cancelled"],
33
+ success: [],
34
+ // 'queued': `markJobFailed` schedules a retry after a transient failure —
35
+ // the job comes back as 'queued' for its next attempt. 'failed' here means
36
+ // "this attempt failed", not always "this job is permanently done"; the
37
+ // caller decides whether to retry before ever reaching a real terminal
38
+ // outcome for the job.
39
+ failed: ["queued"],
40
+ cancelled: [],
41
+ skipped: []
42
+ };
43
+ var STEP_TRANSITIONS = {
44
+ // 'success': a gate's skip-forward decision marks intervening steps
45
+ // success directly (skipped: true in outputs), without ever running them —
46
+ // mirrors JOB_TRANSITIONS.queued's 'success' for the same reason.
47
+ // 'waiting_approval'/'waiting_child': `builtin:approval` and nested
48
+ // `workflow:` invocation steps never call `markStepStarted` at all (see
49
+ // worker.ts's step loop) — they park straight from 'queued', there's no
50
+ // "running" phase to observe for these step kinds.
51
+ queued: ["running", "skipped", "cancelled", "success", "waiting_approval", "waiting_child"],
52
+ running: ["success", "failed", "waiting_approval", "waiting_child"],
53
+ waiting_approval: ["success", "failed"],
54
+ // Reconciliation may restart a parked child-invocation step back to `running`.
55
+ waiting_child: ["success", "failed", "running"],
56
+ success: [],
57
+ failed: [],
58
+ cancelled: [],
59
+ skipped: [],
60
+ dlq: []
61
+ };
62
+ var TRANSITIONS = {
63
+ run: RUN_TRANSITIONS,
64
+ job: JOB_TRANSITIONS,
65
+ step: STEP_TRANSITIONS
66
+ };
67
+ var TERMINAL_STATES = {
68
+ run: /* @__PURE__ */ new Set(["success", "failed", "cancelled", "skipped", "dlq"]),
69
+ job: /* @__PURE__ */ new Set(["success", "failed", "cancelled", "skipped"]),
70
+ step: /* @__PURE__ */ new Set(["success", "failed", "cancelled", "skipped", "dlq"])
71
+ };
72
+ var PARKED_STATES = {
73
+ run: /* @__PURE__ */ new Set([]),
74
+ job: /* @__PURE__ */ new Set(["waiting_approval", "waiting_child", "interrupted"]),
75
+ step: /* @__PURE__ */ new Set(["waiting_approval", "waiting_child"])
76
+ };
77
+ var ACTIVE_STATES = {
78
+ run: /* @__PURE__ */ new Set(["queued", "running"]),
79
+ job: /* @__PURE__ */ new Set(["queued", "running"]),
80
+ step: /* @__PURE__ */ new Set(["queued", "running"])
81
+ };
82
+ function isTerminal(state, kind) {
83
+ return TERMINAL_STATES[kind].has(state);
84
+ }
85
+ function isParked(state, kind) {
86
+ return PARKED_STATES[kind].has(state);
87
+ }
88
+ function isActive(state, kind) {
89
+ return ACTIVE_STATES[kind].has(state);
90
+ }
91
+ var IllegalStateTransitionError = class extends Error {
92
+ constructor(kind, from, to, context) {
93
+ super(`Illegal ${kind} status transition: ${from} \u2192 ${to}`);
94
+ this.kind = kind;
95
+ this.from = from;
96
+ this.to = to;
97
+ this.context = context;
98
+ this.name = "IllegalStateTransitionError";
99
+ }
100
+ kind;
101
+ from;
102
+ to;
103
+ context;
104
+ };
105
+ function assertTransition(kind, from, to, options = {}) {
106
+ if (from === to) {
107
+ return;
108
+ }
109
+ if (options.allowReset) {
110
+ return;
111
+ }
112
+ const legal = TRANSITIONS[kind][from];
113
+ if (!legal || !legal.includes(to)) {
114
+ throw new IllegalStateTransitionError(kind, from, to);
115
+ }
116
+ }
117
+
1
118
  // src/index.ts
2
119
  var RUN_STATES = ["queued", "running", "success", "failed", "cancelled", "skipped", "dlq"];
3
- var JOB_STATES = ["queued", "running", "success", "failed", "cancelled", "skipped", "interrupted"];
120
+ var JOB_STATES = [
121
+ "queued",
122
+ "running",
123
+ "success",
124
+ "failed",
125
+ "cancelled",
126
+ "skipped",
127
+ "interrupted",
128
+ "waiting_approval",
129
+ "waiting_child"
130
+ ];
4
131
  var STEP_STATES = [...RUN_STATES, "waiting_approval", "waiting_child"];
5
132
  var JOB_PRIORITIES = ["high", "normal", "low"];
6
133
  var EVENT_NAMES = {
@@ -75,6 +202,6 @@ var REDIS_URL_ENV = "KB_REDIS_URL";
75
202
  var REDIS_MODE_ENV = "KB_REDIS_MODE";
76
203
  var REDIS_NAMESPACE_ENV = "KB_REDIS_NAMESPACE";
77
204
 
78
- export { CONCURRENCY_TTL_ENV, DEFAULT_REDIS_NAMESPACE, EVENT_NAMES, IDEMPOTENCY_TTL_ENV, JOB_PRIORITIES, JOB_STATES, REDIS_MODE_ENV, REDIS_NAMESPACE_ENV, REDIS_URL_ENV, RUN_STATES, STEP_STATES, WORKFLOW_REDIS_CHANNEL, createRedisKeyFactory };
205
+ export { CONCURRENCY_TTL_ENV, DEFAULT_REDIS_NAMESPACE, EVENT_NAMES, IDEMPOTENCY_TTL_ENV, IllegalStateTransitionError, JOB_PRIORITIES, JOB_STATES, JOB_TRANSITIONS, REDIS_MODE_ENV, REDIS_NAMESPACE_ENV, REDIS_URL_ENV, RUN_STATES, RUN_TRANSITIONS, STEP_STATES, STEP_TRANSITIONS, WORKFLOW_REDIS_CHANNEL, assertTransition, createRedisKeyFactory, isActive, isParked, isTerminal };
79
206
  //# sourceMappingURL=index.js.map
80
207
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":";AAAA,IAAM,UAAA,GAAa,CAAC,QAAA,EAAU,SAAA,EAAW,WAAW,QAAA,EAAU,WAAA,EAAa,WAAW,KAAK;AAC3F,IAAM,UAAA,GAAa,CAAC,QAAA,EAAU,SAAA,EAAW,WAAW,QAAA,EAAU,WAAA,EAAa,WAAW,aAAa;AACnG,IAAM,WAAA,GAAc,CAAC,GAAG,UAAA,EAAY,oBAAoB,eAAe;AACvE,IAAM,cAAA,GAAiB,CAAC,MAAA,EAAQ,QAAA,EAAU,KAAK;AASxC,IAAM,WAAA,GAAc;AAAA,EACzB,GAAA,EAAK;AAAA,IACH,OAAA,EAAS,aAAA;AAAA,IACT,OAAA,EAAS,aAAA;AAAA,IACT,OAAA,EAAS,aAAA;AAAA,IACT,QAAA,EAAU,cAAA;AAAA,IACV,SAAA,EAAW,eAAA;AAAA,IACX,MAAA,EAAQ;AAAA,GACV;AAAA,EACA,GAAA,EAAK;AAAA,IACH,MAAA,EAAQ,YAAA;AAAA,IACR,OAAA,EAAS,aAAA;AAAA,IACT,OAAA,EAAS,aAAA;AAAA,IACT,SAAA,EAAW,eAAA;AAAA,IACX,MAAA,EAAQ,YAAA;AAAA,IACR,SAAA,EAAW,eAAA;AAAA,IACX,OAAA,EAAS;AAAA,GACX;AAAA,EACA,IAAA,EAAM;AAAA,IACJ,MAAA,EAAQ,aAAA;AAAA,IACR,OAAA,EAAS,cAAA;AAAA,IACT,OAAA,EAAS,cAAA;AAAA,IACT,SAAA,EAAW,gBAAA;AAAA,IACX,MAAA,EAAQ,aAAA;AAAA,IACR,SAAA,EAAW,gBAAA;AAAA,IACX,OAAA,EAAS,cAAA;AAAA,IACT,eAAA,EAAiB,sBAAA;AAAA,IACjB,YAAA,EAAc;AAAA,GAChB;AAAA,EACA,GAAA,EAAK;AAAA,IACH,QAAA,EAAU;AAAA;AAEd;AAwBO,IAAM,uBAAA,GAA0B;AAChC,IAAM,sBAAA,GAAyB;AAE/B,SAAS,qBAAA,CACd,OAAA,GAAkC,EAAC,EAClB;AACjB,EAAA,MAAM,SAAA,GACJ,OAAA,CAAQ,SAAA,EAAW,OAAA,CAAQ,SAAA,EAAW,GAAG,CAAA,CAAE,OAAA,CAAQ,KAAA,EAAO,EAAE,CAAA,IAC5D,uBAAA;AACF,EAAA,MAAM,IAAA,GAAO,GAAG,SAAS,CAAA,GAAA,CAAA;AAEzB,EAAA,OAAO;AAAA,IACL,SAAA,EAAW,IAAA;AAAA,IACX,YAAY,GAAA,EAAa;AACvB,MAAA,OAAO,CAAA,EAAG,IAAI,CAAA,OAAA,EAAU,GAAG,CAAA,CAAA;AAAA,IAC7B,CAAA;AAAA,IACA,YAAY,KAAA,EAAe;AACzB,MAAA,OAAO,CAAA,EAAG,IAAI,CAAA,MAAA,EAAS,KAAK,CAAA,CAAA;AAAA,IAC9B,CAAA;AAAA,IACA,IAAI,KAAA,EAAe;AACjB,MAAA,OAAO,CAAA,EAAG,IAAI,CAAA,MAAA,EAAS,KAAK,CAAA,CAAA;AAAA,IAC9B,CAAA;AAAA,IACA,UAAU,KAAA,EAAe;AACvB,MAAA,OAAO,CAAA,EAAG,IAAI,CAAA,WAAA,EAAc,KAAK,CAAA,CAAA;AAAA,IACnC,CAAA;AAAA,IACA,QAAA,CAAS,WAAwB,QAAA,EAAU;AACzC,MAAA,OAAO,CAAA,EAAG,IAAI,CAAA,YAAA,EAAe,QAAQ,CAAA,CAAA;AAAA,IACvC,CAAA;AAAA,IACA,UAAU,KAAA,EAAe;AACvB,MAAA,OAAO,CAAA,EAAG,IAAI,CAAA,aAAA,EAAgB,KAAK,CAAA,CAAA;AAAA,IACrC,CAAA;AAAA,IACA,YAAA,GAAe;AACb,MAAA,OAAO,sBAAA;AAAA,IACT,CAAA;AAAA,IACA,KAAK,IAAA,EAAc;AACjB,MAAA,OAAO,CAAA,EAAG,IAAI,CAAA,OAAA,EAAU,IAAI,CAAA,CAAA;AAAA,IAC9B;AAAA,GACF;AACF;AAEO,IAAM,mBAAA,GAAsB;AAC5B,IAAM,mBAAA,GAAsB;AAC5B,IAAM,aAAA,GAAgB;AACtB,IAAM,cAAA,GAAiB;AACvB,IAAM,mBAAA,GAAsB","file":"index.js","sourcesContent":["const RUN_STATES = ['queued', 'running', 'success', 'failed', 'cancelled', 'skipped', 'dlq'] as const\nconst JOB_STATES = ['queued', 'running', 'success', 'failed', 'cancelled', 'skipped', 'interrupted'] as const\nconst STEP_STATES = [...RUN_STATES, 'waiting_approval', 'waiting_child'] as const\nconst JOB_PRIORITIES = ['high', 'normal', 'low'] as const\n\nexport { RUN_STATES, JOB_STATES, STEP_STATES, JOB_PRIORITIES }\n\nexport type RunState = (typeof RUN_STATES)[number]\nexport type JobState = (typeof JOB_STATES)[number]\nexport type StepState = (typeof STEP_STATES)[number]\nexport type JobPriority = (typeof JOB_PRIORITIES)[number]\n\nexport const EVENT_NAMES = {\n run: {\n created: 'run.created',\n started: 'run.started',\n updated: 'run.updated',\n finished: 'run.finished',\n cancelled: 'run.cancelled',\n failed: 'run.failed',\n },\n job: {\n queued: 'job.queued',\n started: 'job.started',\n updated: 'job.updated',\n succeeded: 'job.succeeded',\n failed: 'job.failed',\n cancelled: 'job.cancelled',\n skipped: 'job.skipped',\n },\n step: {\n queued: 'step.queued',\n started: 'step.started',\n updated: 'step.updated',\n succeeded: 'step.succeeded',\n failed: 'step.failed',\n cancelled: 'step.cancelled',\n skipped: 'step.skipped',\n waitingApproval: 'step.waitingApproval',\n waitingChild: 'step.waitingChild',\n },\n log: {\n appended: 'log.appended',\n },\n} as const\n\nexport type WorkflowEventName =\n | (typeof EVENT_NAMES)['run'][keyof (typeof EVENT_NAMES)['run']]\n | (typeof EVENT_NAMES)['job'][keyof (typeof EVENT_NAMES)['job']]\n | (typeof EVENT_NAMES)['step'][keyof (typeof EVENT_NAMES)['step']]\n | (typeof EVENT_NAMES)['log'][keyof (typeof EVENT_NAMES)['log']]\n\nexport interface RedisKeyFactory {\n namespace: string\n idempotency(key: string): string\n concurrency(group: string): string\n run(runId: string): string\n artifacts(runId: string): string\n jobQueue(priority?: JobPriority): string\n runEvents(runId: string): string\n eventChannel(): string\n lock(name: string): string\n}\n\nexport interface RedisKeyFactoryOptions {\n namespace?: string\n}\n\nexport const DEFAULT_REDIS_NAMESPACE = 'kb'\nexport const WORKFLOW_REDIS_CHANNEL = 'kb:wf:events'\n\nexport function createRedisKeyFactory(\n options: RedisKeyFactoryOptions = {},\n): RedisKeyFactory {\n const namespace =\n options.namespace?.replace(/[:\\s]+/g, ':').replace(/:+$/, '') ||\n DEFAULT_REDIS_NAMESPACE\n const wfNs = `${namespace}:wf`\n\n return {\n namespace: wfNs,\n idempotency(key: string) {\n return `${wfNs}:idemp:${key}`\n },\n concurrency(group: string) {\n return `${wfNs}:conc:${group}`\n },\n run(runId: string) {\n return `${wfNs}:runs:${runId}`\n },\n artifacts(runId: string) {\n return `${wfNs}:artifacts:${runId}`\n },\n jobQueue(priority: JobPriority = 'normal') {\n return `${wfNs}:queue:jobs:${priority}`\n },\n runEvents(runId: string) {\n return `${wfNs}:events:runs:${runId}`\n },\n eventChannel() {\n return WORKFLOW_REDIS_CHANNEL\n },\n lock(name: string) {\n return `${wfNs}:locks:${name}`\n },\n }\n}\n\nexport const IDEMPOTENCY_TTL_ENV = 'KB_WF_IDEMP_TTL_MS'\nexport const CONCURRENCY_TTL_ENV = 'KB_WF_CONC_TTL_MS'\nexport const REDIS_URL_ENV = 'KB_REDIS_URL'\nexport const REDIS_MODE_ENV = 'KB_REDIS_MODE'\nexport const REDIS_NAMESPACE_ENV = 'KB_REDIS_NAMESPACE'\n\nexport type RedisMode = 'standalone' | 'cluster' | 'sentinel'\n\n\n"]}
1
+ {"version":3,"sources":["../src/state-machine.ts","../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { JobState, RunState, StepState } from './index'\n\n/**\n * Single source of truth for legal status transitions across the workflow\n * engine. Every status-write call site (engine.ts, worker.ts) must route\n * through `assertTransition` (via StateStore.transitionRun/Job/Step) instead\n * of mutating `.status` directly — this is what turns \"any status can go to\n * any status\" into a checked invariant.\n *\n * See docs/adr and the workflow status audit for the incident this closes:\n * a job could be marked `failed` while its step still sat in\n * `waiting_approval`, because nothing validated that transition was legal.\n */\n\nexport type EntityKind = 'run' | 'job' | 'step'\n\n// ═══════════════════════════════════════════════════════════════════════\n// Transition tables\n// ═══════════════════════════════════════════════════════════════════════\n\nexport const RUN_TRANSITIONS: Record<RunState, readonly RunState[]> = {\n // 'success'/'failed': a run can finish without ever visiting 'running' —\n // e.g. a run whose only job is skipped via `if:` (`skipJob` marks the job\n // success directly and never calls `markJobStarted`, so `run.status` can\n // still be 'queued' by the time `checkRunCompletion` finalizes the run).\n queued: ['running', 'cancelled', 'success', 'failed'],\n running: ['success', 'failed', 'cancelled', 'dlq'],\n success: [],\n failed: [],\n cancelled: [],\n skipped: [],\n dlq: [],\n}\n\nexport const JOB_TRANSITIONS: Record<JobState, readonly JobState[]> = {\n // 'success': `skipJob` treats an `if:`-skipped job as success directly,\n // without ever passing through 'running' (see engine.ts `skipJob`).\n // 'failed': `cleanupStaleRuns` force-fails abandoned jobs that never even\n // started (daemon restarted before they were dequeued).\n queued: ['running', 'cancelled', 'skipped', 'success', 'failed'],\n // 'queued': a running job can be re-parked back to queued mid-execution\n // when its current step legitimately pauses (waiting_child reconciliation\n // resume, gate restart) — not a hard requirement that 'running' only ever\n // moves forward.\n running: ['success', 'failed', 'cancelled', 'interrupted', 'waiting_approval', 'waiting_child', 'queued'],\n // 'failed': a parked job can be force-failed directly, without ever\n // passing through 'running' again — e.g. `failChildInvocation` fails the\n // parent job when its child run was cancelled/not found/itself failed,\n // while the parent job is still sitting in 'waiting_child'.\n waiting_approval: ['queued', 'cancelled', 'failed'],\n waiting_child: ['queued', 'cancelled', 'failed'],\n interrupted: ['queued', 'cancelled'],\n success: [],\n // 'queued': `markJobFailed` schedules a retry after a transient failure —\n // the job comes back as 'queued' for its next attempt. 'failed' here means\n // \"this attempt failed\", not always \"this job is permanently done\"; the\n // caller decides whether to retry before ever reaching a real terminal\n // outcome for the job.\n failed: ['queued'],\n cancelled: [],\n skipped: [],\n}\n\nexport const STEP_TRANSITIONS: Record<StepState, readonly StepState[]> = {\n // 'success': a gate's skip-forward decision marks intervening steps\n // success directly (skipped: true in outputs), without ever running them —\n // mirrors JOB_TRANSITIONS.queued's 'success' for the same reason.\n // 'waiting_approval'/'waiting_child': `builtin:approval` and nested\n // `workflow:` invocation steps never call `markStepStarted` at all (see\n // worker.ts's step loop) — they park straight from 'queued', there's no\n // \"running\" phase to observe for these step kinds.\n queued: ['running', 'skipped', 'cancelled', 'success', 'waiting_approval', 'waiting_child'],\n running: ['success', 'failed', 'waiting_approval', 'waiting_child'],\n waiting_approval: ['success', 'failed'],\n // Reconciliation may restart a parked child-invocation step back to `running`.\n waiting_child: ['success', 'failed', 'running'],\n success: [],\n failed: [],\n cancelled: [],\n skipped: [],\n dlq: [],\n}\n\nconst TRANSITIONS: Record<EntityKind, Record<string, readonly string[]>> = {\n run: RUN_TRANSITIONS,\n job: JOB_TRANSITIONS,\n step: STEP_TRANSITIONS,\n}\n\n// ═══════════════════════════════════════════════════════════════════════\n// Predicates\n// ═══════════════════════════════════════════════════════════════════════\n\n/**\n * Terminal: no legal outgoing transition. The entity is done, one way or\n * another, and nothing should ever flip it again.\n */\nconst TERMINAL_STATES: Record<EntityKind, ReadonlySet<string>> = {\n run: new Set(['success', 'failed', 'cancelled', 'skipped', 'dlq']),\n job: new Set(['success', 'failed', 'cancelled', 'skipped']),\n step: new Set(['success', 'failed', 'cancelled', 'skipped', 'dlq']),\n}\n\n/**\n * Parked: not terminal, but also not actively holding an executor slot.\n * Something external (a human approval, a child run finishing, a daemon\n * restart recovery pass) is expected to move it back to `queued`/`running`.\n *\n * This is the status class `cleanupStaleRuns` must never force-fail — that\n * was the root cause of the reported bug (job.status=failed while\n * step.status=waiting_approval).\n */\nconst PARKED_STATES: Record<EntityKind, ReadonlySet<string>> = {\n run: new Set([]),\n job: new Set(['waiting_approval', 'waiting_child', 'interrupted']),\n step: new Set(['waiting_approval', 'waiting_child']),\n}\n\n/**\n * Active: currently executing or eligible to be picked up by the next\n * scheduler tick, i.e. genuinely needs a live executor.\n */\nconst ACTIVE_STATES: Record<EntityKind, ReadonlySet<string>> = {\n run: new Set(['queued', 'running']),\n job: new Set(['queued', 'running']),\n step: new Set(['queued', 'running']),\n}\n\nexport function isTerminal(state: string, kind: EntityKind): boolean {\n return TERMINAL_STATES[kind].has(state)\n}\n\nexport function isParked(state: string, kind: EntityKind): boolean {\n return PARKED_STATES[kind].has(state)\n}\n\nexport function isActive(state: string, kind: EntityKind): boolean {\n return ACTIVE_STATES[kind].has(state)\n}\n\n// ═══════════════════════════════════════════════════════════════════════\n// Transition validation\n// ═══════════════════════════════════════════════════════════════════════\n\nexport class IllegalStateTransitionError extends Error {\n constructor(\n public readonly kind: EntityKind,\n public readonly from: string,\n public readonly to: string,\n public readonly context?: Record<string, unknown>,\n ) {\n super(`Illegal ${kind} status transition: ${from} → ${to}`)\n this.name = 'IllegalStateTransitionError'\n }\n}\n\nexport interface AssertTransitionOptions {\n /**\n * Bulk-reset escape hatch for replayRun/cleanupStaleRuns-style resets that\n * deliberately move a terminal entity back to `queued` (or similar) outside\n * the normal transition table. Must be passed explicitly and is grep-able\n * by design — never add a silent bypass inside the table itself.\n */\n allowReset?: boolean\n}\n\n/**\n * Throws IllegalStateTransitionError if `to` is not reachable from `from`\n * per the transition table for `kind`. No-ops (does not throw) when\n * `from === to` — idempotent re-writes of the same status are always legal.\n */\nexport function assertTransition(\n kind: EntityKind,\n from: string,\n to: string,\n options: AssertTransitionOptions = {},\n): void {\n if (from === to) {\n return\n }\n if (options.allowReset) {\n return\n }\n const legal = TRANSITIONS[kind][from]\n if (!legal || !legal.includes(to)) {\n throw new IllegalStateTransitionError(kind, from, to)\n }\n}\n","const RUN_STATES = ['queued', 'running', 'success', 'failed', 'cancelled', 'skipped', 'dlq'] as const\nconst JOB_STATES = [\n 'queued', 'running', 'success', 'failed', 'cancelled', 'skipped', 'interrupted',\n 'waiting_approval', 'waiting_child',\n] as const\nconst STEP_STATES = [...RUN_STATES, 'waiting_approval', 'waiting_child'] as const\nconst JOB_PRIORITIES = ['high', 'normal', 'low'] as const\n\nexport { RUN_STATES, JOB_STATES, STEP_STATES, JOB_PRIORITIES }\n\nexport type RunState = (typeof RUN_STATES)[number]\nexport type JobState = (typeof JOB_STATES)[number]\nexport type StepState = (typeof STEP_STATES)[number]\nexport type JobPriority = (typeof JOB_PRIORITIES)[number]\n\nexport {\n RUN_TRANSITIONS,\n JOB_TRANSITIONS,\n STEP_TRANSITIONS,\n isTerminal,\n isParked,\n isActive,\n assertTransition,\n IllegalStateTransitionError,\n} from './state-machine'\nexport type { EntityKind, AssertTransitionOptions } from './state-machine'\n\nexport const EVENT_NAMES = {\n run: {\n created: 'run.created',\n started: 'run.started',\n updated: 'run.updated',\n finished: 'run.finished',\n cancelled: 'run.cancelled',\n failed: 'run.failed',\n },\n job: {\n queued: 'job.queued',\n started: 'job.started',\n updated: 'job.updated',\n succeeded: 'job.succeeded',\n failed: 'job.failed',\n cancelled: 'job.cancelled',\n skipped: 'job.skipped',\n },\n step: {\n queued: 'step.queued',\n started: 'step.started',\n updated: 'step.updated',\n succeeded: 'step.succeeded',\n failed: 'step.failed',\n cancelled: 'step.cancelled',\n skipped: 'step.skipped',\n waitingApproval: 'step.waitingApproval',\n waitingChild: 'step.waitingChild',\n },\n log: {\n appended: 'log.appended',\n },\n} as const\n\nexport type WorkflowEventName =\n | (typeof EVENT_NAMES)['run'][keyof (typeof EVENT_NAMES)['run']]\n | (typeof EVENT_NAMES)['job'][keyof (typeof EVENT_NAMES)['job']]\n | (typeof EVENT_NAMES)['step'][keyof (typeof EVENT_NAMES)['step']]\n | (typeof EVENT_NAMES)['log'][keyof (typeof EVENT_NAMES)['log']]\n\nexport interface RedisKeyFactory {\n namespace: string\n idempotency(key: string): string\n concurrency(group: string): string\n run(runId: string): string\n artifacts(runId: string): string\n jobQueue(priority?: JobPriority): string\n runEvents(runId: string): string\n eventChannel(): string\n lock(name: string): string\n}\n\nexport interface RedisKeyFactoryOptions {\n namespace?: string\n}\n\nexport const DEFAULT_REDIS_NAMESPACE = 'kb'\nexport const WORKFLOW_REDIS_CHANNEL = 'kb:wf:events'\n\nexport function createRedisKeyFactory(\n options: RedisKeyFactoryOptions = {},\n): RedisKeyFactory {\n const namespace =\n options.namespace?.replace(/[:\\s]+/g, ':').replace(/:+$/, '') ||\n DEFAULT_REDIS_NAMESPACE\n const wfNs = `${namespace}:wf`\n\n return {\n namespace: wfNs,\n idempotency(key: string) {\n return `${wfNs}:idemp:${key}`\n },\n concurrency(group: string) {\n return `${wfNs}:conc:${group}`\n },\n run(runId: string) {\n return `${wfNs}:runs:${runId}`\n },\n artifacts(runId: string) {\n return `${wfNs}:artifacts:${runId}`\n },\n jobQueue(priority: JobPriority = 'normal') {\n return `${wfNs}:queue:jobs:${priority}`\n },\n runEvents(runId: string) {\n return `${wfNs}:events:runs:${runId}`\n },\n eventChannel() {\n return WORKFLOW_REDIS_CHANNEL\n },\n lock(name: string) {\n return `${wfNs}:locks:${name}`\n },\n }\n}\n\nexport const IDEMPOTENCY_TTL_ENV = 'KB_WF_IDEMP_TTL_MS'\nexport const CONCURRENCY_TTL_ENV = 'KB_WF_CONC_TTL_MS'\nexport const REDIS_URL_ENV = 'KB_REDIS_URL'\nexport const REDIS_MODE_ENV = 'KB_REDIS_MODE'\nexport const REDIS_NAMESPACE_ENV = 'KB_REDIS_NAMESPACE'\n\nexport type RedisMode = 'standalone' | 'cluster' | 'sentinel'\n\n\n"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kb-labs/workflow-constants",
3
3
  "description": "Shared constants for the KB Labs workflow engine.",
4
- "version": "2.118.2",
4
+ "version": "2.119.0-canary.0b1ec4151",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
@@ -26,7 +26,7 @@
26
26
  "tsup": "^8.5.0",
27
27
  "typescript": "^5.6.3",
28
28
  "vitest": "^3.2.6",
29
- "@kb-labs/devkit": "2.118.2"
29
+ "@kb-labs/devkit": "2.119.0-canary.0b1ec4151"
30
30
  },
31
31
  "engines": {
32
32
  "node": ">=22.0.0",