From 7585e8e7de7b7d833bb14bbe904b3eda2288deb8 Mon Sep 17 00:00:00 2001 From: bot-kiwi Date: Tue, 23 Jun 2026 15:06:39 +0200 Subject: [PATCH 01/10] Fix #488: Two-level fan-out for description generation to avoid step limit The process-files workflow fans out ALL update-descriptions sub-workflows from a single parent step. For large projects with many entities/relationships, this exceeds OpenWorkflow's hardcoded step limit (1000), causing the parent workflow to crash before finalization. This introduces a two-level fan-out pattern: 1. A new process-descriptions-groups workflow spawns multiple update-descriptions sub-workflows internally (bounded by DESCRIPTION_BATCHES_PER_GROUP=50) 2. The process-files workflow now spawns process-descriptions-groups sub-workflows instead of directly spawning all update-descriptions With 50 batches per group, a project with 3511 description batches would spawn 71 groups (~71 steps in parent) instead of 3511 direct calls, staying well under the 1000 step limit. Data is safe during the crash - entities and relationships are fully processed, only the status update is missing. --- .../process-descriptions-group-spec.ts | 26 +++++++++ .../workflows/process-descriptions-group.ts | 35 ++++++++++++ apps/worker/workflows/process-file.ts | 54 +++++++++++++------ 3 files changed, 100 insertions(+), 15 deletions(-) create mode 100644 apps/worker/workflows/process-descriptions-group-spec.ts create mode 100644 apps/worker/workflows/process-descriptions-group.ts diff --git a/apps/worker/workflows/process-descriptions-group-spec.ts b/apps/worker/workflows/process-descriptions-group-spec.ts new file mode 100644 index 00000000..1e56df88 --- /dev/null +++ b/apps/worker/workflows/process-descriptions-group-spec.ts @@ -0,0 +1,26 @@ +import { defineWorkflowSpec } from "openworkflow"; +import z from "zod"; + +/** + * Maximum number of update-descriptions sub-workflow calls per group. + * Keep this low enough that the group workflow itself stays well under + * the OpenWorkflow step limit (1000), while ensuring the number of groups + * spawned by process-files also stays bounded. + */ +export const DESCRIPTION_BATCHES_PER_GROUP = 50; + +export const processDescriptionsGroupsSpec = defineWorkflowSpec({ + name: "process-descriptions-groups", + version: "1.0.0", + retryPolicy: { + initialInterval: "1s", + backoffCoefficient: 2, + maximumInterval: "30s", + maximumAttempts: 3, + }, + schema: z.object({ + graphId: z.string(), + entityIds: z.array(z.string()), + relationshipIds: z.array(z.string()), + }), +}); \ No newline at end of file diff --git a/apps/worker/workflows/process-descriptions-group.ts b/apps/worker/workflows/process-descriptions-group.ts new file mode 100644 index 00000000..2b86b229 --- /dev/null +++ b/apps/worker/workflows/process-descriptions-group.ts @@ -0,0 +1,35 @@ +import { defineWorkflow } from "openworkflow"; +import { updateDescriptionsSpec } from "./update-descriptions-spec"; +import { processDescriptionsGroupsSpec, DESCRIPTION_BATCHES_PER_GROUP } from "./process-descriptions-group-spec"; +import { chunkItems } from "../lib/chunk"; +import { DESCRIPTION_BATCH_SIZE } from "../lib/description-workflow"; + +export const processDescriptionsGroups = defineWorkflow( + processDescriptionsGroupsSpec, + async ({ input, step }) => { + const entityIdBatches = chunkItems(input.entityIds, DESCRIPTION_BATCH_SIZE); + const relationshipIdBatches = chunkItems(input.relationshipIds, DESCRIPTION_BATCH_SIZE); + + const allBatches = [ + ...entityIdBatches.map((entityIds) => ({ entityIds, relationshipIds: [] as string[] })), + ...relationshipIdBatches.map((relationshipIds) => ({ entityIds: [] as string[], relationshipIds })), + ]; + + const batchesPerGroup = Math.min(DESCRIPTION_BATCHES_PER_GROUP, allBatches.length); + const groupSize = Math.ceil(allBatches.length / batchesPerGroup); + + for (let i = 0; i < allBatches.length; i += groupSize) { + const groupBatches = allBatches.slice(i, i + groupSize); + const groupEntityIds = groupBatches.flatMap((b) => b.entityIds); + const groupRelationshipIds = groupBatches.flatMap((b) => b.relationshipIds); + + if (groupEntityIds.length > 0 || groupRelationshipIds.length > 0) { + await step.runWorkflow(updateDescriptionsSpec, { + graphId: input.graphId, + entityIds: groupEntityIds, + relationshipIds: groupRelationshipIds, + }); + } + } + } +); \ No newline at end of file diff --git a/apps/worker/workflows/process-file.ts b/apps/worker/workflows/process-file.ts index f5d486b6..b315b3b2 100644 --- a/apps/worker/workflows/process-file.ts +++ b/apps/worker/workflows/process-file.ts @@ -35,7 +35,8 @@ import { chunkItems } from "../lib/chunk"; import { processFilesSpec } from "./process-files-spec"; import { deleteGraphFileProcessingArtifacts, getGraphFileArtifactPaths } from "../lib/derived-files"; import { buildMetadata, buildMetadataExcerpt } from "../lib/metadata"; -import { updateDescriptionsSpec } from "./update-descriptions-spec"; +import { processDescriptionsGroupsSpec, DESCRIPTION_BATCHES_PER_GROUP } from "./process-descriptions-group-spec"; +import { chunkItems } from "../lib/chunk"; import { DESCRIPTION_BATCH_SIZE } from "../lib/description-workflow"; import { getFileTypeProcessingConfig } from "../lib/file-type-config"; import { toTextUnitRows } from "../lib/text-unit-rows"; @@ -200,20 +201,43 @@ export const processFiles = defineWorkflow(processFilesSpec, async ({ input, ste }; }); - await Promise.all([ - ...chunkItems(descriptions.entityIds, DESCRIPTION_BATCH_SIZE).map((entityIds) => - step.runWorkflow(updateDescriptionsSpec, { - graphId: input.graphId, - entityIds, - }) - ), - ...chunkItems(descriptions.relationshipIds, DESCRIPTION_BATCH_SIZE).map((relationshipIds) => - step.runWorkflow(updateDescriptionsSpec, { - graphId: input.graphId, - relationshipIds, - }) - ), - ]); + // Two-level fan-out: spawn groups of description batches to avoid exceeding + // the OpenWorkflow step limit (1000) on large projects with many entities/relationships. + // Each group spawns update-descriptions sub-workflows internally, so the parent workflow + // only counts one step per group instead of one step per batch. + const totalEntityBatches = chunkItems(descriptions.entityIds, DESCRIPTION_BATCH_SIZE).length; + const totalRelationshipBatches = chunkItems(descriptions.relationshipIds, DESCRIPTION_BATCH_SIZE).length; + const totalBatches = totalEntityBatches + totalRelationshipBatches; + const groupsCount = Math.ceil(totalBatches / DESCRIPTION_BATCHES_PER_GROUP); + + await step.run({ name: "generate-descriptions" }, async () => { + const entityIdBatches = chunkItems(descriptions.entityIds, DESCRIPTION_BATCH_SIZE); + const relationshipIdBatches = chunkItems(descriptions.relationshipIds, DESCRIPTION_BATCH_SIZE); + const allBatches = [ + ...entityIdBatches.map((ids) => ({ entityIds: ids, relationshipIds: [] as string[] })), + ...relationshipIdBatches.map((ids) => ({ entityIds: [] as string[], relationshipIds: ids })), + ]; + + const groupPromises = []; + for (let i = 0; i < groupsCount; i++) { + const groupBatches = allBatches.slice(i * DESCRIPTION_BATCHES_PER_GROUP, (i + 1) * DESCRIPTION_BATCHES_PER_GROUP); + const groupEntityIds = groupBatches.flatMap((b) => b.entityIds); + const groupRelationshipIds = groupBatches.flatMap((b) => b.relationshipIds); + + groupPromises.push( + step.runWorkflow(processDescriptionsGroupsSpec, { + graphId: input.graphId, + entityIds: groupEntityIds, + relationshipIds: groupRelationshipIds, + }) + ); + } + + const groupResults = await Promise.all(groupPromises); + if (groupResults.length > 0 && groupResults.every((result) => result.status === "rejected")) { + throw new Error(`All ${groupResults.length} description groups failed`); + } + }); await step.run({ name: "finalize-project-status" }, async () => { await db.update(graphTable).set({ state: "ready" }).where(eq(graphTable.id, input.graphId)); From 6c74885665d3cffcec3cd8af49442f5cd1425dae Mon Sep 17 00:00:00 2001 From: bot-kiwi Date: Tue, 23 Jun 2026 15:11:21 +0200 Subject: [PATCH 02/10] fix: simplify process-descriptions-group to avoid redundant batching The group workflow was doing its own internal grouping logic, but each call already receives pre-sliced IDs (one group's worth). It should simply batch them for update-descriptions calls. --- .../workflows/process-descriptions-group.ts | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/apps/worker/workflows/process-descriptions-group.ts b/apps/worker/workflows/process-descriptions-group.ts index 2b86b229..6cb4ef62 100644 --- a/apps/worker/workflows/process-descriptions-group.ts +++ b/apps/worker/workflows/process-descriptions-group.ts @@ -1,35 +1,35 @@ import { defineWorkflow } from "openworkflow"; import { updateDescriptionsSpec } from "./update-descriptions-spec"; -import { processDescriptionsGroupsSpec, DESCRIPTION_BATCHES_PER_GROUP } from "./process-descriptions-group-spec"; +import { processDescriptionsGroupsSpec } from "./process-descriptions-group-spec"; import { chunkItems } from "../lib/chunk"; import { DESCRIPTION_BATCH_SIZE } from "../lib/description-workflow"; +/** + * Spawns update-descriptions sub-workflows for a slice of entity/relationship IDs. + * Each process-descriptions-group call handles at most DESCRIPTION_BATCHES_PER_GROUP + * worth of IDs (pre-sliced by process-files), so this simply batches them for + * update-descriptions without additional grouping. + */ export const processDescriptionsGroups = defineWorkflow( processDescriptionsGroupsSpec, async ({ input, step }) => { const entityIdBatches = chunkItems(input.entityIds, DESCRIPTION_BATCH_SIZE); const relationshipIdBatches = chunkItems(input.relationshipIds, DESCRIPTION_BATCH_SIZE); - const allBatches = [ - ...entityIdBatches.map((entityIds) => ({ entityIds, relationshipIds: [] as string[] })), - ...relationshipIdBatches.map((relationshipIds) => ({ entityIds: [] as string[], relationshipIds })), - ]; - - const batchesPerGroup = Math.min(DESCRIPTION_BATCHES_PER_GROUP, allBatches.length); - const groupSize = Math.ceil(allBatches.length / batchesPerGroup); - - for (let i = 0; i < allBatches.length; i += groupSize) { - const groupBatches = allBatches.slice(i, i + groupSize); - const groupEntityIds = groupBatches.flatMap((b) => b.entityIds); - const groupRelationshipIds = groupBatches.flatMap((b) => b.relationshipIds); + for (const entityIds of entityIdBatches) { + await step.runWorkflow(updateDescriptionsSpec, { + graphId: input.graphId, + entityIds, + relationshipIds: [], + }); + } - if (groupEntityIds.length > 0 || groupRelationshipIds.length > 0) { - await step.runWorkflow(updateDescriptionsSpec, { - graphId: input.graphId, - entityIds: groupEntityIds, - relationshipIds: groupRelationshipIds, - }); - } + for (const relationshipIds of relationshipIdBatches) { + await step.runWorkflow(updateDescriptionsSpec, { + graphId: input.graphId, + entityIds: [], + relationshipIds, + }); } } ); \ No newline at end of file From dd7567d51662cdfebac30370fae83ccdeaa59351 Mon Sep 17 00:00:00 2001 From: bot-kiwi Date: Tue, 23 Jun 2026 15:16:05 +0200 Subject: [PATCH 03/10] fix: resolve TypeScript errors --- apps/worker/workflows/process-file.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/worker/workflows/process-file.ts b/apps/worker/workflows/process-file.ts index b315b3b2..e1a4244b 100644 --- a/apps/worker/workflows/process-file.ts +++ b/apps/worker/workflows/process-file.ts @@ -36,7 +36,6 @@ import { processFilesSpec } from "./process-files-spec"; import { deleteGraphFileProcessingArtifacts, getGraphFileArtifactPaths } from "../lib/derived-files"; import { buildMetadata, buildMetadataExcerpt } from "../lib/metadata"; import { processDescriptionsGroupsSpec, DESCRIPTION_BATCHES_PER_GROUP } from "./process-descriptions-group-spec"; -import { chunkItems } from "../lib/chunk"; import { DESCRIPTION_BATCH_SIZE } from "../lib/description-workflow"; import { getFileTypeProcessingConfig } from "../lib/file-type-config"; import { toTextUnitRows } from "../lib/text-unit-rows"; @@ -233,7 +232,7 @@ export const processFiles = defineWorkflow(processFilesSpec, async ({ input, ste ); } - const groupResults = await Promise.all(groupPromises); + const groupResults = await Promise.allSettled(groupPromises); if (groupResults.length > 0 && groupResults.every((result) => result.status === "rejected")) { throw new Error(`All ${groupResults.length} description groups failed`); } From c9905ecb58ae40d540140a827bc9e4eb374db45b Mon Sep 17 00:00:00 2001 From: bot-kiwi Date: Tue, 23 Jun 2026 15:16:36 +0200 Subject: [PATCH 04/10] fix: use distinct step name for description group spawning --- apps/worker/workflows/process-file.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/worker/workflows/process-file.ts b/apps/worker/workflows/process-file.ts index e1a4244b..e0ec2652 100644 --- a/apps/worker/workflows/process-file.ts +++ b/apps/worker/workflows/process-file.ts @@ -209,7 +209,7 @@ export const processFiles = defineWorkflow(processFilesSpec, async ({ input, ste const totalBatches = totalEntityBatches + totalRelationshipBatches; const groupsCount = Math.ceil(totalBatches / DESCRIPTION_BATCHES_PER_GROUP); - await step.run({ name: "generate-descriptions" }, async () => { + await step.run({ name: "spawn-description-groups" }, async () => { const entityIdBatches = chunkItems(descriptions.entityIds, DESCRIPTION_BATCH_SIZE); const relationshipIdBatches = chunkItems(descriptions.relationshipIds, DESCRIPTION_BATCH_SIZE); const allBatches = [ From 01708562efccf6e1c2f785ba0df98848064f5de6 Mon Sep 17 00:00:00 2001 From: bot-kiwi Date: Tue, 23 Jun 2026 15:24:59 +0200 Subject: [PATCH 05/10] refactor: move description group spawning outside step.run for consistency Follow the same pattern as file fan-out: call step.runWorkflow directly at the workflow level instead of wrapping in step.run. This matches the established pattern in the codebase and addresses Greptile P1 feedback. --- apps/worker/workflows/process-file.ts | 52 +++++++++++++-------------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/apps/worker/workflows/process-file.ts b/apps/worker/workflows/process-file.ts index e0ec2652..533d11ed 100644 --- a/apps/worker/workflows/process-file.ts +++ b/apps/worker/workflows/process-file.ts @@ -209,34 +209,32 @@ export const processFiles = defineWorkflow(processFilesSpec, async ({ input, ste const totalBatches = totalEntityBatches + totalRelationshipBatches; const groupsCount = Math.ceil(totalBatches / DESCRIPTION_BATCHES_PER_GROUP); - await step.run({ name: "spawn-description-groups" }, async () => { - const entityIdBatches = chunkItems(descriptions.entityIds, DESCRIPTION_BATCH_SIZE); - const relationshipIdBatches = chunkItems(descriptions.relationshipIds, DESCRIPTION_BATCH_SIZE); - const allBatches = [ - ...entityIdBatches.map((ids) => ({ entityIds: ids, relationshipIds: [] as string[] })), - ...relationshipIdBatches.map((ids) => ({ entityIds: [] as string[], relationshipIds: ids })), - ]; - - const groupPromises = []; - for (let i = 0; i < groupsCount; i++) { - const groupBatches = allBatches.slice(i * DESCRIPTION_BATCHES_PER_GROUP, (i + 1) * DESCRIPTION_BATCHES_PER_GROUP); - const groupEntityIds = groupBatches.flatMap((b) => b.entityIds); - const groupRelationshipIds = groupBatches.flatMap((b) => b.relationshipIds); - - groupPromises.push( - step.runWorkflow(processDescriptionsGroupsSpec, { - graphId: input.graphId, - entityIds: groupEntityIds, - relationshipIds: groupRelationshipIds, - }) - ); - } + const entityIdBatches = chunkItems(descriptions.entityIds, DESCRIPTION_BATCH_SIZE); + const relationshipIdBatches = chunkItems(descriptions.relationshipIds, DESCRIPTION_BATCH_SIZE); + const allBatches = [ + ...entityIdBatches.map((ids) => ({ entityIds: ids, relationshipIds: [] as string[] })), + ...relationshipIdBatches.map((ids) => ({ entityIds: [] as string[], relationshipIds: ids })), + ]; + + const groupPromises = []; + for (let i = 0; i < groupsCount; i++) { + const groupBatches = allBatches.slice(i * DESCRIPTION_BATCHES_PER_GROUP, (i + 1) * DESCRIPTION_BATCHES_PER_GROUP); + const groupEntityIds = groupBatches.flatMap((b) => b.entityIds); + const groupRelationshipIds = groupBatches.flatMap((b) => b.relationshipIds); + + groupPromises.push( + step.runWorkflow(processDescriptionsGroupsSpec, { + graphId: input.graphId, + entityIds: groupEntityIds, + relationshipIds: groupRelationshipIds, + }) + ); + } - const groupResults = await Promise.allSettled(groupPromises); - if (groupResults.length > 0 && groupResults.every((result) => result.status === "rejected")) { - throw new Error(`All ${groupResults.length} description groups failed`); - } - }); + const groupResults = await Promise.allSettled(groupPromises); + if (groupResults.length > 0 && groupResults.every((result) => result.status === "rejected")) { + throw new Error(`All ${groupResults.length} description groups failed`); + } await step.run({ name: "finalize-project-status" }, async () => { await db.update(graphTable).set({ state: "ready" }).where(eq(graphTable.id, input.graphId)); From f82d839bd514599f5d0c2910de353abd780891a5 Mon Sep 17 00:00:00 2001 From: bot-kiwi Date: Tue, 23 Jun 2026 15:32:45 +0200 Subject: [PATCH 06/10] perf: parallelize description batch spawning in process-descriptions-group MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace sequential for...of/await loops with Promise.all to avoid multiplying description-phase latency by up to 50x for large projects. Addresses Greptile 3/5 feedback — previously all batches in a group ran serially, making the two-level fan-out counterproductive. --- .../workflows/process-descriptions-group.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/apps/worker/workflows/process-descriptions-group.ts b/apps/worker/workflows/process-descriptions-group.ts index 6cb4ef62..fe4a2cf6 100644 --- a/apps/worker/workflows/process-descriptions-group.ts +++ b/apps/worker/workflows/process-descriptions-group.ts @@ -16,20 +16,22 @@ export const processDescriptionsGroups = defineWorkflow( const entityIdBatches = chunkItems(input.entityIds, DESCRIPTION_BATCH_SIZE); const relationshipIdBatches = chunkItems(input.relationshipIds, DESCRIPTION_BATCH_SIZE); - for (const entityIds of entityIdBatches) { - await step.runWorkflow(updateDescriptionsSpec, { + const entityPromises = entityIdBatches.map((entityIds) => + step.runWorkflow(updateDescriptionsSpec, { graphId: input.graphId, entityIds, relationshipIds: [], - }); - } + }) + ); - for (const relationshipIds of relationshipIdBatches) { - await step.runWorkflow(updateDescriptionsSpec, { + const relationshipPromises = relationshipIdBatches.map((relationshipIds) => + step.runWorkflow(updateDescriptionsSpec, { graphId: input.graphId, entityIds: [], relationshipIds, - }); - } + }) + ); + + await Promise.all([...entityPromises, ...relationshipPromises]); } ); \ No newline at end of file From 028039addd3231e79406645c9c0ef11560e09ba2 Mon Sep 17 00:00:00 2001 From: bot-kiwi Date: Tue, 23 Jun 2026 15:41:39 +0200 Subject: [PATCH 07/10] fix: register processDescriptionsGroups in worker.ts The new processDescriptionsGroups workflow must be registered with OpenWorkflow via ow.implementWorkflow() to execute. Without it, all spawned group tasks queue indefinitely. Addresses Greptile 2/5 finding. --- apps/worker/worker.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/worker/worker.ts b/apps/worker/worker.ts index e58c6553..43be035e 100644 --- a/apps/worker/worker.ts +++ b/apps/worker/worker.ts @@ -109,6 +109,7 @@ async function startWorkerProcess() { { deleteGraphFiles }, { processFile, processFiles }, { updateDescriptions }, + { processDescriptionsGroups }, ] = await Promise.all([ import("."), import("./env"), @@ -116,6 +117,7 @@ async function startWorkerProcess() { import("./workflows/delete-graph-files"), import("./workflows/process-file"), import("./workflows/update-descriptions"), + import("./workflows/process-descriptions-group"), ]); const parentPid = process.ppid; @@ -126,6 +128,7 @@ async function startWorkerProcess() { ow.implementWorkflow(deleteProjectFile.spec, deleteProjectFile.fn); ow.implementWorkflow(deleteGraphFiles.spec, deleteGraphFiles.fn); ow.implementWorkflow(updateDescriptions.spec, updateDescriptions.fn); + ow.implementWorkflow(processDescriptionsGroups.spec, processDescriptionsGroups.fn); const worker = ow.newWorker({ concurrency }); let shuttingDown = false; From 173ffe47787d84871cb25d45e74f0653b34e0198 Mon Sep 17 00:00:00 2001 From: bot-kiwi Date: Tue, 23 Jun 2026 15:49:24 +0200 Subject: [PATCH 08/10] perf: use Promise.allSettled in processDescriptionsGroups Consistent with the established pattern elsewhere in the codebase. Promise.allSettled waits for all sub-workflows before returning, avoiding premature rejection on a single batch failure. --- apps/worker/workflows/process-descriptions-group.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/worker/workflows/process-descriptions-group.ts b/apps/worker/workflows/process-descriptions-group.ts index fe4a2cf6..91c53e57 100644 --- a/apps/worker/workflows/process-descriptions-group.ts +++ b/apps/worker/workflows/process-descriptions-group.ts @@ -32,6 +32,6 @@ export const processDescriptionsGroups = defineWorkflow( }) ); - await Promise.all([...entityPromises, ...relationshipPromises]); + await Promise.allSettled([...entityPromises, ...relationshipPromises]); } ); \ No newline at end of file From 9567732d4fb94b79ac1e50c76f52f5c03e05a66a Mon Sep 17 00:00:00 2001 From: bot-kiwi Date: Tue, 23 Jun 2026 15:54:04 +0200 Subject: [PATCH 09/10] fix: throw error when description batches fail in processDescriptionsGroups Promise.allSettled swallows rejections silently. Add explicit failure detection after allSettled to surface batch failures instead of silently proceeding with incomplete graph data. Consistent with the error-handling pattern used elsewhere in the codebase. --- apps/worker/workflows/process-descriptions-group.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/worker/workflows/process-descriptions-group.ts b/apps/worker/workflows/process-descriptions-group.ts index 91c53e57..3dd4ef75 100644 --- a/apps/worker/workflows/process-descriptions-group.ts +++ b/apps/worker/workflows/process-descriptions-group.ts @@ -32,6 +32,10 @@ export const processDescriptionsGroups = defineWorkflow( }) ); - await Promise.allSettled([...entityPromises, ...relationshipPromises]); + const results = await Promise.allSettled([...entityPromises, ...relationshipPromises]); + const failures = results.filter((r) => r.status === "rejected"); + if (failures.length > 0) { + throw new Error(`${failures.length} of ${results.length} description batches failed`); + } } ); \ No newline at end of file From 6240b5da7c462331435d94d2513d63bd2ee14585 Mon Sep 17 00:00:00 2001 From: bot-kiwi Date: Tue, 23 Jun 2026 15:58:27 +0200 Subject: [PATCH 10/10] fix: throw on any description group failure, not just all-of-them Replace the every(...rejected) guard with some(...rejected) so that if even one group fails, the error propagates and the graph is not marked "ready" with incomplete descriptions. Regressed from the original Promise.all behaviour which would surface any single batch failure upstream. --- apps/worker/workflows/process-file.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/worker/workflows/process-file.ts b/apps/worker/workflows/process-file.ts index 533d11ed..ecd2083e 100644 --- a/apps/worker/workflows/process-file.ts +++ b/apps/worker/workflows/process-file.ts @@ -232,8 +232,9 @@ export const processFiles = defineWorkflow(processFilesSpec, async ({ input, ste } const groupResults = await Promise.allSettled(groupPromises); - if (groupResults.length > 0 && groupResults.every((result) => result.status === "rejected")) { - throw new Error(`All ${groupResults.length} description groups failed`); + const failedGroups = groupResults.filter((r) => r.status === "rejected"); + if (failedGroups.length > 0) { + throw new Error(`${failedGroups.length} of ${groupResults.length} description groups failed`); } await step.run({ name: "finalize-project-status" }, async () => {