@kb-labs/workflow-daemon 2.116.14 → 2.117.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/dist/index.js CHANGED
@@ -19,6 +19,7 @@ import { randomUUID } from 'crypto';
19
19
  async function createWorkflowWorker(options) {
20
20
  const {
21
21
  engine,
22
+ workflowService,
22
23
  cliApi,
23
24
  logger,
24
25
  platform,
@@ -46,6 +47,58 @@ async function createWorkflowWorker(options) {
46
47
  workspaceRoot,
47
48
  defaultTimeout
48
49
  });
50
+ async function invokeChildWorkflow(input) {
51
+ if (!workflowService) {
52
+ throw new Error("Workflow invocation is unavailable: WorkflowService was not configured");
53
+ }
54
+ if (!input.workflowRef.startsWith("workspace:")) {
55
+ throw new Error(`Unsupported child workflow reference: workflow:${input.workflowRef}`);
56
+ }
57
+ const workflowId = input.workflowRef.slice("workspace:".length);
58
+ const parentWorkflowId = input.parentRun.metadata?.["workflowId"] ?? input.parentRun.name;
59
+ const ancestors = input.parentRun.metadata?.workflowAncestors ?? [parentWorkflowId];
60
+ if (ancestors.includes(workflowId)) {
61
+ throw new Error(`Child workflow cycle detected: ${[...ancestors, workflowId].join(" -> ")}`);
62
+ }
63
+ const parentDepth = input.parentRun.metadata?.workflowDepth ?? 0;
64
+ if (parentDepth >= engine.maxWorkflowDepth) {
65
+ throw new Error(`Child workflow depth exceeds maxWorkflowDepth=${engine.maxWorkflowDepth}`);
66
+ }
67
+ const workflow = await workflowService.get(workflowId);
68
+ const spec = workflow?.input;
69
+ if (!workflow || !spec?.jobs) {
70
+ throw new Error(`Child workflow not found or is not executable: ${workflowId}`);
71
+ }
72
+ const child = await engine.runFromSpec(spec, {
73
+ trigger: {
74
+ type: "workflow",
75
+ actor: input.parentRun.trigger.actor,
76
+ payload: input.inputs,
77
+ parentRunId: input.parentRun.id,
78
+ parentJobId: input.parentJob.id,
79
+ parentStepId: input.parentStep.id,
80
+ invokedByWorkflowId: parentWorkflowId
81
+ },
82
+ inputs: input.inputs,
83
+ env: input.parentRun.env,
84
+ metadata: {
85
+ workflowId,
86
+ parentRunId: input.parentRun.id,
87
+ parentJobId: input.parentJob.id,
88
+ parentStepId: input.parentStep.id,
89
+ workflowDepth: parentDepth + 1,
90
+ rootRunId: input.parentRun.metadata?.rootRunId ?? input.parentRun.id,
91
+ workflowAncestors: [...ancestors, workflowId],
92
+ // A composed quality workflow must inspect and repair the exact change
93
+ // prepared by its parent, not a fresh worktree at main. The owner keeps
94
+ // lifecycle responsibility; children only borrow this binding.
95
+ executionWorkspace: input.parentRun.metadata?.["executionWorkspace"],
96
+ workspaceOwnerRunId: input.parentRun.metadata?.["workspaceOwnerRunId"] ?? input.parentRun.id
97
+ }
98
+ });
99
+ await engine.markStepWaitingChild(input.parentRun.id, input.parentJob.id, input.parentStep.id, child.id);
100
+ await engine.reconcileChildInvocation(child.id);
101
+ }
49
102
  async function processJob() {
50
103
  const entry = await engine.nextJob();
51
104
  if (!entry) {
@@ -118,7 +171,17 @@ async function createWorkflowWorker(options) {
118
171
  const wsProvider2 = platform.getAdapter("workspace");
119
172
  let runWorkspace = wsProvider2 ? workspaceRoot : process.env["KB_PROJECT_ROOT"] ?? workspaceRoot;
120
173
  let provisionedWorkspaceId;
121
- if (wsProvider2) {
174
+ const workspaceMetadata = run.metadata?.["executionWorkspace"];
175
+ const inheritedWorkspace = typeof run.metadata?.["workspaceOwnerRunId"] === "string" && run.metadata?.["workspaceOwnerRunId"] !== run.id;
176
+ if (workspaceMetadata && typeof workspaceMetadata.rootPath === "string" && typeof workspaceMetadata.workspaceId === "string") {
177
+ runWorkspace = workspaceMetadata.rootPath;
178
+ provisionedWorkspaceId = inheritedWorkspace ? void 0 : workspaceMetadata.workspaceId;
179
+ jobLogger.info("Workspace binding restored", {
180
+ workspaceId: workspaceMetadata.workspaceId,
181
+ ownerRunId: run.metadata?.["workspaceOwnerRunId"] ?? run.id,
182
+ inherited: inheritedWorkspace
183
+ });
184
+ } else if (wsProvider2) {
122
185
  const wsId = `wt_${run.id.slice(0, 8)}`;
123
186
  try {
124
187
  const ws = await wsProvider2.materialize({
@@ -133,8 +196,16 @@ async function createWorkflowWorker(options) {
133
196
  }
134
197
  });
135
198
  if (ws.rootPath) {
136
- runWorkspace = ws.rootPath;
199
+ const rootPath = ws.rootPath;
200
+ runWorkspace = rootPath;
137
201
  provisionedWorkspaceId = ws.workspaceId;
202
+ await engine.getStateStore().updateRun(run.id, (draft) => {
203
+ draft.metadata = {
204
+ ...draft.metadata ?? {},
205
+ executionWorkspace: { workspaceId: ws.workspaceId, rootPath },
206
+ workspaceOwnerRunId: run.id
207
+ };
208
+ });
138
209
  jobLogger.info("Workspace provisioned", {
139
210
  workspaceId: ws.workspaceId,
140
211
  provider: ws.provider,
@@ -174,6 +245,9 @@ async function createWorkflowWorker(options) {
174
245
  if (step.status === "success") {
175
246
  continue;
176
247
  }
248
+ if (step.status === "waiting_child") {
249
+ return;
250
+ }
177
251
  const freshRun = await engine.getRun(run.id);
178
252
  if (freshRun?.status === "cancelled") {
179
253
  jobLogger.info("[worker] Run cancelled \u2014 stopping step execution", {
@@ -332,6 +406,16 @@ async function createWorkflowWorker(options) {
332
406
  }
333
407
  continue;
334
408
  }
409
+ if (step.spec.uses?.startsWith("workflow:")) {
410
+ await invokeChildWorkflow({
411
+ parentRun: run,
412
+ parentJob: job,
413
+ parentStep: step,
414
+ workflowRef: step.spec.uses.slice("workflow:".length),
415
+ inputs: interpolatedWith ?? {}
416
+ });
417
+ return;
418
+ }
335
419
  if (step.spec.uses === "builtin:gate") {
336
420
  const gateInput = interpolatedWith ?? {};
337
421
  const currentIteration = step.metadata?.["iterations"] ?? 0;
@@ -1957,7 +2041,11 @@ var WorkflowHostService = class {
1957
2041
  actor: request.trigger?.user,
1958
2042
  payload: userInputs
1959
2043
  },
1960
- inputs: resolvedInputs
2044
+ inputs: resolvedInputs,
2045
+ metadata: {
2046
+ workflowId: id,
2047
+ workflowAncestors: [id]
2048
+ }
1961
2049
  });
1962
2050
  return {
1963
2051
  runId: run.id,
@@ -1990,7 +2078,11 @@ var WorkflowHostService = class {
1990
2078
  actor: "cli-rerun",
1991
2079
  payload: resolvedInputs
1992
2080
  },
1993
- inputs: resolvedInputs
2081
+ inputs: resolvedInputs,
2082
+ metadata: {
2083
+ workflowId,
2084
+ workflowAncestors: [workflowId]
2085
+ }
1994
2086
  });
1995
2087
  return {
1996
2088
  runId: run.id,
@@ -3249,6 +3341,11 @@ async function bootstrap(_cwd = process.cwd()) {
3249
3341
  await engine.cleanupStaleRuns();
3250
3342
  bootstrapLogger.info("Resuming interrupted jobs");
3251
3343
  await engine.resumeInterruptedJobs();
3344
+ bootstrapLogger.info("Reconciling persisted child workflow invocations");
3345
+ const reconciledChildren = await engine.reconcileChildInvocations();
3346
+ if (reconciledChildren > 0) {
3347
+ bootstrapLogger.info("Reconciled child workflow invocations", { count: reconciledChildren });
3348
+ }
3252
3349
  bootstrapLogger.info("Creating JobBroker");
3253
3350
  const jobBroker = new JobBroker(
3254
3351
  engine,
@@ -3311,6 +3408,7 @@ async function bootstrap(_cwd = process.cwd()) {
3311
3408
  bootstrapLogger.info("Creating WorkflowWorker");
3312
3409
  const worker = await createWorkflowWorker({
3313
3410
  engine,
3411
+ workflowService,
3314
3412
  cliApi,
3315
3413
  logger: createWorkflowLogger("worker", "workflow.worker"),
3316
3414
  analytics: platform.analytics,