Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 90 additions & 10 deletions backend/api/v1/studio_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from backend.schemas import workflow as workflow_schemas
from backend.schemas.common import ApiResponse
from backend.workflow.compiler import compile_workflow_project
from backend.workflow.trigger_scope import scoped_project, select_active_union

router = APIRouter()

Expand Down Expand Up @@ -57,6 +58,60 @@ def _isolated_source_errors(
]


def _parked_diagnostics(
project: workflow_schemas.WorkflowProject,
parked_ids: list[str],
) -> list[workflow_schemas.WorkflowCompileError]:
"""Emit node-anchored diagnostics for every parked canvas node.

Membership comes first (one ``parked_node`` row per parked id, in authored
order). Any original configuration diagnostic for the same node follows in
its existing order so the UI can render each failure cause individually.
"""

parked_set = set(parked_ids)
diagnostics: list[workflow_schemas.WorkflowCompileError] = []
for node_id in parked_ids:
diagnostics.append(
workflow_schemas.WorkflowCompileError(
code="parked_node",
message=f'Workflow node "{node_id}" is not connected to a supported trigger.',
node_id=node_id,
path=["nodes", node_id],
)
)

# Compile parked nodes in isolation to surface configuration diagnostics
# (unknown bindings, missing params, etc.) as warnings without edges that
# would produce irrelevant port-mismatch noise.
if not parked_set:
return diagnostics
parked_nodes = [n for n in project.nodes if n.id in parked_set]
parked_project = workflow_schemas.WorkflowProject(
id=project.id,
name=project.name,
profile=project.profile,
version=project.version,
nodes=parked_nodes,
edges=[],
settings=project.settings,
adapters=list(project.adapters),
agentPermissions=project.agentPermissions,
)
parked_result = compile_workflow_project(parked_project)
for error in parked_result.errors:
if error.node_id and error.node_id in parked_set:
diagnostics.append(
workflow_schemas.WorkflowCompileError(
code=error.code,
message=error.message,
node_id=error.node_id,
path=error.path,
)
)
return diagnostics
Comment on lines +84 to +112

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect _validate_project to see which structural errors precede per-node binding diagnostics.
ast-grep outline backend/workflow/compiler.py --items all --match '_validate_project|compile_workflow_project'
ast-grep run --lang python --pattern 'def _validate_project($$$):
  $$$' backend/workflow/compiler.py
rg -n -C3 'unknown_node_library_binding' backend/workflow

Repository: 2233admin/opencli-Razormind

Length of output: 1532


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== compiler outline =="
ast-grep outline backend/workflow/compiler.py --items all || true

echo "== relevant compiler.py sections =="
sed -n '150,330p' backend/workflow/compiler.py
echo "== compiler.py diagnostics sections =="
sed -n '420,520p' backend/workflow/compiler.py

echo "== studio lifecycle relevant section =="
sed -n '1,140p' backend/api/v1/studio_lifecycle.py

echo "== schemas/workflow around WorkflowProject constraints =="
sed -n '220,260p' backend/schemas/workflow.py

Repository: 2233admin/opencli-Razormind

Length of output: 19991


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== _validate_project continuation =="
sed -n '300,420p' backend/workflow/compiler.py

echo "== source/isolated validation usages =="
rg -n -C 4 'isolated_source|source node|unconnected|require.*source|connected.*source|source.*required' backend tests

echo "== parked diagnostic tests =="
rg -n -C 6 'isolated_source_node|parked_node|_parked_diagnostics|_isolated_source_errors' backend tests

Repository: 2233admin/opencli-Razormind

Length of output: 50384


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import ast
from pathlib import Path

compiler_path = Path("backend/workflow/compiler.py")
studio_path = Path("backend/api/v1/studio_lifecycle.py")

