@kb-labs/workflow-daemon 2.116.14 → 2.118.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,
@@ -2168,7 +2260,7 @@ var WorkflowHostService = class {
2168
2260
  (run) => run.name === workflowId || workflowName && run.name === workflowName
2169
2261
  );
2170
2262
  if (filters?.status) {
2171
- runs = runs.filter((run) => run.status === filters.status);
2263
+ runs = runs.filter((run) => mapRunStatusToJobStatus(run.status) === filters.status);
2172
2264
  }
2173
2265
  runs.sort((a, b) => new Date(b.createdAt ?? 0).getTime() - new Date(a.createdAt ?? 0).getTime());
2174
2266
  const start = filters?.offset ?? 0;
@@ -2179,7 +2271,7 @@ var WorkflowHostService = class {
2179
2271
  runs: page.map((run) => ({
2180
2272
  id: run.id,
2181
2273
  workflowId,
2182
- status: run.status,
2274
+ status: mapRunStatusToJobStatus(run.status),
2183
2275
  trigger: {
2184
2276
  type: run.trigger?.type ?? "manual",
2185
2277
  user: run.trigger?.actor
@@ -2222,7 +2314,11 @@ var WorkflowHostService = class {
2222
2314
  if (!resolved) {
2223
2315
  return null;
2224
2316
  }
2225
- return await this.options.engine.getRun(resolved);
2317
+ const run = await this.options.engine.getRun(resolved);
2318
+ if (!run) {
2319
+ return null;
2320
+ }
2321
+ return { ...run, status: mapRunStatusToJobStatus(run.status) };
2226
2322
  }
2227
2323
  async listRuns(filters) {
2228
2324
  const allRuns = await this.options.engine.getAllRuns();
@@ -3249,6 +3345,11 @@ async function bootstrap(_cwd = process.cwd()) {
3249
3345
  await engine.cleanupStaleRuns();
3250
3346
  bootstrapLogger.info("Resuming interrupted jobs");
3251
3347
  await engine.resumeInterruptedJobs();
3348
+ bootstrapLogger.info("Reconciling persisted child workflow invocations");
3349
+ const reconciledChildren = await engine.reconcileChildInvocations();
3350
+ if (reconciledChildren > 0) {
3351
+ bootstrapLogger.info("Reconciled child workflow invocations", { count: reconciledChildren });
3352
+ }
3252
3353
  bootstrapLogger.info("Creating JobBroker");
3253
3354
  const jobBroker = new JobBroker(
3254
3355
  engine,
@@ -3311,6 +3412,7 @@ async function bootstrap(_cwd = process.cwd()) {
3311
3412
  bootstrapLogger.info("Creating WorkflowWorker");
3312
3413
  const worker = await createWorkflowWorker({
3313
3414
  engine,
3415
+ workflowService,
3314
3416
  cliApi,
3315
3417
  logger: createWorkflowLogger("worker", "workflow.worker"),
3316
3418
  analytics: platform.analytics,