-
Notifications
You must be signed in to change notification settings - Fork 3.3k
fix(executor): resolve block ID for parallel subflow active state #3223
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: staging
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -88,21 +88,38 @@ export function useTerminalFilters() { | |
| let result = entries | ||
|
|
||
| if (hasActiveFilters) { | ||
| result = entries.filter((entry) => { | ||
| // Block ID filter | ||
| // Determine which top-level entries pass the filters | ||
| const visibleBlockIds = new Set<string>() | ||
| for (const entry of entries) { | ||
| if (entry.parentWorkflowBlockId) continue | ||
|
|
||
| let passes = true | ||
| if (filters.blockIds.size > 0 && !filters.blockIds.has(entry.blockId)) { | ||
| return false | ||
| passes = false | ||
| } | ||
|
|
||
| // Status filter | ||
| if (filters.statuses.size > 0) { | ||
| if (passes && filters.statuses.size > 0) { | ||
| const isError = !!entry.error | ||
| const hasStatus = isError ? filters.statuses.has('error') : filters.statuses.has('info') | ||
| if (!hasStatus) return false | ||
| if (!hasStatus) passes = false | ||
| } | ||
| if (passes) { | ||
| visibleBlockIds.add(entry.blockId) | ||
| } | ||
| } | ||
|
|
||
| // Propagate visibility to child workflow entries (handles arbitrary nesting). | ||
| // Keep iterating until no new children are discovered. | ||
| let prevSize = 0 | ||
| while (visibleBlockIds.size !== prevSize) { | ||
| prevSize = visibleBlockIds.size | ||
| for (const entry of entries) { | ||
| if (entry.parentWorkflowBlockId && visibleBlockIds.has(entry.parentWorkflowBlockId)) { | ||
| visibleBlockIds.add(entry.blockId) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return true | ||
| }) | ||
| result = entries.filter((entry) => visibleBlockIds.has(entry.blockId)) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Status filtering leaks unrelated entriesMedium Severity
|
||
| } | ||
|
|
||
| // Sort by executionOrder (monotonically increasing integer from server) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -120,10 +120,10 @@ export function isSubflowBlockType(blockType: string): boolean { | |
| /** | ||
| * Node type for the tree structure | ||
| */ | ||
| export type EntryNodeType = 'block' | 'subflow' | 'iteration' | ||
| export type EntryNodeType = 'block' | 'subflow' | 'iteration' | 'workflow' | ||
|
|
||
| /** | ||
| * Entry node for tree structure - represents a block, subflow, or iteration | ||
| * Entry node for tree structure - represents a block, subflow, iteration, or workflow | ||
| */ | ||
| export interface EntryNode { | ||
| /** The console entry (for blocks) or synthetic entry (for subflows/iterations) */ | ||
|
|
@@ -175,12 +175,17 @@ interface IterationGroup { | |
| * Sorts by start time to ensure chronological order. | ||
| */ | ||
| function buildEntryTree(entries: ConsoleEntry[]): EntryNode[] { | ||
| // Separate regular blocks from iteration entries | ||
| // Separate regular blocks from iteration entries and child workflow entries | ||
| const regularBlocks: ConsoleEntry[] = [] | ||
| const iterationEntries: ConsoleEntry[] = [] | ||
| const childWorkflowEntries = new Map<string, ConsoleEntry[]>() | ||
|
|
||
| for (const entry of entries) { | ||
| if (entry.iterationType && entry.iterationCurrent !== undefined) { | ||
| if (entry.parentWorkflowBlockId) { | ||
| const existing = childWorkflowEntries.get(entry.parentWorkflowBlockId) || [] | ||
| existing.push(entry) | ||
| childWorkflowEntries.set(entry.parentWorkflowBlockId, existing) | ||
| } else if (entry.iterationType && entry.iterationCurrent !== undefined) { | ||
| iterationEntries.push(entry) | ||
| } else { | ||
| regularBlocks.push(entry) | ||
|
|
@@ -338,12 +343,53 @@ function buildEntryTree(entries: ConsoleEntry[]): EntryNode[] { | |
| }) | ||
| } | ||
|
|
||
| // Build nodes for regular blocks | ||
| const regularNodes: EntryNode[] = regularBlocks.map((entry) => ({ | ||
| entry, | ||
| children: [], | ||
| nodeType: 'block' as const, | ||
| })) | ||
| /** | ||
| * Recursively builds child nodes for workflow blocks. | ||
| * Handles multi-level nesting where a child workflow block itself has children. | ||
| */ | ||
| const buildWorkflowChildNodes = (parentBlockId: string): EntryNode[] => { | ||
| const childEntries = childWorkflowEntries.get(parentBlockId) | ||
| if (!childEntries || childEntries.length === 0) return [] | ||
|
|
||
| childEntries.sort((a, b) => { | ||
| const aTime = new Date(a.startedAt || a.timestamp).getTime() | ||
| const bTime = new Date(b.startedAt || b.timestamp).getTime() | ||
| return aTime - bTime | ||
| }) | ||
|
|
||
| return childEntries.map((child) => { | ||
| const nestedChildren = buildWorkflowChildNodes(child.blockId) | ||
| if (nestedChildren.length > 0) { | ||
| return { | ||
| entry: child, | ||
| children: nestedChildren, | ||
| nodeType: 'workflow' as const, | ||
| } | ||
| } | ||
| return { | ||
| entry: child, | ||
| children: [], | ||
| nodeType: 'block' as const, | ||
| } | ||
| }) | ||
| } | ||
|
|
||
| // Build nodes for regular blocks, promoting workflow blocks with children to 'workflow' nodes | ||
| const regularNodes: EntryNode[] = regularBlocks.map((entry) => { | ||
| const childNodes = buildWorkflowChildNodes(entry.blockId) | ||
| if (childNodes.length > 0) { | ||
| return { | ||
| entry, | ||
| children: childNodes, | ||
| nodeType: 'workflow' as const, | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nested workflow logs dropped in subflowsMedium Severity Child workflow entries are attached only when building Additional Locations (1) |
||
| } | ||
| return { | ||
| entry, | ||
| children: [], | ||
| nodeType: 'block' as const, | ||
| } | ||
| }) | ||
|
|
||
| // Combine all nodes and sort by executionOrder ascending (oldest first, top-down) | ||
| const allNodes = [...subflowNodes, ...regularNodes] | ||
|
|
||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Quadratic worst-case in filter propagation
This fixed-point while loop iterates over all entries on every pass until convergence, giving O(N × D) complexity where D is nesting depth. For typical use this is fine, but if a workflow has many entries and deep nesting, this could become slow. Consider building a parent→children adjacency map once and doing a single BFS/DFS pass from the visible roots instead:
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!