tree = ast.parse(compiler_path.read_text())
for fn in tree.body:
    if isinstance(fn, ast.FunctionDef) and fn.name == "_validate_project":
        calls = []
        for stmt in fn.body:
            if isinstance(stmt, ast.Return) and isinstance(stmt.value, ast.Call):
                calls = [ast.unparse(stmt.value)]
            elif isinstance(stmt, ast.Return) and isinstance(stmt.value, ast.Tuple):
                calls = [ast.unparse(call) for call in stmt.value.elts[:3]]
            elif isinstance(stmt, ast.Else):
                for nested in stmt.body:
                    if isinstance(nested, ast.With):
                        for assign in nested.body:
                            if isinstance(assign, ast.Assign):
                                print("assign", ast.unparse(assign))
        print("returns:")
        for c in calls:
            print("-", c)
    if isinstance(fn, ast.FunctionDef) and fn.name == "compile_workflow_project":
        for stmt in fn.body:
            if isinstance(stmt, ast.Assign):
                for target in stmt.targets:
                    if isinstance(target, ast.Name) and target.id in {"project", "errors"}:
                        print("initial", target.id, "=", ast.unparse(stmt.value))

studio_tree = ast.parse(studio_path.read_text())

for fn in studio_tree.body:
    if isinstance(fn, ast.FunctionDef) and fn.name == "_parked_diagnostics":
        print("function", fn.name, "body_count", len(fn.body))
        for stmt_idx, stmt in enumerate(fn.body):
            print(f"{fn.name}.{stmt_idx}: " + ast.unparse(stmt).replace("\n", " ")[:220])
    if isinstance(fn, ast.FunctionDef) and fn.name == "_isolated_source_errors":
        print("function", fn.name, "body_count", len(fn.body))
        for stmt_idx, stmt in enumerate(fn.body):
            print(f"{fn.name}.{stmt_idx}: " + ast.unparse(stmt).replace("\n", " ")[:220])
PY

Repository: 2233admin/opencli-Razormind

Length of output: 389


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import ast
from pathlib import Path

compiler_path = Path("backend/workflow/compiler.py")
studio_path = Path("backend/api/v1/studio_lifecycle.py")

tree = ast.parse(compiler_path.read_text())
for fn in tree.body:
    if isinstance(fn, ast.FunctionDef) and fn.name == "_validate_project":
        print("function", fn.name, "lineno", fn.lineno)
        for stmt_idx, stmt in enumerate(fn.body):
            line = ast.unparse(stmt).replace("\n", " ")[:220]
            print(f"{fn.name}.{stmt_idx}: {line}")
    if isinstance(fn, ast.FunctionDef) and fn.name == "compile_workflow_project":
        print("function", fn.name, "lineno", fn.lineno)
        for stmt_idx, stmt in enumerate(fn.body[:8]):
            line = ast.unparse(stmt).replace("\n", " ")[:220]
            print(f"{fn.name}.{stmt_idx}: {line}")

studio_tree = ast.parse(studio_path.read_text())
for fn in studio_tree.body:
    if isinstance(fn, ast.FunctionDef) and fn.name in {"_parked_diagnostics", "_isolated_source_errors"}:
        print("function", fn.name, "lineno", fn.lineno)
        for stmt_idx, stmt in enumerate(fn.body[:80]):
            line = ast.unparse(stmt).replace("\n", " ")[:220]
            print(f"{fn.name}.{stmt_idx}: {line}")
PY

Repository: 2233admin/opencli-Razormind

Length of output: 4866


Preserve node configuration diagnostics before structural edge errors.

