@kb-labs/workflow-daemon 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.js CHANGED
@@ -4,6 +4,7 @@ import { WorkflowEngine, WorkflowService } from '@kb-labs/workflow-engine';
4
4
  import { getListenOptions, HttpObservabilityCollector, createDaemonServer, createServiceReadyResponse, metricLine } from '@kb-labs/shared-http';
5
5
  import { runService } from '@kb-labs/shared-daemon';
6
6
  import { logDiagnosticEvent, createContextLogger } from '@kb-labs/core-platform';
7
+ import { isTerminal } from '@kb-labs/workflow-constants';
7
8
  import { PluginCronJobSchema, UserCronJobSchema, evaluateExpression, interpolateObject, buildShellSafeCommand, coerceToString, interpolateString } from '@kb-labs/workflow-contracts';
8
9
  import { GateHandler } from '@kb-labs/workflow-steps';
9
10
  import { SandboxRunner } from '@kb-labs/workflow-runtime';
@@ -186,7 +187,6 @@ async function createWorkflowWorker(options) {
186
187
  try {
187
188
  const ws = await wsProvider2.materialize({
188
189
  workspaceId: wsId,
189
- sourceRef: "main",
190
190
  metadata: { runId: run.id, jobId: job.id },
191
191
  onProgress: (event) => {
192
192
  jobLogger.info(`[workspace] ${event.stage}: ${event.message}`, {
@@ -257,6 +257,15 @@ async function createWorkflowWorker(options) {
257
257
  wasCancelled = true;
258
258
  break;
259
259
  }
260
+ const freshJob = freshRun?.jobs.find((j) => j.id === job.id);
261
+ if (freshJob && isTerminal(freshJob.status, "job")) {
262
+ jobLogger.warn("[worker] Job already in a terminal state \u2014 aborting in-process execution", {
263
+ runId: run.id,
264
+ jobId: job.id,
265
+ status: freshJob.status
266
+ });
267
+ return;
268
+ }
260
269
  const exprCtx = {
261
270
  env: freshRun?.env ?? {},
262
271
  trigger: freshRun?.trigger ?? { type: "manual" },
@@ -392,19 +401,13 @@ async function createWorkflowWorker(options) {
392
401
  if (step.status !== "waiting_approval") {
393
402
  await engine.markStepWaitingApproval(run.id, job.id, step.id);
394
403
  }
395
- const approvalResult = await waitForApproval(
396
- engine,
397
- run.id,
398
- job.id,
399
- step,
400
- interpolatedWith,
401
- () => stopRequested,
402
- stepLogger
403
- );
404
- if (approvalResult === "interrupted") {
405
- return;
406
- }
407
- continue;
404
+ stepLogger.info("[approval] Step parked awaiting human decision \u2014 releasing worker slot", {
405
+ runId: run.id,
406
+ jobId: job.id,
407
+ stepId: step.id,
408
+ stepName: step.name
409
+ });
410
+ return;
408
411
  }
409
412
  if (step.spec.uses?.startsWith("workflow:")) {
410
413
  await invokeChildWorkflow({
@@ -833,82 +836,6 @@ function sleep(ms) {
833
836
  setTimeout(resolve, ms);
834
837
  });
835
838
  }
836
- async function waitForApproval(engine, runId, jobId, step, interpolatedWith, isStopRequested, stepLogger) {
837
- const approvalTimeoutMs = interpolatedWith?.["timeoutMs"];
838
- if (!approvalTimeoutMs) {
839
- stepLogger.warn(
840
- "[approval] No timeout configured \u2014 approval may wait indefinitely",
841
- {
842
- runId,
843
- jobId,
844
- stepId: step.id,
845
- stepName: step.name
846
- }
847
- );
848
- }
849
- stepLogger.info("[approval] Waiting for approval", {
850
- runId,
851
- jobId,
852
- stepId: step.id,
853
- stepName: step.name,
854
- context: interpolatedWith
855
- });
856
- const approvalStartMs = Date.now();
857
- let pollCount = 0;
858
- while (!isStopRequested()) {
859
- await sleep(2e3);
860
- pollCount++;
861
- const currentRun = await engine.getRun(runId);
862
- if (currentRun?.status === "cancelled") {
863
- stepLogger.info(
864
- "[approval] Run cancelled \u2014 treating as interrupted, NOT approved",
865
- {
866
- runId,
867
- stepId: step.id,
868
- stepName: step.name,
869
- waitedMs: Date.now() - approvalStartMs
870
- }
871
- );
872
- return "interrupted";
873
- }
874
- const currentJob = currentRun?.jobs.find((j) => j.id === jobId);
875
- const currentStep = currentJob?.steps.find((s) => s.id === step.id);
876
- if (!currentStep || currentStep.status === "success") {
877
- stepLogger.info("[approval] Approval granted", {
878
- runId,
879
- stepId: step.id,
880
- stepName: step.name,
881
- waitedMs: Date.now() - approvalStartMs
882
- });
883
- break;
884
- }
885
- if (currentStep.status === "failed") {
886
- stepLogger.info("[approval] Approval rejected", {
887
- runId,
888
- stepId: step.id,
889
- stepName: step.name,
890
- waitedMs: Date.now() - approvalStartMs
891
- });
892
- break;
893
- }
894
- if (pollCount % 10 === 0) {
895
- stepLogger.info("[approval] Still waiting for approval", {
896
- runId,
897
- stepId: step.id,
898
- stepName: step.name,
899
- waitedMs: Date.now() - approvalStartMs,
900
- pollCount
901
- });
902
- }
903
- }
904
- if (isStopRequested()) {
905
- stepLogger.info("Approval wait interrupted by shutdown", {
906
- stepId: step.id
907
- });
908
- return "interrupted";
909
- }
910
- return "done";
911
- }
912
839
  async function applyGateSkip(engine, run, job, step, decision, stepLogger) {
913
840
  const { skipTo, outputs } = decision;
914
841
  stepLogger.info("[gate] Gate triggered skip-forward", {
@@ -932,8 +859,7 @@ async function applyGateSkip(engine, run, job, step, decision, stepLogger) {
932
859
  if (s.spec.id === skipTo || s.id === skipTo) {
933
860
  break;
934
861
  }
935
- await stateStore.updateStep(run.id, job.id, s.id, (draft) => {
936
- draft.status = "success";
862
+ await stateStore.transitionStep(run.id, job.id, s.id, "success", (draft) => {
937
863
  draft.outputs = { skipped: true };
938
864
  draft.startedAt = (/* @__PURE__ */ new Date()).toISOString();
939
865
  draft.finishedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -1005,21 +931,19 @@ async function applyGateRestart(engine, run, job, step, decision, stepLogger) {
1005
931
  }
1006
932
  const pending = (rj.needs ?? []).filter((n) => resetNames.has(n));
1007
933
  for (const s of rj.steps) {
1008
- await stateStore.updateStep(run.id, rj.id, s.id, (draft) => {
1009
- draft.status = "queued";
934
+ await stateStore.transitionStep(run.id, rj.id, s.id, "queued", (draft) => {
1010
935
  draft.startedAt = void 0;
1011
936
  draft.finishedAt = void 0;
1012
937
  draft.error = void 0;
1013
938
  draft.outputs = void 0;
1014
- });
939
+ }, { allowReset: true });
1015
940
  }
1016
- await stateStore.updateJob(run.id, rj.id, (draft) => {
1017
- draft.status = "queued";
941
+ await stateStore.transitionJob(run.id, rj.id, "queued", (draft) => {
1018
942
  draft.startedAt = void 0;
1019
943
  draft.finishedAt = void 0;
1020
944
  draft.pendingDependencies = [...pending];
1021
945
  draft.blocked = pending.length > 0;
1022
- });
946
+ }, { allowReset: true });
1023
947
  }
1024
948
  const refreshed = await engine.getRun(run.id);
1025
949
  const targetJob = refreshed?.jobs.find((j) => j.jobName === restartFrom);
@@ -1045,17 +969,15 @@ async function applyGateRestart(engine, run, job, step, decision, stepLogger) {
1045
969
  foundTarget = true;
1046
970
  }
1047
971
  if (foundTarget) {
1048
- await stateStore.updateStep(run.id, job.id, s.id, (draft) => {
1049
- draft.status = "queued";
972
+ await stateStore.transitionStep(run.id, job.id, s.id, "queued", (draft) => {
1050
973
  draft.startedAt = void 0;
1051
974
  draft.finishedAt = void 0;
1052
975
  draft.error = void 0;
1053
976
  draft.outputs = void 0;
1054
- });
977
+ }, { allowReset: true });
1055
978
  }
1056
979
  }
1057
- await stateStore.updateJob(run.id, job.id, (draft) => {
1058
- draft.status = "queued";
980
+ await stateStore.transitionJob(run.id, job.id, "queued", (draft) => {
1059
981
  draft.startedAt = void 0;
1060
982
  draft.finishedAt = void 0;
1061
983
  });
@@ -2861,9 +2783,16 @@ function registerWorkflowsAPI(options) {
2861
2783
  reply.hijack();
2862
2784
  const raw = reply.raw;
2863
2785
  const origin = request.headers.origin;
2864
- if (typeof origin === "string" && origin.startsWith("http://localhost")) {
2865
- raw.setHeader("Access-Control-Allow-Origin", origin);
2866
- raw.setHeader("Access-Control-Allow-Credentials", "true");
2786
+ if (typeof origin === "string") {
2787
+ try {
2788
+ const parsed = new URL(origin);
2789
+ const isLocalOrigin = parsed.protocol === "http:" && (parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1" || parsed.hostname === "[::1]");
2790
+ if (isLocalOrigin) {
2791
+ raw.setHeader("Access-Control-Allow-Origin", origin);
2792
+ raw.setHeader("Access-Control-Allow-Credentials", "true");
2793
+ }
2794
+ } catch {
2795
+ }
2867
2796
  }
2868
2797
  raw.setHeader("Content-Type", "text/event-stream");
2869
2798
  raw.setHeader("Cache-Control", "no-cache, no-transform");