compile_workflow_project returns as soon as _validate_project reports any error. When edges=[], structural diagnostics such as orphan-merge/port-mismatch messages can block later node-anchored diagnostics like unknown_node_library_binding. Check for parked_node membership first, collect matching structural errors separately, then append matching node-anchored configuration diagnostics in their original order so parked source nodes and parked nodes with required upstream dependencies surface all relevant failures.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/api/v1/studio_lifecycle.py` around lines 84 - 112, Update
compile_workflow_project and the parked-node handling around parked_set so
parked-node membership is evaluated before structural edge errors. Collect
matching structural errors separately, then append node-anchored configuration
diagnostics in their original order, ensuring parked source nodes and nodes with
required upstream dependencies report all relevant failures.



def _image_generation_nodes(
nodes: object,
*,
Expand Down Expand Up @@ -185,6 +240,9 @@ async def validate_draft(
project_id=project_id,
workflow_id=workflow_id,
)
warnings: list[workflow_schemas.WorkflowCompileError] = []
valid = False
stored_graph: dict[str, Any] | None = None
try:
project = workflow_schemas.WorkflowProject.model_validate(resolved_graph)
except ValidationError as exc:
Expand All @@ -196,25 +254,47 @@ async def validate_draft(
)
for error in exc.errors()
)
valid = False
else:
errors.extend(_isolated_source_errors(project))
if errors:
valid = False
active_union = select_active_union(project)
if not active_union.has_supported_trigger:
# Legacy / media-canvas / non-trigger workflows preserve the
# existing full-graph validation path unchanged.
errors.extend(_isolated_source_errors(project))
if not errors:
result = compile_workflow_project(project)
errors = list(result.errors)
if result.valid and result.plan is not None:
valid = True
stored_graph = resolved_graph
else:
result = compile_workflow_project(project)
errors = result.errors
valid = result.valid

scoped = scoped_project(
project=project,
active_ids=active_union.active_node_ids,
external_ids={
node.id
for node in project.nodes
if isinstance(node.params.get("externalWorkflow"), dict)
},
)
Comment on lines +270 to +278

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Reuse the selector's external-workflow predicate instead of duplicating it.

backend/workflow/trigger_scope.py lines 140-146 already define _external_workflow_ids with the same isinstance(node.params.get("externalWorkflow"), dict) test. This endpoint reimplements it inline. The governed external-workflow exception then has two definitions, and design.md Decision 4 describes it as exact behavior that must not broaden. Any future change to one copy silently changes which nodes reach the published graph.

Rename the helper to external_workflow_ids, export it, and call it here.

♻️ Proposed refactor
-from backend.workflow.trigger_scope import scoped_project, select_active_union
+from backend.workflow.trigger_scope import (
+    external_workflow_ids,
+    scoped_project,
+    select_active_union,
+)
             scoped = scoped_project(
                 project=project,
                 active_ids=active_union.active_node_ids,
-                external_ids={
-                    node.id
-                    for node in project.nodes
-                    if isinstance(node.params.get("externalWorkflow"), dict)
-                },
+                external_ids=external_workflow_ids(project.nodes),
             )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/api/v1/studio_lifecycle.py` around lines 270 - 278, Rename the
private _external_workflow_ids helper in trigger_scope.py to
external_workflow_ids and expose it for import, then update the studio lifecycle
scoped_project call to use that helper instead of its inline externalWorkflow
predicate. Preserve the helper’s exact filtering behavior so both paths share
the same governed exception.

errors.extend(_isolated_source_errors(scoped))
if not errors:
scoped_result = compile_workflow_project(scoped)
errors = list(scoped_result.errors)
if scoped_result.valid and scoped_result.plan is not None:
valid = True
stored_graph = scoped.model_dump(mode="json")
warnings.extend(
_parked_diagnostics(project, active_union.parked_node_ids)
)
Comment on lines +279 to +288

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Emit parked-node warnings even when the active graph is invalid.

warnings.extend(_parked_diagnostics(...)) sits inside the scoped_result.valid branch. Two paths therefore return zero parked_node warnings while parked nodes exist:

  • _isolated_source_errors(scoped) adds an error, so the if not errors guard at line 280 skips compilation.
  • scoped_result.valid is False because the active chain has a defect.

spec.md lines 54-57 require that when validation finds nodes outside every supported trigger-reachable component, "the response exposes deterministic parked-node warnings from which clients can derive the parked count and node identifiers". That requirement does not depend on active-chain validity. The frontend derives parkedCount from these warnings and activeCount from the authored count minus parkedCount, so an author who has one active error plus six parked nodes sees an active count of ten instead of four.

Compute the parked diagnostics once in the scoped branch, before the validity check.

🐛 Proposed fix
             errors.extend(_isolated_source_errors(scoped))
+            warnings.extend(
+                _parked_diagnostics(project, active_union.parked_node_ids)
+            )
             if not errors:
                 scoped_result = compile_workflow_project(scoped)
                 errors = list(scoped_result.errors)
                 if scoped_result.valid and scoped_result.plan is not None:
                     valid = True
                     stored_graph = scoped.model_dump(mode="json")
-                    warnings.extend(
-                        _parked_diagnostics(project, active_union.parked_node_ids)
-                    )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
errors.extend(_isolated_source_errors(scoped))
if not errors:
scoped_result = compile_workflow_project(scoped)
errors = list(scoped_result.errors)
if scoped_result.valid and scoped_result.plan is not None:
valid = True
stored_graph = scoped.model_dump(mode="json")
warnings.extend(
_parked_diagnostics(project, active_union.parked_node_ids)
)
errors.extend(_isolated_source_errors(scoped))
warnings.extend(
_parked_diagnostics(project, active_union.parked_node_ids)
)
if not errors:
scoped_result = compile_workflow_project(scoped)
errors = list(scoped_result.errors)
if scoped_result.valid and scoped_result.plan is not None:
valid = True
stored_graph = scoped.model_dump(mode="json")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/api/v1/studio_lifecycle.py` around lines 279 - 288, Move the
_parked_diagnostics(project, active_union.parked_node_ids) computation in the
scoped validation branch so it runs whenever the scoped graph is processed,
before the compilation and validity checks. Extend warnings independently of
both _isolated_source_errors(scoped) and scoped_result.valid, while preserving
the existing valid-graph handling for stored_graph and valid.

row = StudioWorkflowValidationRun(
workflow_id=workflow_id,
draft_revision=draft.revision,
status="completed" if valid else "failed",
valid=valid,
errors=[error.model_dump(mode="json") for error in errors],
warnings=[],
warnings=[warning.model_dump(mode="json") for warning in warnings],
compile_version=workflow_schemas.WORKFLOW_COMPILE_VERSION,
resolved_graph=resolved_graph if valid else None,
resolved_graph=stored_graph,
)
db.add(row)
await db.flush()
Expand Down
63 changes: 56 additions & 7 deletions backend/workflow/opencli_hda_tracer.py
Original file line number Diff line number Diff line change
Expand Up @@ -305,14 +305,63 @@ async def start_workflow_run(
trace_id = body.traceId or str(uuid.uuid4())
started_at = _utcnow()
prior_events = list(existing_events or [])
# Source-level trigger scope selection runs before authoritative compilation
# so a disconnected, incomplete canvas node cannot block a valid
# trigger-reachable component. The compiled-runtime selector remains as a
# defensive assertion against post-compile drift (e.g. template expansion
# producing a second matching trigger entry).
from backend.workflow.trigger_scope import has_supported_triggers, select_trigger_scope

has_triggers = has_supported_triggers(body.project)
if has_triggers:
scope_result = select_trigger_scope(
body.project,
trigger_kind=body.trigger.kind,
trigger_node_id=body.trigger.triggerNodeId,
)
if scope_result.selection_error is not None:
scope_project = body.project
runtime_nodes: list[CompiledWorkflowNode] = []
errors = [scope_result.selection_error]
events = _compile_failure_events(
workflow_id=body.project.id,
run_id=run_id,
trace_id=trace_id,
errors=errors,
)
stored_events = [*prior_events, *events]
projection = _build_projection(
workflow_id=body.project.id,
run_id=run_id,
trace_id=trace_id,
package_node_id=body.packageNodeId,
started_at=started_at,
valid=False,
errors=errors,
runtime_nodes=[],
events=stored_events,
)
await _store_workflow_run(
run_id,
request=body,
projection=projection,
events=stored_events,
session=session,
workflow_version_id=workflow_version_id,
studio_workflow_version_id=studio_workflow_version_id,
)
return projection
scope_project = scope_result.project
else:
scope_project = body.project
compile_result = (
await compile_managed_dify_workflow_project(
body.project,
scope_project,
graphon_client=graphon_client,
session=session,
)
if graphon_client is not None
else compile_workflow_project(body.project)
else compile_workflow_project(scope_project)
)

if not compile_result.valid or compile_result.plan is None:
Expand Down Expand Up @@ -428,7 +477,7 @@ async def start_workflow_run(
) and (body.packageNodeId is not None or _select_package_id(runtime_nodes, None) is not None)
trace = (
build_opencli_hda_trace(
body.project,
scope_project,
package_node_id=body.packageNodeId,
run_id=run_id,
trace_id=trace_id,
Expand Down Expand Up @@ -1565,8 +1614,8 @@ async def start_workflow_run(
trace_id=trace_id,
package_node_id=(trace.packageNodeId if trace else None) or body.packageNodeId,
started_at=started_at,
valid=trace.valid if trace else True,
errors=trace.errors if trace else [],
valid=compile_result.valid,
errors=list(compile_result.errors) if trace is None else compile_result.errors,
Comment on lines +1617 to +1618

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find assertions tied to trace-level failure codes on run projections.
rg -n -C4 'missing_opencli_hda_package|missing_opencli_hda_sources' --glob '!**/node_modules/**'
rg -n -C3 'trace\.valid|trace\.errors' backend/workflow/opencli_hda_tracer.py

Repository: 2233admin/opencli-Razormind

Length of output: 165


🏁 Script executed:

#!/bin/bash
set -u

printf 'Files matching tracer/compiler:\n'
fd -a 'opencli_hda_tracer.py|compiler.py' . || true

printf '\nRelevant lines in opencli_hda_tracer.py:\n'
if [ -f backend/workflow/opencli_hda_tracer.py ]; then
  wc -l backend/workflow/opencli_hda_tracer.py
  sed -n '340,420p' backend/workflow/opencli_hda_tracer.py | nl -ba -v340
  sed -n '1490,1535p' backend/workflow/opencli_hda_tracer.py | nl -ba -v1490
  sed -n '1595,1650p' backend/workflow/opencli_hda_tracer.py | nl -ba -v1595
fi

printf '\nSearch failure codes in repository:\n'
rg -n 'missing_opencli_hda_package|missing_opencli_hda_sources|compile_result\.valid|compile_result\.errors|trace\.valid|trace\.errors' . --glob '!**/node_modules/**' || true

printf '\nRelevant lines in compiler.py:\n'
if [ -f backend/workflow/compiler.py ]; then
  wc -l backend/workflow/compiler.py
  sed -n '200,255p' backend/workflow/compiler.py | nl -ba -v200
fi

Repository: 2233admin/opencli-Razormind

Length of output: 2919


🏁 Script executed:

#!/bin/bash
set -u

printf 'Files matching tracer/compiler:\n'
fd -a 'opencli_hda_tracer.py|compiler.py' . || true

printf '\nRelevant lines in opencli_hda_tracer.py:\n'
if [ -f backend/workflow/opencli_hda_tracer.py ]; then
  wc -l backend/workflow/opencli_hda_tracer.py
  sed -n '340,420p' backend/workflow/opencli_hda_tracer.py | nl -ba -v340
  sed -n '1490,1535p' backend/workflow/opencli_hda_tracer.py | nl -ba -v1490
  sed -n '1595,1650p' backend/workflow/opencli_hda_tracer.py | nl -ba -v1595
fi

printf '\nSearch failure codes in repository:\n'
rg -n 'missing_opencli_hda_package|missing_opencli_hda_sources|compile_result\.valid|compile_result\.errors|trace\.valid|trace\.errors' . --glob '!**/node_modules/**' || true

printf '\nRelevant lines in compiler.py:\n'
if [ -f backend/workflow/compiler.py ]; t
  wc -l backend/workflow/compiler.py
  sed -n '200,255p' backend/workflow/compiler.py | nl -ba -v200
fi

Repository: 2233admin/opencli-Razormind

Length of output: 2919


🏁 Script executed:

#!/bin/bash
set -u

printf 'backend/workflow/opencli_hda_tracer.py relevant sections:\n'
sed -n '340,410p' backend/workflow/opencli_hda_tracer.py
sed -n '190,280p' backend/workflow/opencli_hda_tracer.py
sed -n '1485,1532p' backend/workflow/opencli_hda_tracer.py
sed -n '1605,1652p' backend/workflow/opencli_hda_tracer.py

printf '\nbackend/workflow/compiler.py compile success path:\n'
sed -n '200,255p' backend/workflow/compiler.py

printf '\nintegration test context around missing_opencli_hda_sources:\n'
sed -n '1150,1225p' tests/integration/test_workflow_opencli_hda_trace_api.py

Repository: 2233admin/opencli-Razormind

Length of output: 14159


🏁 Script executed:

#!/bin/bash
set -u

printf 'Trace result construction / failure return context:\n'
python3 - <<'PY'
from pathlib import Path
p = Path("backend/workflow/opencli_hda_tracer.py")
text = p.read_text()
for name in (
    "def build_opencli_hda_trace(",
    "selected_package_id =",
    "if not dispatches:",
    "return WorkflowOpenCLIHDATraceResponse",
):
    try:
        idx = text.index(name)
    except ValueError:
        print(f"MISSING {name}", idx if "idx" in locals() else "")
        continue
    line = text[:idx].count("\n") + 1
    lines = text[idx:]
    print(f"\n-- {name} at line {line} --")
    end = lines.find("\n")
    while end != -1 and (end == 0 or not lines[end-1:end+5].startswith("\n\n")):
        end = lines.find("\n", end + 1)
    print(lines[:250])
PY

printf '\nCall paths containing build_opencli_hda_trace:\n'
rg -n -C3 'build_opencli_hda_trace\(' . --glob '!**/node_modules/**' || true

Repository: 2233admin/opencli-Razormind

Length of output: 3026


🏁 Script executed:

#!/bin/bash
set -u

printf 'Workflow execution path for trace parameter handling:\n'
sed -n '460,510p' backend/workflow/opencli_hda_tracer.py
printf '\n'
rg -n -C2 'opencliHdaTrace|opencli_hda_trace|traceId|trace_id|WorkflowOpenCLIHDATraceResponse' backend/api backend/tests backend/workflow tests/integration --glob '!**/node_modules/**' || true

Repository: 2233admin/opencli-Razormind

Length of output: 50384


Fold the OpenCLI HDA trace result into the run projection.

should_trace_opencli calls build_opencli_hda_trace(...) whenever OpenCLI/HDA tracing should run, and the returned trace can be valid=False with missing_opencli_hda_package or missing_opencli_hda_sources. The projection currently uses only compile_result.valid and compile_result.errors, so a trace failure can make the run report status == "completed", valid == True, errors == [] even though no dispatch envelope was built. Update the projection and the session-reloaded projection to make trace validity/errors part of the projected validity/errors.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/workflow/opencli_hda_tracer.py` around lines 1617 - 1618, The run
projection currently ignores failures returned by build_opencli_hda_trace,
allowing failed traces to appear valid and completed. Update the projection
using compile_result and trace so validity requires both results to be valid and
errors include trace errors when a trace exists; apply the same combined logic
to the session-reloaded projection. Preserve existing behavior when trace is
None.

runtime_nodes=runtime_nodes,
events=events,
)
Expand All @@ -1589,8 +1638,8 @@ async def start_workflow_run(
package_node_id=(trace.packageNodeId if trace else None)
or body.packageNodeId,
started_at=started_at,
valid=trace.valid if trace else True,
errors=trace.errors if trace else [],
valid=compile_result.valid,
errors=list(compile_result.errors) if trace is None else compile_result.errors,
runtime_nodes=runtime_nodes,
events=stored.events,
)
Expand Down
Loading
Loading